【问题标题】:Type safety in CoVariance and ContraVarianceCoVariance 和 ContraVariance 中的类型安全
【发布时间】:2015-05-19 03:02:20
【问题描述】:

我正在阅读 Jon Skeet 的深度 C#。虽然我已经理解了 CoVariance 和 ContraVariance 的概念,但是我无法理解这一行:

好吧,当 SomeType 只描述以下操作时,协方差是安全的 返回类型参数——当 SomeType 时逆变是安全的 只描述接受类型参数的操作。

有人可以用一个例子来解释一下,为什么两者在一个方向上都是安全的,而在另一个方向上不安全?

更新问题:

从给出的答案中我仍然不明白。我将尝试使用书中相同的示例来解释我的担忧 - C# In Depth

它使用以下类层次结构进行解释:

协方差是:尝试从IEnumerable<Circle> 转换为IEnumerable<IShape>,但有人提到,这种转换只有在我们从某个方法返回时执行时才是类型安全的,而当我们将其传递为时不是类型安全的一个 IN 参数。

IEnumerable<IShape> GetShapes()
{
    IEnumerable<Circle> circles = GetEnumerableOfCircles();
    return circles; // Conversion from IEnumerable<Circle> to IEnumerable<IShape> - COVARIANCE
}

void SomeMethod()
{
    IEnumerable<Circle> circles = GetEnumerableOfCircles();
    DoSomethingWithShapes(circles); // Conversion from IEnumerable<Circle> to IEnumerable<IShape> - COVARIANCE
}

void DoSomethingWithShapes(IEnumerable<IShape> shapes) // Why this COVARIANCE is type unsafe??
{
    // do something with Shapes
}

CONTRA VARIANCE 是:尝试从IEnumerable&lt;IShape&gt; 转换为IEnumerable&lt;Circle&gt;,仅在将其作为IN 参数发送时才提到它是类型安全的。

IEnumerable<Circle> GetShapes()
{
    IEnumerable<IShape> shapes = GetEnumerableOfIShapes();
    return shapes; // Conversion from IEnumerable<IShape> to IEnumerable<Circle> - Contra-Variance
    // Why this Contra-Variance is type unsafe??
}

void SomeMethod()
{
    IEnumerable<IShape> shapes = GetEnumerableOfIShapes();
    DoSomethingWithCircles(shapes); // Conversion from IEnumerable<IShape> to IEnumerable<Circle> - Contra-Variance
}

void DoSomethingWithCircles(IEnumerable<Circle> circles) 
{
    // do something with Circles
}

【问题讨论】:

  • 您能否具体说明您对所提供答案的不理解之处,以便可以改进它们?它们相当详细,因此您对什么感到困惑并不是很明显。
  • 这两个问题在试图解释同一个概念的方式上是相关的。但它们只是相关的,并不完全相同。

标签: c#


【解决方案1】:

协方差

当 SomeType 只描述返回类型参数的操作时,协方差是安全的

IEnumerable&lt;out T&gt; 接口可能是最常见的协方差示例。它是安全的,因为它只返回 T 类型的值(嗯,特别是 IEnumerator&lt;out T&gt; 但不接受任何 T 对象作为参数。

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

这是因为IEnumerator&lt;T&gt; 也是协变的并且只返回T

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
    T Current { get; }
}

如果您有一个名为 Base 的基类和一个名为 Derived 的派生类,那么您可以这样做:

IEnumerable<Derived> derivedItems = Something();
IEnumerable<Base> baseItems = derivedItems;

这是可行的,因为derivedItems 中的每个项目也是Base 的一个实例,因此完全可以按照我们刚才的方式分配它。但是,我们不能以其他方式分配:

IEnumerable<Base> baseItems = Something();
IEnumerable<Derived> derivedItems = baseItems; // No good!

这是不安全的,因为不能保证Base 的每个实例也是Derived 的一个实例。

逆变

当 SomeType 只描述接受类型参数的操作时,逆变是安全的

Action&lt;in T&gt; 委托是逆变的一个很好的例子。

public delegate void Action<in T>(T obj);

它是安全的,因为它只接受T 作为参数,但不返回T

逆变让你做这样的事情:

Action<Base> baseAction = b => b.DoSomething()
Action<Derived> derivedAction = baseAction;

Derived d = new Derived();
// These 2 lines do the same thing:
baseAction(d);
derivedAction(d);

这是可行的,因为将Derived 的实例传递给baseAction 是完全可以接受的。但是,反过来就不行了:

Action<Derived> derivedAction = d => d.DoSomething()
Action<Base> baseAction = derivedAction; // No good!

Base b = new Base();
baseAction(b);    // This is OK.
derivedAction(b); // This does not work because b may not be an instance of Derived!

这是不安全的,因为不能保证Base 的实例也将是Derived 的实例。

【讨论】:

    【解决方案2】:

    假设您创建了一个ILogger&lt;in T&gt; 接口,该接口知道如何记录T 的详细信息。假设您有一个Request 类和一个ExpeditedRequest 子类。当然ILogger&lt;Request&gt; 应该可以转换为ILogger&lt;ExpeditedRequest&gt;。毕竟,它可以记录任何请求。

    Interface ILogger<in T> where T: Request {
         void Log(T arg);
    }
    

    现在想象另一个接口IRequestProducer&lt;out T&gt; 在某个队列中获取下一个请求。您的系统中有不同的请求来源,当然,其中一些仍然可以有不同的子类。在这种情况下,我们不能依赖将IRequestProducer&lt;Request&gt; 转换为IRequestProducer&lt;ExpeditedRequest&gt;,因为它可能会产生非加急请求。但是反向转换会起作用。

    Interface IRequestProducer<T> where T: Request {
        T GetNextRequest();
    }
    

    【讨论】:

      【解决方案3】:

      我从MSDN上看了之后明白了,下面两页:

      https://msdn.microsoft.com/en-us/library/dd469484.aspx
      https://msdn.microsoft.com/en-us/library/dd469487.aspx

      实际上,当我到达本书的C# 4 部分时,我认为这会变得很清楚,它将用Type ParametersGenerics 解释inout 关键字。现在,我正在阅读本书C# 1 部分中的Limitations of Generics in C#

      我想理解的语句是这样的:

      好吧,当 SomeType 只描述以下操作时,协方差是安全的 返回类型参数——当 SomeType 时逆变是安全的 只描述接受类型参数的操作。

      除了安全之外,也不可能将接口方法写在其他方向,因为编译器会报错,如msdn上面两页所述:

      A type can be declared contravariant in a generic interface or delegate if it is used only as a type of method arguments and not used as a method return type.

      In a generic interface, a type parameter can be declared covariant if it satisfies the following conditions: The type parameter is used only as a return type of interface methods and not used as a type of method arguments.

      现在有两点让我对这个说法感到困惑:

      首先,我误解了语句本身——我认为只有当Inteface&lt;T&gt; 本身的实例从某个方法返回而不是作为某个方法的输入传递时,Covairance 才是安全的。但是,它与类型参数T 和接口方法有关。对ContraVariance 也是如此。

      第二,现在当我理解了这句话的意思时——它是关于向/从接口方法传递/返回泛型类型参数T。我想知道为什么只有在 Covariance 中的接口方法返回 T 时它才是类型安全的,以及为什么只有在将 T 作为输入传递给 ContraVariance 中的接口方法时它才是类型安全的。

      为什么只有在 CoVariance 中返回 T 时它才是类型安全的:

      interface IBase<out T>
      {
          T Return_T(); // Valid and Type Safe
          void Accept_T(T input) // Invalid and Type UnSafe
      }
      class Sample<T> : IBase<T> { }
      class BaseClass {}
      class DerivedClass : BaseClass
      {}
      IBase<BaseClass> ibase = new Sample<BaseClass>();
      IBase<DerivedClass> iderived = new Sample<DerivedClass>();
      
      ibase = iderived; // Can be assinged because `T` is Covariant
      
      BaseClass b = new BaseClass();
      DerivedClass d = new DerivedClass();
      
      ibase.Return_T(); //  At runtime, this will return `DerivedClass` which can be assinged to variable of both base and derived class and is type safe
      ibase.Accept_T(b); // The compiler will accept this statement, because at compile time, it accepts an instance of `BaseClass`, but at runtime, it actually needs an instance of `DerivedClass`. So, we are eventually assigning an instance of `BaseClass` to `DerivedClass` which is type unsafe.
      

      为什么只有在将 T 作为输入参数传递给 ContraVariance 时它才是类型安全的:

      interface IBase<in T>
      {
          T Return_T(); // Invalid and Type UnSafe
          void Accept_T(T input) // Valid and Type Safe
      }
      class Sample<T> : IBase<T> { }
      class BaseClass {}
      class DerivedClass : BaseClass
      {}
      IBase<BaseClass> ibase = new Sample<BaseClass>();
      IBase<DerivedClass> iderived = new Sample<DerivedClass>();
      
      iderived = ibase; // Can be assinged because `T` is Contravariant
      
      BaseClass b = new BaseClass();
      DerivedClass d = new DerivedClass();
      
      iderived.Accept_T(d); // This is Type Safe, because both at compile time and runtime, either instance of `DerivedClass` can be assinged to `BaseClass` or instance of `BaseClass` can be assinged to `BaseClass`
      
      DerivedClass d2 = iderived.Return_T(); // This is type unsafe, because this statement is valid at compile time, but at runtime, this will return an instance of `BaseClass` which is getting assinged to `DerivedClass`
      

      【讨论】:

      • 这就是我理解的方式,所以我选择了我的答案。为什么要投反对票?
      • 我看不出有任何理由对此投反对票;这对我来说似乎是一个很好的答案。
      猜你喜欢
      • 2023-03-15
      • 1970-01-01
      • 1970-01-01
      • 2016-10-15
      • 2011-09-13
      • 1970-01-01
      • 2013-05-01
      • 1970-01-01
      • 2013-05-17
      相关资源
      最近更新 更多