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.
1 2 3 4 | public interface Test<in TIn, out TOut> { TOut F (Func<TIn, TOut> transform); } |
.net
1 | public delegate TResult Func<in T, out TResult> (T arg); |
为什么编译器抱怨
编辑
对我来说主要的限制是我希望我的Test界面将TOut作为协变,以便使用它:
1 2 3 4 | public Test<SomeClass, ISomeInterface> GetSomething () { return new TestClass<SomeClass, AnotherClass> (); } |
鉴于
I need more information about variance in generics and delegates.
我写了一系列有关此功能的博客文章。虽然其中一些已经过时 - 因为它是在设计最终确定之前编写的 - 那里有很多好的信息。特别是如果您需要对方差有效性的正式定义,您应该仔细阅读:
有关相关主题,请参阅我在MSDN和WordPress博客上的其他文章。
Why the compiler complains about TIn being contravariant and TOut - covariant while the Func expects exactly the same variance?
让我们稍微重写您的代码并查看:
1 2 3 4 | public delegate R F<in T, out R> (T arg); public interface I<in A, out B>{ B M(F<A, B> f); } |
编译器必须证明这是安全的,但事实并非如此。
我们可以通过假设它是,然后发现它如何被滥用来说明它是不安全的。
假设我们有一个具有明显关系的Animal层次结构,例如,Mammal是Animal,Giraffe是哺乳动物,等等。我们假设你的方差注释是合法的。我们应该可以说:
1 2 3 4 5 6 |
我希望你同意这是一个完全有效的实现。现在我们可以这样做:
1 |
现在我们可以这样做:
1 |
对于这个委托来说,这是一个完全合法的lambda,它匹配以下签名:
1 | i.M(f); |
会发生什么?
显然,这绝不允许发生,但沿途的每一步都是合法的。我们必须得出结论,方差本身并不是安全的,实际上并非如此。编译器拒绝这一点是正确的。
获得正确的方差不仅仅是简单地匹配输入和输出注释。你必须以不允许存在这种缺陷的方式这样做。
这就解释了为什么这是非法的。要解释它是如何非法,编译器必须检查以下是否为
-
B 是协同有效的。因为它被宣布为"out",所以它是。 -
F 违反有效。它不是。泛型委托的"有效逆转"定义的相关部分是:如果第i个类型参数被声明为逆变,则Ti必须是协变有效的。好。第一个类型参数T 被声明为逆变。因此,第一个类型参数A 必须是协变有效的。但它并不是有效的,因为它被宣布为逆变。这就是你得到的错误。类似地,B 也很糟糕,因为它必须是有效的,但B 是协变的。在找到第一个问题后,编译器不会继续查找其他错误;我考虑过它,但拒绝它是一个太复杂的错误信息。
我还注意到,即使代表不是变体,你仍会遇到这个问题;在我的反例中没有任何地方我们使用F在其类型参数中是变体的事实。如果我们尝试,将报告类似的错误
1 | public delegate R F<T, R> (T arg); |
代替。
方差是指能够用比最初声明的更多或更少的派生类型替换类型参数。例如,
对于像
那么,您尝试编写的代码在这方面如何工作?
当您声明
同时,让我们考虑一下
重要的是,您的接口方法
换句话说,每一层呼叫都会颠倒方差感。接口中的成员必须使用协变类型参数作为输出,并使用逆变参数作为输入。但是当这些参数在传递给接口成员或从接口成员返回的委托类型中使用时,这些参数在意义上变得相反,并且必须遵守这方面的差异。
一个具体的例子:
假设我们有一个接口的实现,
但是您的接口实现要求代码传递类型为
所以,事实上,编译器正在完全按照预期做的:它阻止你编写非类型安全的代码。如声明的那样,如果允许您以变体方式使用该接口,那么实际上您可以编写在编译时允许的代码,可能会在运行时中断。这与泛型的整个要点完全相反:能够在编译时确定代码是类型安全的!
现在,如何解决困境。不幸的是,在你的问题中没有足够的背景知道什么是正确的方法。您可能只需要放弃差异。通常,实际上并不需要制作类型变体;在某些情况下它很方便,但不是必需的。如果是这种情况,那么就不要让接口的参数变异。
或者,你可能真的想要方差,并认为以不同的方式使用界面是安全的。这很难解决,因为你的基本假设是不正确的,你需要以其他方式实现代码。如果您可以反转
同样,没有更多的背景,就不可能说"其他方式"对你有用。 但是,现在希望你能够按照你现在编写的方式理解代码中的危险,你可以重新审视导致你进入这种非类型安全的接口声明的设计决策,并且可以提出一些有效的东西。 如果您遇到问题,请发布一个新问题,提供更详细的信息,说明您认为这是安全的,您将如何使用该界面,您考虑的替代方案,以及为什么这些都不适合您。
好。
但 -
如果你传递类a
为什么不能呢?好吧,我已经提到过该类可以将
您可以通过消除方差来消除问题 - 摆脱
删除进出关键词:
1 2 3 4 | 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
如果类型仅用作一种方法参数而不用作方法返回类型,则可以在泛型接口或委托中声明逆变
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-generic-modifier
type参数仅用作接口方法的返回类型,不用作方法参数的类型。
从接口定义中删除输入和输出关键字:
1 2 3 | public interface Test<TIn, TOut>{ TOut F (Func<TIn, TOut> transform); } |