c#中的泛型


      這篇文章主要來講講c#中的泛型,因為泛型在c#中有很重要的位置,對於寫出高可讀性,高性能的代碼有着關鍵的作用。當我多次看到自己團隊的代碼中包含着大量的非泛型集合,隱式的裝箱和拆箱操作時,我都會建議他們補一補泛型基礎。

     1,什么是泛型

    • 泛型是c#2中非常重要的一個新特性,它增強了代碼的可讀性,將大量的安全檢查從執行期轉移到編譯期,從而提高代碼的安全性和性能。從根本上來說,泛型實現了類型和方法的參數化。

     2,為什么要使用泛型,泛型解決了什么問題

我們先來看看下面的代碼(代碼只是為了演示泛型,沒有實際的意義),看看有什么問題?   

 1 class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             ArrayList array = new ArrayList();
 6             array.Add(1);
 7             array.Add(2);
 8             array.Add(3);
 9             ArrayList resultArray = NumberSqrt(array);
10             foreach (var item in resultArray)
11             {
12                 Console.WriteLine(item);
13             }
14         }
15         public static ArrayList NumberSqrt(ArrayList array)
16         {
17             ArrayList sqrtArray = new ArrayList();
18             foreach (var item in array)
19             {
20                 sqrtArray.Add(Math.Sqrt((double)(int)item));
21             }
22             return sqrtArray;
23 
24         }
25     }
View Code
    • 首先ArrayList是一個非泛型集合,它需要的參數是object,那么我如果往集合里面裝配其它的類型,在編譯時也無法判斷錯誤(比如說我裝配一個字符串進去,完全是可以編譯成功的)。
    • 由於ArrayList需要的是一個object,那么在你將值類型裝配到集合中時,會進行隱式裝箱操作,在使用集合中的數據時,需要進行拆箱操作。從而影響應用程序的性能。(關於值類型裝箱請參考c#中的引用類型和值類型)。
    • 代碼可以可讀性差,完全看不明白ArrayList應該裝配什么類型。

下面來看看泛型是如何解決這些問題的:

 1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             List<double> array = new List<double>();
 6             array.Add(1);
 7             array.Add(2);
 8             array.Add(3);
 9             List<double> resultArray = NumberSqrt(array);
10             foreach (var item in resultArray)
11             {
12                 Console.WriteLine(item);
13             }
14         }
15         public static List<double> NumberSqrt(List<double> array)
16         {
17             List<double> sqrtArray = new List<double>();
18             foreach (var item in array)
19             {
20                 sqrtArray.Add(Math.Sqrt((double)(int)item));
21             }
22             return sqrtArray;
23 
24         }
25 
26     }
View Code
    • 由於List<double>只能裝配double,所以當你裝配其它的類型時編譯器會報錯,提高代碼的安全性。
    • 由於List<double>只能裝配double類型時,這樣的話就避免了編譯器隱式的裝箱和拆箱操作,提高應用程序的性能。
    • 一眼就可以看出集合需要裝配的類型,提高代碼的可讀性。
    • NumberSqrt方法在這里沒什么用去,如果你要寫一個算法,把NumberSqrt改為泛型方法:NumberSqrt<T>(比如說排序算法),可以實現算法重用。

     3,如何使用泛型:語法和規則

上面已經說了什么是泛型,以及為什么要用泛型,下面我們來聊聊如何使用泛型

    • 泛型的語法:泛型主要有泛型方法(例如:static void Swap<T>(ref T lhs, ref T rhs)),泛型接口( 例如: public interface IEnumerable<out T> : IEnumerable),泛型委托( 例如: public delegate void Action<in T>(T obj);)
    • 在FCL中內建了許多泛型的接口定義(常用的接口都在System.Collections.Generic),為了使泛型能夠正常的工作,微軟的開發人員將會做如下工作,
      • 創建新的IL指令,使之能夠識別類型參數。
      • 改變元數據的格式,使之能夠識別泛型參數和泛型方法。
    • 開放類型和封閉類型:當為一個泛型類型沒有指定實際的數據類型時,就稱為開放類型,例如List<T>,對於任何開放類型都不能創建該類型的實例,例如下面的代碼在運行是將會報錯未:經處理的異常:  System.ArgumentException: 無法創建 ConsoleApplication2.Demo`1[T] 的實例,因為 Type.ContainsGenericParameters 為 True。
 1     public class Demo<T> { }
 2     class Program
 3     {
 4         static void Main(string[] args)
 5         {
 6             object o = null;
 7             Type t = typeof(Demo<>);
 8             o = Activator.CreateInstance(t);
 9         }
10     }
View Code

               如果為泛型類型的所有類型實參傳遞的都是實際數據類型,類型就稱為封閉類型,CLR允許構造封閉類型的實例。

    • 泛型類型和繼承:泛型類型只是一個特殊的類型,所以它可以從其他任何類型派生。比如List<T>是從object派生的,那么List<String>也是從object派生的。換句話說,類型實參的指定和繼承層次結構沒有任何關系-理解這一點,有助於你判斷那些轉型是否能夠進行,哪些轉型是不能進行的。
    • 編譯器如何解決“代碼爆炸”的問題:在使用泛型類型或者方法方法時,Clr獲取IL代碼,用指定的類型實參進行替換。如果Clr為每種不同的類型/方法組合都生成不同的本地代碼,他可能會增大應用程序的工作集,從而影響性能。我們將這種現象稱為”代碼爆炸“。那么編譯器又是如何解決這個問題的呢?編譯器認為所有引用類型的實參是完全相同的,所以代碼能夠共享(為什么這么說呢?是因為所有引用類型的實參或者變量實際只是指向堆上的對象的指針,而對象的指針全部是以相同的方式來操操縱的)。但是如果某個類型實參是值類型的話,CLR就得專門為那個值類型生成本地代碼,這是值類型的大小不定。
    • 泛型接口:為什么要使用泛型接口呢?因為你每次試圖使用一個非泛型接口來操作一個值類型時,都會發生裝箱,從而失去編譯時的類型安全性。所以CLR提供了對泛型接口的支持。下面來看一個例子:
       1    // 摘要:
       2     //     支持在泛型集合上進行簡單迭代。
       3     //
       4     // 類型參數:
       5     //   T:
       6     //     要枚舉的對象的類型。
       7     public interface IEnumerator<out T> : IDisposable, IEnumerator
       8     {
       9         // 摘要:
      10         //     獲取集合中位於枚舉數當前位置的元素。
      11         //
      12         // 返回結果:
      13         //     集合中位於枚舉數當前位置的元素。
      14         T Current { get; }
      15     }
      View Code
    • 泛型委托:CLR之所以要支持泛型委托,主要目的是保證任何類型的對象都能以一種類型安全的方式傳給一個回調方法,並且保證了將一個值類型實例傳遞給一個回調方法時不執行任何裝箱處理。
    • 泛型方法:定義泛型類,結構或者接口時,這些類型中定義的任何方法都可引用由類型指定的一個類型參數。類型參數可以作為方法的參數/返回值,或者作為方法內部的一個局部變量來使用。下面來看一個例子:
       1  class Program
       2     {
       3         static void Main(string[] args)
       4         {
       5             MyClass<string> myclass = new MyClass<string>();
       6             myclass.ShowInfo("myClass");
       7         }
       8     }
       9 
      10     class MyClass<Ta>
      11     {
      12         public void ShowInfo(Ta a)
      13         {
      14             Type t = typeof(Ta);
      15             Console.WriteLine(t.FullName + "非泛型方法");
      16         }
      17         public void ShowInfo<Ta>(Ta a)
      18         {
      19             Type t = typeof(Ta);
      20             Console.WriteLine(t.FullName + "泛型方法");
      21         }
      22         public void ShowInfo<Tb>(Ta a, Tb b)
      23         {
      24             Type t = typeof(Tb);
      25             Console.WriteLine(t.FullName);
      26         }
      27     }
      View Code

       泛型方法的類型推斷:c#語法中包含大量"<"和">"符號,所以導致代碼的可讀性和可維護性降低了,所以為了改變這種情況,c#編譯器支持在調用一個方法時進行類型推斷。例如下面的代碼:myclass.ShowInfo("myClass", myclass);會調用ShowInfo<string>(string a, string b);

    • 可驗證性和約束:編譯器在編譯泛型代碼時,會對它進行分析,以確保代碼適用於當前已有或將來可能定義的任何類型。請查看下面的方法:
      1      public static Boolean MethodTakingAnyType<T>(T o) {
      2             T temp = o;
      3             Console.WriteLine(o.ToString());
      4             Boolean b = temp.Equals(o);
      5             return b;
      6         }
      View Code

      看這個方法里面有一個臨時變量temp。方法里面執行兩次變量賦值和幾次方法調用,無論T是值類型還是引用類型還是接口類型或者是委托類型這個方法都能工作。這個方法適用於當前存在的所以類型,也適用於將來可能定義的任何類型。比如你再看一下下面這個方法:

      1      public static T MethodTakingAnyType<T>(T o1,T o2) {
      2             if (o1.CompareTo(o2) < 0)
      3             {
      4                 return o1;
      5             }
      6             else {
      7                 return o2;
      8             }
      9         }
      View Code

      在編譯時會報如下錯誤(error CS0117:"T"不包含"CompareTo"的定義),因為並不是所有的類型都提供了CompareTo方法。那么在什么情況下T應該是什么類型呢?幸好,編譯器和CLR支持一個稱為約束的機制,可利用它使泛型變得真正有用!

      • 約束的語法:請看下面這個方法名字后面的where
        1 public static Boolean MethodTakingAnyType<T>(T o) where T : struct
        2         {
        3             T temp = o;
        4             Console.WriteLine(o.ToString());
        5             Boolean b = temp.Equals(o);
        6             return b;
        7         }
        View Code

        c#的where關鍵字告訴編譯器,為T指定的任何類型都必須是值類型。所以當你為T指定其它類型時,編譯器會報錯,例如你指定為string類型時(MethodTakingAnyType<string>("");)它會報錯誤    1    類型“string”必須是不可以為 null 值的類型才能用作泛型類型或方法

      • 主要約束:一個類型參數可以指定零個或一個主要約束(也就是第一個約束)。主要約束可以是一個引用類型,它標識了一個沒有密封的類,不能指定以下特殊類型:System.Object;System.Array;System.Delegate;System.MulticastDelegate;System.ValueType;System.Enum;
      • 次要約束:一個類型參數可以指定零個或多個次要約束。次要約束是一個接口類型,指定一個接口類型約束時,是告訴編譯器類型實參必須實現這個接口。還有一種次要約束稱為類型參數約束,有時也稱為裸類型約束,看下面的代碼
        1  public static Boolean MethodTakingAnyType<T, TBase>(T o) where T : TBase
        2         {
        3             T temp = o;
        4             Console.WriteLine(o.ToString());
        5             Boolean b = temp.Equals(o);
        6             return b;
        7         }
        View Code

        T類型參數由TBase類型單數約束,也就是說不管T為什么類型,都必須兼容於TBase指定的類型實參。

      • 構造器約束:一個類型參數可以指定零個或一個構造器約束。指定構造器約束相當於告訴編譯器一個指定的類型實參必須要實現了公共無參構造器。請看下面的的代碼:
        1       public static Boolean MethodTakingAnyType<T, TBase>(T o) where T : new()
        2         {
        3             T temp = o;
        4             Console.WriteLine(o.ToString());
        5             Boolean b = temp.Equals(o);
        6             return b;
        7         }
        View Code
      • 其它可驗證性的問題:1,泛型類型變量的轉型。下面的類型編譯時出錯,因為T可能為任何類型,無法保證能轉型成功!
        1     public static void MethodTakingAnyType<T, TBase>(T o)
        2         {
        3             int x = (int)o;
        4             string s = (string)o;
        5         }
        View Code

        2,將一個泛型類型變量設置為默認值:o = default(T);這樣的話,不管T為值類型還是引用類型都可以成功,如果T為引用類型時就設置為null,如果T為值類型時將默認值設為0;

        3,將一個泛型類型變量與Null進行比較:使用==或者=!將一個泛型類型變量於null進行比較都是合法的。但是如果T為值類型時,o永遠都不會為null查看下面的代碼:
        1        public static void MethodTakingAnyType<T, TBase>(T o)
        2         {
        3             if (o == null) { 
        4             //do something
        5             }
        6         }
        View Code

        4,將兩個泛型類型變量相互比較:如果T是值類型,下面的代碼就是非法的。

        1    public static void MethodTakingAnyType<T>(T o1,T o2)
        2         {
        3             if (o1 == o2) { 
        4             
        5             }
        6         }
        View Code

     4,泛型在使用過程的注意事項

    • 泛型不支持協變性
    • 缺乏操作符約束或者“數值”約束
    • 缺乏泛型屬性,索引器和其它成員類型

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM