泛型中 Func 的协变和逆变

Covariance and Contravariance with Func in generics

我需要有关泛型和委托方差的更多信息。以下代码片段无法编译:

Error CS1961 Invalid variance: The type parameter 'TIn' must be covariantly valid on 'Test.F(Func)'. 'TIn' is contravariant.

public interface Test<in TIn, out TOut>
{
    TOut F (Func<TIn, TOut> transform);
}

.netFunc定义如下:

public delegate TResult Func<in T, out TResult> (T arg);

为什么编译器抱怨 TIn 是逆变的,而 TOut 是协变的,而 Func 期望完全相同的方差?

编辑

我的主要限制是我希望我的测试接口具有 TOut 作为协变,以便像这样使用它:

public Test<SomeClass, ISomeInterface> GetSomething ()
{
    return new TestClass<SomeClass, AnotherClass> ();
}

鉴于 public class AnotherClass : ISomeInterface.

从接口定义中删除 inout 关键字:

public interface Test<TIn, TOut>{
    TOut F (Func<TIn, TOut> transform);
}

去掉进出关键词:

public interface Test<TIn, TOut>
{
    TOut F (Func<TIn, TOut> transform);
}

您可以在这里阅读它们的含义:

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/in-generic-modifier

如果一个类型仅用作方法参数的类型而不用作方法,则可以在泛型接口或委托中声明它是逆变的return类型

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-generic-modifier

类型参数仅用作return类型的接口方法,不用作方法参数的类型。

变体是关于能够用比最初声明的派生类型更多或更少的派生类型替换类型参数。例如,IEnumerable<T> 对于 T 是协变的,这意味着如果您从对 IEnumerable<U> 对象的引用开始,您可以将该引用分配给类型为 IEnumerable<V> 的变量,其中V 可从 U 分配(例如 U 继承 V)。这是可行的,因为任何尝试使用 IEnumerable<V> 的代码都希望仅接收 V 的值,并且由于 V 可从 U 分配,因此仅接收 [=15] 的值=] 也是有效的。

对于 T 这样的协变参数,您必须分配目标类型与 T 相同的类型,或者可从 T 分配。对于逆变参数,它必须走另一条路。目标类型必须与类型参数相同或可分配给类型参数。

那么,您尝试编写的代码在这方面如何工作?

当您声明 Test<in TIn, out TOut> 时,您承诺将该接口 Test<TIn, TOut> 的实例分配给类型为 Test<U, V> 的任何目的地都是有效的,其中 U 可以分配给 TIn 并且 TOut 可以分配给 V(或者它们是相同的,当然)。

同时,让我们考虑一下您的 transform 代表的期望。 Func<T, TResult> 类型的变体要求,如果你想把那个值赋给别的东西,它也满足变体规则。即,目的地 Func<U, V> 必须具有可从 T 分配的 U 和可从 V 分配的 TResult。这确保了期望接收 U 值的委托目标方法将获得其中之一,并且可以接受由该方法编辑的具有类型 V 的值 return通过接收它的代码。

重要的是,您的接口方法 F() 是接收方法! 接口声明承诺 TOut 仅用作接口成员。但是通过使用 transform 委托,方法 F() 接收 TOut,使 输入 方法。同样,允许方法 F()TIn 的值传递给 transform 委托,使其成为接口实现的 output,甚至尽管您已承诺 TIn 仅用作输入。

换句话说,每一层调用都反转了方差的意义。接口中的成员必须使用协变类型参数作为输出,逆变参数作为输入。但是当这些参数在传递给接口成员或从接口成员return编辑的委托类型中使用时,这些参数在意义上会发生逆转,并且必须遵守这方面的差异。

一个具体的例子:

假设我们有一个接口的实现,Test<object, string>。如果编译器允许您的声明,您将被允许将该实现的值 Test<object, string> 分配给类型为 Test<string, object> 的变量。也就是说,最初的实现承诺允许输入任何具有类型 object 和 return 的东西,只有具有类型 string 的值。声明为 Test<string, object> 的代码使用它是安全的,因为它会将 string 对象传递给需要 objects 值的实现(string 是一个 object ),并且它将从 returns string 值的实现中接收类型为 object 的值(同样,string 是一个 object,因此也是安全的).

但是您的接口实现期望代码传递 Func<object, string> 类型的委托。如果允许您(如上所述)将接口实现视为 Test<string, object>,那么使用重铸实现的代码将能够将 Func<string, object> 的委托传递给方法 F().实现中的方法 F() 允许将类型 object 的任何值传递给委托,但该委托的类型 Func<string, object> 只期望具有类型 [=55] 的值=] 传递给它。如果 F() 传递其他内容,例如只是一个普通的旧 new object(),委托实例将无法使用它。期待 string!

因此,事实上,编译器正在做它应该做的事情:它阻止您编写类型不安全的代码。如声明的那样,如果您被允许以变体方式使用该接口,那么您实际上可以编写在编译时允许但可能在 运行 时中断的代码。这与泛型的全部要点完全相反:能够在编译时确定代码是类型安全的!

现在,如何解决困境。不幸的是,您的问题中没有足够的上下文来了解正确的方法是什么。您可能只需要放弃方差。通常,实际上不需要使类型变体;在某些情况下这很方便,但不是必需的。如果是这样,那就不要让接口的参数变体了。

或者,您可能真的想要变体并且认为以变体方式使用界面是安全的。这更难解决,因为您的基本假设是不正确的,您需要以其他方式实现代码。如果您可以反转 Func<T, TResult> 中的参数,代码将编译。 IE。使方法 F(Func<TOut, TIn> transform)。但是你的问题中没有任何内容表明在你的场景中这实际上是可能的。

同样,如果没有更多上下文,就不可能说 "other way" 对您有用。但是,希望现在您已经理解了您现在编写代码的方式中的危险,您可以重新审视导致您进行这种非类型安全接口声明的设计决策,并可以想出一些可行的方法。如果您对此有困难,post 一个新问题提供了更多详细信息,说明为什么您认为这是安全的,您将如何使用该界面,您考虑过哪些替代方案,以及为什么 [=其中 115=] 适合您。

TIn = class 知道如何读取它,并且允许实现将其视为比实际派生更少的类型。您可能会向它传递一个比预期更派生的实例,但这并不重要,因为派生的 class 可以完成基 class 可以做的所有事情。

TOut = 实现知道要生成一个,并且允许实现生成比调用者期望的更派生的类型。同样,没关系——调用者可以毫无问题地将派生程度更高的 class 分配给派生程度更低的变量。

但是——

如果您将 class 传递给 Func<TIn, TOut>,并且您希望 class 能够调用它,那么 class 将必须能够产生一个TIn读取一个TOut。与上面相反。

为什么不能呢?好吧,我已经提到 class 可以将 TIn 视为派生较少的东西。如果它试图用派生较少的参数调用函数,它将不起作用(如果函数期望能够调用 string.Length 但 class 传递给它一个 object?)。此外,如果它试图将函数的结果读取为更派生的东西,那也会失败。

您可以通过消除方差来消除问题——去掉 inout 关键字——这将使 class 无法替代 less/more 派生类型(这称为 "invariance"),但将允许您读取和写入类型。

I need more information about variance in generics and delegates.

我写了一系列关于此功能的博客文章。尽管其中一些已经过时——因为它是在设计定稿之前编写的——但那里有很多有用的信息。特别是如果您需要方差有效性的正式定义,您应该仔细阅读:

https://blogs.msdn.microsoft.com/ericlippert/2009/12/03/exact-rules-for-variance-validity/

有关相关主题,请参阅我在 MSDN 和 WordPress 博客上的其他文章。


Why the compiler complains about TIn being contravariant and TOut - covariant while the Func expects exactly the same variance?

让我们稍微重写一下代码看看:

public delegate R F<in T, out R> (T arg);
public interface I<in A, out B>{
  B M(F<A, B> f);
}

编译器必须证明这是安全,但事实并非如此。

我们可以通过假设它是安全的然后发现它如何被滥用来说明它是不安全的。

假设我们有一个具有明显关系的动物层次结构,例如,哺乳动物是一种动物,长颈鹿是一种哺乳动物,等等。假设您的差异注释是合法的。我们应该可以说:

class C : I<Mammal, Mammal>
{
  public Mammal M(F<Mammal, Mammal> f) {
    return f(new Giraffe());
  }
}

我希望你同意这是一个完全有效的实现。现在我们可以这样做了:

I<Tiger, Animal> i = new C();

C实现了I<Mammal, Mammal>,我们说过第一个可以更具体,第二个可以更通用,所以我们做到了。

现在我们可以这样做了:

Func<Tiger, Animal> f = (Tiger t) => new Lizard();

对于该委托人来说,这是一个完全合法的 lambda,它与以下人的签名相匹配:

i.M(f);

然后会发生什么? C.M 期待一个包含长颈鹿和 returns 哺乳动物的函数,但它被赋予了一个包含老虎和 returns 蜥蜴的函数,所以有人会有一个非常糟糕的天。

显然不能允许这种情况发生,但是沿途的每一步都是合法的。我们必须得出结论,方差本身并不能证明是安全的,事实上,事实并非如此。编译器拒绝这个是对的。

获得正确的差异不仅仅是简单地匹配输入和输出注释。 您必须以不允许此类缺陷存在的方式这样做。

这解释了为什么这是非法的。要解释 如何 它是非法的,编译器必须检查 B M(F<A, B> f); 以下是否成立:

  • B 协变有效。既然声明了"out",就是.
  • F<A, B> 逆变有效。它不是。泛型委托的 "valid contravariantly" 定义的相关部分是:如果第 i 个类型参数被声明为逆变,则 Ti 必须协变有效。 OK。第一个 类型参数 T 被声明为逆变。因此,第一个 类型参数 A 必须 协变有效 。但它 not 协变有效,因为它被声明为逆变的。这就是你得到的错误。同样,B 也不好,因为它必须逆变有效,但 B 是协变的。编译器在这里发现第一个问题后不会继续寻找其他错误;我考虑过,但拒绝了,因为它是一条过于复杂的错误消息。

我还注意到,即使委托不是变体,您仍然会遇到这个问题;在我的反例中,我们没有在任何地方使用 F 在其类型参数中是可变的这一事实。如果我们尝试

会报告类似的错误
public delegate R F<T, R> (T arg);

相反。