簡體   English   中英

為什么類類型參數的方差必須與其方法的返回/參數類型參數的方差相匹配?

[英]Why does the variance of a class type parameter have to match the variance of its methods' return/argument type parameters?

以下提出了投訴:

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<Derived>被分配給ICovariant<Base>時,我錯誤地認為ICovariant::M的類型簽名仍然是Func<IInvariant<Derived>> 然后,將M分配給Func<IInvariant<Base>> 看起來很好,來自ICovariant<Base> ,但當然是非法的。 為什么不禁止最后這個顯然是非法的演員? (所以我認為)

正如埃里克·利珀特Eric Lippert)所指出的那樣,我覺得這種錯誤和切向猜測會減少這個問題,但出於歷史目的,這個被剪切的部分:

對我來說最直觀的解釋是,以ICovariant為例,協變性TCov意味着方法IInvariant<TCov> M()可以被轉換為某些IInvariant<TSuper> M() ,其中TSuper super TCov ,這違反了不變性IInvariantTInv 然而,這種暗示似乎並不是必要的:通過禁止M的演員,可以很容易地強制執行IInvariantTInv的不變性。

讓我們看一個更具體的例子。 我們將對這些接口進行一些實現:

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<object>ICovariant<string>替換因為該協方差。

但是我們遇到了問題。 Foo ,我們調用ICovariant<object>.M()並期望它返回一個IInvariant<object>因為這就是ICovariant接口所說的。 但它不能這樣做,因為我們傳遞的實際實現實際上實現了ICovariant<string>並且它的M方法返回IInvariant<string> ,由於該接口的不變性,它與IInvariant<object> 無關 它們是完全不同的類型。

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

為什么類類型參數的方差必須與其方法的返回/參數類型參數的方差相匹配?

它沒有,所以問題是基於錯誤的前提。 實際規則如下:

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> “有效ICage<T> ”嗎?

如果類型是:1)指針類型或非泛型類,則該類型是有效的... NOPE 2)數組類型... NOPE 3)泛型類型參數類型... NOPE 4)構造類,struct,enum,interface或delegate type X<T1, … Tk> YES! ...如果第i個類型參數被聲明為不變量,那么Ti必須是不變的有效。

TAnimal是不變ICage<TAnimal>所以TICage<T>必須是有效的不變地。 是嗎? 否。要有效目不暇接,它必須是有效的兩個協變和contravariantly,但它是有效的只有協變。

因此這是一個錯誤。

對逆變情況進行分析是一項練習。

為什么類類型參數的方差必須與其方法的返回/參數類型參數的方差相匹配?

它沒有!

返回類型和參數類型不需要與封閉類型的方差匹配。 在您的示例中,它們需要對兩個封閉類型都是協變的。 這聽起來違反直覺,但原因將在下面的解釋中變得明顯。


為什么您提出的解決方案無效

協變性TCov意味着方法IInvariant<TCov> M()可以轉換為某些IInvariant<TSuper> M() ,其中TSuper super TCov ,它違反了IInvariantTInv的不變性。 然而,這種暗示似乎並不是必要的:通過禁止M的演員,可以很容易地強制執行IInvariantTInv的不變性。

  • 您所說的是具有變體類型參數的泛型類型可以分配給同一泛型類型定義的另一種類型和不同的類型參數。 那部分是正確的。
  • 但是你也說過,為了解決潛在的子類型違規問題,方法的明顯簽名不應該在這個過程中發生變化。 那不對!

例如, ICovariant<string>有一個方法IInvariant<string> M() “禁止M ICovariant<string> ”意味着當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<>

因此,當泛型類型被分配給具有更多通用類型參數的另一個泛型類型時,使用相應類型參數的成員簽名也必須更改為更通用的類型。 如果我們想要保持類型安全,就無法繞過它。 現在讓我們看看在每種情況下“更一般”意味着什么。


為什么ICovariant<TCov>要求IInvariant<TInv>是協變的

對於string的類型參數,編譯器“看到”這個具體類型:

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

並且(如上所述) object的類型參數,編譯器“看到”這個具體類型:

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的類型參數,編譯器“看到”這個具體類型:

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

對於string的類型參數,編譯器“看到”這個具體類型:

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 所以我們再次查看協變類型參數。


現在你可能想知道為什么會出現不匹配。 協方差和逆變的二元性在哪里? 它仍然存在,但形式不太明顯:

  • 當您位於輸出側時,引用類型的方差與封閉類型的方差方向相同。 由於封閉類型在這種情況下可以是協變的或不變的,因此引用的類型也必須分別是協變的或不變的。
  • 當你在的輸入側,該被引用類型的方差變為逆着封閉類型的方差的方向。 由於封閉類型在這種情況下可以是逆變的或不變的,因此引用的類型現在必須分別是協變的或不變的。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM