【问题标题】:C# Why an explicit cast isn't needed here?C# 为什么这里不需要显式强制转换?
【发布时间】:2018-04-01 11:07:40
【问题描述】:

如果没有显式强制转换,这段代码如何工作?

static void Main()
{
    IList<Dog> dogs = new List<Dog>();
    IEnumerable<Animal> animals = dogs;
}

我说的是这一行:

IEnumerable<animal> animals = dogs;

您可以在此处看到,我可以在没有显式转换的情况下传递变量 dogs。但这怎么可能?为什么编译器允许我这样做?我不应该先做这样的演员吗:

IEnumerable<Animal> animals = (List<Dog>) dogs;

代码可以在显式转换和不显式转换的情况下工作,但我不明白为什么它允许我在没有显式转换的情况下将狗分配给动物引用变量。

【问题讨论】:

  • 请参阅this 关于 IEnumerable 和协方差的回答。
  • 即使在 C# 4 中引入它仍然是当今 stackoverflow 上关于 C# 的最常见问题之一。
  • IEnumerable 是一个非常简单的接口,你不能用它来破坏列表。无法将猫添加到该狗列表中或让那些狗说喵。所以转换是安全的,你所能做的就是从列表中获取狗并让它们看起来像一个 Animal 是可以的,因为它是 Dog 的基类。它是框架中为数不多的允许这样做的接口之一。

标签: c# .net casting


【解决方案1】:

IEnumerable&lt;T&gt; 是一个covariant interface。这意味着只要存在从T1T2隐式引用转换身份转换,就存在从IEnumerable&lt;T1&gt;IEnumerable&lt;T2&gt; 的隐式身份转换。有关此术语的更多详细信息,请参阅documentation on conversions

如果T2T1 的基类(直接或间接),或者是T1 实现的接口,则最常见的情况。如果T1T2 是同一类型,或者T2dynamic,也是这种情况。

在您的情况下,您使用的是IList&lt;Dog&gt;,而IList&lt;T&gt; 实现了IEnumerable&lt;T&gt;。这意味着任何IList&lt;Dog&gt; 也是IEnumerable&lt;Dog&gt;,因此存在到IEnumerable&lt;Animal&gt; 的隐式引用转换。

需要注意的一些重要事项:

  • 只有接口和委托类型可以是协变或逆变的。例如,List&lt;T&gt; 不是协变的,也不可能是协变的。例如,你不能写:

    // Invalid
    List<Animal> animals = new List<Dog>();
    
  • 并非所有接口都是协变或协变的。例如,IList&lt;T&gt; 不是协变的,所以这也是无效的:

    // Invalid
    IList<Animal> animals = new List<Dog>();
    
  • Variance 不适用于值类型,因此这是无效的:

    // Invalid
    IEnumerable<object> objects = new List<int>();
    

仅在安全的情况下才允许使用泛型变量:

  • 协方差依赖于类型参数仅出现在任何签名中的“out”位置,即值可以从实现中出来,但永远不会被接受。
  • 逆变依赖于仅出现在任何签名中的“输入”位置的类型参数,即值可以传递到实现中,但永远不会返回

当签名接受一个本身是逆变的参数时,它会变得有点混乱——即使一个参数通常是一个“输入”位置,它在某种程度上被逆变反转了。例如:

public interface Covariant<out T>
{
    // This is valid, because T is in an output position here as
    // Action<T> is contravariant in T
    void Method(Action<T> input);
}

请务必阅读链接文档以获取更多信息!

【讨论】:

  • 这是一个很好的答案,但您对规则的初步描述略微低估了情况。例如,IEnumerable&lt;Dog&gt; --> IEnumerable&lt;dynamic&gt; 即使dynamic 不是Dog 的基类。或者,IEnumerable&lt;Action&lt;Animal&gt;&gt; --> IEnumerable&lt;Action&lt;Dog&gt;&gt; 即使Action&lt;Dog&gt; 不是Action&lt;Animal&gt; 的基本情况
  • 您可以通过说规则是从 T1 到 T2 必须有一个隐式引用转换或身份转换来使您的解释更简单、更准确,正如您注意到的那样, T1 和 T2 必须是引用类型。
  • @EricLippert:谢谢,已修复。我将基类和实现的接口作为一个常见示例,否则“隐式引用转换”可能只会引起另一层混乱。我会尝试查找相关文档,或在必要时直接参考规范。
  • 好的,我已阅读 Microsoft Docs 中的协方差。我玩过它,这很令人困惑,但我理解了一般原则。它与委托的协变和逆变基本相同,但添加了通用接口。除了使用集合之外,我是否需要将此概念与通用接口一起使用?
  • @BoSsYyY:我真的无法预测你是否会这样做。我想说这是迄今为止最最常见使用接口变体的方法,但我敢肯定还有其他接口使用它——无论是在框架、您可能使用的第三方库还是其他应用内的代码。
【解决方案2】:

这是因为IEnumerable&lt;T&gt; 接口是协变的。更多信息在这里: https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance

【讨论】:

    【解决方案3】:

    因为IList< T> 继承自IEnumerable&lt;T&gt;。我猜你的Dog 类也继承自Animal。分配给父类时不需要强制转换。

    编辑:这是可能的,因为IEnumerable 类是covariant

    【讨论】:

    • 这不是真的。
    • 但是从IList&lt;Dog&gt;ICollection&lt;Animal&gt; 不起作用,即使IList&lt;T&gt; 实现了ICollection&lt;T&gt;
    • 好吧,是的,我不知道协方差。检查@MistyK的答案
    【解决方案4】:

    尽管您没有显示它,但我认为 DogAnimal 的子类型。

    您始终可以将子类型分配给其父类的实例。

    所以List &lt;Dog&gt; 可以包含所有类型的狗,因此如果你有一个Poodle 的类,继承自Dog,你可以将它放入狗列表中。

    所以IEumerable&lt;Animal&gt; 可以包含DogCatPoodle 实例。

    在这种情况下,它恰好只包含DogDog 的子类。

    但是你不能这样做:

    var animals=new List &lt;Animal&gt;();

    Ienumerable&lt;Dog&gt; dogs= animals;

    即使您只将Dog 实例插入动物。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-06
      • 1970-01-01
      • 2021-03-19
      • 2012-11-16
      相关资源
      最近更新 更多