泛型中的协方差与Func的协方差

时间:2018-01-09 22:18:13

标签: c# generics variance

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

  

错误CS1961无效方差:类型参数' TIn'一定是   在' Test.F(Func)'上共同有效。 '锡'是   逆变

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

.net Func定义如下:

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

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

修改

对我来说,主要的限制是我希望我的Test界面将TOut作为协变,以便使用它:

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

鉴于public class AnotherClass : ISomeInterface

5 个答案:

答案 0 :(得分:8)

  

我需要有关泛型和代理方差的更多信息。

我写了一系列有关此功能的博客文章。虽然其中一些已经过时 - 因为它是在设计最终确定之前编写的 - 那里有很多好的信息。特别是如果你需要一个关于方差有效性的正式定义,你应该仔细阅读:

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);
}

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

我们可以说明它是不安全的,然后发现它是如何被滥用的。

让我们假设我们有一个具有明显关系的动物等级,例如,哺乳动物是动物,长颈鹿是哺乳动物,等等。我们假设你的方差注释是合法的。我们应该可以说:

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期待一种能够接受长颈鹿并返回哺乳动物的功能,但是它被赋予一种能够接受老虎并返回蜥蜴的功能,所以有人会有一个非常糟糕的一天。

显然,这绝不允许发生,但一路上的每一步都是合法的。我们必须得出结论,方差本身并不是安全的,实际上并非如此。编译器拒绝这一点是正确的。

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

这解释了为什么这是非法的。要解释如何它是非法的,编译器必须检查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);

代替。

答案 1 :(得分:5)

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

对于T等协变参数,您必须分配目标类型与T相同的类型,或者从T分配。对于逆变参数,它必须采用另一种方式。目标类型必须与类型参数相同或可分配。

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

当您声明Test<in TIn, out TOut>时,您承诺将该接口Test<TIn, TOut>的实例分配给Test<U, V>类型的U所在的目标是有效的被分配到TInTOut可以分配到V(当然,它们是相同的)。

与此同时,让我们考虑一下您的transform代表的期望。 Func<T, TResult>类型差异要求如果您要将该值分配给其他值,则它还符合差异规则。也就是说,目标Func<U, V>必须U可以从T分配,TResult可以从V分配。这可以确保期望接收值U的委托目标方法将获得其中一个,并且接收它的代码可以接受方法返回的类型为V的值

重要的是,您的接口方法F() 是进行接收的接口!接口声明承诺TOut将仅用作接口成员的输出。但是,通过使用transform委托,方法F()接收TOut,使输入到方法。同样,允许方法F()将值TIn传递给transform委托,使其成为接口实现的输出,即使您&# 39;我承诺TIn仅用作输入。

换句话说,每一层呼叫都颠倒了方差感。接口中的成员必须使用协变类型参数作为输出,并使用逆变参数作为输入。但是当这些参数在传递给接口成员或从接口成员返回的委托类型中使用时,它们在某种意义上会被颠倒过来,并且必须遵守这方面的差异。

一个具体的例子:

假设我们有一个接口实现Test<object, string>。如果编译器允许您的声明,则允许您将该实现Test<object, string>的值分配给类型为Test<string, object>的变量。也就是说,原始实现承诺允许任何具有类型object的东西作为输入,并且仅返回具有类型string的值。声明为Test<string, object>的代码可以安全地使用它,因为它会将string个对象传递给需要objects值的实现(string是{ {1}}),它将从返回object值的实现中接收类型为object的值(同样,stringstring,因此也是安全的)

但是您的接口实现要求代码传递类型为object的委托。如果允许您将接口实现视为Func<object, string>,那么使用重构实现的代码将能够将Test<string, object>的委托传递给方法{{1 }}。允许实现中的方法Func<string, object>将任何类型F()的值传递给委托,但该类型F()的委托只期望具有{{1}类型的值要传递给它。如果object传递了其他内容,例如只是一个普通的Func<string, object>,委托实例无法使用它。它期待string

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

现在,如何解决困境。不幸的是,在你的问题中没有足够的背景知道什么是正确的方法。您可能只需要放弃差异。通常,实际上并不需要制作类型变体;在某些情况下它很方便,但不是必需的。如果是这种情况,那么就不要让界面的参数变异。

或者,您可能确实想要方差,并认为以不同的方式使用界面是安全的。这很难解决,因为你的基本假设是不正确的,你需要以其他方式实现代码。如果您可以反转F()中的参数,代码将编译。即制作方法new object()。但是,你的问题中并没有任何暗示在你的情景中实际可行的事情。

同样,如果没有更多的背景,就不可能说出什么&#34;其他方式&#34;会对你有用。但是,希望现在您已经按照您现在编写的方式了解代码中的危险,您可以重新审视导致您进入这种非类型安全接口声明的设计决策,并且可以提出一些有效的方法。如果您遇到问题,请发布一个新问题,详细说明您认为这样安全的原因,您将如何使用该界面,您考虑过哪些替代方案,以及为什么没有那些为你工作。

答案 2 :(得分:1)

TIn =类知道如何读取它,并允许实现将其视为一种派生类型少于实际的类型。您可以传递一个比预期更多派生的实例,但这无关紧要,因为派生类可以执行基类可以执行的所有操作。

TOut =实现知道生成一个,并且允许实现生成一个比调用者期望更多派生的类型。同样,无关紧要 - 调用者可以将更多派生类分配给派生程度较低的变量而没有问题。

可是 -

如果你传递了一个Func<TIn, TOut>类,并且你希望该类能够调用它,那么该类必须能够生成一个TIn阅读 TOut。与上述情况相反。

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

您可以通过消除差异来消除问题 - 删除inout关键字 - 这将使该类无法替换更少/更多的派生类型(这称为“不变量“),但允许你读取和写入类型。

答案 3 :(得分:0)

从界面定义中删除 out -keywords中的

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

答案 4 :(得分:0)

删除输入和输出关键字:

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参数仅用作接口方法的返回类型,不用作方法参数的类型。