協變和抗變
一.定義
public class Sharp
{
}
public class Rectange : Sharp
{
}
上面定義了兩個簡單的類,一個是圖形類,一個是矩形類;它們之間有簡單的繼承關係。接下來是常見的一種正確寫法:Sharp sharp = new Rectange();
就是說“子類引用可以直接轉化成父類引用”,或者說Rectange類和Sharp類之間存在一種安全的隱式轉換。Sharp[] sharps=new Rectange[3];
編譯通過,這說明Rectange[]和Sharp[]之間存在安全的隱式轉換。Rectange[] rectanges = new Sharp[3];
Sharp sharp = new Rectange();
IEnumerable<Sharp> sharps = new List<Rectange>();
public interface IEnumerable<out T> : IEnumerable
二.泛型接口中的協變和抗變
public interface ICovariant<T>
{
}
並且讓上面的兩個類各自繼承一下該接口: public class Sharp : ICovariant<Sharp>
{
}
public class Rectange : Sharp,ICovariant<Rectange>
{
}
編寫測試代碼: static void Main(string[] args)
{
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
isharp = irect;
}
編譯並不能通過,原因是無法將ICovariant<Rectange>隱式轉化爲ICovariant<Sharp>! public interface ICovariant<out T>
{
}
編譯順利通過。這裏我爲泛型接口的類型參數增加了一個修飾符out,它表示這個泛型接口支持對類型T的協變。那我如果反過來呢,考慮如下代碼:
static void Main(string[] args)
{
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
irect = isharp;
// isharp =irect;
}
發現編譯又不通過了,
public interface ICovariant<in T>
{
}
編譯順利通過。這裏我將泛型接口的類型參數T修飾符修改成in,它表示這個泛型接口支持對類型參數T的抗變。 //這時候,無論如何修飾T,都不能編譯通過
public interface ICovariant<out T>
{
T Method1();
void Method2(T param);
}
發現無論用out還是in修飾T參數,根本編譯不通過。 ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
isharp = irect;
Sharp sharp = isharp.Method1();
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
isharp = irect;
isharp.Method2(new Sharp());
即如果執行最後一行代碼,會發現參數中,Sharp類型並不能安全轉化成Rectange類型,因爲Method2(Sharp)實際上已經被替換成
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
//isharp = irect;
irect = isharp;
irect.Method2(new Rectange());
ICovariant<Sharp> isharp = new Sharp();
ICovariant<Rectange> irect = new Rectange();
//isharp = irect;
irect = isharp;
Rectange rect = irect.Method1();
執行最後一句代碼,同樣將會是不安全的! public interface ICovariant<out T>
{
T Method1();
}
public interface IContravariant<in T>
{
void Method2(T param);
}
.net中很多接口都僅將參數用於函數返回類型或函數參數類型,如:
public interface IComparable<in T>
public interface IEnumerable<out T> : IEnumerable
2.值類型不參與協變或抗變,IFoo<int>永遠無法協變成IFoo<object>,不管有無聲明out。因爲.NET泛型,每個值類型會生成專屬的封閉構造類型,與引用類型版本不兼容。
3.聲明屬性時要注意,可讀寫的屬性會將類型同時用於參數和返回值。因此只有只讀屬性才允許使用out類型參數,只寫屬性能夠使用in參數。
接下來將接口代碼改成:
public interface ICovariant<out T>
{
T Method1();
void Method3(IContravariant<T> param);
}
public interface IContravariant<in T>
{
void Method2(T param);
}
同樣是可以編譯通過的. public interface IContravariant<in T>
{
}
public interface ICovariant<out T>
{
}
public interface ITest<out T1, in T2>
{
ICovariant<T1> test1();
IContravariant<T2> test2();
}
我們看到和剛剛正好相反,如果一個接口需要對類型參數T進行協變或抗變,那麼這個接口所有方法的返回值類型必須支持對T同樣方向的協變或抗變(如果有某些方法的返回值是T類型)。這就是方法返回值的協變-抗變一致原則。也就是說,即使in參數也可以用於方法的返回值類型,只要藉助一個可以抗變的類型作爲橋樑即可。
三.泛型委託中的協變和抗變
泛型委託的協變抗變,與泛型接口協變抗變類似。繼續延用Sharp,Rectange類作爲示例: public delegate void MyDelegate1<T>();
測試代碼: MyDelegate1<Sharp> sharp1 = new MyDelegate1<Sharp>(MethodForParent1);
MyDelegate1<Rectange> rect1 = new MyDelegate1<Rectange>(MethodForChild1);
sharp1 = rect1;
public static void MethodForParent1()
{
Console.WriteLine("Test1");
}
public static void MethodForChild1()
{
Console.WriteLine("Test2");
}
public delegate void MyDelegate1<out T>();
編譯順利用過。 MyDelegate1<Sharp> sharp1 = new MyDelegate1<Sharp>(MethodForParent1);
MyDelegate1<Rectange> rect1 = new MyDelegate1<Rectange>(MethodForChild1);
//sharp1 = rect1;
rect1 = sharp1;
只需將修飾符改爲in即可: public delegate void MyDelegate1<in T>();
考慮第二個委託:
public delegate T MyDelegate2<out T>();
測試代碼: MyDelegate2<Sharp> sharp2 = new MyDelegate2<Sharp>(MethodForParent2);
MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild2);
sharp2 = rect2;
其中兩個方法爲: public static Sharp MethodForParent2()
{
return new Sharp();
}
public static Rectange MethodForChild2()
{
return new Rectange();
}
該委託對類型參數T進行協變沒有任何問題,編譯通過;如果我要對T進行抗變呢?是否只要將修飾符改成in就OK了? public delegate T MyDelegate2<in T>();
MyDelegate2<Sharp> sharp2 = new MyDelegate2<Sharp>(MethodForParent2);
MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild2);
//sharp2 = rect2;
rect2 = sharp2;
錯誤如下: Rectange rectange = rect2();
那麼這將是一個從Sharp類到Rectange類的不安全的類型轉換!所以如果類型參數T抗變,並且要用於方法返回類型,那麼方法的返回類型也必須支持抗變。即上面所說的方法返回類型協變-抗變一致原則。 public delegate Contra<T> MyDelegate2<in T>();
public delegate void Contra<in T>();
具體的方法也需要對應着修改一下: public static Contra<Sharp> MethodForParent3()
{
return new Contra<Sharp>(MethodForParent1);
}
public static Contra<Rectange> MethodForChild3()
{
return new Contra<Rectange>(MethodForChild1);
}
測試代碼: MyDelegate2<Sharp> sharp2 = new MyDelegate2<Sharp>(MethodForParent3);
MyDelegate2<Rectange> rect2 = new MyDelegate2<Rectange>(MethodForChild3);
rect2 = sharp2;
編譯通過。 public delegate T MyDelegate3<T>(T param);
首先,對類型參數T進行協變: public delegate T MyDelegate3<out T>(T param);
對應的方法及測試代碼: public static Sharp MethodForParent4(Sharp param)
{
return new Sharp();
}
public static Rectange MethodForChild4(Rectange param)
{
return new Rectange();
}
MyDelegate3<Sharp> sharp3 = new MyDelegate3<Sharp>(MethodForParent4);
MyDelegate3<Rectange> rect3 = new MyDelegate3<Rectange>(MethodForChild4);
sharp3 = rect3;
和泛型接口類似,這裏的委託類型參數T被同時用作方法返回類型和方法參數類型,不管修飾符改成in或out,編譯都無法通過。所以如果用out修飾T,那麼方法參數param的參數類型T就需藉助一樣東西來轉換一下:一個對類型參數T能抗變的泛型委託。即:
public delegate T MyDelegate3<out T>(Contra<T> param);
兩個方法也需對應着修改: public static Sharp MethodForParent4(Contra<Sharp> param)
{
return new Sharp();
}
public static Rectange MethodForChild4(Contra<Rectange> param)
{
return new Rectange();
}
這就是上面所說的方法參數的協變-抗變互換原則同理,如果對該委託類型參數T進行抗變,那麼根據方法返回類型協變-抗變一致原則,方法返回參數也是要藉助一個對類型參數能抗變的泛型委託:
public delegate Contra<T> MyDelegate3<in T>(T param);
兩個方法也需對應着修改爲: public static Contra<Sharp> MethodForParent4(Sharp param)
{
return new Contra<Sharp>(MethodForParent1);
}
public static Contra<Rectange> MethodForChild4(Rectange param)
{
return new Contra<Rectange>(MethodForChild1);
}
推廣到一般的泛型委託: public delegate T1 MyDelegate4<T1,T2,T3>(T2 param1,T3 param2);
可能三個參數T1,T2,T3會有各自的抗變和協變,如: public delegate T1 MyDelegate4<out T1,in T2,in T3>(T2 param1,T3 param2);
這是一種最理想的情況,T1支持協變,用於方法返回值;T2,T3支持抗變,用於方法參數。 public delegate T1 MyDelegate4<in T1,out T2,in T3>(T2 param1,T3 param2);
那麼對應的T1,T2類型參數就會出問題,原因上面都已經分析過了。於是就需要修改T1對應的方法返回類型,T2對應的方法參數類型,如何修改?只要根據上面提到的: public delegate Contra<T1> MyDelegate4<in T1, out T2, in T3>(Contra<T2> param1, T3 param2);
以上,協變和抗變記錄到此。