C#-弄懂泛型和協變、逆變


腦圖概覽

image

泛型聲明和使用

協變和逆變

《C#權威指南》上在委托篇中這樣定義:

  • 協變:委托方法的返回值類型直接或者間接地繼承自委托前面的返回值類型;
  • 逆變:委托簽名中的參數類型繼承自委托方法的參數類型;

在泛型篇中這樣定義:

  • 協變:泛型參數定義的類型只能作為方法的返回類型,不能作為方法的參數類型,且該類型直接或者間接地繼承自接口方法的返回值類型;可以使用out關鍵字聲明協變參數。
  • 逆變:泛型參數定義的類型只能作為方法參數的類型,不能作為返回值類型,且該類型是接口方法的參數類型的基類型;可以使用in關鍵字聲明逆變參數

一直沒弄懂,或者一時弄懂了也沒記住,一定不怪我!看到一個博主這樣解釋,秒懂:

  • 協變性:如:string->object (子類到父類的轉換)
  • 逆變性:如:object->string (父類到子類的轉換)
  • 不變性

《CLR via C#》 第三版這樣定義:

  • 協變量:意味着泛型類型參數可以從一個派生類更改為它的基類(子類可以轉為父類);
  • 逆變量:意味着泛型類型參數可以從一個基類改為該類的派生類(父類可以轉為子類);
  • 不變量:意味着泛型參數不能更改;

總結:協變逆變中的協逆是相對於繼承關系的繼承鏈方向而言的

《CLR via C#》 底部有注解說:
協變性指定返回類型的兼容性,而逆變性指定參數的兼容性。

基類和子類的轉換

面向對象中有一個規則是:子類向上可以轉為基類,但是基類不能向下轉為子類
比如子類Student可以通過以下方式轉為基類Person:

Person p=new Student();
或者
Student s=new Student();
Person p=s;

但是基類轉為子類這樣轉編譯器就會出錯:

Person p=new Person();
Student s=p;
或者
Object obj=new Object();
string str=obj;

Func泛型委托的出參只有協變

既然子類可以轉為父類,父類不能轉為子類,那我這樣做行不行?新定義了一個委托,委托返回一個類型

delegate T MyFunc<T>();
void Main()
{
	Student s = new Student() { Name = "張三" };
	Person p = s;

	MyFunc<Student> func1 = () => new Student() { Name = "李四" };
	MyFunc<Person> func2 = func1;

	Func<Student> func3 = () => new Student() { Name = "王五" };
	Func<Person> func4 = func3;
}
public class Person
{
	public string Name { get; set; }
}
public class Student : Person
{
	public string Number { get; set; }
}
public class Teacher
{
	public string TeacherBook { get; set; }
}

這樣不行,MyFunc<Person> func2 = func1;編譯器不認編譯失敗,這句代碼卻Func<Person> func4 = func3;正常。
我們知道Student轉Person是合法的轉換,但是編譯器不知道,需要告訴編譯器這段轉換時合法的,沒有必要做強制的類型安全轉換。

怎么做?聲明類型時指定out關鍵字,如下:

delegate T MyFunc<out T>();

可以看到編譯器通過。out關鍵字會告訴編譯器Student到Person是有效轉換
其實Func委托的定義也是如此:

public delegate TResult Func<out TResult>();//無輸入參數,有返回值。

但要是加如下代碼呢?

MyFunc<Teacher> func5 = func1;

編譯還是會不給過的!因為轉換不合法。

那要是 out關鍵字加在委托的入參類型呢

delegate void MyFunc<out T>(T t);

會告訴你無效

image

那出參可以協變,入參是否可以協變呢?

完全不可以,就像父類轉為子類一樣是不合法的

泛型委托Action的入參只有逆變

以下代碼可以正常編譯為什呢?明明object類型不可以轉為string類型

Action<object> action1 = t => { Console.WriteLine(t.GetType()); };
Action<string> action2 = action1;

而反過來卻是錯的

Action<string> action3 = t => { Console.WriteLine(t.GetType()); };
Action<object> action4 = action3;

看看Action是如何定義的?

public delegate void Action<in T>(T obj);//泛型委托,無返回值

in關鍵會告訴編譯器,要么傳遞T作為委托的參數類型,要么傳遞T的派生類型。string是oject的派生類,所有是正常的.
所以這里不能單純理解為object轉為string類型了,而要理解為string可以安全的替換掉object,因為string是object的子類呀,
object有的,string都有,這個轉換肯定是安全的。

泛型中的協變和逆變

泛型中的協變和逆變原理與泛型委托一樣

out和in總結

out: 輸出(作為結果),in:輸入(作為參數)
所以如果有一個泛型參數標記為out,則代表它是用來輸出的,只能作為結果返回,而如果有一個泛型參數標記為in,則代表它是用來輸入的,也就是它只能作為參數。

為什么in只能作為輸入參數的逆變?

void Main()
{
	var p = new Person();
	var s = new Student();
	var gs = new GoodStudent();
	this.Method(p);//編譯出錯
	this.Method(s);
	this.Method(gs);
}
public void Method(Student stu)
{
}
public class Person
{
	public string Name { get; set; }
}
public class Student : Person
{
	public string Number { get; set; }
}
public class GoodStudent : Student
{
}

方法體形參可以把子類當成父類來用(傳遞Student還是GoodStudent對象都無所謂),但是不能把父類當成子類來用(傳遞Person對象就出錯了)。【里氏替換原則】

為什么out只能作為返回值的協變?

通常定義一個變量來接受返回值,父類可以接收子類的數據(這里可以理解為object obj=str),子類能接收父類的數據嗎(這里理解為string str = (string)objcet)?肯定不是不能。所以只能是協變

相關單詞:

  • Covariant:協變量
  • Contravariant:逆變量
  • Covariance:協變性
  • Contravariance:逆變性

使用注意

C#4.0之前 IEnumerable<T> 、 IComparable<T> 、 IQueryable<T> 等接口都不支持可變性,在4.0及之后才支持。因為4.0之前定義的泛型接口沒有添加out、in關鍵字,有興趣可以切換版本看看。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM