泛型

一、泛型入門:

我們先來看一個最爲常見的泛型類型List<T>的定義
(真正的定義比這個要複雜的多,我這裏刪掉了很多東西)

複製代碼

[Serializable]public class List<T> : IList<T>, ICollection<T>, 
IEnumerable<T>{    
public T this[int index] { get; set; }    
public void Add(T item);    
public void Clear();    
public bool Contains(T item);    
public int IndexOf(T item);    
public bool Remove(T item);    
public void Sort();    
public T[] ToArray();
}

複製代碼

List後面緊跟着一個<T>表示它操作的是一個未指定的數據類型
(T代表着一個未指定的數據類型)

可以把T看作一個變量名,T代表着一個類型,
在List<T>的源代碼中任何地方都能使用T

T被用作方法的參數和返回值
Add方法接收T類型的參數,ToArray方法返回一個T類型的數組

注意:

  泛型參數應該以T開頭,要麼就叫T,要麼就叫TKey、TValue之類的;
  這跟接口要以I開頭是一樣的,這是約定。

下面來看一段使用泛型類型的代碼

複製代碼

 var a = new List<int>();
     a.Add(1);
     a.Add(2);           
     //這是錯誤的,因爲你已經指定了泛型類型爲int,就不能在這個容器中放入其他的值    
     //這是編譯器錯誤,更提升了排錯效率,如果是運行期錯誤,不知道要多麼煩人
     a.Add("3");            
     var item = a[2];

複製代碼

請注意上面代碼裏的註釋

二、泛型的作用(1):

作爲程序員,寫代碼時刻不忘代碼重用。
代碼重用可以分成很多類,其中算法重用就是非常重要的一類

假設你要爲一組整型數據寫一個排序算法,又要爲一組浮點型數據寫一個排序算法
如果沒有泛型類型,你會怎麼做呢?

你可能想到了方法的重載
寫兩個同名方法,一個方法接收整型數組,另一個方法接收浮點型的數組

但有了泛型,你就完全不必這麼做,只要設計一個方法就夠用了,你甚至可以用這個方法爲一組字符串數據排序

三、泛型的作用(2):

假設你是一個方法的設計者,
這個方法需要有一個輸入參數,但你並能確定這個輸入參數的類型
那麼你會怎麼做呢?

有一部分人可能會馬上反駁:“不可能有這種時候!”
那麼我會跟你說,編程是一門經驗型的工作,你的經驗還不夠,還沒有碰到過類似的地方。

另一部分人可能考慮把這個參數的類型設置成Object的
這確實是一種可行的方案
但會造成下面兩個問題

如果我給這個方法傳遞整形的數據
(值類型的數據都一樣)
就會產生額外的裝箱、拆箱操作
造成性能損耗

如果你這個方法裏的處理邏輯不適用於字符串的參數
而使用者又傳了一個字符串進來
編譯器是不會報錯的,
只有在運行期纔會報錯
(如果質管部門沒有測出這個運行期BUG,那麼不知道要造成多大的損失呢)
這就是我們常說的:類型不安全

四、泛型的示例:

像List<T>和Dictionary<TKey,TValue>之類的泛型類型我們經常用到
下面我介紹幾個不常用到的泛型類型

ObservableCollection<T>
當這個集合發生改變後會有相應的事件得到通知
請看如下代碼:

複製代碼

static void Main(string[] args)
{    var a = new ObservableCollection<int>();
    a.CollectionChanged += a_CollectionChanged;
}
static void a_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{    //可以通過Action來判斷是什麼操作觸發了事件  
//e.Action == NotifyCollectionChangedAction.Add   
 //可以根據以下兩個屬性來得到更改前和更改後的內容    
 //e.NewItems;    //e.OldItems;}

複製代碼

使用這個集合需要引用如下兩個名稱空間

using System.Collections.ObjectModel;using System.Collections.Specialized;

 

BlockingCollection<int>是線程安全的集合
來看看下面這段代碼

複製代碼

var bcollec = new BlockingCollection<int>(2);//試圖添加1-50Task.Run(() =>{    //並行循環
    Parallel.For(1, 51, i =>
    {
        bcollec.Add(i);
        Console.WriteLine("加入:" + i);
    });
});

Thread.Sleep(1000);
Console.WriteLine("調用一次Take");
bcollec.Take();//等待無限長時間Thread.Sleep(Timeout.Infinite);

複製代碼

輸出結果爲:

加入:1加入:37調用一次Take
加入:13

BlockingCollection<int>還可以設置CompleteAdding和IsCompleted屬性來拒絕加入新元素
.NET類庫還提供了很多的泛型類型,在這裏就不一一例舉了

五、泛型的繼承:

在.net中一切都繼承自Object
泛型也不例外
泛型類型可以繼承自其他類型
來看一下如下代碼

複製代碼

public class MyType
{    public virtual string getOneStr()
    {        return "base object Str";
    }
}public class MyOtherType<T> : MyType
{    public override string getOneStr()
    {        return typeof(T).ToString();
    }
}class Program
{    static void Main(string[] args)
    {
        MyType target = new MyOtherType<int>();
        Console.WriteLine(target.getOneStr());
        Console.ReadKey();
    }
}

複製代碼

泛型類型MyOtherType<T>成功的重寫了非泛型類型MyType的方法
如果我試圖按如下方式從MyOtherType<T>類型派生子類型就會導致編譯器錯誤

//編譯期錯誤public class MyThirdType : MyOtherType<T>{
}

但是如果寫成這種方式,就不會出錯

複製代碼

    public class MyThirdType : MyOtherType<int>
    {        public override string getOneStr()
        {            return "MyThirdType";
        }
    }

複製代碼

如果一個方法接收MyThirdType類型的參數,
那麼不能將一個MyOtherType<int>的實例傳遞給這個方法
然而一個方法如果接收MyOtherType<int>類型的參數
卻可以把MyThirdType類型的實例傳遞給這個方法
寫成如下方式也不會出錯

複製代碼

    public class MyThirdType<T> : MyOtherType<T>
    {        public override string getOneStr()
        {            return typeof(T).ToString() + " from MyThirdType";
        }
    }

複製代碼

此中訣竅,只可意會,不可言傳

六、泛型接口

.NET類庫裏有很多泛型的接口
比如:IEnumerator<T>、IList<T>等
這裏不對這些接口做詳細描述了
值說說爲什麼要有泛型接口。

其實泛型接口出現的原因和泛型出現的原因類似
拿IComparable這個接口來說,
此接口只描述了一個方法:

int CompareTo(object obj);

大家看到,如果是值類型的參數,勢必會導致裝箱和拆箱操作
同時,也不是強類型的,不能在編譯期確定參數的類型
有了IComparable<T>就解決掉這個問題了

int CompareTo(T other);

七、泛型委託

委託描述方法,
泛型委託的由來和泛型接口類似

定義一個泛型委託也比較簡單:

public delegate void MyAction<T>(T obj);

這個委託描述一類方法
這類方法接收T類型的參數,沒有返回值
來看看使用這個委託的方法

複製代碼

public delegate void MyAction<T>(T obj);static void Main(string[] args)
{    var method = new MyAction<int>(printInt);
    method(3);
    Console.ReadKey();
}static void printInt(int i)
{
    Console.WriteLine(i);
}

複製代碼

由於定義委託比較繁瑣
.NET類庫在System名稱空間,下定義了三種比較常用的泛型委託

Predicate<T>委託:

public delegate bool Predicate<T>(T obj);

這個委託描述的方法爲接收一個T類型的參數,返回一個BOOL類型的值,一般用於比較方法

Action<T>委託

public delegate void Action<T>(T obj);
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);

這個委託描述的方法,接收一個或多個T類型的參數(最多16個,我這裏只寫了兩種類型的定義方式),沒有返回值

Func<T>委託

public delegate TResult Func<TResult>();
public delegate TResult Func<T, TResult>(T arg);

這個委託描述的方法,接收零個或多個T類型的參數(最多16個,我這裏只寫了兩種類型的定義方式),
與Action委託不同的是,它有一個返回值,返回值的類型爲TResult類型的

關於委託的描述,您還可以看我這篇文章
30分鐘LINQ教程

八、泛型方法

泛型類型中的T可以用在這個類型的任何地方
然而有些時候,我們不希望在使用類型的時候就指定T的類型
我們希望在使用這個類型的方法時,再指定T的類型
來看看如下代碼:

複製代碼

    public class MyClass
    {        public TParam CompareTo<TParam>(TParam other)
        {
            Console.WriteLine(other.ToString());            return other;
        }
    }

複製代碼

上面的代碼中MyClass並不是一個泛型類型
但這個類型中的CompareTo<TParam>()卻是一個泛型方法
TParam可以用在這個方法中的任何地方。

使用泛型方法一般用如下代碼就可以了:

obj.CompareTo<int>(4);
obj.CompareTo<string>("ddd");

然而,你可以寫的更簡單一些,寫成如下的方式

obj.CompareTo(2);
obj.CompareTo("123");

有人會問:“這不可能,沒有指定CompareTo方法的TParam類型,肯定會編譯出錯的”
我告訴你:不會的,編譯器可以幫你完成類型推斷的工作。

注意:
如果你爲一個方法指定了兩個泛型參數,而且這兩個參數的類型都是T,
那麼如果你想使用類型推斷,你必須傳遞兩個相同類型的參數給這個方法
不能一個參數用string類型,另一個用object類型,這會導致編譯錯誤。

九、泛型約束

我們設計了一個泛型類型
很多時候,我們不希望使用者傳入任意類型的參數
也就是說,我們希望“約束”一下T的類型
來看看如下代碼:

複製代碼

    public class MyClass<T> where T : IComparable<T>
    {        public int CompareTo(T other)
        {            return 0;
        }
    }

複製代碼

上面的代碼要求T類型必須實現了IComparable<T>接口
如你所見:泛型的約束通過關鍵字where來實現。

泛型方法當然也可以通過類似的方式對泛型參數進行約束
請看如下代碼:

複製代碼

public class MyClass
{    public TParam CompareTo<TParam>(TParam other) where TParam:class
    {
        Console.WriteLine(other.ToString());        return other;
    }
}

複製代碼

上面代碼中用了class關鍵字約束泛型參數TParam;具體稍後解釋。

注意1:
如果我有一個類型也定義爲MyClass<T>但沒有做約束,
那麼這個時候,做過約束的MyClass<T>將與沒做約束的MyClass<T>衝突,編譯無法通過

注意2:

當你重寫一個泛型方法時,如果這個方法指定了約束
在重寫這個方法時,不能再指定約束了

注意3:

雖然我上面的例子寫的是接口約束,但你完全可以寫一個類型,比如說BaseClass
而且,只要是繼承自BaseClass的類型都可以當作T類型使用,你不要試圖約束T爲Object類型,編譯不會通過的。(傻子才這麼幹)

注意4:

有兩個特殊的約束:class和struct。
where T : class   約束T類型必須爲引用類型
where T : struct  約束T類型必須爲值類型

注意5:
如果你沒有對T進行class約束,
那麼你不能寫這樣的代碼:T obj = null;  這無法通過編譯,因爲T有可能是值類型的。
如果你沒有對T進行struct約束,也沒有對T進行new約束
那麼你不能寫這樣的代碼:T obj = new T();  這無法通過編譯,因爲值類型肯定有無參數構造器,而引用類型就不一定了。
如果你對T進行了new約束:where T : new();  那麼new T()就是正確的,因爲new約束要求T類型有一個公共無參構造器。

注意6:
就算沒有對T進行任何約束,也有一個辦法來處理值類型和引用類型的問題
T temp = default(T);
如果T爲引用類型,那麼temp就是null;如果T爲值類型,那麼temp就是0;

注意7:
試圖對T類型的變量進行強制轉化,一般情況下會報編譯期錯誤。
但你可以先把T轉化成object再把object轉化成你要的類型(一般不推薦這麼做,你應該考慮把T轉化成一個約束兼容的類型)
你也可以考慮用as操作符進行類型轉化,這一般不會報錯,但只能轉化成引用類型。

關於泛型約束的內容,我在這篇文章裏也有提到
30分鐘linq教程

十、逆變和協變

 一般情況下,我們使用泛型時,由T標記的泛型類型是不能更改的
也就是說,如下兩種寫法都是錯誤的

var a = new List<object>();
List<string> b = a;var c = new List<string>();
List<object> d = c;

注意:這裏沒有寫強制轉換,即使寫了強制轉換也是錯誤的,編譯就無法通過

然而泛型提供了逆變和協變的特性,
有了這兩種特性,這種轉換就成爲了可能。

逆變:
泛型類型T可以從基類型更改爲該類的派生類型,
用in關鍵字標記逆變形式的類型參數,
而且這個參數一般作輸入參數。

協變:
泛型類型T可以從派生類型更改爲它的基類型,
用out關鍵字來標記協變形式的類型參數,
而且這個參數一般作爲返回值

如果我們定義了一個這樣的委託:

public delegate TResult MyAction<in T,out TResult>(T obj);

那麼,就可以讓如下代碼通過編譯(不用強制轉換)

var a = new MyAction<object, ArgumentException>(o => new ArgumentException(o.ToString()));
MyAction<string, Exception> b = a;

這就是逆變和協變的威力。

注意:
只有接口和委託的泛型類型纔可以使用逆變和協變的特性


原文地址:http://www.cnblogs.com/liulun/archive/2013/05/02/3033599.html


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章