为什么泛型类型约束会导致无隐式引用转换错误?

Ren*_*ens 41 c# generics type-constraints implicit-conversion

我创建了几个用于处理议程约会的接口和泛型类:

interface IAppointment<T> where T : IAppointmentProperties
{
    T Properties { get; set; }
}

interface IAppointmentEntry<T> where T : IAppointment<IAppointmentProperties>
{
    DateTime Date { get; set; }
    T Appointment { get; set; }
}

interface IAppointmentProperties 
{
    string Description { get; set; }
}

class Appointment<T> : IAppointment<T> where T : IAppointmentProperties
{
    public T Properties { get; set; }
}

class AppointmentEntry<T> : IAppointmentEntry<T> where T : IAppointment<IAppointmentProperties>
{
    public DateTime Date { get; set; }
    public T Appointment { get; set; }
}

class AppointmentProperties : IAppointmentProperties
{
    public string Description { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我正在尝试对类型参数使用一些约束,以确保只能指定有效类型.但是,在指定T必须实现的约束时IAppointment<IAppointmentProperties>,编译器在使用以下类时会出错Appointment<AppointmentProperties>:

class MyAppointment : Appointment<MyAppointmentProperties>
{
}

// This goes wrong:
class MyAppointmentEntry : AppointmentEntry<MyAppointment>
{
}

class MyAppointmentProperties : AppointmentProperties
{
    public string ExtraInformation { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

错误是:

The type 'Example.MyAppointment' cannot be used as type parameter 'T' in the generic type or method 'Example.AppointmentEntry<T>'. There is no implicit reference conversion from 'Example.MyAppointment' to 'Example.IAppointment<Example.IAppointmentProperties>'.

任何人都可以解释为什么这不起作用?

Eri*_*ert 119

让我们简化一下:

interface IAnimal { ... }
interface ICage<T> where T : IAnimal { void Enclose(T animal); } 
class Tiger : IAnimal { ... }
class Fish : IAnimal { ... }
class Cage<T>  : ICage<T> where T : IAnimal { ... }
ICage<IAnimal> cage = new Cage<Tiger>();
Run Code Online (Sandbox Code Playgroud)

你的问题是:为什么最后一行是非法的?

既然我已经重写了代码以简化它,那么应该很清楚. 一个ICage<IAnimal>在一个笼子可以在其中放置任何动物,但Cage<Tiger>可以只持有虎,所以这一定是非法的.

如果它不是非法的那么你可以这样做:

cage.Enclose(new Fish());
Run Code Online (Sandbox Code Playgroud)

嘿,你只是把鱼放进虎笼里.

类型系统不允许转换,因为这样做会违反源类型的功能不得小于目标类型的功能的规则.(这是着名的"Liskov替代原则"的一种形式.)

更具体地说,我会说你滥用泛型.事实上,你做的类型关系太复杂了,你无法分析自己,这证明你应该简化整个事情; 如果你没有保持所有的类型关系并且你写了这个东西,那么你的用户肯定也无法保持直线.

  • "你只是把鱼放进虎笼里." 它是老虎鱼吗?:-) (4认同)
  • @StephenZeng:我是那本书的主编,确实值得推荐! (3认同)
  • 这是一个非常好/简单的解释.我记得深入阅读Jon Skeet的书c#,它很好地解释了c#泛型协方差逆变.极力推荐. (2认同)

Ste*_*eng 14

埃里克已经有了一个非常好的答案.只想借此机会在这里讨论不变性,协方差逆变性.

有关定义,请参阅https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance


假设有一个动物园.

abstract class Animal{}
abstract class Bird : Animal{}
abstract class Fish : Animal{}
class Dove : Bird{}
class Shark : Fish{}
Run Code Online (Sandbox Code Playgroud)

动物园正在重新安置,所以它的动物需要从旧动物园搬到新动物园.

不变性

在我们移动它们之前,我们需要将动物放入不同的容器中.容器都做同样的操作:将动物放入其中或从中取出动物.

interface IContainer<T> where T : Animal
{
    void Put(T t);
    T Get(int id);
}
Run Code Online (Sandbox Code Playgroud)

显然对于鱼我们需要一个坦克:

class FishTank<T> : IContainer<T> where T : Fish
{
    public void Put(T t){}
    public T Get(int id){return default(T);}
}
Run Code Online (Sandbox Code Playgroud)

所以鱼可以放入并从水箱中出来(希望还活着):

IContainer<Fish> fishTank = new FishTank<Fish>(); //Invariance, the two types have to be the same
fishTank.Put(new Shark());          
var fish = fishTank.Get(8);
Run Code Online (Sandbox Code Playgroud)

假设我们被允许改变它IContainer<Animal>,那么你可能会意外地将一只鸽子放入坦克,这将发生不经意的悲剧.

IContainer<Animal> fishTank = new FishTank<Fish>(); //Wrong, some animal can be killed
fishTank.Put(new Shark());
fishTank.Put(new Dove()); //Dove will be killed
Run Code Online (Sandbox Code Playgroud)

逆变

为了提高效率,动物园管理团队决定将加载和卸载过程分开(管理总是这样做).所以我们有两个独立的操作,一个只用于加载,另一个用于卸载.

interface ILoad<in T> where T : Animal
{
    void Put(T t);
}
Run Code Online (Sandbox Code Playgroud)

然后我们有一个鸟笼:

class BirdCage<T> : ILoad<T> where T : Bird
{
    public void Put(T t)
    {
    }
}

ILoad<Bird> normalCage = new BirdCage<Bird>();
normalCage.Put(new Dove()); //accepts any type of birds

ILoad<Dove> doveCage = new BirdCage<Bird>();//Contravariance, Bird is less specific then Dove
doveCage.Put(new Dove()); //only accepts doves
Run Code Online (Sandbox Code Playgroud)

协方差

在新的动物园里,我们有一个卸载动物的团队.

interface IUnload<out T> where T : Animal
{
    IEnumerable<T> GetAll();
}

class UnloadTeam<T> : IUnload<T> where T : Animal
{
    public IEnumerable<T> GetAll()
    {
        return Enumerable.Empty<T>();
    }
}

IUnload<Animal> unloadTeam = new UnloadTeam<Bird>();//Covariance, since Bird is more specific then Animal
var animals = unloadTeam.GetAll();
Run Code Online (Sandbox Code Playgroud)

从团队的角度来看,里面的东西并不重要,他们只是从容器中卸下动物.


Pet*_*uck 7

因为您MyAppointment使用具体类型而不是接口声明了您的类.你应该声明如下:

class MyAppointment : Appointment<IAppointmentProperties> {
}
Run Code Online (Sandbox Code Playgroud)

现在转换可以隐式发生.

通过AppointmentEntry<T>约束声明where T: IAppointment<IAppointmentProperties>您正在创建一个合同,其中未指定的类型AppointmentEntry<T> 必须适应声明的任何类型IAppointmentProperties.通过使用具体类声明类型,您违反了该合同(它实现了一种类型IAppointmentProperties但不是任何类型).