CLR via C#學習筆記-第十二章-泛型基礎結構
12.2 泛型基礎結構
12.2.1 開放型別和封閉型別
具有泛型型別引數的型別仍然是型別,CLR同樣會為他建立內部的型別物件。
然而具有泛型型別引數的型別稱為開放型別,CLR禁制構造開放型別的任何例項。類似於CLR禁止構造介面型別的例項。
程式碼引用泛型類時可指定一組泛型型別實參。為所有型別引數都傳遞了實際的資料型別,型別就成為封閉型別。
CLR允許構造封閉型別的例項。然而程式碼引用泛型型別的時候,可能留下一些泛型型別實參未指定。
這會在CLR中建立新的開放型別物件,而且不能建立該型別的例項:
internal sealed class DictionaryStringKey<Tvalue> : Dictionary<String, Tvalue> { }public static class Program{ public static void Main(string[] args){ Object o = null; //1.Dictionary<,>是開放型別 Type t = typeof(Dictionary<,>); //建立例項,失敗 o = CreateInstance(t); //2.DictionaryStringKey<>是開放型別,有一個型別引數 t = typeof(DictionaryStringKey<>);//建立例項,失敗 o = CreateInstance(t); //3.DictionaryStringKey<Guid>是封閉型別 t = typeof(DictionaryStringKey<Guid>); //建立例項,成功 o = CreateInstance(t); } private static Object CreateInstance(Type t){ Object o = null; try{ o = Activator.CreateInstance(t); Console.WriteLine($"已建立{t.ToString()}的例項"); } catch(ArgumentException e){ Console.WriteLine(e.Message); } return o; } }
封閉型別靜態構造器的作用
每個封閉型別都有自己的靜態欄位。換言之,假如List<T>定義了任何靜態欄位,這些欄位不會在一個List<DateTime>或List<String>之間共享。
每個封閉型別物件都有自己的每個封閉型別,這樣構造器都會執行一次。
泛型型別定義靜態構造器的目的是保證傳遞的型別實參滿足特定條件。
例如我們可以像下面這樣定義只能處理列舉型別的泛型型別:
internal sealed class GenericTypeThatRequiresAnEnum{ static GenericTypeThatRequiresAnEnum(){ if(!typeof(T).IsEnum){ throw new ArgumentException("T must be an enumerated type"); } } }
CLR提供了名為約束的功能,可以更好地制定有效的型別實參。遺憾的是約束無法將型別實參限制為僅列舉型別。
所以上例需要用靜態構造器來保證型別是一個列舉型別。
12.2.2 泛型型別和繼承
泛型型別仍然是型別,所以能從其他任何型別派生。
使用泛型型別並指定型別實參時,實際是在CLR中定義一個新的型別物件,新的型別物件從泛型型別派生自的那個型別派生。
換言之,由於List<T>從Object派生,所有List<String>等也稱Object派生。
指定型別實參不影響繼承層次結構。
假定下面這樣定義一個連結串列節點類:
internal sealed class Node<T>{ public T m_data; public Node<T> m_next; public Node(T data):this(data,null){} public Node(T data,Node<T> next){ m_data=data; m_next=next; } public override String ToString(){ return m_data.ToString()+((m_next!=null)?m_next.ToString():String.Empty); } }
那麼可以寫程式碼來構造連結串列:
private static void SameDataLinkedList(){ Node<Char> head=new Node<Char>('C'); head=new Node<Char>('B',head); head=new Node<char>('A',head); Console.WriteLine(head.ToString);//顯示"ABC" }
泛型類繼承非泛型基類
在上面這個Node類中,對於m_next欄位引用的另一個節點來說,其m_data欄位必須包含相同的資料型別。
所以更好的辦法是定義非泛型Node基類,再定義非泛型TypedNode類繼承Node基類。
這樣就可以建立一個連結串列,其中每個結點都可以是一種具體的資料型別,除了不能是Object。
同時獲得編譯時的型別安全性,並防止值型別裝箱。下面是新的型別定義:
internal class Node{ public Node<T> m_next; public Node(Node<T> next){ m_next=next; } } internal sealed class TypedNode<T>:Node{ public T m_data; public Node(T data):this(data,null){} public Node(T data,Node<T> next){ m_data=data; } public override String ToString(){ return m_data.ToString()+((m_next!=null)?m_next.ToString():String.Empty); } }
現在可以寫程式碼建立一個連結串列,其中每個結點都是不同的資料型別。
private static void DifferentDataLinkedList(){ Node head=new TypedNode<char>('.'); head=new TypedNode<DateTime>(DateTime.Now,head); head=new TypedNode<String>("Today is",head); Console.WriteLine(head.ToString()); }
12.2.3 泛型型別同一性
錯誤的簡化
為了簡化下面這樣的程式碼:
List<DataTime> dt1=new Lis<DateTime>();
一些開發人員可能首先定義下面這樣的類:
internal sealed class DataTimeList:List<DateTime>{/*無需放入任何程式碼*/}
然後就可以簡化建立列表的程式碼了:
DateTimeList dt1=new DateTimeList();
這樣雖然方便了,但是絕對不要單純出於增強原始碼可讀性的目的來定義一個新類。
這樣會喪失同一性identity和相等性equivalence,如下所示:
Boolean sameType=(typeof(List<DateTime>)==typeof(DateTimeList));
上述程式碼執行時,sameType會被初始化為false,因為比較的是兩個不同的型別的物件。
也意味著如果方法的原型接受一個DateTimeList就不可以將一個DList<DateTime>傳給它。
然而如果方法的原型接受一個List<DateTime>,可以將一個DateTimeList傳給他,因為後者從前者派生。使人糊塗。
使用using簡化語法
C#允許使用簡化的語法來引用泛型封閉型別,同時不會影響型別的相等性,其要求在原始檔頂部使用傳統using指令:
using DateTimeList=System.Collection.Generic.List<System.DateTime>;
using指令實際定義的是名為DateTimeList的符號。
編譯時會將所有DateTimeList替換成System.Collection.Generic.List<System.DateTime>。
這樣型別的同一性和相等性得到了維持。
此外可以利用C#的隱式型別區域性變數功能,讓便一起根據表示式的型別來推斷方法的區域性變數的型別。