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

本文关键字:引用 转换 错误 泛型类型 约束 为什么 | 更新日期: 2023-09-27 18:20:43

我创建了几个用于处理议程预约的接口和通用类:

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; }
}

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

class MyAppointment : Appointment<MyAppointmentProperties>
{
}
// This goes wrong:
class MyAppointmentEntry : AppointmentEntry<MyAppointment>
{
}
class MyAppointmentProperties : AppointmentProperties
{
    public string ExtraInformation { get; set; }
}

错误为:

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>'.

有人能解释一下为什么不起作用吗?

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

让我们简化一下:

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>();

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

现在我已经重写了代码以简化它,它应该很清楚了ICage<IAnimal>是一个笼子,您可以将任何动物放入其中,但Cage<Tiger>只能容纳老虎,因此这一定是非法的

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

cage.Enclose(new Fish());

嘿,你只是把一条鱼放进老虎笼里。

类型系统不允许该转换,因为这样做会违反源类型的功能不能小于目标类型的功能的规则。(这是著名的"利斯科夫替代原理"的一种形式。)

更具体地说,我想说你在滥用仿制药。事实上,你建立的类型关系太复杂了,你无法分析自己,这证明你应该简化整个事情;如果你没有保持所有类型关系的正确性,并且你写了这个东西,那么你的用户肯定也无法保持它的正确性。

Eric已经给出了一个非常好的答案。我只是想借此机会在这里讨论一下不变性协方差反方差

有关定义,请参阅https://learn.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{}

动物园正在搬迁,所以它的动物需要从旧动物园搬到新动物园。

不变性

在我们移动它们之前,我们需要将动物放入不同的容器中。所有的容器都做着相同的操作:把动物放进去或把动物从里面拿出来。

interface IContainer<T> where T : Animal
{
    void Put(T t);
    T Get(int id);
}

显然,对于鱼类,我们需要一个水箱:

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

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

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

假设我们被允许将其更改为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

对比

为了提高效率,动物园管理团队决定将装载和卸载过程分开(管理层总是这样做)。因此,我们有两个单独的操作,一个仅用于加载,另一个用于卸载。

interface ILoad<in T> where T : Animal
{
    void Put(T t);
}

然后我们有一个鸟笼:

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

协方差

在新动物园里,我们有一个卸货队。

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();

从团队的角度来看,里面是什么并不重要,他们只是把动物从集装箱里卸下来。

因为您使用具体类型而不是接口来声明MyAppointment类。您应该声明如下:

class MyAppointment : Appointment<IAppointmentProperties> {
}

现在,转换可以隐式发生。

通过使用约束where T: IAppointment<IAppointmentProperties>声明AppointmentEntry<T>,您正在创建一个契约,其中AppointmentEntry<T>的未指定类型必须容纳使用IAppointmentProperties声明的任何类型。通过用具体类声明类型,您违反了该约定(它实现了IAppointmentProperties类型,但没有实现任何类型)。

如果您从重新定义示例接口,它将起作用

interface ICage<T>

interface ICage<out T>

(请注意out关键字)

那么以下语句是正确的:

ICage<IAnimal> cage = new Cage<Tiger>();

如果其他人也有这样的错误消息:我发现在不同的命名空间中定义了两次相同的接口,并且试图链接在一起的类没有使用相同的接口。