[C# 基礎知識系列]專題八: 深入理解泛型(二)

來源:互聯網
上載者:User

引言:

  本專題主要是承接上一個專題要繼續介紹泛型的其他內容,這裡就不多說了,就直接進入本專題的內容的。

 

一、類型推斷

  在我們寫泛型代碼的時候經常有大量的"<"和">"符號,這樣有時候代碼一多,也難免會讓開發人員在閱讀代碼過程中會覺得有點暈的,此時我們覺得暈的時候肯定就會這樣想:是不是能夠省掉一些"<" 和">"符號的呢?你有這種需求了, 當然微軟這位好人肯定也會幫你解決問題的,這樣就有了我們這部分的內容——類型推斷,意味著編譯器會在調用一個泛型方法時自動判斷要使用的類型,(這裡要注意的是:類型推斷只使用於泛型方法,不適用於泛型型別),下面是示範代碼:

using System;namespace 類型推斷例子{    class Program    {        static void Main(string[] args)        {            int n1 = 1;            int n2 = 2;            // 沒有類型推斷時需要寫的代碼            // GenericMethodTest<int>(ref n1, ref n2);            // 有了類型推斷後需要寫的代碼            // 此時編譯器可以根據傳遞的實參 1和2來判斷應該使用Int類型實參來調用泛型方法            // 可以看出有了類型推斷之後少了<>,這樣代碼多的時候可以增強可讀性            GenericMethodTest(ref n1, ref n2);            Console.WriteLine("n1的值現在為:" + n1);            Console.WriteLine("n2的值現在為:" + n2);            Console.Read();                                   //string t1 = "123";            //object t2 = "456";            //// 此時編譯出錯,不能推斷類型            //// 使用類型推斷時,C#使用變數的資料類型,而不是使用變數引用對象的資料類型            //// 所以下面的代碼會出錯,因為C#編譯器發現t1是string,而t2是一個object類型            //// 即使 t2引用的是一個string,此時由於t1和t2是不同資料類型,編譯器所以無法推斷出類型,所以報錯。            //GenericMethodTest(ref t1, ref t2);        }        // 類型推斷的Demo        private static void GenericMethodTest<T>(ref T t1,ref T t2)        {            T temp = t1;            t1 = t2;            t2 = temp;        }    }}

代碼中都有詳細的注釋,這裡就不解釋了。

二、類型約束

  如果大家看了我的上一個專題的話,就應該會注意到我在實現泛型類的時候用到了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關鍵字,所以為無效的)

 

三、利用反射調用泛型方法

  下面就直接通過一個例子來示範如何利用反射來動態調用泛型方法的(關於反射的內容可以我部落格中的這篇文章: http://www.cnblogs.com/zhili/archive/2012/07/08/AssemblyLoad_and_Reflection.html),示範代碼如下:

using System;using System.Reflection;namespace ReflectionGenericMethod{    class Program    {        static void Main(string[] args)        {            Test test = new Test();            Type type = test.GetType();            // 首先,獲得方法的定義            // 如果不傳入BindFlags實參,GetMethod方法只返回公用成員            // 這裡我指定了NonPublic,也就是返回私人成員            // (這裡要注意的是,如果指定了Public或NonPublic的話,            // 必須要同時指定Instance|Static,否則不返回成員,具體大家可以用代碼來測試的)            MethodInfo methodefine = type.GetMethod("PrintTypeParameterMethod", BindingFlags.NonPublic|BindingFlags.Instance|BindingFlags.Static);            MethodInfo constructed;            // 使用MakeGenericMethod方法來獲得一個已構造的泛型方法            constructed = methodefine.MakeGenericMethod(typeof(string));            // 泛型方法的調用            constructed.Invoke(null,null);            Console.Read();        }    }    public class Test    {        private  static void PrintTypeParameterMethod<T>()        {            Console.WriteLine(typeof(T));        }    }}

  上面代碼在調用泛型方法時傳入的兩個實參都是null,傳入第一個為null是因為調用的是一個靜態方法, 第二null是因為調用的方法是個無參的方法。 運行結果(結果是輸出出 類型實參的類型,結果和我們預期的一樣):

四、小結

  說到這裡泛型的內容都已經介紹完了,本系列用了三個專題來介紹泛型,文章內容都基本採用提出疑問(為什麼有泛型)到解釋疑問,再到深入理解泛型的方式(個人認為這樣的講解方式不錯的,如果大家有更好的講解方式可以在下面留言給我),希望這種方式可以讓大家知道泛型的起源,從而更好的理解泛型。後面一專題將和大家介紹了C#4.0中對泛型的改進——泛型的可變性

泛型專題中用到的所有Demo的原始碼:http://files.cnblogs.com/zhili/GeneralDemo.zip

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.