为什么类类型参数的方差必须与其方法的方差相匹配?返回/参数类型参数?

时间:2016-05-17 14:27:33

标签: c# generics generic-variance

以下提出了投诉:

interface IInvariant<TInv> {}
interface ICovariant<out TCov> {
    IInvariant<TCov> M(); // The covariant type parameter `TCov'
                          // must be invariantly valid on
                          // `ICovariant<TCov>.M()'
}
interface IContravariant<in TCon> {
    void M(IInvariant<TCon> v); // The contravariant type parameter
                                // `TCon' must be invariantly valid
                                // on `IContravariant<TCon>.M()'
}

但我无法想象这不会是类型安全的。 (剪断*)这是不允许这样做的原因,还是有其他违反我不知道的类型安全的情况?

*我最初的想法确实令人费解,但尽管如此,答案非常彻底,@Theodoros Chatzigiannakis甚至以令人印象深刻的准确性解剖了我的初步假设。

除了回顾过去的好评之外,我意识到当ICovariant::MFunc<IInvariant<Derived>>分配给ICovariant<Derived>时,我错误地认为ICovariant<Base>的类型签名仍为M 。然后,将Func<IInvariant<Base>>分配给ICovariant<Base>看起来来自ICovariant,但当然是非法的。为什么不禁止最后这个显然是非法的演员? (所以我想)

我认为这种错误和切向猜测会减少这个问题,正如Eric Lippert也指出的那样,但出于历史目的,这个被剪切的部分:

  

对我来说最直观的解释是,以TCov为例,协变IInvariant<TCov> M()意味着方法IInvariant<TSuper> M()可以转换为某些TSuper super TCov其中TInv {1}},违反了IInvariantIInvariant的不变性。但是,这种暗示似乎并不是必要的:TInv M的不变性很容易通过禁止{{1}}的演员来强制执行。

3 个答案:

答案 0 :(得分:6)

让我们看一个更具体的例子。我们将对这些接口进行一些实现:

class InvariantImpl<T> : IInvariant<T>
{
}

class CovariantImpl<T> : ICovariant<T>
{
    public IInvariant<T> M()
    {
        return new InvariantImpl<T>();
    }
}

现在,让我们假设编译器没有抱怨这个并尝试以一种简单的方式使用它:

static IInvariant<object> Foo( ICovariant<object> o )
{
    return o.M();
}

到目前为止一切顺利。 oICovariant<object>,该接口保证我们有一个可以返回IInvariant<object>的方法。我们不必在这里进行演员表或转换,一切都很好。现在让我们调用方法:

var x = Foo( new CovariantImpl<string>() );

由于ICovariant是协变的,因此这是一种有效的方法调用,我们可以用ICovariant<string>代替ICovariant<object>因为该协方差而需要Foo

但我们遇到了问题。在ICovariant<object>.M()内,我们会调用IInvariant<object>并希望它返回ICovariant,因为这是ICovariant<string>界面所说的内容。但是它无法做到这一点,因为我们通过的实际实现实际上实现了M,而其IInvariant<string>方法返回IInvariant<object>,其中没有由于该接口的不变性而与 app.controller('resultCrtl',function($scope,myService,$http,$location,$window) { /*guetting query from previous view*/ $scope.userquery=myService.get(); var uri; /*testing the refresh */ if(!myService.get()) { var loc=$location.search('query'); $window.alert(loc); /* it returns an empty object*/ } else { $http({ method : 'GET', url : '/response/' + $scope.userquery }).success(function(data) { $scope.result = data; uri=encodeURI($scope.userquery); $location.search('query',uri); }).error(function(data, status, headers, config) { alert( "failure"); }); $scope.search=function(){ /* same code to get data and update the url */ } }); 有关。它们是完全不同的类型。

答案 1 :(得分:5)

到目前为止,我不确定你的答案是否真的得到了答案。

  

为什么类类型参数的方差必须与其方法的方差相匹配?返回/参数类型参数?

它没有,所以问题是基于错误的前提。实际规则如下:

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

现在考虑一下:

interface IInvariant<TInv> {}
interface ICovariant<out TCov> {
   IInvariant<TCov> M(); // Error
}
  

这是不允许这样做的原因,还是有其他一些违反我不知道的类型安全的情况?

我没有按照您的解释,所以我们只是说明为什么在不参考您的解释的情况下不允许这样做。在这里,让我用一些等价的类型替换这些类型。 IInvariant<TInv>可以是T中不变的任何类型,让我们说ICage<TCage>

interface ICage<TAnimal> {
  TAnimal Remove();
  void Insert(TAnimal contents);
}

也许我们有Cage<TAnimal>类型来实现ICage<TAnimal>

让我们用

替换ICovariant<T>
interface ICageFactory<out T> {
   ICage<T> MakeCage();
}

让我们实现界面:

class TigerCageFactory : ICageFactory<Tiger> 
{ 
  public ICage<Tiger> MakeCage() { return new Cage<Tiger>(); }
}

一切都进展顺利。 ICageFactory是协变的,所以这是合法的:

ICageFactory<Animal> animalCageFactory = new TigerCageFactory();
ICage<Animal> animalCage = animalCageFactory.MakeCage();
animalCage.Insert(new Fish());

我们只是把鱼放进虎笼里。那里的每一步都完全合法,我们最终违反了类型系统。我们得出的结论是,首先使ICageFactory协变一定不合法。

让我们来看看你的逆变例子;它基本相同:

interface ICageFiller<in T> {
   void Fill(ICage<T> cage);
}

class AnimalCageFiller : ICageFiller<Animal> {
  public void Fill(ICage<Animal> cage)
  {
    cage.Insert(new Fish());
  }
}

现在,界面是逆变的,所以这是合法的:

ICageFiller<Tiger> tigerCageFiller = new AnimalCageFiller();
tigerCageFiller.Fill(new Cage<Tiger>());

我们再一次将鱼放入虎笼中。我们再次得出结论,首先制造逆变型一定是非法的。

现在让我们考虑一下我们如何知道这些都是非法的问题。在第一种情况下,我们有

interface ICageFactory<out T> {
   ICage<T> MakeCage();
}

相关规则是:

  

所有非void接口方法的返回类型必须是covariantly有效。

ICage<T>&#34;是否有效并且#34;?

  

如果类型是:   1)指针类型,或非泛型类... NOPE   2)数组类型... NOPE   3)泛型类型参数类型... NOPE   4)构造的类,结构,枚举,接口或委托类型X<T1, … Tk>是的! ...如果第i个类型参数被声明为不变量,那么Ti必须是不变的有效。

TAnimal中{p> ICage<TAnimal>不变,因此T中的ICage<T>必须无限制有效。是吗?不能。为了无限制地有效,它必须是两者共同和不相容,但它只能共同有效。

因此这是一个错误。

对逆变情况进行分析是一项练习。

答案 2 :(得分:1)

  

为什么类类型参数的方差必须与其方法的方差相匹配?返回/参数类型参数?

它没有!

返回类型和参数类型不需要匹配封闭类型的方差。在您的示例中,对于两种封闭类型,它们都需要协变。这听起来违反直觉,但原因将在下面的解释中变得明显。

为什么您提出的解决方案无效

  

协变TCov意味着方法IInvariant<TCov> M()可以转换为某些IInvariant<TSuper> M() TSuper super TCov,这违反了TInv IInvariant的不变性}}。但是,这种暗示似乎没有必要:IInvariant TInv的不变性很容易通过禁止M的演员来强制执行。

  • 您所说的是具有变体类型参数的泛型类型可以分配给同一泛型类型定义的另一种类型和不同的类型参数。那部分是正确的。
  • 但您还要说,为了解决潜在的子类型违规问题,该方法的明显签名不应在此过程中发生变化。这不正确!

例如,ICovariant<string>有一个方法IInvariant<string> M()。 &#34;不允许M&#34;意味着当ICovariant<string>分配给ICovariant<object>时,它仍会保留带有签名IInvariant<string> M()的方法。如果允许,那么这个完全有效的方法就会出现问题:

void Test(ICovariant<object> arg)
{
    var obj = arg.M();
}

编译器应该针对obj变量的类型推断出什么类型?它应该是IInvariant<string>吗?为什么不IInvariant<Window>IInvariant<UTF8Encoding>IInvariant<TcpClient>?所有这些都是有效的,请亲自看看:

Test(new CovariantImpl<string>());
Test(new CovariantImpl<Window>());
Test(new CovariantImpl<UTF8Encoding>());
Test(new CovariantImpl<TcpClient>());

显然,方法(M())的静态已知返回类型可能不依赖于实现的接口(ICovariant<>)对象的运行时类型!

因此,当泛型类型被分配给具有更多通用类型参数的另一个泛型类型时,使用相应类型参数的成员签名也必须更改为更通用的类型。如果我们想要保持类型安全,那就没办法了。现在让我们看一下&#34;更通用的&#34;在每种情况下都意味着。

为什么ICovariant<TCov>需要IInvariant<TInv>协变

对于string的类型参数,编译器&#34;看到&#34;这个具体类型:

interface ICovariant<string>
{
    IInvariant<string> M();
}

并且(如上所述)对于object的类型参数,编译器&#34;看到&#34;相反,这个具体类型:

interface ICovariant<object>
{
    IInvariant<object> M();
}

假设实现前一个接口的类型:

class MyType : ICovariant<string>
{
    public IInvariant<string> M() 
    { /* ... */ }
}

请注意,此类型中M()的实际实施仅涉及返回IInvariant<string> ,而不关心差异。牢记这一点!

现在通过设置ICovariant<TCov>协变的类型参数,您声称ICovariant<string>应该可以分配给ICovariant<object>,如下所示:

ICovariant<string> original = new MyType();
ICovariant<object> covariant = original;

...而你断言你现在可以这样做:

IInvariant<string> r1 = original.M();
IInvariant<object> r2 = covariant.M();

请记住,original.M()covariant.M()是对同一方法的调用。实际的方法实现只知道它应该返回Invariant<string>

因此,在执行后一个调用期间的某个时刻,我们隐式地将IInvariant<string>(由实际方法返回)转换为IInvariant<object>(这是协变签名所承诺的)。为此,IInvariant<string>必须可分配给IInvariant<object>

要概括起来,相同的关系必须适用于IInvariant<S>IInvariant<T> S : T。这正是协变类型参数的描述。

为什么IContravariant<TCon> 要求IInvariant<TInv>协变

对于object的类型参数,编译器&#34;看到&#34;这个具体类型:

interface IContravariant<object>
{
    void M(IInvariant<object> v); 
}

对于string的类型参数,编译器&#34;看到&#34;这个具体类型:

interface IContravariant<string>
{
    void M(IInvariant<string> v); 
}

假设实现前一个接口的类型:

class MyType : IContravariant<object>
{
    public void M(IInvariant<object> v)
    { /* ... */ }
}

同样,注意到M()的实际实现假设它会从您那里获得IInvariant<object> 并且它并不关心方差。

现在通过创建IContravariant<TCon>的类型参数,您声称IContravariant<object>应该可以像这样分配给IContravariant<string> ......

IContravariant<object> original = new MyType();
IContravariant<string> contravariant = original;

...而你断言你现在可以这样做:

IInvariant<object> arg = Something();
original.M(arg);
IInvariant<string> arg2 = SomethingElse();
contravariant.M(arg2);

同样,original.M(arg)contravariant.M(arg2)是对同一方法的调用。该方法的实际实现要求我们传递任何IInvariant<object>

因此,在执行后一个调用的某个时刻,我们隐式地将IInvariant<string>(这是逆变签名期望从我们这里得到的)转换为IInvariant<object>(这就是实际的方法预期)。为此,IInvariant<string>必须可分配给IInvariant<object>

要概括起来,每个IInvariant<S>都应该分配到IInvariant<T> S : T。所以我们再次查看协变类型参数。

现在你可能想知道为什么会出现不匹配。协方差和逆变的二元性在哪里?它仍然存在,但形式不太明显:

  • 当您位于输出侧时,引用类型的方差与封闭类型的方差方向相同。由于封闭类型在这种情况下可以是协变的或不变的,因此引用的类型也必须分别是协变的或不变的。
  • 当您位于输入侧时,引用类型的方差将计数器转换为封闭类型的方差方向。由于封闭类型在这种情况下可以是逆变的或不变的,因此引用的类型现在必须分别是协变的或不变的。
相关问题