我們先來看一個(gè)最為常見的泛型類型List<T>的定義
(真正的定義比這個(gè)要復(fù)雜的多,我這里刪掉了很多東西)
[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后面緊跟著一個(gè)<T>表示它操作的是一個(gè)未指定的數(shù)據(jù)類型(T代表著一個(gè)未指定的數(shù)據(jù)類型)
可以把T看作一個(gè)變量名,T代表著一個(gè)類型,在List<T>的源代碼中任何地方都能使用T。
T被用作方法的參數(shù)和返回值。
Add方法接收T類型的參數(shù),ToArray方法返回一個(gè)T類型的數(shù)組
注意:
泛型參數(shù)必須以T開頭,要么就叫T,要么就叫TKey或者TValue;
這跟接口要以I開頭是一樣的,這是約定。
下面來看一段使用泛型類型的代碼
var a = new List<int>();
a.Add(1);
a.Add(2);
//這是錯(cuò)誤的,因?yàn)槟阋呀?jīng)指定了泛型類型為int,就不能在這個(gè)容器中放入其他的值
//這是編譯器錯(cuò)誤,更提升了排錯(cuò)效率,如果是運(yùn)行期錯(cuò)誤,不知道要多么煩人
a.Add("3");
var item = a[2];
請注意上面代碼里的注釋
二、泛型的作用(1):
作為程序員,寫代碼時(shí)刻不忘代碼重用。
代碼重用可以分成很多類,其中算法重用就是非常重要的一類,假設(shè)你要為一組整型數(shù)據(jù)寫一個(gè)排序算法,又要為一組浮點(diǎn)型數(shù)據(jù)寫一個(gè)排序算法,如果沒有泛型類型,你會怎么做呢?
你可能想到了方法的重載。
寫兩個(gè)同名方法,一個(gè)方法接收整型數(shù)組,另一個(gè)方法接收浮點(diǎn)型的數(shù)組。
但有了泛型,你就完全不必這么做,只要設(shè)計(jì)一個(gè)方法就夠用了,你甚至可以用這個(gè)方法為一組字符串?dāng)?shù)據(jù)排序。
三、泛型的作用(2):
假設(shè)你是一個(gè)方法的設(shè)計(jì)者,這個(gè)方法需要有一個(gè)輸入?yún)?shù),但你并能確定這個(gè)輸入?yún)?shù)的類型,那么你會怎么做呢?
有一部分人可能會馬上反駁:“不可能有這種時(shí)候!”
那么我會跟你說,編程是一門經(jīng)驗(yàn)型的工作,你的經(jīng)驗(yàn)還不夠,還沒有碰到過類似的地方。
另一部分人可能考慮把這個(gè)參數(shù)的類型設(shè)置成Object的,這確實(shí)是一種可行的方案,但會造成下面兩個(gè)問題,如果我給這個(gè)方法傳遞整形的數(shù)據(jù)(值類型的數(shù)據(jù)都一樣),就會產(chǎn)生額外的裝箱、拆箱操作,造成性能損耗。
如果你這個(gè)方法里的處理邏輯不適用于字符串的參數(shù),而使用者又傳了一個(gè)字符串進(jìn)來,編譯器是不會報(bào)錯(cuò)的,只有在運(yùn)行期才會報(bào)錯(cuò)。
(如果質(zhì)管部門沒有測出這個(gè)運(yùn)行期BUG,那么不知道要造成多大的損失呢)
這就是我們常說的:類型不安全。
四、泛型的示例:
像List<T>和Dictionary<TKey,TValue>之類的泛型類型我們經(jīng)常用到,下面我介紹幾個(gè)不常用到的泛型類型。
ObservableCollection<T>
當(dāng)這個(gè)集合發(fā)生改變后會有相應(yīng)的事件得到通知。
請看如下代碼:
static void Main(string[] args)
{
var a = new ObservableCollection<int>();
a.CollectionChanged += a_CollectionChanged;
}
static void a_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
//可以通過Action來判斷是什么操作觸發(fā)了事件
//e.Action == NotifyCollectionChangedAction.Add
//可以根據(jù)以下兩個(gè)屬性來得到更改前和更改后的內(nèi)容
//e.NewItems;
//e.OldItems;
}
使用這個(gè)集合需要引用如下兩個(gè)名稱空間
using System.Collections.ObjectModel;
using System.Collections.Specialized;
BlockingCollection<int>是線程安全的集合
來看看下面這段代碼
var bcollec = new BlockingCollection<int>(2);
//試圖添加1-50
Task.Run(() =>
{
//并行循環(huán)
Parallel.For(1, 51, i =>
{
bcollec.Add(i);
Console.WriteLine("加入:" + i);
});
});
Thread.Sleep(1000);
Console.WriteLine("調(diào)用一次Take");
bcollec.Take();
//等待無限長時(shí)間
Thread.Sleep(Timeout.Infinite);
輸出結(jié)果為:
加入:1
加入:37
調(diào)用一次Take
加入:13
BlockingCollection<int>還可以設(shè)置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>類型派生子類型就會導(dǎo)致編譯器錯(cuò)誤。
//編譯期錯(cuò)誤
public class MyThirdType : MyOtherType<T>
{
}
但是如果寫成這種方式,就不會出錯(cuò)
public class MyThirdType : MyOtherType<int>
{
public override string getOneStr()
{
return "MyThirdType";
}
}
注意:
如果按照如上寫法,會造成類型不統(tǒng)一的問題,
如果一個(gè)方法接收MyThirdType類型的參數(shù),
那么不能將一個(gè)MyOtherType<int>的實(shí)例傳遞給這個(gè)方法,
然而一個(gè)方法如果接收MyOtherType<int>類型的參數(shù),
卻可以把MyThirdType類型的實(shí)例傳遞給這個(gè)方法,
這是CLR內(nèi)部實(shí)現(xiàn)機(jī)制造成的,
這看起來確實(shí)很怪異!
寫成如下方式也不會出錯(cuò):
public class MyThirdType<T> : MyOtherType<T>
{
public override string getOneStr()
{
return typeof(T).ToString() + " from MyThirdType";
}
}
此中訣竅,只可意會,不可言傳。
六、泛型接口
.NET類庫里有很多泛型的接口,比如:IEnumerator<T>、IList<T>等,這里不對這些接口做詳細(xì)描述了,值說說為什么要有泛型接口。
其實(shí)泛型接口出現(xiàn)的原因和泛型出現(xiàn)的原因類似,拿IComparable這個(gè)接口來說,此接口只描述了一個(gè)方法:
int CompareTo(object obj);
大家看到,如果是值類型的參數(shù),勢必會導(dǎo)致裝箱和拆箱操作。
同時(shí),也不是強(qiáng)類型的,不能在編譯期確定參數(shù)的類型,有了IComparable<T>就解決掉這個(gè)問題了:
int CompareTo(T other);
七、泛型委托
委托描述方法,泛型委托的由來和泛型接口類似。
定義一個(gè)泛型委托也比較簡單:
public delegate void MyAction<T>(T obj);
這個(gè)委托描述一類方法,這類方法接收T類型的參數(shù),沒有返回值。
來看看使用這個(gè)委托的方法:
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);
這個(gè)委托描述的方法為接收一個(gè)T類型的參數(shù),返回一個(gè)BOOL類型的值,一般用于比較方法。
Action<T>委托
public delegate void Action<T>(T obj);
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
這個(gè)委托描述的方法,接收一個(gè)或多個(gè)T類型的參數(shù)(最多16個(gè),我這里只寫了兩種類型的定義方式),沒有返回值。
Func<T>委托
public delegate TResult Func<TResult>();
public delegate TResult Func<T, TResult>(T arg);
這個(gè)委托描述的方法,接收零個(gè)或多個(gè)T類型的參數(shù)(最多16個(gè),我這里只寫了兩種類型的定義方式),與Action委托不同的是,它有一個(gè)返回值,返回值的類型為TResult類型的。
關(guān)于委托的描述,您還可以看我這篇文章。
八、泛型方法
泛型類型中的T可以用在這個(gè)類型的任何地方,然而有些時(shí)候,我們不希望在使用類型的時(shí)候就指定T的類型,我們希望在使用這個(gè)類型的方法時(shí),再指定T的類型。
來看看如下代碼:
public class MyClass
{
public TParam CompareTo<TParam>(TParam other)
{
Console.WriteLine(other.ToString());
return other;
}
}
上面的代碼中MyClass并不是一個(gè)泛型類型,但這個(gè)類型中的CompareTo<TParam>()卻是一個(gè)泛型方法,TParam可以用在這個(gè)方法中的任何地方。
使用泛型方法一般用如下代碼就可以了:
obj.CompareTo<int>(4);
obj.CompareTo<string>("ddd");
然而,你可以寫的更簡單一些,寫成如下的方式:
obj.CompareTo(2);
obj.CompareTo("123");
有人會問:“這不可能,沒有指定CompareTo方法的TParam類型,肯定會編譯出錯(cuò)的”
我告訴你:不會的,編譯器可以幫你完成類型推斷的工作。
注意:
如果你為一個(gè)方法指定了兩個(gè)泛型參數(shù),而且這兩個(gè)參數(shù)的類型都是T,那么如果你想使用類型推斷,你必須傳遞兩個(gè)相同類型的參數(shù)給這個(gè)方法,不能一個(gè)參數(shù)用string類型,另一個(gè)用object類型,這會導(dǎo)致編譯錯(cuò)誤。
九、泛型約束
我們設(shè)計(jì)了一個(gè)泛型類型,很多時(shí)候,我們不希望使用者傳入任意類型的參數(shù),也就是說,我們希望“約束”一下T的類型。
來看看如下代碼:
public class MyClass<T> where T : IComparable<T>
{
public int CompareTo(T other)
{
return 0;
}
}
上面的代碼要求T類型必須實(shí)現(xiàn)了IComparable<T>接口。
如你所見:泛型的約束通過關(guān)鍵字where來實(shí)現(xiàn)。
泛型方法當(dāng)然也可以通過類似的方式對泛型參數(shù)進(jìn)行約束。
請看如下代碼:
public class MyClass
{
public TParam CompareTo<TParam>(TParam other) where TParam:class
{
Console.WriteLine(other.ToString());
return other;
}
}
上面代碼中用了class關(guān)鍵字約束泛型參數(shù)TParam;具體稍后解釋。
注意1:
如果我有一個(gè)類型也定義為MyClass<T>但沒有做約束,那么這個(gè)時(shí)候,做過約束的MyClass<T>將與沒做約束的MyClass<T>沖突,編譯無法通過。
注意2:
當(dāng)你重寫一個(gè)泛型方法時(shí),如果這個(gè)方法指定了約束,在重寫這個(gè)方法時(shí),不能再指定約束了。
注意3:
雖然我上面的例子寫的是接口約束,但你完全可以寫一個(gè)類型,比如說BaseClass。而且,只要是繼承自BaseClass的類型都可以當(dāng)作T類型使用,你不要試圖約束T為Object類型,編譯不會通過的。(傻子才這么干)
注意4:
有兩個(gè)特殊的約束:class和struct。
where T : class 約束T類型必須為引用類型
where T : struct 約束T類型必須為值類型
注意5:
如果你沒有對T進(jìn)行class約束,
那么你不能寫這樣的代碼:T obj = null; 這無法通過編譯,因?yàn)門有可能是值類型的。
如果你沒有對T進(jìn)行struct約束,也沒有對T進(jìn)行new約束。
那么你不能寫這樣的代碼:T obj = new T(); 這無法通過編譯,因?yàn)橹殿愋涂隙ㄓ袩o參數(shù)構(gòu)造器,而引用類型就不一定了。
如果你對T進(jìn)行了new約束:where T : new(); 那么new T()就是正確的,因?yàn)閚ew約束要求T類型有一個(gè)公共無參構(gòu)造器。
注意6:
就算沒有對T進(jìn)行任何約束,也有一個(gè)辦法來處理值類型和引用類型的問題。
T temp = default(T);
如果T為引用類型,那么temp就是null;如果T為值類型,那么temp就是0;
注意7:
試圖對T類型的變量進(jìn)行強(qiáng)制轉(zhuǎn)化,一般情況下會報(bào)編譯期錯(cuò)誤。
但你可以先把T轉(zhuǎn)化成object再把object轉(zhuǎn)化成你要的類型(一般不推薦這么做,你應(yīng)該考慮把T轉(zhuǎn)化成一個(gè)約束兼容的類型)。
你也可以考慮用as操作符進(jìn)行類型轉(zhuǎn)化,這一般不會報(bào)錯(cuò),但只能轉(zhuǎn)化成引用類型。
關(guān)于泛型約束的內(nèi)容,我在這篇文章里也有提到。
十、逆變和協(xié)變
一般情況下,我們使用泛型時(shí),由T標(biāo)記的泛型類型是不能更改的。
也就是說,如下兩種寫法都是錯(cuò)誤的:
var a = new List<object>();
List<string> b = a;
var c = new List<string>();
List<object> d = c;
注意:這里沒有寫強(qiáng)制轉(zhuǎn)換,即使寫了強(qiáng)制轉(zhuǎn)換也是錯(cuò)誤的,編譯就無法通過,然而泛型提供了逆變和協(xié)變的特性,有了這兩種特性,這種轉(zhuǎn)換就成為了可能。
逆變:
泛型類型T可以從基類型更改為該類的派生類型,用in關(guān)鍵字標(biāo)記逆變形式的類型參數(shù),而且這個(gè)參數(shù)一般作輸入?yún)?shù)。
協(xié)變:
泛型類型T可以從派生類型更改為它的基類型,用out關(guān)鍵字來標(biāo)記協(xié)變形式的類型參數(shù),而且這個(gè)參數(shù)一般作為返回值。
如果我們定義了一個(gè)這樣的委托:
public delegate TResult MyAction<in T,out TResult>(T obj);
那么,就可以讓如下代碼通過編譯(不用強(qiáng)制轉(zhuǎn)換)
var a = new MyAction<object, ArgumentException>(o => new ArgumentException(o.ToString()));
MyAction<string, Exception> b = a;
這就是逆變和協(xié)變的威力。
更多信息請查看IT技術(shù)專欄