泛型相比反射,委托等較為抽象的概念要更接地氣得多扔嵌,而且在平常工作時(shí),我們幾乎時(shí)刻都和泛型有接觸夺颤。大部分人對(duì)泛型都是比較熟悉的痢缎。
泛型集合是類(lèi)型安全的集合。相對(duì)于泛型System.Collections.Generic世澜,我們有類(lèi)型不安全的集合System.Collections独旷,其中的成員均為Object類(lèi)型。一個(gè)經(jīng)典的例子是ArrayList宜狐。
在使用ArrayList時(shí)势告,我們可以插入任意類(lèi)型的數(shù)據(jù),如果插入值類(lèi)型的數(shù)據(jù)抚恒,其都會(huì)裝箱為Object類(lèi)型咱台。這造成類(lèi)型不安全,我們不知道取出的數(shù)據(jù)是不是想要的類(lèi)型俭驮。泛型(集合)的數(shù)據(jù)類(lèi)型是統(tǒng)一的回溺,是類(lèi)型安全的春贸,沒(méi)有裝箱和拆箱問(wèn)題,提供了更好的性能遗遵。為泛型變量設(shè)置默認(rèn)值時(shí)常使用default關(guān)鍵字進(jìn)行:T temp = default(T)萍恕。如果T為引用類(lèi)型,則temp為null车要,如果T為值類(lèi)型允粤,則temp為0。
ArrayList的泛型集合版本為L(zhǎng)ist<T>翼岁。T稱(chēng)為類(lèi)型參數(shù)类垫。調(diào)用時(shí)指定的具體類(lèi)型叫做實(shí)際參數(shù)(實(shí)參)。
面試必須知道的泛型三大好處:類(lèi)型安全琅坡,增強(qiáng)性能悉患,代碼復(fù)用。
泛型集合的使用契機(jī):幾乎任何時(shí)候榆俺,都不考慮不用泛型集合代替泛型集合售躁。很多非泛型集合也有了自己的泛型版本,例如棧茴晋,隊(duì)列等登疗。
泛型方法
泛型方法的使用契機(jī)一般為傳入類(lèi)型可能有很多種柏腻,但處理方式卻相同的情境屎鳍。這時(shí)我們可以不需要寫(xiě)很多個(gè)重載也糊,而考慮用泛型方法達(dá)到代碼復(fù)用的目的。配合泛型約束掀虎,可以寫(xiě)出更嚴(yán)謹(jǐn)?shù)姆椒ā7盒臀幸部梢钥闯墒欠盒头椒ǖ囊环N應(yīng)用付枫。
例如交換兩個(gè)同類(lèi)型變量的值:
static void Swap<T>(ref T lhs, ref T rhs)
{
T temp;
temp = lhs;
lhs = rhs;
rhs = temp;
}
泛型約束
約束的作用是限制能指定成泛型實(shí)參(即T的具體類(lèi)型)的數(shù)量烹玉。通過(guò)限制類(lèi)型的數(shù)量,可以對(duì)這些類(lèi)型執(zhí)行更多的操作阐滩。例如下面的方法二打,T被約束為必須是實(shí)現(xiàn)了IComparable接口的類(lèi)型。此時(shí)掂榔,傳入的T除了擁有object類(lèi)型的方法之外继效,還額外多了一個(gè)CompareTo方法。由于保證了傳入的T必須是實(shí)現(xiàn)了IComparable接口的類(lèi)型装获,就可以肯定T類(lèi)型一定含有CompareTo方法瑞信。如果去掉約束,o1是沒(méi)有CompareTo方法的穴豫。
static int Compare<T>(T o1, T o2) where T : IComparable<T>
{
return o1.CompareTo(o2);
}
此時(shí)如果將object類(lèi)型的數(shù)據(jù)傳入方法凡简,則會(huì)報(bào)錯(cuò)逼友。因?yàn)閛bject沒(méi)有實(shí)現(xiàn)IComparable<T>接口。
泛型約束分為如下幾類(lèi):
- 接口約束:泛型實(shí)參必須實(shí)現(xiàn)某個(gè)接口秤涩。接口約束可以有多個(gè)帜乞。
- 基類(lèi)型約束:泛型實(shí)參必須是某個(gè)基類(lèi)的派生類(lèi)。特別的筐眷,可以指定T : class / T : struct黎烈,此時(shí)T分別只能為引用類(lèi)型或值類(lèi)型≡纫ィ基類(lèi)型約束必須放在其他約束之前怨喘。
- 構(gòu)造函數(shù)new()約束:泛型實(shí)參必須具有可訪問(wèn)的無(wú)參數(shù)構(gòu)造函數(shù)(默認(rèn)的也可)。new()約束出現(xiàn)在where子句的最后振定。
如果泛型方法沒(méi)有任何約束必怜,則傳入的對(duì)象會(huì)被視為object。它們的功能比較有限后频。不能使用 != 和 == 運(yùn)算符梳庆,因?yàn)闊o(wú)法保證具體類(lèi)型參數(shù)能支持這些運(yùn)算符。
協(xié)變和逆變
可變性是以一種類(lèi)型安全的方式卑惜,將一個(gè)對(duì)象作為另一個(gè)對(duì)象來(lái)使用膏执。其對(duì)應(yīng)的術(shù)語(yǔ)則是不變性(invariant)。
可變性
可變性是以一種類(lèi)型安全的方式露久,將一個(gè)對(duì)象作為另一個(gè)對(duì)象來(lái)使用更米。例如對(duì)普通繼承中的可變性:若某方法聲明返回類(lèi)型為Stream,在實(shí)現(xiàn)時(shí)可以返回一個(gè)MemoryStream毫痕≌髀停可變性有兩種類(lèi)型:協(xié)變和逆變。
協(xié)變性:可以建立一個(gè)較為一般類(lèi)型的變量消请,然后為其賦值栏笆,值是一個(gè)較為特殊類(lèi)型的變量。例如:
string str = "test";
// An object of a more derived type is assigned to an object of a less derived type.
object obj = str;
因?yàn)閟tring肯定是一個(gè)object臊泰,所以這樣的變化非常正常蛉加。
逆變性:在上面的例子中,我們無(wú)法將str和一個(gè)新的object對(duì)象畫(huà)等號(hào)缸逃。如果強(qiáng)行要實(shí)現(xiàn)的話针饥,只能這么干:
string s = (string) new object();
但這樣還是會(huì)在運(yùn)行時(shí)出錯(cuò)。這也告訴我們需频,逆變性是很不正常的丁眼。
泛型的協(xié)變與逆變
協(xié)變性和out關(guān)鍵字搭配使用,用于向調(diào)用者返回某項(xiàng)操作的值贺辰。例如下面的接口僅有一個(gè)方法户盯,就是生產(chǎn)一個(gè)T類(lèi)型的實(shí)例嵌施。那么我們可以傳入一個(gè)特定類(lèi)型。如我們可以將IFactory<Pizza>視為IFactory<Food>莽鸭。這也適用于Food的所有子類(lèi)型吗伤。(即將其視為一個(gè)更一般類(lèi)型的實(shí)現(xiàn))
interface IFactory<T>
{
T CreateInstance();
}
逆變性則相反,和in關(guān)鍵字搭配使用硫眨,指的是API將會(huì)消費(fèi)值足淆,而不是生產(chǎn)值。此時(shí)一般類(lèi)型出現(xiàn)在參數(shù)中:
interface IPrint<T>
{
void Print(T value);
}
這意味著如果我們實(shí)現(xiàn)了IPrint<Code>礁阁,我們就可以將其當(dāng)做IPrint<CsharpCode>使用巧号。(即將其視為一個(gè)更具體類(lèi)型的實(shí)現(xiàn))
如果存在雙向的傳遞,則什么也不會(huì)發(fā)生姥闭。這種類(lèi)型是不變體(invariant)丹鸿。
interface IStorage<T>
{
byte[] Serialize(T value);
T Deserialize(byte[] data);
}
這個(gè)接口是不變體。我們不能將它視為一個(gè)更具體或更一般類(lèi)型的實(shí)現(xiàn)棚品。
假設(shè)有如下繼承關(guān)系People –> Teacher靠欢,People –> Student。
如果我們以協(xié)變的方式使用(假設(shè)你建立了一個(gè)IStorage< Teacher >的實(shí)例铜跑,并將其視為IStorage<People>)則我們可能會(huì)在調(diào)用Serialize時(shí)產(chǎn)生異常门怪,因?yàn)镾erialize方法不支持協(xié)變(如果參數(shù)是People的其他子類(lèi),例如Student锅纺,則IStorage< Teacher >將無(wú)法序列化Student)掷空。
如果我們以逆變的方式使用(假設(shè)你建立了一個(gè)IStorage<People>的實(shí)例,并將其視為IStorage< Teacher >)囤锉,則我們可能會(huì)在調(diào)用Deserialize時(shí)產(chǎn)生異常坦弟,因?yàn)镈eserialize方法不支持逆變,它只能返回People不能返回Teacher嚼锄。
使用in和out表示可變性
如果類(lèi)型參數(shù)用于輸出减拭,就使用out,如果用于輸入区丑,就使用in。注意修陡,協(xié)變和逆變性體現(xiàn)在泛型類(lèi)T和T的派生類(lèi)沧侥。目前out 和in 關(guān)鍵字只能在接口和委托中使用。
IEnumerable<out T>支持協(xié)變性
IEnumerable<T>支持協(xié)變性魄鸦,它允許一個(gè)類(lèi)似下面簽名
void 方法(IEnumerable<T> anIEnumberable)
的方法宴杀,該方法傳入更具體的類(lèi)型(T的派生類(lèi)),但在方法內(nèi)部拾因,類(lèi)型會(huì)被看成IEnumerable<T>旺罢。注意out關(guān)鍵字旷余。
下面的例子演示了協(xié)變性。我們利用IEnumerable<T>的協(xié)變性扁达,傳入較為具體的類(lèi)型Circle正卧。編譯器會(huì)將其看成較為抽象的類(lèi)型Shape。
public class Program
{
public static void Main(string[] args)
{
var circles = new List<Circle>
{
new Circle(new Point(0, 0), 15),
new Circle(new Point(10, 5), 20),
};
var list = new List<IShape>();
//泛型的協(xié)變:
//AddRange傳入的是特殊的類(lèi)型List<Circle>跪解,但要求是一般的類(lèi)型List<IShape>
//AddRange方法簽名:void AddRange(IEnumerable<T> collection)
//IEnumerable<out T>允許協(xié)變(對(duì)于LINQ來(lái)說(shuō)炉旷,協(xié)變尤其重要,因?yàn)楹芏郃PI都表示為IEnumerable<T>)
list.AddRange(circles);
//C# 4.0之前只能這么做
list.AddRange(circles.Cast<IShape>());
}
}
public sealed class Circle : IShape
{
private readonly Point center;
public Point Center { get { return center; } }
private readonly double radius;
public double Radius { get { return radius; } }
public Circle(Point center, int radius)
{
this.center = center;
this.radius = radius;
}
public double Area
{
get { return Math.PI * radius * radius; }
}
}
public interface IShape
{
double Area { get; }
}
IComparer<in T>支持逆變性
IComparer支持逆變性叉讥。我們可以簡(jiǎn)單的實(shí)現(xiàn)一個(gè)可以比較任何圖形面積的方法窘行,傳入的輸入類(lèi)型(in)是最General的類(lèi)型IShape。之后图仓,在使用時(shí)罐盔,我們獲得的結(jié)果是較為具體的類(lèi)型Circle。因?yàn)槿魏螆D形都可以比較面積救崔,圓形當(dāng)然也可以惶看。
注意IComparer的簽名是public interface IComparer<in T>。
public class Program
{
public static void Main(string[] args)
{
var circles = new List<Circle>
{
new Circle(new Point(0, 0), 15),
new Circle(new Point(10, 5), 20),
};
//泛型的逆變:
//AreaComparer可以比較任意圖形的面積帚豪,但我們可以傳入具體的圖形例如圓或正方形
//Compare方法簽名:Compare(IShape x, IShape y)
//IComparer<in T>支持逆變
//傳入的是圓形Circle碳竟,但要求的輸入是IShape
circles.Sort(new AreaComparer());
}
}
class AreaComparer : IComparer<IShape>
{
public int Compare(IShape x, IShape y)
{
return x.Area.CompareTo(y.Area);
}
}
C#中泛型可變性的限制
不支持類(lèi)的類(lèi)型參數(shù)的可變性。只有接口和委托可以擁有可變的類(lèi)型參數(shù)狸臣。in 和 out 修飾符只能用來(lái)修飾泛型接口和泛型委托莹桅。
可變性只支持引用轉(zhuǎn)換≈蛞啵可變性只能用于引用類(lèi)型诈泼,禁止任何值類(lèi)型和用戶(hù)定義的轉(zhuǎn)換,如下面的轉(zhuǎn)換是無(wú)效的:
- 將 IEnumerable<int> 轉(zhuǎn)換為 IEnumerable<object> ——裝箱轉(zhuǎn)換
- 將 IEnumerable<short> 轉(zhuǎn)換為 IEnumerable<int> ——值類(lèi)型轉(zhuǎn)換
- 將 IEnumerable<string> 轉(zhuǎn)換為 IEnumerable<XName> ——用戶(hù)定義的轉(zhuǎn)換
- 類(lèi)型參數(shù)使用了 out 或者 ref 將禁止可變性煤禽。對(duì)于泛型類(lèi)型參數(shù)來(lái)說(shuō)铐达,如果要將該類(lèi)型的實(shí)參傳給使用 out 或者 ref 關(guān)鍵字的方法,便不允許可變性檬果,如:
delegate void someDelegate<in T>(ref T t)
這段代碼編譯器會(huì)報(bào)錯(cuò)瓮孙。
可變性必須顯式指定。從實(shí)現(xiàn)上來(lái)說(shuō)編譯器完全可以自己判斷哪些泛型參數(shù)能夠逆變和協(xié)變选脊,但實(shí)際卻沒(méi)有這么做杭抠,這是因?yàn)镃#的開(kāi)發(fā)團(tuán)隊(duì)認(rèn)為:必須由開(kāi)發(fā)者明確的指定可變性,因?yàn)檫@會(huì)促使開(kāi)發(fā)者考慮他們的行為將會(huì)帶來(lái)什么后果恳啥,從而思考他們的設(shè)計(jì)是否合理偏灿。
多播委托與可變性不能混用。下面的代碼能夠通過(guò)編譯钝的,但是在運(yùn)行時(shí)會(huì)拋出 ArgumentException 異常:
Func<string> stringFunc = () => "";
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + stringFunc;
這是因?yàn)樨?fù)責(zé)鏈接多個(gè)委托的 Delegate.Combine方法要求參數(shù)必須為相同的類(lèi)型翁垂,而上面的兩個(gè)泛型委托的輸出一個(gè)為字符串铆遭,另一個(gè)為object。上面的示例我們可以修改成如下正確的代碼:
Func<string> stringFunc = () => "";
Func<object> defensiveCopy = new Func<object>(stringFunc);
Func<object> objectFunc = () => new object();
Func<object> combined = objectFunc + defensiveCopy;
此時(shí)兩個(gè)泛型委托的輸出均為object沿猜。
協(xié)變與逆變的相互作用
以下的代碼中枚荣,接口IBar中有一個(gè)方法,其接受另一個(gè)接口IFoo作為參數(shù)邢疙。IFoo是支持協(xié)變的棍弄。這樣會(huì)出現(xiàn)一個(gè)問(wèn)題。
interface IFoo<in T>
{
}
interface IBar<in T>
{
void Test(IFoo<T> foo);
}
假設(shè)T為字符串類(lèi)型疟游。則如果有一類(lèi)Bar <T>: IBar<T>呼畸,另一類(lèi)Foo<T>:IFoo<T>,則Bar的某個(gè)實(shí)例應(yīng)該可以這樣調(diào)用方法:aBar.Test (foo)颁虐。
class Bar<T> : IBar<T>
{
public void Test(IFoo<T> foo)
{
throw new NotImplementedException();
}
}
class Foo<T> : IFoo<T>
{
}
class Program
{
public static void Main()
{
Bar<string> aBar = new Bar<string>();
Foo<object> foo = new Foo<object>();
aBar.Test(foo);
}
}
當(dāng)調(diào)用方法之后蛮原,傳入的參數(shù)類(lèi)型是Foo<object>。我們?cè)倏纯捶椒ǖ暮灻?/p>
interface IBar<in T>
{
void Test(IFoo<T> foo);
}
現(xiàn)在我們的aBar的類(lèi)型參數(shù)T是string另绩,所以儒陨,我們期待的Test方法的傳入類(lèi)型也應(yīng)該是IFoo<string>,或者能夠變化成IFoo<string>的類(lèi)型笋籽,但傳入的卻是一個(gè)object蹦漠。所以,這兩個(gè)接口的方法的寫(xiě)法是有問(wèn)題的车海。
interface IFoo<out T>
{
}
當(dāng)把IFoo接口的簽名改用out修飾之后笛园,問(wèn)題就解決了。此時(shí)由于允許逆變侍芝,F(xiàn)oo<object>就可以變化成IFoo<string>了研铆。不過(guò)本人眼光短淺,目前還沒(méi)發(fā)現(xiàn)這個(gè)特點(diǎn)在實(shí)際工作中有什么應(yīng)用州叠。
參考資料
http://www.cnblogs.com/LoveJenny/archive/2012/03/13/2392747.html
http://www.cnblogs.com/xinchufa/p/3524452.html
http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html