Rob*_*tas 9 c# generics variance
我需要有关泛型和委托方差的更多信息.以下代码段无法编译:
错误CS1961无效方差:类型参数'TIn'必须在'Test.F(Func)'上协变有效.'TIn'是逆变的.
public interface Test<in TIn, out TOut>
{
TOut F (Func<TIn, TOut> transform);
}
Run Code Online (Sandbox Code Playgroud)
.net Func定义如下:
public delegate TResult Func<in T, out TResult> (T arg);
Run Code Online (Sandbox Code Playgroud)
为什么编译器抱怨TIn逆变和TOut- 协变而Func期望完全相同的方差?
编辑
对我来说主要的限制是我希望我的Test界面将TOut作为协变,以便使用它:
public Test<SomeClass, ISomeInterface> GetSomething ()
{
return new TestClass<SomeClass, AnotherClass> ();
}
Run Code Online (Sandbox Code Playgroud)
鉴于此public class AnotherClass : ISomeInterface.
我需要有关泛型和委托方差的更多信息.
我写了一系列有关此功能的博客文章.虽然其中一些已经过时 - 因为它是在设计最终确定之前编写的 - 那里有很多好的信息.特别是如果您需要对方差有效性的正式定义,您应该仔细阅读:
https://blogs.msdn.microsoft.com/ericlippert/2009/12/03/exact-rules-for-variance-validity/
有关相关主题,请参阅我在MSDN和WordPress博客上的其他文章.
为什么编译器抱怨TIn是逆变和TOut - 协变而Func期望完全相同的方差?
让我们稍微重写您的代码并查看:
public delegate R F<in T, out R> (T arg);
public interface I<in A, out B>{
B M(F<A, B> f);
}
Run Code Online (Sandbox Code Playgroud)
编译器必须证明这是安全的,但事实并非如此.
我们可以通过假设它是,然后发现它如何被滥用来说明它是不安全的.
假设我们有一个具有明显关系的Animal层次结构,例如,Mammal是Animal,Giraffe是哺乳动物,等等.我们假设你的方差注释是合法的.我们应该可以说:
class C : I<Mammal, Mammal>
{
public Mammal M(F<Mammal, Mammal> f) {
return f(new Giraffe());
}
}
Run Code Online (Sandbox Code Playgroud)
我希望你同意这是一个完全有效的实现.现在我们可以这样做:
I<Tiger, Animal> i = new C();
Run Code Online (Sandbox Code Playgroud)
C实现I<Mammal, Mammal>,我们已经说过第一个可以更具体,第二个可以更通用,所以我们已经做到了.
现在我们可以这样做:
Func<Tiger, Animal> f = (Tiger t) => new Lizard();
Run Code Online (Sandbox Code Playgroud)
对于这个委托来说,这是一个完全合法的lambda,它匹配以下签名:
i.M(f);
Run Code Online (Sandbox Code Playgroud)
会发生什么? C.M正在期待一种能够接受长颈鹿并返回哺乳动物的功能,但它已被赋予一种能够接受老虎并返回蜥蜴的功能,因此有人会有一个非常糟糕的一天.
显然,这绝不允许发生,但沿途的每一步都是合法的.我们必须得出结论,方差本身并不是安全的,实际上并非如此.编译器拒绝这一点是正确的.
获得正确的方差不仅仅是简单地匹配输入和输出注释. 你必须以不允许存在这种缺陷的方式这样做.
这就解释了为什么这是非法的.要解释它是如何非法,编译器必须检查以下内容是否正确B M(F<A, B> f);:
B是有效的协变.因为它被宣布为"out",所以它是.F<A, B>是有效的逆转.它不是.泛型委托的"有效逆转"定义的相关部分是:如果第i个类型参数被声明为逆变,那么Ti必须是协变的有效.好.第一个类型参数,T被宣布为逆变.因此,第一个类型参数 A必须是协变有效的.但它并不是有效的,因为它被宣布为逆变.这就是你得到的错误.同样,B也很糟糕,因为它必须是有效的,但是B是协变的.在找到第一个问题后,编译器不会继续查找其他错误; 我考虑过它,但拒绝它是一个太复杂的错误信息.我还注意到,即使代表不是变体,你仍会遇到这个问题; 在我的反例中没有任何地方我们使用F在其类型参数中是变体的事实.如果我们尝试,将报告类似的错误
public delegate R F<T, R> (T arg);
Run Code Online (Sandbox Code Playgroud)
代替.
方差是指能够用比最初声明的更多或更少的派生类型替换类型参数.例如,IEnumerable<T>covariant for T,意思是如果从IEnumerable<U>对象的引用开始,则可以将该引用分配给具有类型的变量IEnumerable<V>,其中V可以从U(例如Uinherits V)分配.这是有效的,因为任何试图使用IEnumerable<V>想要只接收值的代码V,并且因为V可以分配U,只接收值U也是有效的.
对于协变参数T,您必须分配目标类型T与之相同或可分配的类型T.对于逆变参数,它必须采用另一种方式.目标类型必须与type参数相同或可分配.
那么,您尝试编写的代码如何在这方面起作用?
当声明Test<in TIn, out TOut>,则是有希望的,这将是有效的到该接口的一个实例分配Test<TIn, TOut>到具有任何类型的目的地Test<U, V>,其中U可以被分配到TIn和TOut可被分配给V(或它们是相同的,当然).
与此同时,让我们考虑一下您的transform代表期望的内容.该Func<T, TResult>类型的差异要求,如果你想该值分配给别的东西,它也符合方差规则.也就是说,目的地Func<U, V>必须具有U可分配T和TResult可分配的目的地V.这确保了期望接收值的委托目标方法U将获得其中之一,并且V接收它的代码可以接受具有类型的方法返回的值.
重要的是,您的接口方法F() 是接收方法!接口声明承诺TOut仅用作接口成员的输出.但是通过使用transform委托,该方法F()将获得一个值TOut,使该方法的输入.同样,F()允许该方法将值传递TIn给transform委托,使其成为接口实现的输出,即使您已经承诺TIn仅用作输入.
换句话说,每一层呼叫都会颠倒方差感.接口中的成员必须使用协变类型参数作为输出,并使用逆变参数作为输入.但是当这些参数在传递给接口成员或从接口成员返回的委托类型中使用时,这些参数在意义上变得相反,并且必须遵守这方面的差异.
一个具体的例子:
假设我们有一个接口的实现,Test<object, string>.如果编译器允许您的声明,则允许您将该实现的值分配给Test<object, string>具有该类型的变量Test<string, object>.也就是说,原始实现承诺允许任何具有类型的东西作为输入object并且仅返回具有该类型的值string.声明为使用它的代码是安全的Test<string, object>,因为它会将string对象传递给需要objects值(string是一个object)的实现,并且它将接收具有object返回string值的实现中的类型的值(同样,string是一个object,所以也是安全的).
但是您的接口实现需要代码传递类型的委托Func<object, string>.如果允许您将接口实现视为(如上所述)Test<string, object>,那么使用重新构建实现的代码将能够传递Func<string, object>给该方法的委托F().F()允许实现中的方法将任何类型的值传递object给委托,但该类型的委托Func<string, object>只期望具有string要传递给它的类型的值.如果F()传递其他内容,例如只是普通的旧代理new object(),则委托实例将无法使用它.它期待着string!
所以,事实上,编译器正在完全按照预期做的:它阻止你编写非类型安全的代码.如声明的那样,如果允许您以不同的方式使用该接口,那么您实际上能够编写在编译时允许的代码,可能会在运行时中断.这与泛型的整个要点完全相反:能够在编译时确定代码是类型安全的!
现在,如何解决困境.不幸的是,在你的问题中没有足够的背景知道什么是正确的方法.您可能只需要放弃差异.通常,实际上并不需要制作类型变体; 在某些情况下它很方便,但不是必需的.如果是这种情况,那么就不要让接口的参数变异.
或者,您可能确实想要方差,并认为以不同的方式使用界面是安全的.这很难解决,因为你的基本假设是不正确的,你需要以其他方式实现代码.如果您可以反转中的参数,代码将编译Func<T, TResult>.即制作方法F(Func<TOut, TIn> transform).但是你的问题中没有任何东西表明在你的场景中实际上是可行的.
同样,没有更多的背景,就不可能说"其他方式"对你有用.但是,现在希望你能够按照你现在编写的方式理解代码中的危险,你可以重新审视导致你进入这种非类型安全的接口声明的设计决策,并且可以提出一些有效的东西.如果您遇到问题,请发布一个新问题,提供更详细的信息,说明您认为这是安全的,您将如何使用该界面,您考虑的替代方案,以及为什么这些都不适合您.