C#筆記 – 泛型
泛型類型與泛型參數
-
泛型無論是否有傳遞實際的數據類型進去,只要具有泛型類型參數,它就是一個類型。
-
CLR會為它創建內部的類型對象
-
適用於引用、值、接口、委托類型
-
-
類型參數「接受」信息;類型實參「提供」信息
- 類型實參必須為類型,編譯時才能知道類型實參的信息
-
開放類型(未綁定泛型類型(unbound generic type))
-
只有泛型參數,沒有具體的類型實參
-
CLR禁止構造開放類型的實例
- 代碼如果在引用泛型類型時,留下一些未指定類型實參的泛型類型,會在CLR中創建新的開放類型對象,因此也不能創建該類型的實例
-
class Program { static void Main(string[] args) { //未指定類型實參,開放類型 Type t = typeof(DictionaryStringKey<>); } } //部分指定的開放類型 public sealed class DictionaryStringKey<TValue> : Dictionary<string, TValue> { }
-
-
封閉類型( 已構造類型(constructed type))
-
指定了實際的數據類型
-
CLR允許構造封閉類型的實例
-
class Program { static void Main(string[] args) { //指定了類型實參,封閉類型 t = typeof(DictionaryStringKey<int>); } } //部分指定的開放類型 public sealed class DictionaryStringKey<TValue> : Dictionary<string, TValue> { }
-
-
類型名以 ” ’ ‘“字符和一個數字結尾
-
數字 => 類型參數數量
-
TypeDef #2 (02000003) ------------------------------------------------------- TypDefName: CLR_Ch12.DictionaryStringKey`1 (02000003) Flags : [Public] [AutoLayout] [Class] [Sealed] [AnsiClass] [BeforeFieldInit] (00100101) Extends : 1B000001 [TypeSpec] [TypeSpec] 1 Generic Parameters (0) GenericParamToken : (2a000001) Name : TValue flags: 00000000 Owner: 02000003
-
-
聲明泛型類時,會聲明出尖括號以及裡面的類型實參,但泛型類的構造函數並不會在尖括號裡列出類型參數
-
因為類型參數從屬於類型,而不是從屬於某個特定的構造函數,所以才只會在聲明類型時聲明
-
成員方法僅在引入新的類型參數時才需要聲明
-
-
泛型類型可以重載
-
可以定義 MyType, MyType<T, U>, MyType<T, U, V>…這些定義都可以放到一個命名空間內
-
泛型類型的名稱不重要,重要的是類型參數個數
-
除了類型參數的數量,兩個方法的簽名可以完全相同
-
泛型類型的繼承
-
泛型類型依然是類型,為泛型類型指定類型實參時。如果該泛型類型繼承自某個基類,指定類型實參後的泛型類型同樣會繼承自該基類
-
class Program { static void Main(string[] args) { BaseClass<SonA> s = new BaseClass<SonA>(); //inherit from Root Console.WriteLine(s.prop); //1 BaseClass<SonB> i = new BaseClass<SonB>(); //inherit from Root Console.WriteLine(i.prop); //1 } } public class Root { public int prop = 1; } public class BaseClass<T> : Root { } public class SonA { } public class SonB { }
-
-
因此,可以以一個非泛型類型的類作為基類,然後由一個泛型類型繼承自該基類。從而可以使多個不同類型實參的泛型類型之間相互聯繫
-
public class Node { protected Node next; public Node(Node next) { this.next = next; } } public class GenericNode<T> : Node { public T data; public GenericNode(T data) : this(data, null) { } public GenericNode(T data, Node next) : base(next) { this.data = data; } } class Program { static void Main(string[] args) { Node head = new GenericNode<char>('.'); head = new GenericNode<string>("Hello World"); head = new GenericNode<int>(10); } }
-
泛型委托與接口
-
泛型接口
-
public interface IEnumerator<T> : IDisposable, IEnumerator{ T Current { get; } } -
接口的可變性
-
C#4 能使用out修飾符指定類型參數的協變性;使用in修飾符指定逆變性
-
聲明完成後,就可以對相關的類型進行隱式轉換
-
使用in和out表示可變性
-
//協變 public interface IEnumerable<out T> //逆變 public interface IComparer<in T> -
如果類型參數只用於輸出,就使用out;如果只用於輸入,就用in
-
-
class Circle : IShape { } class Square : IShape { } List<Circle> circles = new List<Circle> { new Circle(); new Circle(); } List<Square> squares = new List<Square> { new Square(); new Square(); } -
協變性
-
List<IShape> shapesByAdding = new List<IShape>(); shapesByAdding.AddRange(circles); shapesByAdding.AddRange(squares); List<IShape> shapeByConcat = circles.Concat<IShape>(squares).ToList(); -
每次將circle和square轉換為IShape時,都用到了協變性
-
List
.AddRange的參數為IEnumerable 類型,因此這種情況下會將circles和squares看成IEnumerable -
circles.Concat
(squares)也會根據協變性而隱式轉換為IEnumerable
-
-
這種轉換不會改變他們的值,只是改變了編譯器如何看待這些值
-
-
逆變性
-
class AreaComparer : IComparer<IShape> { public int Compare(IShape x, IShape y) { return x.Area.CompareTo(y.Area); } } IComparer<IShape> areaComparer = new AreaComparer(); circles.Sort(areaComparer); -
當circle調用Sort時,原本應該為IComparer
的參數會隱式轉換為IComparer 類型
-
-
-
-
泛型委托
-
public delegate TReturn TestDelegate<TReturn, TKey>(TKey key); -
委托的可變性
-
delegate T Func<out T>(); delegate void Action<in T>(T obj); -
Func<Square> squareFactory = () => new Square(); Func<IShape> shapeFactory = squareFactory; //使用協變性轉換Func<T> Action<IShape> shapePrinter = shape => Console.WriteLine(shape.Area); Action<Circle> circlePrinter = shapePrinter; //使用逆變性轉換Action<T>
-
-
CLR支持泛型委托,目的是保証任何類型的對象都能以類型安全的方法傳給回調方法
- 同時,允許了值類型實例在傳給回調方法時不進行任何裝箱
-
委托實際上是提供了4個方法的一個類定義:
-
構造器
-
Invoke方法
-
BeginInvoke方法
-
EndInvoke方法
-
-
如果定義的委托類型指定了類型參數,編譯器會定義委托類的方法,用指定的類型參數替換方法的參數類型和返回值類型,上述的委托定義將會被轉換成:
-

逆變和協變泛型類型實參
-
委托的泛型類型參數可標記為協變量/逆變量,從而支持把泛型委托類型的變量轉換為相同的委托類型(泛型參數類型不同)
-
不變量
- 雙向傳遞的值
-
逆變量:
- 指定泛型類型參數可轉換為它的一個子類(父 -> 子)
-
使用in關鍵字進行標記
-
只能出現在輸入位置,如作為方法的參數
- 從API返回的值,向調用者返回一個更一般的類型的值
-
- 指定泛型類型參數可轉換為它的一個子類(父 -> 子)
-
協變量
-
指定泛型類型參數可轉換為它的一個父類(子 -> 父)
-
使用out關鍵字進行標記
-
只能出現在輸出位置,如作為方法的返回值
- 傳入API的值,調用者可傳入一個更特定的類型的值
-
-
-
例如FCL預定義的泛型委托:Func
-
public delegate TResult Func<in T, out TResult>(T arg);-
T 使用 in關鍵字標記,成為逆變量
-
TResult 使用 out關鍵字標記,成為協變量
-
因此,這樣定義的一個委托:
-
Func<Object, ArgumentException> fn1 = null
-
-
不需要顯示轉型就可以轉為這樣的一個委托
-
//Base:Object -> Inherit:String, using in keyword //Inherit:ArgumentException -> Base:Exception, using out keyword Func<string, Exception> fn2 = fn1;
-
-
-
-
複雜情況
- 同時使用協變和逆變
Converter<object, string> converter = x => x.ToString(); //input object, output string
Converter<string, string> contravariance = converter; //逆變 input object => string ; 由於input被使用的時候,只會被輸入,因此可以逆變
Converter<object, object> covariance = converter; //協變 output string => object ; 由於output只會被返回出去,因此可以協變
Converter<string, object> both = converter; //同時使用逆變(object => string)和協變(string => object)
-
可變性的限制
-
不支持類的類型參數可變性
- 只有接口和委托可以擁有可變的類型參數
-
可變性只有在編譯器能驗證類型之間存在引用轉換時才有用
-
而類型之間的引用轉換驗證需要裝箱
-
因此,禁止任何值類型和用戶自定義的轉換
-
//Invalid Transition IEnumerable<int> => IEnumerable<object> //裝箱轉換 IEnumerable<short> => IEnumerable<int> //值類型轉換 IEnumerable<string> => IEnumerable<XName> //用戶自定義轉換
-
-
-
另一方面,可以理解成任何使用了協變和逆變的轉換都是引用轉換
- 因此,轉換之後將返回相同的引用。它不會創建新的對象,只是認為現有引用與目標類型匹配。
-
-
對於泛型類型參數,如果要將該類型的實參傳給使用out/ref關鍵字的方法,便不允許可變性
-
使用out/ref關鍵字的方法參數,必須是不變量
-
delegate void SomeDelegate<in T>(ref T t); //Can't Compile
-
-
out參數不是輸出參數
-
delegate bool TryParser<T>(string input, out T value); -
在這裡T並不是協變的
-
CLR不了解out參數,在它看來,out參數只是應用了[Out]特性的ref參數
-
C#以明確賦值(out)的方式為特性附加特殊含義,但CLR沒有
-
ref參數意味著數據是雙向傳遞的,所以如果類型T為ref參數,也就意味著T是不變的
-
-
-
可變性必須顯式指定
-
注意破壞性修改
- 每當新的轉換可用,當前代碼都有被破壞的風險
-
多播委托與可變性不能混用
-
當多個可變委托類型組合時,雖然可以通過編譯,但因無法確定要創建甚麼類型的委托
-
Func<string> stringFunc = () => ""; Func<object objectFunc = () => new object(); Func<object> combiner = objectFunc + stringFunc; -
將Func
類型的表達式轉換為Func
-
-
-