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


引言:

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

 

一、類型推斷

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

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

 

 


免責聲明!

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



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