看完這篇,終於知道自己會不會 C# 泛型了
作者 | 羽生結弦
責編 | 胡巍巍
在開發過程中,同一段代碼處出現多次調用,並且會有不同的類型在使用,這種就叫做跨類型代碼復用。一般情況下跨類型代碼復用我們會用到如下兩種方法:
1. 繼承;
2. 泛型。
繼承是通過父類來代表代碼復用,而泛型是通過帶有佔位符的模板來代碼復用,其中佔位符指的是類型。
比如:int、syting和實體等。本片主要講解泛型的相關知識,下面就來詳細講解一下泛型。
零、泛型類型
泛型會聲明類型參數,消費者需要提供類型參數來把佔位符類型填充上。我們先來看一個例子:
public class GenericClass<T>
{
T tArray=new T[10];
int position=0;
public void Push(T t) =>tArray[position++]=t;
public T Pop=>tArray[--position];
public T GetTs => tArray;
}
class Program
{
static void Main(string[] args)
{
var genericStr=new GenericClass<string>;
generic.Push("張三");
generic.Push("李四");
generic.Push("王五");
generic.Pop;
var genericInt=new GenericClass<int>;
generic.Push(1);
generic.Push(2);
generic.Push(3);
generic.Pop;
}
}
在上面的代碼中當我們將string傳入泛型類中,將會隱式動態的創建類型,這種操作被稱為合成,合成將發生在運行時,而非編譯時。當我們在代碼中傳入非string類型的值時,在編譯時將報錯。同樣在上面的代碼中我們也看到了int傳入泛型類中的情況,這就說明泛型類可以跨類型復用。
小知識1:
我們將GenericClass<T>稱為開放類型(OpenType),而將GenericClass<string>稱為封閉類型(CloseType),開放類型在編譯後就變成了封閉類型,在運行時所有的泛型類型都是封閉類型,因為佔位符已經被具體類型填充完畢。
小知識2:
對於每一種封閉類型,靜態數據都是唯一的,例如:
class Program
{
static void Main(string[] args)
{
// 輸出1
Console.WriteLine(++MyClass<int>.Count);
// 輸出2
Console.WriteLine(++MyClass<int>.Count);
// 輸出1
Console.WriteLine(++MyClass<string>.Count);
}
}
class MyClass<T>
{
public static int Count;
}
上面代碼中,MyClass中存在一個靜態欄位Count,我們看到前兩次的調用輸出的分別是1和2,但是第三次輸出的確實1,那麼這是為什麼呢,原因就是前兩次的類型參數和泛型類型種的靜態欄位的類型一致,而第三次的類型參數不一致導致的。
在泛型類型餓子類中可以繼續讓父類參數保持開放,也剋及關閉父類的類型參數,同樣子類也可以引入新的類型,我們來看一下例子:
// 父類繼續保持開放
class Father<T>{}
class Children<T>:Father<T>{}
//關閉父類
class Father<T>{}
class Children:Father<string>{}
//引入新參數類型
class Father<T>{}
class Children<T,U>:Father<T>{}
小知識:
封閉參數類型的時候,該類型可以把自己作為具體的類型,例子如下:
class AClass<T>{}
class BClass : AClass<BClass> { }
泛型方法
泛型方法在方法簽名內聲明類型參數,例如下:
class Program
{
static void Main(string[] args)
{
GenericFun<int>(12, 4);
}
static void GenericFun<T>(T x, T y)
{
T tmp = x;
x = y;
y = tmp;
Console.WriteLine("x:" + x + " y:" + y);
}
}
在上面的代碼中我們看到 ```generic.GenericFun<int>(12,4);``` ,我們將int傳入泛型方法中,現在我們將這行代碼改寫成如下形式 ```generic.GenericFun(12,4);```。
我們發現現在這段代碼和前面那段代碼缺少了 <int> ,那麼這麼寫代碼是否有錯呢?
答案是要看情況,當編譯器可以推斷出參數類型的話,我們可以省略掉參數類型,但是當編譯器無法推斷出參數類型的話我們就必須寫上參數類型了。
當然上面這段代碼是可以正確運行的,因為編譯器可以正確的推斷出參數類型。我們還有如下幾點需要注意的:
1. 泛型類中的方法,如果方法引入了參數類型,那它就是泛型方法,反之就不是泛型方法;
2. 除了class、struct、interface、delegate 和方法可以引入類型參數外,屬性、欄位、索引器、事件和構造函數等都不能聲明類型參數,但是可以使用所在泛型類的類型參數。
小知識:
泛型類型和泛型方法可以有多個參數類型,例如 ```class a<T,U>``` 調用方法和單個參數類型一樣。結合這一點我們就可以推斷出泛型類型和泛型方法可以出現重載,只要參數類型的數量不同就沒問題。
在一些情況下我們需要獲取參數類型的默認值,這時我們就可以使用default(T)來獲得。
約束
我們雖然可以使用所有類型作為泛型類型參數,但是我們在實際開發時很少會這麼使用,一般會將類型參數約束到指定的範圍內。泛型可用約束如下:
1. base-class:某一個父類的子類;
2. interface:必須是實現了指定的介面;
3. class:必須是引用類型;
4. struct:必須是非空值類型;
5. new:必須包含無參構造函數;
6. U:T:U必須繼承T
我們使用的時候是這樣的:
class Aclass {}
interface Binterface { }
class Generic1<T> where T : Aclass { }
class Generic2<T,U>
where T:Aclass,Binterface
where U : new
{ }
上面的代碼表示 Generic1 類的參數類型T繼承自Aclass,Generic2 類的T繼承子Aclass,並且實現了 Binterface 介面,而且U包含了無參構造函數。
注意:約束可以用於泛型類型和泛型方法
類型參數與轉換
C#種轉換支持如下幾種:
1. 數值轉換;
2. 引用轉換;
3. 裝箱拆箱轉換;
4. 自定義轉換。
在發生編譯的時候,會根據一直類型的操作數來決定採用那種轉換,但是在泛型中我們不知道具體的類型是什麼,編譯器就會默認使用自定義轉換,那麼就有可能出現錯誤。
為了解決這個問題,我們引入了as ,例如我們將傳進來的值轉換成StringBuilder,這時我們可以這麼做:
StringBuilder ToFloat<T>(T t)
{
StringBuilder f = t as StringBuilder;
return f;
}
variance 轉換
講解variance 轉換前,先來簡單了解一下協變、逆變和不變。
1. 協變(Covariance):當T作為返回值輸出的時候;
2. 逆變(Contravariance):當T作為輸出值的時候;
3. 不變(Invariance):當T即是輸入又是輸出時。
注意:以上這三種就是variance,只能用在介面和委託中。
所謂variance 轉換就是上面三種之間的相互轉換,variance 轉換是引用轉換的一個方法,從a轉換到b如果是本體轉換或者隱式引用轉換,那麼就是正確的。例如:
IEnumerable<string> to IEnumerable<object>
IEnumerable<IDisposable> to IEnumerable<object>
作者簡介:朱鋼,筆名羽生結弦,CSDN博客專家,.NET高級開發工程師,7年一線開發經驗,參與過電子政務系統和AI客服系統的開發,以及互聯網招聘網站的架構設計,目前就職於北京恆創融慧科技發展有限公司,從事企業級安全監控系統的開發。
※不要讓 Chrome 成為下一個 IE
※2019 年度程序員吸金榜:你排第幾?
TAG:CSDN |