C# 之泛型
一、何爲泛型
C#泛型的說明抄自MSDN: 泛型是 2.0 版 C# 語言和公共語言運行庫 (CLR) 中的一個新功能。泛型將類型參數的概念引入 .NET Framework,類型參數使得設計如下類和方法成爲可能:這些類和方法將一個或多個類型的指定推遲到客戶端代碼聲明並實例化該類或方法的時候。例如,通過使用泛型類型參數 T,您可以編寫其他客戶端代碼能夠使用的單個類,而不致引入運行時強制轉換或裝箱操作的成本或風險。
如以下代碼:
using System;
namespace Demo
{
public class Program
{
public static void Main(string[] args)
{
GenericTest<String> t = new GenericTest<String>();
t.obj = "Hello";
// t.obj = 3; Error
Console.WriteLine(t.obj.GetType().Name);
}
}
public class GenericTest<T>
{
public T obj ;
}
}
t的類型爲GenericTest<String>,那麼在GenericTest<T>類中,所有的T都將會被String替代。因此,我們不能將3賦值給t.obj。
二、爲什麼要用泛型
使用泛型主要是從類型的安全性和效率上考慮的。在不使用泛型的時候:
using System.Collections;
using System;
namespace Demo
{
public class Program
{
public static void Main(string[] args)
{
ArrayList arr = new ArrayList();
arr.Add("547");
arr.Add(3);
foreach (var i in arr)
{
int.Parse((string)i);
}
}
}
}
C#中有一個ArrayList類,它沒有使用泛型,其中保存的是Object類型的成員,由於所有類都繼承於Object,因此它可以容納所有對象。在上段代碼中主要有2個問題:1、沒有類型的安全檢查,例如我們本來想在其中放入String對象,但是可能因爲手誤或者其他原因混進了別的類型,導致代碼出錯。2、在操作對象的時候,進行反覆地拆包、裝包工作,導致效率的降低。因此,可以用泛型來替代:
using System;
using System.Collections.Generic;
namespace Demo
{
public class Program
{
public static void Main(string[] args)
{
List<String> arr = new List<String>();
arr.Add("547");
//arr.Add(3); Error
foreach (var i in arr)
{
int.Parse((string)i);
}
}
}
}
上一段代碼中,arr再也無法添加數字3了,因爲它不是String。
三、泛型是一種單獨的類型嗎
是。
如下一段C#代碼:
using System;
using System.Collections.Generic;
namespace Demo
{
public class Program
{
public static void Main(string[] args)
{
List<String> genericStr = new List<String>();
List<Int32> genericInt = new List<Int32>();
Console.WriteLine(genericStr.GetType().FullName);
Console.Write(genericStr.GetType() == genericInt.GetType());
}
}
}
最後控制檯輸出的是false,說明List<String>和List<Int32>確確實實是兩個不同的類型。因爲如此,C#中的泛型用起來才十分方便。
在Java中,泛型是一個比較難以理解的機制。如Java中的List<String>、List<Integer>,它們都會被看做是List類,List類被稱爲原生類型(Raw type),因此無論是List<String>還是List<Integer>的實例,它們的類型總是相同的。另外,Java有一個擦除機制,即List<T>的T只存在於編譯器,在運行時,T類型的對象實質上是Object類型,因此你無法獲得運行時它的實際類型,如無法用T a = new T()、obj instanceof T這樣的語句來進行判斷。
四、泛型的約束
有時候,我們希望泛型T是繼承於某類、某接口,或者擁有默認構造方法等。
using System;
namespace Demo
{
public class Program
{
public static void Main(string[] args)
{
B<A> test = new B<A>();
B<Int32> test2 = new B<Int32>(); //編譯錯誤,因爲Int32不繼承於A
}
}
public class A
{
public void Test() { }
}
public class B<T> where T : A
{
public T obj;
}
}
B類的定義後面加了 where T : A,表明 T 必須要繼承A。一下是 where 所有的用法:
where T : struct -> T一定要爲值類型
where T : class -> T一定要爲引用類型
where T : A -> T一定要繼承A (A可以爲類名或接口名)
where T : new() -> T一定要有無參構造方法
where T1 : T2 -> T1一定要繼承於T2 (裸類型約束)
值得一提的是 where T : new(),只有定義了這樣的約束,纔可以執行 T a = new T()這樣的語句(而這樣的語句是絕對不可能在Java中運行的)。
泛型約束的意義究竟在哪裏呢?請看下面的例子:
using System;
namespace Demo
{
public class A
{
public void Test() { }
}
public class B<T> where T : A
{
public T obj;
public void Test()
{
obj.Test();
}
}
}
只有說明了T是繼承於A的,才能夠調用obj.Test。因爲,繼承於A的T裏面一定有Test方法,將where子句去掉,程序將無法通過編譯。
五、協變與逆變(抗變)
首先請看以下代碼:
using System.Collections.Generic;
namespace Demo
{
class Food { };
class Bread : Food { }
class Program
{
public static void Main()
{
IEnumerable<Bread> bread = new List<Bread>();
IEnumerable<Food> food = bread;
}
}
}
我們發現,bread可以將值賦予food,我們從Food和Bread的定義可知,Bread是Food的子類。像這樣將接口類型賦予下層級別(子類)(Bread -> Food),成爲協變,也就是基類類型的接口充當一個更大的容器(能夠裝食物的容器肯定可以裝麪包)。
而以下例子又有所不同:
using System;
namespace Demo
{
class Program
{
public static void Main()
{
Action<object> actObject = (o) => { };
Action<string> actString = actObject;
}
}
}
我們發現,actString被賦予了actObject,這種是從 Object -> String 的轉變,將一個抽象的對象具體化,和協變相反,我們稱之爲逆變。它是對象具體化的一個過程。
目前,只有接口、委託纔可以定義泛型參數類型。在參數前加out表示協變,加in表示逆變。如IEnumerable<out T>、Action<in T>。
六、泛型方法
之前我們提到了泛型類、泛型接口,以及剛剛提及了泛型委託,其實方法也可以使用泛型。
using System;
namespace Demo
{
class Program
{
public static void Main()
{
int i = GetItem<int>(5);
}
public static T GetItem<T>(T item)
{
return item;
}
}
}
GetItem爲一泛型方法,我們顯示強調我們要使用GetItem<int>,意味着GetItem的第一個參數一定要爲int。但是,我們也可以用如下方法調用GetItem方法:
string i = GetItem("s");
我們並沒有說明GetItem中泛型T的類型,但是我們很快意識到我們也沒有必要說明。編譯器發現GetItem後的第一個參數爲一個string,於是就把T當做String來處理,這樣,我們就不用在意這個方法是泛型方法,而可以把它當成普通方法來看待。
以上兩種做法各有好處。第一種方法可以顯示強調正在使用泛型方法,並且編譯器會檢查參數類型是否正確,但是花費的筆墨太多,第二種方法的優缺點和第一種方法相反。
七、總結
泛型是一個有用的機制。學習Java的同學最開始很容易被Java的泛型搞得一頭霧水,但是C#中泛型類就是一個單獨的類,List<int>和List<bool>就是兩種不同的類型。泛型多用於容器(List<T>)和委託(Action<T>, Func<T1, TResult>)。泛型的作用可能會接近於“多態”,以至於不知道到底該用泛型還是用基類,如ArrayList和List<T>。這個需要在實際情況中結合類型安全性和效率來考慮。