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<T>.AddRange的參數為IEnumerable<T>類型,因此這種情況下會將circles和squares看成IEnumerable<IShape>

          • circles.Concat<IShape>(squares)也會根據協變性而隱式轉換為IEnumerable<IShape>

        • 這種轉換不會改變他們的值,只是改變了編譯器如何看待這些值

      • 逆變性

        • 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<IShape>的參數會隱式轉換為IComparer<Circle>類型

  • 泛型委托

    • 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<string>類型的表達式轉換為Func<object>是協變的引用轉換。但對象本身仍然為Func<string>,并且實際進行處理的Delegate.Combine方法要求參數必須為相同的類型

        • 因此執行時會報錯

      • 只能基於可變委托新建一個類型正確的委托對象,然後再與同一類型的另一個委托進行組合

        • Func<string stringFunc = () => "";
          Func<object> defensiveCopy = new Func<object>(stringFunc);
          Func<object> objectFunc = () => new object();
          Func<object> combiner = objectFunc + defensiveCopy;
    • 不存在調用者指定的可變性,也不存在部分可變性

泛型方法類型推斷
  • 返回類型(泛型列表) 方法名   類型參數     參數類型(泛型委托)   參數名
    List<TOutput> ConvertAll<TOutput>(Converter<T, TOutput> conv);
    • 類型推斷只適用於泛型方法,不適用於泛型類型

    • 根據方法實參,對泛型方法類型實參進行推斷,並驗證所有結果是否一致

    • 不能讓編譯器推斷一部分,自己顯式指定一部分。要麼全部推斷,要麼全部顯式指定

  • 調用泛型方法時,C#編譯器會自動判斷要使用的類型

    • static void CallingSwapUsingReference()
      {
         int n1 = 1;
         int n2 = 2;
         Swap(ref n1, ref n2);

         string s1 = "Alan";
         object s2 = "Walker";
         Swap(ref s1, ref s2); //無法推斷
      }

      static void Swap<T>(ref T o1, ref T o2)
      {
         T temp = o1;
         o1 = o2;
         o2 = temp;
      }
    • 推斷類型時,C#會使用變量的數據類型,而不是變量引用的對象的實際類型

      • 在string和object的例子中,雖然s2引用的對象的實際類型為string,但其變量類型為object

        • 所以,由於object和string為不同的類型,Swap將無法推斷其泛型類型參數應該用哪個類型實參

  • 如果存在一個接受具體類型的方法和一個泛型方法,C#編譯器會優先考慮更明確的匹配,再考慮泛型匹配

    • 如果方法調用明確使用了類型實參,編譯器就不會去推斷類型實參,而是直接使用指定的類型實參

      • 如:

        • Swap<string>(ref s3, ref s4)
    • static void CallingSwapUsingReference()
      {
         int n1 = 1;
         int n2 = 2;
         
         string s3 = "Hello";
         string s4 = "World";

         Swap(ref s3, ref s4); //Using Swap(string, string)
         Swap(ref n1, ref n2); //Using Swap<T>(T, T)
         
         //該調用告訴編譯器不要嘗試推斷類型,應使用顯式指定的類型實參
         Swap<string>(ref s3, ref s4); //Using Swap<T>(T, T)
      }

      //This one will be chosen
      static void Swap(ref string s1, ref string s2)
      {
         string temp = s1;
         s1 = s2;
         s2 = temp;
      }

      static void Swap<T>(ref T o1, ref T o2)
      {
         T temp = o1;
         o1 = o2;
         o2 = temp;
      }
泛型約束
  • 使用where關鍵字
  • 通過泛型約束,可以限制能指定成泛型類型實參的類型數量,從而與「編譯器」約定類型實參的類型,換回來編譯器提供的,這些類型的功能。

    • 如果沒有約束,編譯器將無法確定某些操作是否能適用於所有可能的類型實參,使得很多特定的操作無法通過編譯

    • 不約束

      • //無法通過編譯
        static T Min<T>(T o1, T o2)
          {
               if(o1.CompareTo(o2) < 0)
              {
                   return o1;
              }
               return o2;
          }
    • 約束

      • //編譯通過且有效
        static T Min<T>(T o1, T o2)
           where T : IComparable
          {
               if(o1.CompareTo(o2) < 0)
              {
                   return o1;
              }
               return o2;
          }
  • 約束可應用於泛型類型、方法的類型參數,但是不可以通過約束來進行方法重載

    • //無效
      static T Min<T>(T o1, T o2)
         where T : IComparable
        {
             if(o1.CompareTo(o2) < 0)
            {
                 return o1;
            }
             return o2;
        }

      static T Min<T>(T o1, T o2)
      {
         return default;
      }
    • 泛型方法的重載只能基於類型參數個數實行

  • 重寫虛泛型方法時,重寫的方法必須指定相同數量的類型參數,且類型參數會繼承在基類方法上指定的約束

    • public class ConstrictBase
      {
         public virtual void VirtualGeneric<T>(T arg)
             where T : class { }
      }

      public class ConstrictInherit : ConstrictBase
      {
         //Compile Failed
         //public override void VirtualGeneric<T>(T arg)
         //   where T : Node
         //{
         //   base.VirtualGeneric(arg);
         //}
      }
  • 類型參數有三種約束:

    • 主要約束

    • 次要約束

    • 構造器約束

主要約束
  • 可指定0個或1個

  • 代表非密封類的引用類型

    • 如果沒有指定,則默認為Object

  • 指定引用類型約束時,相當於向編譯器承諾:一個指定的類型實參要麼是與約束類型相同的類型,要麼是從約束類型派生的類型

  • //Main Constrict
    public class Primary<T>
       where T : class
      { }    
    • class 和 struct是兩個特殊的主要約束

      • class承諾類型實參是引用類型,確保使用的類型是引用類型(類、接口、數組、委托等等)

        • struct RefSample<T> where T : class
        • 雖然被約束的類型為引用類型,但是RefSample本身依然是struct(值類型)

          • 只是在API中指定T的地方要使用引用類型

          • 引用類型參數可以使用 == 和 != 來比較引用

            • 可以對使用該類型實參數據類型的屬性進行空判斷

        • 如果不約束,類型實參可能為值類型,因而不能判空 

      • struct承諾類型實參是值類型,確保使用的是值類型

        • class ValSample<T> where T : struct
        • 雖然被約束的類型為值類型,但是ValSample本身依然為class(引用類型)

          • 只是在API中指定T的地方要使用值類型

          • 值類型參數不能使用 == 和 != 來比較

        • 可空值類型(Nullable<T>,如int?)為特殊類型,不屬於struct

        • 事實上Nullable<T>自身限制了類型實參為struct類型,因此不可以用可空值類型作為約束,否則會出現遞歸類型 => Nullable<Nullable<T>>

次要約束
  • 可指定0個或n個

  • 代表接口類型

  • 指定該約束相當於向編譯器承諾:類型實參實現了某接口

  • //Vice Constrict
    public class Secondary<T>
       where T:IComparable, IEnumerable<T>        
      { }
  • 特殊次要約束:類型參數約束(祼類型約束)

    • 該約束允許一個泛型類型/方法規定:指定的類型實參要麼就是約束的類型,要麼是約束的類型的派生類

    • 一個類型參數可以指定1~n個類型參數約束

    • public class NakedConstrict<T, TBase, TRoot>
         where T : TBase, TRoot
        { }
構造器約束
  • 可指定0~1個

  • 承諾類型實參是實現了公共無參構造器的非抽象類型,

  • 使得指定的類型實參對象可被實例化

  • //Constructor Constrict
    public class Construct<T>
       where T:new()
      {
           public T NewObject()
          {
               return new T();
          }
      }
  • 適用於:

    • 所有值類型

    • 沒有顯式聲明構造函數的非靜態、非抽象類

    • 顯式聲明了一個公共無參構造函數的非抽象類

    • public T CreateInstance<T>() where T : new()
      {
         return new T();
      }
  • 但是沒有辦法規定類型必須具備其他構造函數簽名

轉換類型約束

  • 允許指定另一個類型,規定類型實參必須可以通過一致性/引用/裝箱轉換隱式地轉換為該類型

  • class Sample<T> where T : Stream => Sample<Stream> //一致性轉換
    struct Sample<T> where T : IDisposable => Sample<SqlConnection> //引用轉換
    class Sameple<T> where T : IComparable<T> => Sample<int> //裝箱轉換
    class Sample<T, U> where T : U => Sample<Stream, IDisposable> //引用轉換
  • 可以指定多個接口,但只能指定一個類

  • 指定的類型不可以是以下類型:

    • 結構(struct)

    • 密封類(sealed)

    • System.Object

    • System.Enum

    • System.ValueType

    • System.Delegate

  • 無法指定枚舉和委托的約束,看上去是CLR的限制,但實際上,如果在IL中構造適當的代碼,是可以工作的。

組合約束
  • 每個類型參數的約束列表都要單獨用一個where引入

  • class Sample<T> where T : class, IDisposable, new()
    class Sample<T> where T : struct, IDisposable
    class Sample<T, U> where T : class where U : struct, T
    class Sample<T, U> where T : Stream where U : IDisposable
其他可驗證性問題
  • 泛型類型變量的轉型

    • 除非有泛型約束,且轉型目標類型與約束兼容,否則轉型為非法

  • 泛型變量的默認值

    • 使用default關鍵字

      • default(T);
      • 告訴C#編譯器和CLR的JIT編譯器,如果T是引用類型,就把泛型變量設為null;如果是值類型,就設為0

  • 泛型變量空判斷

    • 即使泛型類型沒被約束(類型實參可能為值類型/引用類型),使用== 或 !=將泛型類型變量與null進行判斷都是合法的

      • static void CompareWithNoConstrict<T>(T obj)
        {
           if(obj == null)
          {
               Console.WriteLine("True");
          }
        }
      • 只是如果類型實參為值類型:

        • 實參 == null永遠為false

        • 實參 != null 永遠為true

    • 如果被約束為struct,則無法進行空判斷

      • static void CompareWithConstrict<T>(T obj)
           where T : struct
          {
               //Can't Compile
               //if(obj == null)
               //{
               //   Console.WriteLine("True");
               //}
          }
  • 泛型變量比較

    • 如果被約束為引用類型

      • 一般而言 == 和 != 只進行簡單的引用比較

      • 如果該類型的 == 和 != 操作符被重載了,那這個重載操作是不會被使用的

        • 編譯器在編譯未綁定的泛型類型時,就分析好所有方法重載,而不是等到執行時

          • 因此,編譯器根本不知道有哪些重載可以用

    • 如果泛型類型參數不能肯定是引用類型,進行比較則是非法的

      • 引用類型直接比較引用對象

      • 未約束的類型參數,只能將該類型的值與null進行比較時才能使用 == 或 != 操作符。

        • 不能直接比較兩個T類型的值。

          • 如果類型實參是一個引用類型,會進行正常的引用比較

          • 如果是非可空值類型,與null進行比較的結果總是顯示不相等

          • 如果是可空值類型,那自然與類型的空值進行比較

      • 而值類型,則基本完全無法比較,那怕該值類型重載了對應的操作符

        • 約束為struct => C#對於非基元值的值類型,不知道如何生成代碼來進行比較

        • 不可以約束為具體的值類型

        • 沒有類型可以從值類型派生

        • static bool CompareGenericParam<T>(T n1, T n2)
             where T : Comparable
            {
                 //Compile Failed
                 //return n1 == n2;
            }
          }
          public struct Comparable
          {
             public static bool operator == (Comparable left, Comparable right)
            {
                 return true;
            }        
             public static bool operator != (Comparable left, Comparable right)
            {
                 return false;
            }

          public override int GetHashCode()
            {
                 return base.GetHashCode();
            }
             
          public override bool Equals(object obj)

            {
                 return base.Equals(obj);
            }
          }
  • 泛型變量作為操作數

    • 編譯器在編譯時確定不了類型,所以不能向泛型類型的變量應用任何操作符

JIT編譯器對泛型的處理
  • 對於不同的封閉類型,JIT的職責是將泛型類型的IL轉換成本地代碼

  • 以List為例

    • JIT為每個以值類型作為類型實參的封閉類型都創建不同的代碼

    • 而所有使用引用類型作為類型實參的封閉類型都共享相同的本地代碼

      • 因為所有引用都具有相同的大小(32位CLR上限是4字節,64位CLR上限是8字節)

      • 無論實際引用的是甚麼,引用數組的大小不變,棧上所需的空間始終是相同的,都可以使用相同的寄存器優化措施

  • 泛型的性能優勢

    • 以List和ArrayList為例

      • 如果將byte加到ArrayList中,需要對每個byte進行裝箱,並存儲對每個已裝箱值的引用

      • 而List用一個byte[]類型的成員數組替代了ArrayList中的object[]。

        • byte[]具有恰當的類型、會占用恰當的空間,因此在List中,是直接用一個byte[]來存儲數組元素的

    • 效能比較(假設使用的是一個32位CLR)

      • ArrayList

        • 每個已裝箱的字節都要產生8字符的對象開銷

        • 另加4字節用於數據本身(本身是1字節,但要向上取整至4字節)

        • 引用本身也要消耗4字節

        • 8 + 4 + 4 => 每個有效元素最少16字節

      • List

        • 每個字節佔用元素數據中一個字節的空間

      • 節省了空間,加快了執行速度,不需要花時間裝箱,不需要因拆箱而檢查類型,不需要對不再引用的已裝箱值進行回收

泛型反射
  • 反射的一切都是圍繞「檢查對象及其類型」展開,最重要的是獲取System.Type對象的引用。這樣就可以訪問與特定類型有關的所有信息。

  • 對泛型使用typeof操作符

    • 獲取泛型類型定義

      • 提供聲明的類型名稱,刪除所有類型參數名稱,但保留逗號

    • 獲取特定的已構造類型

      • 採取與聲明泛型類型變量時相同的方式指定類型實參即可

    • static void DemonstrateTypeof<T>()
      {
         Console.WriteLine(typeof(T)); //顯示類型參數
         
         //顯示泛型類型
         Console.WriteLine(typeof(List<>));
         Console.WriteLine(typeof(Dictionary<, >));
         
         //顯示封閉類型(使用了類型參數)
         Console.WriteLine(typeof(List<T>));
         Console.WriteLine(typeof(Dictionary<string, T>));
         
         //顯示封閉類型
         Console.WriteLine(typeof(List<long>));
         Console.WriteLine(typeof(Dictionary<long, Guid>));
      }
      ...
      DemonstrateTypeof<int>();

      //Output
      //在IL中,類型參數的數量是在框架所用的完整類型名稱中指定的
      //在完整的名稱之後有一個`字符,之後的數字是參數數量,再然後顯示參數類型的方括號
      System.Int32
         
      System.Collections.Generic.List`1[T]
      System.Collections.Generic.Dictionary`2[TKey, TValue]
         
      System.Collection.Generic.List`1[System.Int32]
      System.Collections.Generic.Dictionary`2[System.String, System.Int32]
         
      System.Collections.Generic.List`1[System.Int64]
      System.Collections.Generic.Dictionary`2[System.Int64, System.Guid]
  • System.Type的屬性和方法

    • 最重要的兩個方法:

      • GetGenericTypeDefinition

        • 作用於已構造的類型,獲取它的泛型類型定義

      • MakeGenericType

        • 作用於泛型類型定義,返回一個已構造類型

    • 和普通類型一樣,任何特定的類型只有一個Type對象

      • 如果調用兩次MakeGenericType,每次都傳遞相同的類型作為參數,就返回同一個引用

      • 如果用同一個泛型類型定義構造了兩個類型,並對這兩個類型調用GetGenericTypeDefinition,同樣會返回相同的結果,即使構造的類型不相同

  • 反射泛型方法

    • 通過反射來調用泛型方法

    • namespace CSharpInDepth
      {
         class Program
        {
             static void Main(string[] args)
            {
                 //獲取目標方法所在類的Type
                 Type type = typeof(Snippet);
                 
      //通過type.GetMethod得到泛型方法定義

                 //如果要獲取類型參數數量不同的多個重載方法,.NET Framework在Type中,
                 //沒有任何方法允許指定類型參數的數量,只能通過Type.GetMethods(注意有s)獲取所有方法,并從中查找合適的那一個
                 MethodInfo definition = type.GetMethod("PrintTypeParameter");
                 
      //通過MakeGenericMethod返回一個已構造的泛型方法

                 MethodInfo constructed = definition.MakeGenericMethod(typeof(string));
                 constructed.Invoke(null, null);
      Console.ReadLine();
            }
        }

      class
      Snippet

        {
              public static void PrintTypeParameter<T>()
            {
                Console.WriteLine(typeof(T));
            }
        }
      }
    • 另外,從泛型類型定義獲取的方法不能直接調用,相反,必須從一個已構造的類型獲取方法。

泛型的好處
  • 安全性:編譯器能提前驗證類型的有效性

  • 性能:消除大部分情況的裝拆箱處理,尤其是值類型

  • 代碼可讀性提高

泛型的限制
  • 可變性的缺乏
    • 泛型不支持可變性(協變性和逆變性),它是「不變體」(invariant)

      • 協變(Covariance):派生類轉基類

      • 逆變(Contravariance):基類轉派生類

    • 為甚麼不支持協變性

      • class Animal{}
        class Cat : Animal{}
        class Turtle : Animal{}

        //非泛型,編譯時有效,執行時無效
        Animal[] animals = new Cat[5];
        animals[0] = new Turtle();

        //泛型,編譯與執行時無效
        List<Animal> animals = new List<Cat>();
        animals.Add(new Turtle());
      • 編譯器對於兩者的第二行代碼都沒有問題,但是泛型例子第一行會報錯

        • error CS0029: Cannot implicity convert type ‘System.Collections.Generic.List‘ to ‘System.Collections.Generic.List

      • 在非泛型例子中,animals實際引用的是一個Cat[];在泛型例子中,animals實際引用的是一個List

        • 非泛型例子雖然可以編譯,但執行時同樣會失敗

          • 在C#語言的第一個版本中,希望盡可能面對多的用戶,因此允許支持從Java中編譯過來的代碼。

          • 所以,.NET之所以有協變數組,是因為Java有協變數組

    • 當進行「輸入」行為時,協變性的允許會破壞類型安全;當進行「輸出」行為時,逆變性的允許也會破壞類型安全


  • 代碼爆炸
    • 使用泛型類型參數的方法在進行JIT編譯時,CLR獲取方法的IL,用指定的類型實參替換,再創建對應的本機代碼

      • 每個封閉類型都有它自己的靜態字段集,靜態初始化程序,靜態構造函數

        • 每個不同類型實參列表都被看做一個不同的封閉類型

        • 任何封閉類型的靜態構造函數只執行一次

      • static void Main(string[] args)
        {
           int num1 = 10;
           TestFunc(num1);

        string
        str1 = "Halo";

           TestFunc(str1);

        Node
        n = new Node(null);

           TestFunc(n);
        }

        public static void TestFunc<T>(T arg)
        {            
           Console.WriteLine(typeof(T));
        }
      • 因此CLR需要為各種不同的方法/類型組合生成本機代碼,造成「代碼爆炸」

    • CLR內建一些對於代碼爆炸的優化措施

      • 首先,如果為特定類型實參調用了一個方法,以後再用相同類型實參調用該方法時,CLR就不會再重新編譯一次該方法

      • 另外,CLR認為所有引用類型的實參都完全相同,因此,使用引用類型作為泛型方法的類型實參的方法調用代碼可以共享

        • 如:CLR為List<String>方法編譯的代碼可直接用於List<Node>方法

        • 對於所有引用類型都會使用相同的代碼

          • 因為所有引用類型的實參/變量實際只是指向堆上對象的指針,而所有對象指針都以相同方式操縱

      • 但如果指定的類型實參為值類型,CLR則必須專門為那個值生成本機代碼,因為值類型的大小不定

        • 即使大小一樣(如int32和uint32),CLR也無法共享代碼,因為可能要使用不同的本機CPU指令去操縱這些值


參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter
  • 《深入理解C#》(第3版) Jon Skeet