SpringCloud微服務初體驗
泛型(generic)是C#語言2.0和通用語言執行時(CLR)的一個新特性。泛型為.NET框架引入了型別引數(type parameters)的概念。型別引數使得設計類和方法時,不必確定一個或多個具體引數,其的具體引數可延遲到客戶程式碼中宣告、實現。泛型允許我們宣告型別引數化( type-parameterized)的程式碼,可以用不同的型別進行例項化。也就是說,我們可以用“型別佔位符”來寫程式碼,然後在建立類的例項時指明真實的型別。這意味著使用泛型的型別引數T,寫一個類MyList<T>,客戶程式碼可以這樣呼叫:MyList<int>, MyList<string>或 MyList<MyClass>。這避免了執行時型別轉換或裝箱操作的代價和風險
泛型概述
泛型廣泛用於容器(collections)和對容器操作的方法中。.NET框架2.0的類庫提供一個新的名稱空間System.Collections.Generic,其中包含了一些新的基於泛型的容器類。
針對早期版本的通用語言執行時和C#語言的侷限,泛型提供了一個解決方案。以前型別的泛化(generalization)是靠型別與全域性基類System.Object的相互轉換來實現。.NET框架基礎類庫的ArrayList容器類,就是這種侷限的一個例子。ArrayList是一個很方便的容器類,使用中無需更改就可以儲存任何引用型別或值型別。但是這種便利是有代價的,這需要把任何一個加入ArrayList的引用型別或值型別都隱式地向上轉換成System.Object。如果這些元素是值型別,那麼當加入到列表中時,它們必須被裝箱;當重新取回它們時,要拆箱。型別轉換和裝箱、拆箱的操作都降低了效能
另一個侷限是缺乏編譯時的型別檢查,當一個ArrayList把任何型別都轉換為Object,就無法在編譯時預防類似這樣的操作:比如,客戶程式碼把string和int變數放在一個ArrayList中,這是不合理的。ArrayList和其他相似的類真正需要的是一種途徑,能讓客戶程式碼在例項化之前指定所需的特定資料型別。這樣就不需要向上型別轉換為Object,而且編譯器可以同時進行型別檢查。換句話說,ArrayList需要一個型別引數。這正是泛型所提供的。與ArrayList相比,在客戶程式碼中唯一增加的List<T>語法是宣告和例項化中的型別引數。程式碼略微複雜的回報是,你建立的表不僅比ArrayList更安全,而且明顯地更加快速
測試證明,使用泛型,比傳統的靠型別與全域性基類System.Object的相互轉換要節省一半的時間。
C#中的泛型
C#提供了5種泛型:類、結構、介面、委託和方法。注意,前面4個是型別,而方法是成員。在泛型型別或泛型方法的定義中,型別引數是一個佔位符(placeholder),通常為一個大寫字母,如T。
約束
若要檢查表中的一個元素,以確定它是否合法或是否可以與其他元素相比較,那麼編譯器必須保證:客戶程式碼中可能出現的所有型別引數,都要支援所需呼叫的操作或方法。這種保證是通過在泛型類的定義中,應用一個或多個約束(constrain)而得到的。一個約束型別是一種基類約束,它通知編譯器,只有這個型別的物件或從這個型別派生的物件,可被用作型別引數。一旦編譯器得到這樣的保證,它就允許在泛型類中呼叫這個型別的方法。上下文關鍵字where用以實現約束。
共有5種類型的約束:
約束 | 描述 |
---|---|
where T: struct | 型別引數必須為值型別。 |
where T : class | 型別引數必須為引用型別。 |
where T : new() | 型別引數必須有一個公有、無參的建構函式。當於其它約束聯合使用時,new()約束必須放在最後。 |
where T : <base class name> | 型別引數必須是指定的基型別或是派生自指定的基型別。 |
where T : <interface name> | 型別引數必須是指定的介面或是指定介面的實現。可以指定多個介面約束。介面約束也可以是泛型的。 |
型別引數的約束,增加了可呼叫的操作和方法的數量。這些操作和方法受約束型別及其派生層次中的型別的支援。因此,設計泛型類或方法時,如果對泛型成員執行任何賦值以外的操作,或者是呼叫System.Object中所沒有的方法,就需要在型別引數上使用約束。
泛型類
泛型類封裝了不針對任何特定資料型別的操作。泛型類常用於容器類,如連結串列、雜湊表、棧、佇列、樹等等。這些類中的操作,如對容器新增、刪除元素,不論所儲存的資料是何種型別,都執行幾乎同樣的操作。
對大多數情況,推薦使用.NET框架2.0類庫中所提供的容器類。當建立一個簡單的泛型類和宣告普通類差不多,區別如下。
- 在類名之後放置一組尖括號。
- 在尖括號中用逗號分隔的佔位符字串來表示希望提供的型別。這叫做型別引數(type parameter)
- 泛型類宣告的主體中使用型別引數來表示應該替代的型別
泛型方法
與其他泛型不一樣,方法是成員,不是型別。泛型方法可以在泛型和非泛型類以及結構和介面中宣告。
另外在擴充套件方法中,它也可以和泛型類結合使用。它允許我們將類中的靜態方法關聯到不同的泛型類上,還允許我們像呼叫類構造例項的例項方法一樣來呼叫方法。
和非泛型類一樣,泛型類的擴充套件方法:
- 必須宣告為 static
- 必須是靜態類的成員
- 第一個引數型別中必須有關鍵字this,後面是擴充套件的泛型類的名字。
如下程式碼給出了一個叫做Print的擴充套件方法,擴充套件了叫做 Holder<T>的泛型類。
static class ExtendHolder
{
public static void Print<T>(this Holder<T> h)
{
T[] vals = h.GetValues();
Console.WriteLine("{0}\t{1}\t{2}, vals[0], vals[1], vals[2]", vals[0], vals[1], vals[2]);
}
}
class Holder<T>
{
T[] Vals = new T[3];
public Holder(T v0, T v1, T v2)
{
Vals[0] = v0;
Vals[1] = v1;
Vals[2] = v2;
}
public T[] GetValues()
{
return Vals;
}
}
class Program
{
static void Main(string[] args)
{
var intHolder = new Holder<int>(3, 5, 7);
var stringHolder = new Holder<string>("a1", "b2", "b3");
intHolder.Print();
stringHolder.Print();
}
}
泛型結構
與泛型類相似,泛型結構可以有型別引數和約束。泛型結構的規則和條件與泛型類是一樣的。
泛型委託
無論是在類定義內還是類定義外,委託可以定義自己的型別引數。引用泛型委託的程式碼可以指定型別引數來建立一個封閉構造型別,這和例項化泛型類或呼叫泛型方法一樣。
泛型介面
不論是為泛型容器類,還是表示容器中元素的泛型類,定義介面是很有用的。把泛型介面與泛型類結合使用是更好的用法,比如用IComparable<T>而非IComparable,以避免值型別上的裝箱和拆箱操作。
泛型介面允許我們編寫引數和介面成員返回型別是泛型型別引數的介面。泛型介面的宣告和非泛型介面的宣告差不多,但是需要在介面名稱之後的尖括號中放置型別引數。
泛型程式碼中的 default 關鍵字
在泛型類和泛型方法中會出現的一個問題是,如何把預設值賦給引數化型別,此時無法預先知道以下兩點:
l T將是值型別還是引用型別
l 如果T是值型別,那麼T將是數值還是結構
對於一個引數化型別T的變數t,僅當T是引用型別時,t = null語句才是合法的; t = 0只對數值的有效,而對結構則不行。這個問題的解決辦法是用default關鍵字,它對引用型別返回空,對值型別的數值型返回零。而對於結構,它將返回結構每個成員,並根據成員是值型別還是引用型別,返回零或空。
執行時中的泛型
當泛型類或泛型方法被編譯為微軟中間語言(MSIL)後,它所包含的元資料定義了它的型別引數。根據所給的型別引數是值型別還是引用型別,對泛型型別所用的MSIL也是不同的。
當第一次以值型別作為引數來構造一個泛型型別,執行時用所提供的引數或在MSIL中適當位置被替換的引數,來建立一個專用的泛型型別。
例如,假設你的程式程式碼聲名一個由整型構成的棧,如:
Stack<int> stack;
此時,執行時用整型恰當地替換了它的型別引數,生成一個專用版本的棧。此後,程式程式碼再用到整型棧時,執行時複用已建立的專用的棧。下面的例子建立了兩個整型棧的例項,它們共用一個Stack
Stack<int> stackOne = new Stack<int>();
Stack<int> stackTwo = new Stack<int>();
然而,如果由另一種值型別——如長整型或使用者自定義的結構——作為引數,在程式碼的其他地方建立另一個棧,那麼執行時會生成另一個版本的泛型型別。這次是把長整型替換到MSIL中的適當的位置。由於每個專用泛型類原本就包含值型別,因此不需要再轉換。
對於引用型別,泛型的工作略有不同。當第一次用任何引用型別構造泛型類時,執行時在MSIL中建立一個專用泛型類,其中的引數被物件引用所替換。之後,每當用一個引用型別作為引數來例項化一個已構造型別時,就忽略其型別,執行時複用先前建立的專用版本的泛型類。這可能是由於所有的引用的大小都相同。
此外,當用型別引數實現一個泛型C#類時,想知道它是指型別還是引用型別,可以在執行時通過反射確定它的真實型別和它的型別引數。
協變與逆變
逆變與協變只能放在泛型介面和泛型委託的泛型引數裡面,
在泛型中out修飾泛型稱為協變,協變修飾返回值 ,協變的原理是把子類指向父類的關係,拿到泛型中;在泛型中in 修飾泛型稱為逆變, 逆變修飾傳入引數,逆變的原理是把父類指向子類的關係,拿到泛型中。
當建立泛型型別的例項時,編譯器會接受泛型型別宣告以及型別引數來建立構造型別。但是,大家通常會犯的一個錯誤就是將派生型別分配給基型別的變數。
如下:
public class Bird
{
public int Id { get; set; }
}
public class Sparrow : Bird
{
public string Name { get; set; }
}
當申明類Bird 以及 其子類Sparrow,我們可以建立其例項bird1,bird2。
Bird bird1 = new Bird();
Bird bird2 = new Sparrow();
每一個變數都有一種型別,我們可以將派生類物件的例項賦值給基類的變數,這叫做賦值相容性。基類是Bird,有一個Sparrow類從Bird類派生。我們可以建立了一個Sparrow型別的物件,並且將它賦值給Bird型別的變數bird2。
但當建立一個具有Bird的List陣列時,如下:
List<Bird> birdList1 = new List<Sparrow>();
則會出現問題。 這是為什麼呢?難道賦值相容性的原則不成立了?
不是,這個原則還是成立,但是對於這種情況不適用!問題在於儘管Sparrow是Bird的派生類但是List
如果我們通過增加out關鍵字改變介面宣告,並通過介面來例項化List,程式碼就可以通過編譯了,並且可以正常工作,如下:
using System.Collections;
using System.Collections.Generic;
namespace System.Collections.Generic
{
public interface IEnumerable<out T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
}
此時,就可以將具有Sparrow的List物件例項化為IEnumerable
IEnumerable<Bird> birdList1 = new List<Bird>();
IEnumerable<Bird> birdList2 = new List<Sparrow>();
這就是協變(convariance)。
逆變(contravariance)則相反,增加in關鍵字改變口宣告時,
協變和逆變的意義在於避免不必要的型別轉換,簡化程式碼和提高效能。
有關可變性的其他一些重要的事項如下。
- 變化處理的是使用派生類替換基類的安全情況,反之亦然。因此變化只適用於引用型別,因為不能從值型別派生其他型別。
- 顯式變化使用in和out關鍵字只適用於委託和介面,不適用於類、結構和方法。
- 不包括in和out關鍵字的委託和介面型別引數叫做不變。這些型別引數不能用於協變或逆變。