C#沉澱-泛型

泛型特性提供了一種優雅的方式,可以讓多個型別共享一組程式碼
泛型允許宣告 型別引數化 的程式碼,可以用不同的型別進行例項化
即使用“型別佔位符”來寫程式碼,然後在建立例項的時候指明真實的型別
泛型也是一種型別的模板
C#提供了5種泛型:類、結構、介面、委託和方法
通過一個示例來認識泛型:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { //定義一個泛型類 //使用<T>佔位符來定義這個類 public class MyClass<T> { T Value; public T GetValue(T x) { this.Value = x; return Value; } } class Program { static void Main(string[] args) { //例項化自定義的泛型類 //為佔位符提供真實型別 //這個型別被稱為構造型別 MyClass<int> mc = new MyClass<int>(); var y = mc.GetValue(5); //通過var自動推斷型別 Console.WriteLine("值:" + y + ",型別:" + y.GetType()); Console.ReadKey(); } } }
解釋:
- 在類後面放置<T>
- T代表型別佔位符
- T可以用任何識別符號,只要讓識別符號用<>包圍起來即可
- 在類的例項中,第一個T都會被替換成實際型別
泛型類的使用
泛型類的宣告
class SomeClass<T1, T2> { public T1 SomveVar = new T1(); public T2 OtherVar = new T2(); }
說明:
- 在類名後面放一組尖括號
- 在尖括號中使用逗號分隔希望提供的型別的佔位符,這叫做 型別引數
- 在泛型類宣告的主體中使用型別引數來表示替代的型別
- 這裡的
T1 ,T2
叫做型別引數的佔位符
構造型別
通過列出類名並在尖括號中提供真實型別來替代型別引數(這叫做 型別實參 ),來建立構造型別(用來建立真實物件的模板)
SomeClass<int, short>
上例中, int,short
是型別實參,將會替換型別引數 T1, T2
,
SomeClass<int, short>
是一個構造型別,了就是將要被建立的物件的一個模板
變數和例項
SomeClass<int, short> sc1; sc1 = new SomeClass<int, short>(); //或者 var sc2 = new SomeClass<int, short>();
可以通過構造型別來建立一個泛型類的例項,然後賦值給一個變數;也可以使用var讓編譯器自動推斷物件的型別
一個完整的程式碼
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { //宣告一個泛型類 class MyStack<T> { //宣告一個T型別的陣列 T[] StackArray; int StackPointer = 0; //向陣列中新增值 public void Push(T x) { if (!IsStackFull) StackArray[StackPointer++] = x; //向陣列中新增一個值 } //彈出陣列中的一個值 public T Pop() { return (!IsStackFull) ? StackArray[--StackPointer] : StackArray[0]; } //指定陣列最大長度 const int MaxStack = 10; //用於判斷陣列是否已滿 bool IsStackFull {get { return StackPointer >= MaxStack; }} //用於判斷陣列是否為空 bool IsStackEmpty {get { return StackPointer <= 0; }} //初始化類 public MyStack() { //例項化一個T型別的陣列 StackArray = new T[MaxStack]; } //逆向輸出陣列中的值 public void Print() { for (int i = StackPointer-1; i >= 0; i--) Console.WriteLine("value: {0}",StackArray[i]); } } class Program { static void Main(string[] args) { //建立一個int型別的MyStack物件 MyStack<int> static_int = new MyStack<int>(); //建立一個stirng型別的MyStack物件 MyStack<string> static_stirng = new MyStack<string>(); //新增數值並輸出 static_int.Push(3); static_int.Push(5); static_int.Push(7); static_int.Push(9); static_int.Print(); //新增字串並輸出 static_stirng.Push("Hello"); static_stirng.Push("World"); static_stirng.Print(); Console.ReadKey(); } } }
泛型類的特點:
- 不管構造型別的數量有多少,只需要一個實現
- 可執行檔案中只要出現有構造型別的型別
- 比較難寫,因為它比較抽象
- 但易於維護,因為只需要更改一個地方
型別引數的約束
如果使用泛型類的時候,傳遞了一個Int與一個string大小比較操作,則會報錯;所以需要對引數做出 約束
Where子句
型別引數的約束需要用到 Where子句
如果形參有多個約束,它們在Where子句中使用逗號分隔
Where子句語法如下:
where TypeParam: contraint, contraint, ...
- TypeParam表示型別引數
- contraint, contraint, ...表示約束列表
- 它們在型別引數列表的關閉尖括號之後列出
- 它們不使用逗號或其他符號分隔
- 它們可以以任何次序列出
- where是上下文關鍵字,所以可以在其他上下文中使用
示例:
class MyClass<T1, T2, T3> where T2: Customer where T3: ICustomer { ... }
約束型別和次序
- 類名 - 只有這個型別的類或從它繼承的類才能用作型別引數
- class - 任何引用型別,包括類、陣列、委託和介面都可以用作型別引數
- struct - 任何值型別都可以用作型別引數
- 介面名 - 只有這個介面或實現這個介面的型別才能用作型別引數
- new() - 任何帶有無參公共建構函式的型別都可以用作型別實參;這叫建構函式約束
如果最多隻能有一個主約束,如果有則必須放在第一位;可以有任意多的介面約束;如果存在建構函式約束,則必須放在最後
示例:
//T約定:只能是Access型別或者Access的子型別 public class BaseAccess<T> where T : Access {...} //T約定:T只能傳入介面的本身和實現了此介面的類 public class BaseAccess<T> where T : IAggregateRoot {...} //引用型別約束演示 public class BaseAccess<T> where T : class {...} //值型別約束演示 public class BaseAccess<T> where T : struct {...} //構造器約束 public class BaseAccess<T> where T : new() {...} //一個型別佔位符有兩個約束 //必須是引用型別,必須提供建構函式 public class BaseAccess<T> where T : class,new() {...} //K必須約定是一個引用型別 //V必須約定是一個值型別 public class BaseAccess<K, V> where K : class,new() where V : struct {...} //泛型引數K必須繼承V //K,V必須是引用型別,必須提供建構函式 public class BaseAccess<K, V> where K : V where K : class,new() where V : class,new() {...}
泛型方法
泛型方法具有兩個引數列表:
- 封閉在圓括號內的 方法引數列表
- 封閉在尖括號內的 型別引數列表
示例:
public void Func<S, T> (S s, T t) where S: Person {...}
解析:
在方法名稱之後方法引數列表之前放置型別引數列表
在方法引數列表之後放置可選的約束子句
呼叫泛型方法
語法:
Func<Person, int>(person, 5);
在呼叫的時候需要提供實參
一個完整的示例:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { class Program { //泛型方法 //約束T2必須是值型別的 static void Func<T1, T2>(T1 t1, T2 t2) where T2: struct { T1 valueA = t1; T2 valueB = t2; Console.WriteLine("valueA 值:"+valueA+", 型別:"+valueA.GetType()); Console.WriteLine("valueB 值:" + valueB + ", 型別:" + valueB.GetType()); } static void Main(string[] args) { //如果將第二個引數指定為非值型別的,將會報錯 //Func<string, string>("Hello","World"); Func<string, long>("Hello", 2000); Console.ReadKey(); } } }
讓編譯器推斷型別
public void Func<T>(T t) { T1 valueA = t1; Console.WriteLine("valueA 值:"+valueA+", 型別:"+valueA.GetType()); } int value = 15; //正常呼叫泛型方法 Func<int>(value); //讓泛型方法自動推斷引數型別以簡化程式碼 Func(value);
擴充套件方法和泛型類
擴充套件方法允許將類中的靜態方法關聯到不同的泛型類上
允許像呼叫類構造例項的例項方法一樣來呼叫方法
泛型類的擴充套件方法要求:
- 必須宣告static
- 必須是靜態類的成員
- 第一個引數型別中必須有關鍵字this,後面是擴充套件的泛型類的名字
示例:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { //建立一個靜態類 static class ExtendHoder { //建立一個靜態的列印方法,用來擴充套件Holder類 public static void Print<T>(this Holder<T> h) { //引數中需要指定被擴充套件的類,前面必須有this關鍵字 //訪問被擴充套件的類,從而針對性的做出一些操作 T[] vals = h.GetValues(); Console.WriteLine("{0}, \t{1}, \t{2}", vals[0], vals[1], vals[2]); } } //定義一個基本的泛型類 //本類將會被擴充套件 class Holder<T> { //一個泛型陣列 T[] Vals = new T[3]; //初始化泛型陣列 public Holder(T v1, T v2, T v3) { Vals[0] = v1; Vals[1] = v2; Vals[2] = v3; } //獲取泛型陣列方法 public T[] GetValues() { return Vals; } } class Program { static void Main(string[] args) { //例項化一個<int>構造型別的Holder物件 var intHolder = new Holder<int>(3, 5, 7); //例項化一個<string>構造型別的Holder物件 var stringHolder = new Holder<string>("a1", "b2", "c3"); //Holder類中本身是沒有Print方法的 //可以看出,通過擴充套件,可以在Holder類中訪問到擴充套件的方法 intHolder.Print(); stringHolder.Print(); Console.ReadKey(); } } }
在通過 Holer
的例項來呼叫擴充套件方法 Print
的時候,IDE會自動提示它是一個擴充套件方法
擴充套件方法的好處是可以在不更改原有方法的情況下,不其增加新的功能
泛型結構
與泛型類相似,泛型結構可以有型別引數和約束。泛型結構的規則和條件與泛型類是一樣的。
示例:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { //定義一個泛型結構 struct MyData<T> { T _data; //初始化結構中的資料 public MyData(T data) { this._data = data; } //獲取資料 public T Data { get { return _data; } set { _data = value; } } } class Program { static void Main(string[] args) { //例項化兩個不同型別的泛型結構結構 MyData<int> md_1 = new MyData<int>(5); MyData<string> md_2 = new MyData<string>("Hello"); //列印屬性值 Console.WriteLine(md_1.Data); Console.WriteLine(md_2.Data); Console.ReadKey(); } } }
泛型委託
泛型委託與非泛型委託非常相似,不過型別引數決定了能接受什麼樣的方法
語法:
delegate R DelName<T, R>(T value);
第一個 R
表示返回型別; <T, R>
表示型別引數; (T value)
是委託的形參
需要注意的是,這裡的型別引數列表負責的範圍包括:
- 返回值型別
- 形參列表型別
- 約束子句
示例:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { //定義一個泛型委託型別 delegate void MyDelegate<T>(T value); class Simple { //定義一個用於匹配委託的方法 static public void PrintString(string s) { //列印 Console.WriteLine(s); } //定義一個用於匹配委託的方法 static public void PrintUpperString(string s) { //列印為大寫形式 Console.WriteLine("{0}", s.ToUpper()); } } class Program { static void Main(string[] args) { //初始化一個泛型委託型別 var myDel = new MyDelegate<string>(Simple.PrintString); //新增另一個方法 myDel += Simple.PrintUpperString; //呼叫委託,並傳遞引數 myDel("Hello World!"); Console.ReadKey(); } } }
另一個示例:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { //通過型別引數列表來手指定返回型別 delegate R MyDelegate<T, R>(T value); class Simple { //委託的匹配方法 static public string GetString(int x) { return x.ToString(); } } class Program { static void Main(string[] args) { //int表示傳入的實參型別;string表示返回的型別 var myDel = new MyDelegate<int, string>(Simple.GetString); Console.WriteLine(myDel(100)); Console.ReadKey(); } } }
泛型介面
泛型介面允許我們編寫引數和介面成員返回型別是泛型型別引數的介面。泛型介面的宣告和非泛型型別介面的宣告差不多,但是需要在介面名稱之後的尖括號中放置型別引數
示例:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { // 定義一個泛型介面 interface IMyIfc<T> { T GetValue(T value); } //想要實現這個泛型介面的類必須是一個泛型類 class MyClass<S> : IMyIfc<S> { public S GetValue(S value) { return value; } } class Program { static void Main(string[] args) { var my_class = new MyClass<int>(); var value = my_class.GetValue(100); Console.WriteLine(value); var _class = new MyClass<string>(); var _value = _class.GetValue("Hello"); Console.WriteLine(_value); Console.ReadKey(); } } }
泛型介面的另一種用法:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { //定義一個泛型介面 interface IMyIfc<T> { T GetValue(T value); } //顯示的告訴要實現的介面是什麼型別 //MyClass則不必非得是一個泛型類 //並且可以實現多個型別的泛型介面 class MyClass : IMyIfc<int>, IMyIfc<string> { public int GetValue(int value) { return value; } public string GetValue(string value) { return value; } } class Program { static void Main(string[] args) { var my_class = new MyClass(); var value = my_class.GetValue(100); Console.WriteLine(value); var _class = new MyClass(); var _value = _class.GetValue("Hello"); Console.WriteLine(_value); Console.ReadKey(); } } }
當然,上面的示例在使用的時候一定要注意,如果要實現的多個介面有相同型別的,那就會實現出相同簽名與返回型別的方法,那到是會出現問題的,所以在使用的時候造成小心 (不過最新的C#已經避免了這種情況)
協變
相關概念:每一個變數都有一種型別,可以將其派生類物件的例項賦值給基類的變數,這叫做 賦值相容性 ,也就是說一個 狗狗型別的物件 可以賦值給一個 動物型別的變數
示例:下面程式碼存在錯誤
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { class Animal { public int Legs = 4; } class Dog : Animal { } delegate T Factory<T>(); class Program { static Dog MakeDog() { return new Dog(); } static void Main(string[] args) { Factory<Dog> dogMaker = new Factory<Dog>(MakeDog); Factory<Animal> animalMaker = dogMaker; //這裡的錯誤無法將Factory<Dog>隱式的轉換為Factory<Animal>型別 Console.WriteLine(animalMaker().Legs.ToString()); Console.ReadKey(); } } }
雖然 Dog
類是 Animal
的派生類,但是是委託 Factory<Dog>
沒有從委託 Factory<Animal>
派生,所以無法將將 Factory<Dog>
隱式的轉換為 Factory<Animal>
型別,賦值相容性自然不適用
示例:可以正常通過的程式碼
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { class Animal { public int Legs = 4; } class Dog : Animal { } delegate T Factory<out T>(); class Program { static Dog MakeDog() { return new Dog(); } static void Main(string[] args) { Factory<Dog> dogMaker = new Factory<Dog>(MakeDog); Factory<Animal> animalMaker = dogMaker; //這裡的錯誤無法將Factory<Dog>隱式的轉換為Factory<Animal>型別 Console.WriteLine(animalMaker().Legs.ToString()); Console.ReadKey(); } } }
這種情況叫做 協變 ,“協變”是指能夠使用與原始指定的派生型別相比,派生程度更大的型別,即由小變大
上例通過在型別引數中顯性的使用 out 關鍵字來支援協變
對於泛型型別引數, out 關鍵字指定該引數是協變的, in 關鍵字指定該引數是逆變的,它們可以在泛型與委託中使用
更多關於 協變 與 逆變 的知識,可以參考 ofollow,noindex">深入理解 C# 協變和逆變
逆變
瞭解了 協變 的道理,自然得知 逆變 就是由大變小的過程
“逆變”則是指能夠使用派生程度更小的型別。
示例:通過在型別引數中顯示的使用關鍵字 in 來支援逆變
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { class Animal { public int Legs = 4; } class Dog : Animal { } class Program { delegate void Action1<in T>(T a); static void ActOnAnimal(Animal a) { Console.WriteLine(a.Legs); } static void Main(string[] args) { Action1<Animal> act1 = ActOnAnimal; Action1<Dog> dog1 = act1; // 逆變 dog1(new Dog()); Console.ReadKey(); } } }
我對 out 與 in 的理解
- out 指示型別引數將只作為輸出使用,並且如果型別引數T僅在方法的返回值中出現,可通過 out 關鍵字來告訴編譯器,一些隱式轉換是合法的;
delegate T Factory<out T>();
表示T是協變的,所以Factory<Dog> dogMaker = new Factory<Dog>(MakeDog);
相當於轉換為Factory<Animal> dogMaker = new Factory<Animal>(MakeDog);
- in 指示型別引數將只作為輸入使用,可通過 in 關鍵字來告訴編譯器,一些隱式轉換是合法的;
delegate T Factory<out T>();
表示T是協變的,所以Factory<Animal> dogMaker = new Factory<Animal>(MakeDog);
相當於轉換為Factory<Dog> dogMaker = new Factory<Dog>(MakeDog);
介面的協變與逆變
直接上程式碼:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { //定義一個Animal基類 class Animal { public string Name; } //定義一個Dog類,派生於Animal類 class Dog : Animal { } //定義一個泛型介面,out指定型別引數是協變的,即可向上轉換 interface IMyIfc<out T> { T GetFirst(); } //定義一個泛型類,實現IMyIfc介面 class SimpleReturn<T> : IMyIfc<T> { public T[] items = new T[2]; public T GetFirst() { return items[0]; } } class Program { //定義一個方法,接收一個IMyIfc<Animal>型別的引數 static void DoSomething(IMyIfc<Animal> returner) { Console.WriteLine(returner.GetFirst().Name); } static void Main(string[] args) { //定義一個Dog型別引數的SimpleReturn類 SimpleReturn<Dog> dogReturner = new SimpleReturn<Dog>(); //將該類中的陣列新增一個值,這個值是個有名字的Dog類 dogReturner.items[0] = new Dog() { Name = "Hello"}; //IMyIfc<Animal>型別可以接收SimpleReturn<Dog>型別的物件 //協變的體現 IMyIfc<Animal> animalReturner = dogReturner; //呼叫DoSomething方法,傳入animalReturner物件 //體現協變 DoSomething(dogReturner); Console.ReadKey(); } } }
某些情況下,編譯器可以自動識別某個已構建的委託是協變或是逆變並且自動進行型別強制轉換,這通常發生在沒有為為物件的型別賦值的時候
示例:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CodeForT { class Animal { public int Legs = 4; } class Dog : Animal { } delegate T Factory<out T>(); class Program { static Dog MakeDog() { return new Dog(); } static void Main(string[] args) { //隱式的轉換,在這裡,委託型別中的引數沒有out關鍵字也是可以通過的 Factory<Animal> animalMaker = MakeDog; //MakeDog等於new Factory<Dog>(MakeDog);而這裡Dog引數隱匿的轉換為Animal //顯性轉換,需要out識別符號,如果委託型別中的引數沒有out引數是無法通過的 Factory<Dog> dogMaker = MakeDog; Factory<Animal> animalMaker2 = dogMaker; //顯性轉換的另一種寫法 Factory<Animal> animalMaker3 = new Factory<Dog>(MakeDog); Console.ReadKey(); } } }
有關可變性的注意事項
- 變化只適用於引用型別,因為不能直接從值型別派生其他型別
- 顯示變化使用in和out關鍵字只適用於委託和介面,不適用於類、結構和方法
- 不包括in和out關鍵字的委託和介面型別引數叫做 不變