[英]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
,這違反了不變性IInvariant
的TInv
。 然而,這種暗示似乎並不是必要的:通過禁止M
的演員,可以很容易地強制執行IInvariant
對TInv
的不變性。
讓我們看一個更具體的例子。 我們將對這些接口進行一些實現:
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();
}
到現在為止還挺好。 o
是ICovariant<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>
所以T
在ICage<T>
必須是有效的不變地。 是嗎? 否。要有效目不暇接,它必須是有效的兩個協變和contravariantly,但它是有效的只有協變。
因此這是一個錯誤。
對逆變情況進行分析是一項練習。
為什么類類型參數的方差必須與其方法的返回/參數類型參數的方差相匹配?
它沒有!
返回類型和參數類型不需要與封閉類型的方差匹配。 在您的示例中,它們需要對兩個封閉類型都是協變的。 這聽起來違反直覺,但原因將在下面的解釋中變得明顯。
協變性
TCov
意味着方法IInvariant<TCov> M()
可以轉換為某些IInvariant<TSuper> M()
,其中TSuper super TCov
,它違反了IInvariant
中TInv
的不變性。 然而,這種暗示似乎並不是必要的:通過禁止M
的演員,可以很容易地強制執行IInvariant
對TInv
的不變性。
例如, 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.