【问题标题】:Why does a generic type constraint result in a no implicit reference conversion error?为什么泛型类型约束会导致无隐式引用转换错误?
【发布时间】:2013-06-30 08:49:08
【问题描述】:

我创建了几个接口和通用类来处理日程安排:

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&lt;IAppointmentProperties&gt; 的约束时,编译器在使用Appointment&lt;AppointmentProperties&gt; 的类时会出错:

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&lt;T&gt;'. There is no implicit reference conversion from 'Example.MyAppointment' to 'Example.IAppointment&lt;Example.IAppointmentProperties&gt;'.

谁能解释为什么这不起作用?

【问题讨论】:

  • 这很奇怪。 但是:这是对泛型的公然过度使用。我几乎看不懂(我认为是)非常非常简化的代码。

标签: c# generics implicit-conversion type-constraints


【解决方案1】:

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

class MyAppointment : Appointment<IAppointmentProperties> {
}

现在可以隐式进行转换。

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

【讨论】:

  • 是的,你是对的。但是我想使用具体类型MyAppointmentProperties 声明MyAppointment 类(直到现在它不在示例中,我很抱歉)来扩展IAppointmentProperties 的属性。是否可以指定允许这样做的合同?
  • 你可以用两个显式的泛型类型参数来声明而不是嵌套它们:class AppointmentEntry&lt;TAppointment, TProperties&gt; : IAppointmentEntry&lt;TAppointment&gt; where TAppointment: IAppointment&lt;TProperties&gt; 但是,我警告你(和其他人一样)不要过度约束你的类型层次结构,除非有非常令人信服的理由所以。
【解决方案2】:

让我们简化一下:

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&lt;IAnimal&gt; 是一个笼子,您可以在其中放置任何动物,但Cage&lt;Tiger&gt; 只能关住老虎,所以这一定是非法的。强>

如果它不违法,那么你可以这样做:

cage.Enclose(new Fish());

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

类型系统不允许这种转换,因为这样做会违反源类型的能力不得小于目标类型的能力的规则。 (这是著名的“里氏替换原则”的一种形式。)

更具体地说,我会说您在滥用泛型。你建立的类型关系太复杂以至于你无法分析自己,这一事实证明你应该简化整个事情。如果您没有保持所有类型关系正确并且您编写了内容,那么您的用户肯定也无法保持正确。

【讨论】:

  • 这是一个非常好的/简单的解释。我记得深入阅读 Jon Skeet 的书 c#,它很好地解释了 c# 泛型协变逆变。强烈推荐。
  • “你只是把一条鱼放进了老虎笼子里。”是虎鱼吗? :-)
  • @StephenZeng:我是那本书的编辑,确实值得推荐!
  • @EricLippert 我深度预购了新版本的 C#。
  • 我遇到了同样的问题。你的建议——如果你自己分析起来太复杂,那么你的用户肯定也无法直截了当——是黄金。
【解决方案3】:

如果您从以下位置重新定义示例界面,它将起作用:

interface ICage<T>

interface ICage<out T>

(请注意out关键字)

那么下面的说法是正确的:

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

【讨论】:

    【解决方案4】:

    Eric 已经给出了很好的答案。只是想借此机会在这里谈谈不变性协变逆变性

    有关定义,请参阅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{}
    

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

    不变性

    在我们移动它们之前,我们需要将动物放入不同的容器中。这些容器都执行相同的操作:将动物放入其中或从中取出动物。

    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&lt;Animal&gt;,那么您可能会不小心将一只鸽子放入水箱中,这样就会发生悲剧。

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

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

    【讨论】:

    • 这个答案是现代编程中的宝贵一课!
    【解决方案5】:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多