進階系列(1)——泛型

1、泛型的是什麼

  泛型的英文解釋爲generic,固然咱們查詢這個單詞時,更多的解釋是通用的意思,然而有些人會認爲明明是通用類型,怎麼成泛型了的,其實這二者並不衝突的,泛型原本表明的就是通用類型,只是微軟可能有一個比較官方的詞來形容本身引入的特性而已,既然泛型是通用的, 那麼泛型類型就是通用類型的,即泛型就是一中模子。 在生活中,咱們常常會看到模子,像咱們日常生活中用的桶子就是一個模子,咱們能夠用桶子裝水,也能夠用來裝油,牛奶等等,然而把這些都裝進桶子裏面以後,它們都會具備桶的形狀(水,牛奶和油原本是沒有形的),即具備模子的特徵。一樣,泛型也是像桶子同樣的模子,咱們能夠用int類型,string類型,類去實例化泛型,實例化以後int,string類型都會具備泛型類型的特徵就是說可使用泛型類型中定義的方法,如List<T>泛型,若是用int去初始化它後,List<int>的實例就能夠用List<T>泛型中定義的全部方法,用string去初始化它也同樣,和咱們生活中的用桶裝水,牛奶,油等很是相似
html

2、C# 2.0爲何要引入泛型

   你們經過第一部分知道了什麼是泛型,然而C#2.0中爲何要引入泛型的?這答案固然是泛型有不少好處的。下面經過一個例子來講明C# 2.0中爲何要引入泛型,而後再介紹下泛型所帶來的好處有哪些。算法

    當咱們要寫一個比較兩個整數大小的方法時,咱們可能很快會寫出下面的代碼:編程

public class Compare
    {
        // 返回兩個整數中大的那一項
        public static int Compareint(int int1, int int2)
        {
            if (int1.CompareTo(int2) > 0)
            {
                return int1;
            }

            return int2;
        }
    }

然而需求改變爲又要實現比較兩個字符串的大小的方法時,咱們又不得不在類中實現一個比較字符串的方法:數組

若是需求又改成要實現比較兩個對象之間的大小時,這時候咱們又得實現比較兩個對象大小的方法,然而咱們中需求中能夠看出,需求中只是比較的類型不同的,其實現方式是徹底同樣的,這時候咱們就想有沒有一種類型是通用的,咱們能夠把任何類型當作參數傳入到這個類型中去實例化爲具體類型的比較,正是有了這個想法,同時微軟在C#2.0中也想到了這個問題,因此就致使了C#2.0中添加了泛型這個新的特性,泛型就是——通用類型,有了泛型以後就能夠很好的幫助咱們剛纔遇到的問題的,這樣就解決了咱們的第一個疑問——爲何要引入泛型。下面是泛型的實現方法:安全

public class Compare<T> where T : IComparable
    {
        public  static T CompareGeneric(T t1, T t2)
        {
            if (t1.CompareTo(t2) > 0)
            {
                return t1;
            }
            else
            {
                return t2;
            }
        }
    }

這樣咱們就不須要針對每一個類型實現一個比較方法,咱們能夠經過下面的方式在主函數中進行調用的:函數

public class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(Compare<int>.CompareGeneric(3, 4));
            Console.WriteLine(Compare<string>.CompareGeneric("abc", "a"));
            Console.Read();
        }
    }

經過上面的代碼你們確定能夠理解C# 2.0中爲何要引入泛型的,然而泛型能夠給咱們帶什麼好處的呢?從上面的例子能夠看出,泛型能夠幫助咱們實現代碼的重用,你們很清楚——面向對象中的繼承也能夠實現代碼的重用,然而泛型提供的代碼的重用,確切的說應該是 「算法的重用」(我理解的算法的重用是咱們在實現一個方法中,咱們只要去考慮如何去實現算法,而不須要考慮算法操做的數據類型的不一樣,這樣的算法實現更好的重用,泛型就是提供這樣的一個機制)。工具

      咱們在來看一個熟悉的算法——冒泡排序,冒泡排序中咱們能夠對不一樣類型的數據進行排序,其中,基本的算法邏輯是徹底相同的,僅僅是數據類型的不一樣,咱們爲了適應程序的靈活性,和重用性,咱們可使用泛型來定義一個排序的模子,對多種數據類型進行排序。post

class Program
    {
        static void Main(string[] args)
        {
            int[] array = {12,23,16,32,89,5};
            SortHelper<int> sort = new SortHelper<int>();
            sort.BubbleSort(array, (a,b) => a > b);
            Console.ReadKey();
        }
    }

    public delegate bool Contrast<T>(T t1, T t2);//傳入兩個參數來做比較 

    public class SortHelper<T>
    {
        public  void BubbleSort( T [] array, Contrast<T> contrast)
        {       
            for (int i = 0; i < array.Length - 1; i++)
            {
                for (int j = 0; j < array.Length - 1-i; j++)
                {                 
                    if (contrast(array[j] , array[j + 1]) )
                    {
                        T temp = array[j];
                        array[j] = array[j+1];
                        array[j+1] = temp;
                    }
                }
            }
            Console.WriteLine("排序後的數組");
            for (int i = 0; i < array.Length - 1; i++)
            {
                Console.WriteLine("{0}", array[i]);
            }          
        }
    }

運行結果:性能

然而泛型除了實現代碼的重用的好處外,還有能夠提供更好的性能和類型安全,下面經過下面一段代碼來解釋下爲何有這兩個好處的。ui

class Program
    {
        public static int constintListSize = 500000;
        static void Main(string[] args)
        {
            UseArrayList();
            UseGenericList();
            Console.ReadKey();
        }
        private static void UseArrayList()
        {
            ArrayList list = new ArrayList();
            long startTicks = DateTime.Now.Ticks;
            for (int i = 0; i < constintListSize; i++)
            {
                list.Add(i);
            }

            for (int i = 0; i < constintListSize; i++)
            {
                int value = (int)list[i];
            }
            long endTicks = DateTime.Now.Ticks;
            Console.WriteLine("使用ArrayList,耗時:{0} ticks", endTicks - startTicks);
        }

        private static void UseGenericList()
        {
            List<int> list = new List<int>();
            long startTicks = DateTime.Now.Ticks;
            for (int i = 0; i < constintListSize; i++)
            {
                list.Add(i);
            }
            for (int i = 0; i < constintListSize; i++)
            {
                int value = list[i];
            }
            long endTicks = DateTime.Now.Ticks;
            Console.WriteLine("使用List<int>,耗時:{0} ticks", endTicks - startTicks);
        }
    }

使用ArrayList,耗時:468750 ticks
使用List<int>,耗時:156250 ticks

爲何使用泛型的效率會這麼高呢?接下來咱們一探究竟,用反翻譯軟件,咱們能夠看出:

IL_001f:  ldloc.1
  IL_0020:  ldloc.3
  IL_0021:  box        [mscorlib]System.Int32
  IL_0026:  callvirt   instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
  IL_002b:  pop
  IL_002c:  nop
  IL_002d:  ldloc.3
  IL_002e:  ldc.i4.1
  IL_002f:  add

在上面的IL代碼中,我用紅色的標記的代碼主要是在執行裝箱操做(裝箱過程確定是要消耗的事件的吧, 就像生活中寄包裹同樣,包裝起來確定是要花費必定的時間的, 裝箱操做一樣會,然而對於泛型類型就能夠避免裝箱操做,下面會貼出使用泛型類型的IL代碼的截圖)——這個操做也是影響非泛型的性能不如泛型類型的根本緣由。然而爲何使用ArrayList類型在調用Add方法來向數組添加元素以前要裝箱的呢?緣由其實主要出在Add方法上的, 你們能夠用Reflector反射工具查看ArrayList的Add方法定義,下面是一張Add方法原型的截圖:

從上面截圖能夠看出,Add(objec value)須要接收object類型的參數,然而咱們代碼中須要傳遞的是int實參,此時就須要會發生裝箱操做(值類型int轉化爲object引用類型,這個過程就是裝箱操做),這樣也就解釋了爲何調用Add方法會執行裝箱操做的, 同時也就說明泛型的高性能的好處。

下面是使用泛型List<T>的IL代碼截圖(從圖片中能夠看出,使用泛型時,沒有執行裝箱的操做,這樣就少了裝箱的時間,這樣固然就運行的快了,性能就行了。):

同時泛型可以提供的另外一個好處就是類型安全,這是什麼意思呢?看下面一段代碼:

ArrayList list = new ArrayList();
int i = 100;
list.Add(i);
string value = (string)list[0];

有讀者一眼就能夠看出這段代碼有問題,由於類型不匹配,添加到ArrayList中的是一個int類型,而獲取時卻想將它轉換爲string類型。 惋惜的是,編譯器沒法知道,由於對它來講,無論是int也好,string也好,它們都是Object類型。 在編寫代碼時,編譯器提供給開發
者的最大幫助之一就是能夠檢查出錯誤,也就是常稱的編譯時錯誤(Compile timeerror)。 當使用ArrayList時,對於上面的問題,編譯器無能爲力,由於它認爲其是合法的,編譯能夠順利經過。 這種錯誤有時候隱藏在程序中很難發現,最糟糕的狀況是產品已經交付用戶,而當用戶在使用時不巧執行到這段代碼,便會拋出一個異常,這時的錯誤,稱爲運行時錯誤(Runtime error)。
經過使用泛型集合,這種狀況將不復存在,當試圖進行相似上面的轉換時,根本沒法經過編譯,這樣有助於儘早發現問題:

List<int> list = new List<int>();
int i = 100;
list.Add(i);
string value = (string)list[0]; //編譯錯誤

3、泛型類型和類型參數

   泛型類型和其餘int,string同樣都是一種類型,泛型類型有兩種表現形式的:泛型類型(包括類、接口、委託和結構,可是沒有泛型枚舉的)和泛型方法。那什麼樣的類、接口、委託和方法才稱做泛型類型的呢 ?個人理解是類、接口、委託、結構或方法中有類型參數就是泛型類型,這樣就有類型參數的概念的。 類型參數 ——是一個真實類型的一個佔位符(我想到一個很形象的比喻的,好比你們在學校的時候,一到中午下課的時候食堂人特別多的,因此不少應該都有用書本佔位置的習慣的, 書本就至關於一個佔位符,真真坐在位置上的固然是本身的,講到佔位置,之前聽過我同窗說,他們班有個很牛逼的MM,中午下完課的時候用手機佔位子的,等它打完飯回來的時候手機已經不見, 當時聽完我就和我同窗說,大家班這位女生真牛逼的,後面咱們就),泛型聲明中,類型參數必須放在一對尖括號裏面(即<>這個符號),而且用逗號分隔多個類型參數,如List<T>類中T就是類型參數,使用泛型類型或方法的時候,咱們要用真實類型來代替,就像用書本佔位子一個,書本只是暫時的在那個位置上,等打好飯了就要換成你坐在位置上了,一樣在C#中泛型也是一樣道理,類型參數只是暫時的在那個位置,真真使用中要用真實的類型去代替它的位置,此時咱們把真實類型又取名爲類型實參,如上一專題的代碼中List<int>,類型實參就是int(代替T的位置)。

  若是沒有爲類型參數提供類型實參,此時咱們就聲明瞭一個未綁定的泛型類型,若是指定了類型實參,此時的類型就叫作已構造類型(這裏一樣能夠以書佔位置去理解),然而已構造類型又能夠是開放類型或封閉類型的,這裏先給出這個兩個概念的定義的:開放類型——具備類型參數的類型就是開放類型(全部的未綁定的泛型類型都屬於開放類型的),封閉類型——爲每一個類型參數都傳遞了實際的數據類型。對於開放類型,咱們建立開放類型的實例

  注意:在C#代碼中,咱們惟一能夠看到未綁定泛型類型的地方(除了做爲聲明以外)就是在typeof操做符裏。

下面經過如下代碼來更好的說明這點:

using System;
using System.Collections.Generic;

namespace CloseTypeAndOpenType
{
    // 聲明開放泛型類型
    public sealed class DictionaryStringKey<T> : Dictionary<string, T>
    {
 
    }

   public class Program
    {
        static void Main(string[] args)
        {
            object o = null;

            // Dictionary<,>是一個開放類型,它有2個類型參數
            Type t = typeof(Dictionary<,>);

            // 建立開放類型的實例(建立失敗,出現異常)
            o = CreateInstance(t);
            Console.WriteLine();

            // DictionaryStringKey<>也是一個開放類型,但它有1個類型參數
            t = typeof(DictionaryStringKey<>);

            // 建立該類型的實例(一樣會失敗,出現異常)
            o = CreateInstance(t);
            Console.WriteLine();

            // DictionaryStringKey<int>是一個封閉類型
            t = typeof(DictionaryStringKey<int>);

            // 建立封閉類型的一個實例(成功)
            o = CreateInstance(t);

            Console.WriteLine("對象類型 = " + o.GetType());
            Console.Read();
        }


       // 建立類型
        private static object CreateInstance(Type t)
        {
            object o = null;
            try
            {
                // 使用指定類型t的默認構造函數來建立該類型的實例
                o = Activator.CreateInstance(t);
                Console.WriteLine("已建立{0}的實例", t.ToString());
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

            return o;
        }
    }
}

運行結果爲(從結果中也能夠看出開放類型不能建立該類型的一個實例,異常信息中指出類型中包含泛型參數):

4、類型約束

  若是你們看了個人上一個專題的話,就應該會注意到我在實現泛型類的時候用到了where T : IComparable,在上一個專題並無和你們介紹這個是泛型的什麼用法,這個用法就是這個部分要講的類型約束,其實where T : IComparable這句代碼也很好理解的,猜猜也明白的(若是是我不知道的話,應該是猜類型參數T要知足IComparable這個接口條件,由於Where就表明符合什麼條件的意思,然而真真意思也確實如此的)下面就讓咱們具體看看泛型中的類型參數有哪幾種約束的。   首先,編譯泛型代碼時,C#編譯器確定會對代碼進行分析,若是咱們像下面定義一個泛型類型方法時,編譯器就會報錯:

// 比較兩個數的大小,返回大的那個
        private static T max<T>(T obj1, T obj2) 
        {
            if (obj1.CompareTo(obj2) > 0)
            {
                return obj1;
            }

            return obj2;
        }

若是像上面同樣定義泛型方法時,C#編譯器會提示錯誤信息:「T」不包含「CompareTo」的定義,而且找不到可接受類型爲「T」的第一個參數的擴展方法「CompareTo」。 這是由於此時類型參數T能夠爲任意類型,然而許多類型都沒有提供CompareTo方法,因此C#編譯器不能編譯上面的代碼,這時候咱們(編譯器也是這麼想的)確定會想——若是C#編譯器知道類型參數T有CompareTo方法的話,這樣上面的代碼就能夠被C#編譯器驗證的時候經過,就不會出現編譯錯誤的(C#編譯器感受很人性化的,都會按照人的思考方式去解決問題的,那是由於編譯器也是人開發出來的,固然會人性化的,由於開發人員當時就是這麼想的,因此就把邏輯寫到編譯器的實現中去了),這樣就讓咱們想對類型參數做出必定約束,縮小類型參數所表明的類型數量——這就是咱們類型約束的目的,從而也很天然的有了類型參數約束這裏經過對遇到的分析而後去想辦法的解決的方式來引出類型約束的概念,主要是讓你們能夠明白C#中的語言特性提出來都是有緣由,並非說微軟想提出來就提出來的,主要仍是由於用戶會有這樣的需求,這樣的方式我以爲可讓你們更加的明白C#語言特性的發展歷程,從而更加深刻理解C#,從我前面的專題也看的出來我這樣介紹問題的方式的,不過這樣也是我我的的理解,但願這樣引入問題的方式對你們會有幫助,讓你們更好的理解C#語言特性,若是你們對於對於有任何意見和建議的話,均可以在留言中提出的,若是以爲好的話,也麻煩表示承認下)。因此上面的代碼能夠指定一個類型約束,讓C#編譯器知道這個類型參數必定會有CompareTo方法的,這樣編譯器就不會報錯了,咱們能夠將上面代碼改成(代碼中T:IComparable<T>爲類型參數T指定的類型實參都必須實現泛型IComparable接口):

 // 比較兩個數的大小,返回大的那個
        private static T max<T>(T obj1, T obj2) where T:IComparable<T>
        {
            if (obj1.CompareTo(obj2) > 0)
            {
                return obj1;
            }

            return obj2;
        }

 類型約束就是用where 關鍵字來限制能指定類型實參的類型數量,如上面的where T:IComparable<T>語句。C# 中有4種約束可使用,然而這4種約束的語法都差很少。(約束要放在泛型方法或泛型類型聲明的末尾,而且要使用Where關鍵字)

(1) 引用類型約束

  表示形式爲 T:class, 確保傳遞的類型實參必須是引用類型(注意約束的類型參數和類型自己沒有關係,意思就是說定義一個泛型結構體時,泛型類型同樣能夠約束爲引用類型,此時結構體類型自己是值類型,而類型參數約束爲引用類型),能夠爲任何的類、接口、委託或數組等;可是注意不能指定下面特殊的引用類型:System.Object,System.Array,System.Delegate,System.MulticastDelegate,System.ValueType,System.Enum和System.Void.

以下面定義的泛型類:

using System.IO;  
public class samplereference<T> where T : Stream
        {
             public void Test(T stream)
             {
                 stream.Close(); 
             }
        }

 上面代碼中類型參數T設置了引用類型約束,Where T:stream的意思就是告訴編譯器,傳入的類型實參必須是System.IO.Stream或者從Stream中派生的一個類型,若是一個類型參數沒有指定約束,則默認T爲System.Object類型(至關於一個默認約束同樣,就想每一個類若是沒有指定構造函數就會有默認的無參數構造函數,若是指定了帶參數的構造函數,編譯器就不會生成一個默認的構造函數)。然而,若是咱們在代碼中顯示指定System.Object約束時,此時會編譯器會報錯:約束不能是特殊類「object」(這裏你們能夠本身試試看的)

(2)值類型約束

  表示形式爲T:struct,確保傳遞的類型實參時值類型,其中包括枚舉,可是可空類型排除,(可空類型將會在後面專題有所介紹),以下面的示例:

// 值類型約束
         public class samplevaluetype<T> where T : struct
         {
             public static T Test()
             {
                 return new T();
             }
         }

 在上面代碼中,new T()是能夠經過編譯的,由於T 是一個值類型,而全部值類型都有一個公共的無參構造函數,然而,若是T不約束,或約束爲引用類型時,此時上面的代碼就會報錯,由於有的引用類型沒有公共的無參構造函數的。

(3)構造函數類型約束

  表示形式爲T:new(),若是類型參數有多個約束時,此約束必須爲最後指定。確保指定的類型實參有一個公共無參構造函數的非抽象類型,這適用於:全部值類型;全部非靜態、非抽象、沒有顯示聲明的構造函數的類(前面括號中已經說了,若是顯示聲明帶參數的構造函數,則編譯器就不會爲類生成一個默認的無參構造函數,你們能夠經過IL反彙編程序查看下的,這裏就不貼圖了);顯示聲明瞭一個公共無參構造函數的全部非抽象類。(注意: 若是同時指定構造器約束和struct約束,C#編譯器會認爲這是一個錯誤,由於這樣的指定是多餘的,全部值類型都隱式提供一個無參公共構造函數,就如定義接口指定訪問類型爲public同樣,編譯器也會報錯,由於接口必定是public的,這樣的作只多餘的,因此會報錯。)

(4)轉換類型約束

  表示形式爲 T:基類名 (確保指定的類型實參必須是基類或派生自基類的子類)或T:接口名(確保指定的類型實參必須是接口或實現了該接口的類) 或T:U爲 T 提供的類型參數必須是爲 U 提供的參數或派生自爲 U 提供的參數)。轉換約束的例子以下:

聲明

已構造類型的例子

Class Sample<T> where T: Stream

Sample<Stream>有效的

Sample<string>無效的

Class Sample<T> where T:  IDisposable

Sample<Stream >有效的

Sample<StringBuilder>無效的

Class Sample<T,U> where T: U

Sample<Stream,IDispsable>有效的

Sample<string,IDisposable>無效的

(5)組合約束(第五種約束就是前面的4種約束的組合)

  將多個不一樣種類的約束合併在一塊兒的狀況就是組合約束了。(注意,沒有任何類型即時引用類型又是值類型的,因此引用約束和值約束不能同時使用)若是存在多個轉換類型約束時,若是其中一個是類,則類必須放在接口的前面。不一樣的類型參數能夠有不一樣的約束,可是他們分別要由一個單獨的where關鍵字。下面看一些有效和無效的例子來讓你們加深印象:

有效:

class Sample<T> where T:class, IDisposable, new();

class Sample<T,U> where T:class where U: struct

無效的:

class Sample<T> where T: class, struct (沒有任何類型即時引用類型又是值類型的,因此爲無效的)

class Sample<T> where T: Stream, class (引用類型約束應該爲第一個約束,放在最前面,因此爲無效的)

class Sample<T> where T: new(), Stream (構造函數約束必須放在最後面,因此爲無效)

class Sample<T> where T: IDisposable, Stream(類必須放在接口前面,因此爲無效的)

class Sample<T,U> where T: struct where U:class, T (類型形參「T」具備「struct」約束,所以「T」不能用做「U」的約束,因此爲無效的)

class Sample<T,U> where T:Stream, U:IDisposable(不一樣的類型參數能夠有不一樣的約束,可是他們分別要由一個單獨的where關鍵字,因此爲無效的)

 參考資料《NET之美》

相關文章
相關標籤/搜索