重學c#系列——逆變和協變[二十四]

敖毛毛發表於2022-11-23

前言

簡單整理一下逆變和協變。

正文

什麼是逆變和協變呢?

首先逆變和協變都是術語。

協變表示能夠使用比原始指定的派生型別的派生程度更大的型別。

逆變表示能夠使用比原始指定的派生型別的派生程度更小的型別。

這裡student 繼承 person。

這裡這個報錯合情合理。

這裡可能有些剛入門的人認為,person 不是 student 的父類啊,為什麼不可以呢?

一個列表student 同樣也是一個列表的 person啊。

這可能是初學者的一個疑問。

但是實際情況是list 是一個型別, list 是一個型別。

所以他們無法隱式轉換是正常的。

但是這樣寫就可以:

static void Main(string[] args)
{
	IEnumerable<Student> students = new List<Student>();
	IEnumerable<Person> peoples = students;
}

這樣寫沒有報錯,理論上IEnumerable是一種型別,IEnumerable是一種型別,不應該能隱私轉換啊。

為什麼呢?因為支援協變。

協變表示能夠使用比原始指定的派生型別的派生程度更大的型別。

他們的結構如上。因為student是person的派生類,IEnumerable的派生程度比IEnumerable大。

協變怎麼宣告呢:

public interface IEnumerable<out T> : IEnumerable
{
	//
	// 摘要:
	//     Returns an enumerator that iterates through the collection.
	//
	// 返回結果:
	//     An enumerator that can be used to iterate through the collection.
	new IEnumerator<T> GetEnumerator();
}

這裡協變有個特點,那就是協變引數T,只能用於返回型別。

原因是在執行時候還是new List(),返回自然是Student,那麼student 可以賦值給person,這沒問題。

那麼協變引數T,不能用於引數呢? 是這樣的。

比如 IEnumerable裡面有一個方法是:

public void test(T a)
{
    
}

在IEnumerable 中原本要傳入一個Student,現在使用了IEnumerable,那麼就可以傳入person。

person 要轉換成student,顯然是不符合的。

那麼協變是這樣的,那麼逆變呢?

public interface ITest<in T>
{
	public void Run(T obj);
}

public class Test<T> : ITest<T>
{
	public void Run(T obj)
	{
		throw new NotImplementedException();
	}
}

然後這樣使用:

static void Main(string[] args)
{
	ITest<Person> students = new Test<Person>();
	ITest<Student> peoples = students;
	peoples.Run(new Student());
}

這裡的逆變只能作用於引數。

先說一下為什麼能夠作用於引數,就是在執行的時候本質還是new Test(),要傳遞的是一個person,如果傳遞一個student,那麼也是可以的。

然後為什麼不能作用於返回值呢?

假如ITest 可以這樣:

public interface ITest<in T>
{
	public T Run()
	{ 
	}
}

在執行時候是Test(),那麼呼叫run返回的是person,但是賦值給了Student型別,和上面同樣的問題哈。

所以協變不能作用於引數,逆變不能作用於返回值。

那麼也就是說要摸只能協變,要摸只能逆變。

下面是委託中的逆變:

Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());

原理就是Derived繼承自Base,原本需要傳入base,現在傳入Derived,當然也是可以的。

之所以這麼設計是一個哲學問題,那就是子類可以賦值給父類,父類能辦到的子類也能辦到,他們分別對應的是協變和逆變。

下一節委託。

相關文章