【问题标题】:Collection with inherited template class parameter具有继承模板类参数的集合
【发布时间】:2013-02-02 18:31:53
【问题描述】:

对我来说这样做是完美的:

public class A { }
public class B : A { }

public class C
{
    public List<A> b = new List<B>();
}

List 期望元素属于 A 类,对于 List 也是如此。我知道存在类型不匹配,但从逻辑上讲,编译器允许这种不匹配是完全合理的。为什么不呢?

【问题讨论】:

  • 通常的polymorphism 表示当B 派生自A 时,可以说A b = new B();。但请注意,List&lt;B&gt; not 派生自 List&lt;A&gt;,因此我们不能使用它。因此,您需要的是协方差。但是由于 List&lt;&gt; 既可以读取又可以写入(输出和输入),因此它不可能是协变的。想想这个简单的例子:List&lt;Bear&gt; list1 = new List&lt;Bear&gt;(); List&lt;Animal&gt; list2 = list1 /* would be OK if List&lt;&gt; were covariant */ ; list2.Add(new Zebra()); Add 调用必须失败,否则熊列表中会包含斑马。
  • 尽管我刚才说过,数组确实有这种“疯狂”的协方差。因此你可以说A[] bArray = new B[100];。之后bArray[0] = new A(); 可以正常编译,但会破坏运行时(异常)。

标签: c# templates inheritance


【解决方案1】:

List&lt;B&gt; 不能分配给 List&lt;A&gt; 是有充分理由的。

让我们假设不匹配是允许的,让我们想象一下你的类C中的以下假想方法:

public void DoSomething()
{
    b.Add(new A()); // (1)
    List<B> tmp = (List<B>)b; // (2)
    foreach (B item in tmp) { // (3)
        // ...
    }
}
  • 第 (1) 行有效,因为 b 被键入为 A 项目的列表,所以很自然,我们可以向 b 添加一个新的 A 实例。
  • 第 (2) 行有效,因为b 引用的实例实际上是List&lt;B&gt; 的实例,所以强制转换是有效的。
  • 第 (3) 行将崩溃,因为 b 包含不属于 B 类型的项目。 tmpList&lt;B&gt; 类型的事实保证列表中的所有项目都是 B 类型,但如果允许将 List&lt;B&gt; 分配给 List&lt;A&gt;,则不再是这种情况。

因此,编译器不允许这种不匹配。

【讨论】:

  • @YochaiTimmer:即使在 .NET 4.5 中,List&lt;T&gt; class 的通用参数也不是协变或逆变的,正如我在回答中概述的确切原因。
  • @O.R.Mapper 类不存在协方差/逆变。
  • @Antonijn:啊,谢谢,对,这是一个额外的障碍。不过,这是一个任意限制;我认为通过说明如果将List&lt;B&gt; 分配给List&lt;A&gt; 会发生什么,我认为为什么在List&lt;T&gt; 的特定情况下没有意义更容易理解。
【解决方案2】:

您期望 List&lt;T&gt;T 中是协变的,但事实并非如此。

在 C# 中,一些接口 是协变的,但类不是。上面有AB,可以这么说

IEnumerable<A> b = new List<B>();

IReadOnlyList<A> b = new List<B>();

因为有问题的接口是协变的,即像 in 一样用“out”声明

public interface IEnumerable<out T> ...

public interface IReadOnlyLies<out T> ...

【讨论】:

    【解决方案3】:

    因为如果列表存储在List&lt;A&gt; 变量中,程序希望能够将A 类型的对象放入该变量中。将List&lt;B&gt; 对象存储在List&lt;A&gt; 变量中会阻止您将A 类型的对象放入明确声明能够保存A 类型的列表中。

    即:

    List<A> b = new List<B>()
    // compiler knows list should be of type A, so it expects this to work:
    b.add(new A());
    

    但是,如果您可以将 List&lt;B&gt; 分配给 List&lt;A&gt; 变量,则会产生类型错误,即使编译器知道变量 b 的类型为 List&lt;A&gt;所以应该能够持有A 类型的对象。

    相反,您只需使用new List&lt;A&gt; 并向其中添加B 类型的元素,这是允许的,或者您将变量的类型更改为List&lt;B&gt;

    【讨论】:

      【解决方案4】:

      这取决于通用容器是否设置了 covariantcontravariant 通用参数。

      您可以查看 MSDN 以了解它是如何声明的。

      【讨论】:

        【解决方案5】:

        您无法使用类来实现这一点。您可以使用接口(查找协变和逆变),但不能在使用List/IList 时使用。想象以下场景:

        public class MyClass<T> 
        {
            T GetT() { /* Blah blah */ }
            void SetT(T value) { /* Blah Blah */ }
        }
        

        这样写:

        MyClass<object> example = new MyClass<string>();
        

        这适用于第一种方法; example 应该返回一个objectMyClass&lt;string&gt; 返回一个字符串,由于多态性,这是合法的。

        第二种方法问题更大。假设你后来写了这个:

        example.SetT(new object());
        

        这是非法的,因为 MyClass&lt;string&gt; 期望 string 但得到 object。不好。

        您可以使用前面提到的协变和逆变来使接口工作。你可以这样写一个界面:

        public interface Covariant<out T>
        {
            T FunctionReturningT();
        }
        
        public class MyInterfaceImplementation<T> : Covariant<T>
        {
            public T FunctionReturningT() { /* Blah Blah */ }
        }
        

        使界面协变,意思是写是合法的:

        Covariant<object> example = new MyInterfaceImplementation<string>();
        

        而您也可以编写以下内容:

        public interface Contravariant<in T>
        {
            void FunctionAskingForT(T value);
        }
        
        public class MyInterfaceImplementation<T> : Contravariant<T>
        {
            public void FunctionAskingForT(T value) { /* Blah Blah */ }
        }
        

        您刚刚使该接口逆变,这意味着这样写是合法的:

        Contravariant<string> example = new MyInterfaceImplementation<object>();
        

        【讨论】:

          猜你喜欢
          • 2012-05-12
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-04-25
          • 1970-01-01
          • 2014-07-15
          • 2016-04-19
          • 1970-01-01
          相关资源
          最近更新 更多