【问题标题】:What violations does using a covariant type in a contravariant position here enable?在逆变位置使用协变类型会导致哪些违规行为?
【发布时间】:2016-07-20 16:59:30
【问题描述】:

考虑具有协变类型T 的接口。我正在研究使用T 的该接口的所有派生类中的属性都是只读的,如果是泛型类则为协变的情况。假设这个接口定义了一个使用T 作为参数类型的方法。它允许哪些违规行为?

例如,考虑:

interface ICov<out T> {
  void maybe_safe_set(T v);
}
class ImplCov<T> : ICov<T> {
  public readonly T a;
  public readonly IEnumerable<T> b;
  public readonly IEnumerable<IEnumerable<T>> c;
  // public readonly IList<T> d; // but not this

  public void maybe_safe_set(T v) {
    // do things that can't modify state: the type of our 
    // readonly, covariant IEnumerable members can't be modified
  }
}

在 C# 中,我收到错误:

无效方差:类型参数“T”必须在“ConsoleApplication.ICov.maybe_safe_set(T)”上逆变有效。 'T' 是协变的。

这并不奇怪,因为T 处于逆变位置。但是,我想不出这里可能发生的违规行为。

【问题讨论】:

  • 协变意味着类型只从接口消费,从不传递给它。这样想,out 关键字意味着该类型应该只从接口中出来,但是您将它作为maybe_safe_set 方法的参数输入。 T 可以改为定义为逆变式。接口是一个契约,它不知道给定的实现可能会做什么。
  • public static class C&lt;T&gt; { public static T Value }public void maybe_safe_set(T v) { C&lt;T&gt;.Value = v }((ICov&lt;object&gt;)new ImplCov&lt;string&gt;()).maybe_safe_set(new object())。现在您尝试将object 分配给C&lt;string&gt;.Value 类型为string
  • 如果允许,您可以将 ImpleConv&lt;string&gt; 转换为 ICov&lt;object&gt;,然后您可以调用 maybe_safe_set 并传递不是 string 的东西,然后您将有一个运行时类型错误。所以实际上它与实现类的实现细节没有任何关系。
  • 你尝试的时候发生了什么?简单地尝试它不能回答你的问题吗?这里的评论解释了为什么将T 的值传入 maybe_safe_set() 方法对于out T 类型参数是非法的。协方差/逆变方差的全部意义在于,当您滥用泛型类型时,编译器会发出错误。因此,如果您遇到错误,则说明您在滥用它。如果“举手”是指“产生编译器错误”,您应该这样说,提供错误的确切文本,并解释您对错误的不理解。
  • @juharr 这是一个很好的观点。如果没有静态类,我也想不出可以作用于特定类型的实现,但 PetSerAl 的评论也是一个完美的例子。

标签: c# covariance


【解决方案1】:

你:

interface ICov<out T>    // BAD!
{
  void maybe_safe_set(T v);
}

问题来了。像往常一样,我们有:

class Animal { /* ... */ }
class Dog : Animal { public void Woof() { }  /* ... */ }
class Cat : Animal { /* ... */ }

然后考虑:

class Impl : ICov<Dog>
{
  public void maybe_safe_set(Dog v)
  {
    v.Woof(); // our 'Dog' v can really bark
  }
}

编译得很好。

然后这个:

var impl1 = new Impl();
ICov<Dog> impl2 = impl1;    // OK, implements that
ICov<Animal> impl3 = impl2; // OK, you claim interface is covariant ('out')! 

var badAnimal = new Cat();
impl3.maybe_safe_set(badAnimal);  // ICov<Animal> takes in Animal, right?

// oh my God, you mad a 'Cat' bark!

当人们询问协变和逆变时,总是同样的例子。

【讨论】:

  • 我认为我有一个很好的例子。但是你不能打败猫叫。那个将永远被归档以备将来抄袭。
  • “当人们询问协变和逆变时,这总是同一个例子”——是的,总是这样。这暗示了一个问题:为什么要再次回答这个问题,而不是将其标记为具有相同示例的那些问题之一的重复?
【解决方案2】:

我不了解其他人,但我也觉得这些东西很混乱。为了理解它,我需要创建一个示例来查看编译器正在防止哪些违规行为。

我们先看看没有接口中的方法。这都是有效的:

interface ICov<out T> {}

public class BaseClass { }
public class InheritedClass: BaseClass { } 

ICov<BaseClass> x = new MyCov<InheritedClass>();

因为T 是协变的,所以ICov&lt;BaseClass&gt; 类型的变量可以引用MyCov&lt;T&gt; 的实例,其中T 派生自BaseClass

现在,如果我们可以在接口中添加一个对 T 类型进行操作的方法(编译器阻止了什么?)

interface ICov<out T>
{
    List<T> ListOfT { get; set; }
    // ^^^ compiler doesn't like this.
}

public class BaseClass { }
public class InheritedClass: BaseClass { } 

public class MyCov<T> : ICov<T> {
    public List<T> ListOfT { get; set; } = new List<T>();
}

现在我们可以确切地看到这会产生的问题:

var usesInheritedClass = new MyCov<InheritedClass>();

//This is legal, because the type parameter in ICov is covariant
ICov<BaseClass> usesBaseClass = usesInheritedClass;

//Here's where it goes bad.
usesBaseClass.ListOfT.Add(new BaseClass());

usesInheritedClass 包含InheritedClass 的列表。但是因为我可以将其转换为ICov&lt;BaseClass&gt;,所以现在我可以将BaseClass 添加到InheritedClass 的列表中,这是行不通的。

因此,尽管编译器错误令人困惑,但它在正确的位置。它不会尝试整理和修复我的下游错误。它通过防止这些类型可能混淆的情况来完全防止这些下游错误。

【讨论】:

  • 实际上,我将readonlyIEnumerable 带入了这个问题,以避免这种确切的违规行为!我认为状态是问题所在,所以我试图使其尽可能不可变,但正如 Jeppe 指出的那样,类型化方法仍然存在问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-01-08
  • 2015-04-11
  • 2018-01-16
  • 2013-11-29
  • 2011-12-27
  • 2020-01-25
  • 1970-01-01
相关资源
最近更新 更多