object 變量可指向任何類的實例衔憨,這讓你能夠創(chuàng)建可對任何數(shù)據(jù)類型進程處理的類严卖。然而,這種方法存在幾個嚴重的問題梢什。使用object時闻牡,類無法將輸入限定為特定類型;要對數(shù)據(jù)執(zhí)行有意義的操作绳矩,必須將其從object轉(zhuǎn)換為更具體的類型罩润。這不但增加了復雜性,還在編譯階段犧牲了類型安全翼馆。
C#泛型避免了轉(zhuǎn)換(即裝箱和取消裝箱)割以,讓通用化在編譯階段是類型安全的,從而解決了上述問題应媚。泛型提供了非泛型類無法實現(xiàn)的類型安全严沥、可重用性和效率。泛型通常與集合一起使用中姜,但也可使用泛型來創(chuàng)建自定義泛型類型和泛型方法消玄。
一跟伏、為什么應使用泛型
由于數(shù)組每個元素的數(shù)據(jù)類型都被顯式地聲明為int,編譯器將確保只能將int值賦給每個元素翩瓜。還使用為int值定義的方法和運算符對元素進行了操作受扳。
如下所示的代碼可用于找出任何int數(shù)組中的最小值。
public int Min(int[] values)
{
int min = values[0];
foreach (int value in values)
{
if (value.CompareTo(min) < 0)
{
min = value;
}
}
return min;
}
如果希望這些代碼可用于任何數(shù)值數(shù)組兔跌,該怎么辦呢勘高?如果不使用泛型,就需要為每種數(shù)值類型編寫一個版本的Min坟桅,這些版本只是數(shù)據(jù)類型不同华望。這雖然可行,但是代碼很復雜仅乓,并且很多代碼是重復的赖舟。
如果知道IComparable接口定義了一個CompareTo方法,就可使用object編寫更通用的代碼夸楣。這只需編寫代碼一次建蹄,而這些代碼可用于任何數(shù)值類型的數(shù)組,如下代碼所示:
public int Min(object[] values)
{
IComparable min = (IComparable)values[0];
foreach (object value in values)
{
if (((IComparable)value).CompareTo(min) < 0)
{
min = (IComparable)value;
}
}
return min;
}
不幸的是裕偿,雖然只需編寫代碼一次帶來了一定的好處洞慎,但是類型安全也喪失殆盡。另外嘿棘, int數(shù)組不能轉(zhuǎn)換為object數(shù)組劲腿。鑒于這個方法適用于object,如果給它傳遞一個類似于下面這樣的數(shù)組鸟妙,結(jié)果將如何呢焦人?
object[] array = { 5, 3, "a", "hello" };
這是合法的,因為數(shù)組存儲的是object元素重父,因此任何值都將隱式地轉(zhuǎn)換為object花椭。
不僅類型安全喪失殆盡,還執(zhí)行了n+1次轉(zhuǎn)換操作房午,其中n是數(shù)組包含的元素數(shù)矿辽。數(shù)組越大,這個方法的開銷越高郭厌。
借助于泛型袋倔,這個問題將變得很簡單。只需編寫代碼一次折柠,而不會喪失類型安全或執(zhí)行多次轉(zhuǎn)換操作宾娜。如下代碼顯示了使用泛型定義的Min方法
public T Min<T>(T[] values) where T : IComparable<T>
{
T min = values[0];
foreach (T value in values)
{
if (value.CompareTo(min) < 0)
{
min = value;
}
}
return min;
}
ps:where T : IComparable<T>
這個約束可能有點令人迷惑,因為看起來好像存在循環(huán)依存關系扇售。事實上前塔,它很簡單嚣艇,意味著T必須是可比較的類型。
最大的不同在于华弓,泛型版本使用了泛型類型參數(shù) T食零,這是在方法名后面使用語法<T>指定的。類型參數(shù)充當編譯階段提供的實際類型的占位符该抒。在這個例子中,用于替換T的實際類型必須實現(xiàn)了接口IComparable<T>顶燕,其中T是泛型方法的類型參數(shù)凑保。這要求類型參數(shù)只能是實現(xiàn)了該接口的類型。
ps:C#泛型涌攻、C++模板和Java泛型
雖然C#泛型欧引、C++模板和Java泛型都支持參數(shù)化類型,但是它們之間有幾項重要的差別恳谎。C#泛型的語法與Java泛型類似芝此,但比C++模板簡單。
C#泛型的所有類型替換都是在運行階段進行的因痛,從而保留了對象的泛型類型信息婚苹。在Java中,泛型是一種語言結(jié)構(gòu)鸵膏,只在編譯器中以類型擦除(type erasure)的方式實現(xiàn)膊升。因此,在運行階段無法獲悉對象的泛型類型信息谭企。這不同于C++模板廓译,C++在編譯階段展開,為每種模板類型生成額外的代碼债查。
在有些情況下非区,C#泛型的靈活性沒有C++模板和Java泛型高。例如盹廷, C#泛型不像Java泛型那樣支持類型參數(shù)通配符征绸;也不能像在C++模板中那樣,可調(diào)用類型參數(shù)的算術運算符俄占。
1.1 泛型類型參數(shù)
方法有參數(shù)歹垫,而在運行階段,這些形參的值為實參颠放。同樣排惨,泛型類型和泛型方法也有類型參數(shù)和類型實參,其中類型參數(shù)充當編譯階段提供的類型實參的占位符碰凶。
這不僅僅是簡單的文本替換——使用提供的類型提供類型參數(shù)暮芭。泛型類型或泛型方法被編譯后鹿驼,生成的 CIL 包含元數(shù)據(jù),指出它有類型參數(shù)辕宏。在運行階段畜晰,JIT 用提供的類型參數(shù)進行替換,創(chuàng)建出構(gòu)造類型(constructed type)瑞筐。
泛型類型和泛型方法可以有多個用逗號(,)分隔的類型參數(shù)凄鼻。有多個泛型集合類使用了多個類型參數(shù),如Dictionary<TKey, TValue>和KeyValuePair<TKey,TValue>聚假。Tuple類也是泛型块蚌,最多可以有8個類型參數(shù)。
約束
約束讓您能夠指定哪些類型可在編譯階段用作類型實參膘格。這些限制是使用關鍵字 where指定的峭范。通過約束類型參數(shù),便可使用約束類型及其繼承鏈中所有類型都支持的操作和方法瘪贱。
約束共有6種纱控,如下所示
約束 | 描述 |
---|---|
where T : struct | 類型實參必須是值類型,但可以為null的值類型除外 |
where T : class | 類型實參必須是引用類型菜秦,這適用于任何類甜害、接口、委托和數(shù)據(jù)類型 |
where T : new() | 類型實參必須有不接受任何參數(shù)的共有構(gòu)造函數(shù)球昨,且為具體類型唾那。與其他約束一起使用時,new()約束必須位于最后面 |
where T : <base class name> | 類型實參必須是指定的基類或其派生類 |
where T : <interface name> | 類型實參必須能夠隱式地轉(zhuǎn)換為指定的接口褪尝。約束接口可以使泛型的闹获,還可指定多個接口約束 |
where T : U | 類型實參必須是U(另一個泛型類型參數(shù))指定的類型或從它派生而來的 |
約束告訴編譯器,類型參數(shù)支持哪些運算符和方法河哑。沒有約束的類型參數(shù)為無約束類型參數(shù)避诽,只支持簡單賦值以及System.Object支持的方法。對于無約束類型參數(shù)璃谨,不能將運算符!=和==用于它們沙庐,因為編譯器不知道它們是否能獲得支持。
單個類型參數(shù)可以有多個約束佳吞,也可以給多個類型參數(shù)指定約束:
CustomDictionary<TKey, TValue>
where TKey : IComparable
where TValue : class, new()
ps:泛型的值相等性檢測
即使指定了約束where T : class拱雏,也不應將運算符==和!=用于類型參數(shù)。這些運算符檢測引用是否相同底扳,而不是值是否相等铸抑。
即使在類型中重載了這些運算符,情況也是如此衷模。因為編譯器只知道T是引用類型鹊汛。因此蒲赂,它只能使用 System.Object 定義的可用于所有引用類型的默認運算符。
要進行值相等性檢測刁憋,推薦使用約束where T : IComparable<T>滥嘴,并確保將用于構(gòu)造泛型類的所有類都實現(xiàn)了該接口。通過指定該約束至耻,可使用方法CompareTo進行值相等性測試
類型參數(shù)約束(type parameter constrain)是一個這樣的泛型類型參數(shù)若皱,即用于約束另一個類型參數(shù)。類型參數(shù)約束最常用于這樣的情形:泛型方法需要將其類型參數(shù)約束為其所屬類型的類型參數(shù)尘颓,如下代碼所示走触。
在這個示例中,T是方法Add的一個類型參數(shù)約束泥耀,該方法接受一個List<U>饺汹,其中U必須是T或從T派生而來的蛔添。
public class List<T>
{
public void Add<U>(List<U> items) where U : T
{
}
}
類型參數(shù)約束還可用于泛型類痰催,以指定兩個類型參數(shù)之間的關系,如下代碼所示迎瞧。在這個例子中夸溶,Example有3個類型參數(shù)(T、U和V)凶硅,其中T必須是V或從V派生而來缝裁,而U和V之間沒有約束
public class Example<T, U, V> where T : V
{
}
1.2 泛型類型的默認值
C#是一種強類型語言,要求使用變量前給它賦值足绅。為方便滿足這種要求捷绑,每種類型都有默認值。顯然氢妈,對于泛型類型粹污,無法預先知道默認值應為 null、0 還是用零初始化的結(jié)構(gòu)首量,那么如何為泛型類型指定適合任何類型的默認值呢壮吩?
C#提供了關鍵字default,它表示適合類型參數(shù)的默認值加缘,其具體值隨指定的實際類型而異鸭叙。這意味著對于參數(shù)類型,它返回 null拣宏;對于所有數(shù)值類型沈贝,它返回 0;如果類型實參是結(jié)構(gòu)勋乾,則根據(jù)其每個成員的數(shù)據(jù)類型缀程,將它們初始化為null或零搜吧;對于可以為null的值類型,則返回null杨凑。
二滤奈、泛型方法
泛型方法與非泛型方法類似,但使用一組泛型類型參數(shù)而不是具體類型定義撩满。泛型方法是在運行階段用于生成方法的設計圖蜒程。
ps:非泛型類中的泛型方法
并非只有泛型類才能有泛型方法,非泛型類也可包含泛型方法伺帘,這完全合法昭躺,也很常見。
另外伪嫁,泛型類也可包含非泛型方法领炫,且后者可訪問前者的類型參數(shù)。
通過給泛型方法指定約束张咳,可使用約束保證可用的具體操作帝洪。泛型方法和非泛型方法都可使用泛型類定義的類型參數(shù),因此如果泛型方法定義了與其所屬的類相同的類型參數(shù)脚猾,給泛型方法提供的實參T將隱藏給類提供的實參T葱峡,而編譯器將發(fā)出警告。如果希望方法使用的類型實參與實例化類時提供的類型實參不同龙助,應給類型參數(shù)指定不同的標識符砰奕,如下代碼所示:
class GenericClass<T>
{
void GenerateWarning<T>()
{
}
void NoWarning<U>()
{
}
}
調(diào)用泛型方法時,必須給它定義的類型參數(shù)提供實際的數(shù)據(jù)類型提鸟。如下代碼所示演示了圖和調(diào)用方法Min<T>:
public static class Program
{
static void Main()
{
int[] array = { 3, 5, 7, 0, 2, 4, 6 };
Console.WriteLine(Min<int>(array));
}
}
雖然這是可以接受的军援,但是在大多數(shù)情況下是不必要的,這要歸功于類型推斷(type inference)称勋。如果省略了類型實參胸哥,編譯器將根據(jù)方法實參推斷出類型。如下代碼利用類型推斷進行了相同的調(diào)用:
public static class Program
{
static void Main()
{
int[] array = { 3, 5, 7, 0, 2, 4, 6 };
Console.WriteLine(Min(array));
}
}
由于類型推斷依賴于方法實參铣缠,因此它無法僅根據(jù)約束或返回類型推斷出類型烘嘱。這意味著不能將其用于沒有參數(shù)的方法。
對泛型方法來說蝗蛙,類型參數(shù)是方法簽名的一部分蝇庭。可以這樣重載泛型方法:聲明多個泛型方法捡硅,它們的形參列表相同哮内,但類型參數(shù)不同。
ps
:類型推斷和重載解析
類型推斷發(fā)生在編譯階段,并在編譯器試圖解析重載的方法簽名之前進行北发。進行類型替換后纹因,非泛型方法和泛型方法的簽名可能相同。在這種情況下琳拨,將使用最具體的方法(總是為非泛型方法)瞭恰。
三、創(chuàng)建泛型類
泛型類最常用于集合狱庇,因為無論存儲的數(shù)據(jù)類型是什么惊畏,集合的行為都相同。泛型方法是運行階段用于生成方法的設計圖密任,同樣颜启,泛型類也是運行階段用于構(gòu)造類的設計圖。
除使用.NET Framework提供的泛型類外浪讳,您還可以創(chuàng)建自定義泛型類缰盏。這與創(chuàng)建非泛型類沒有什么不同,只是您需要提供類型參數(shù)而不是實際數(shù)據(jù)類型淹遵。
創(chuàng)建自定義泛型類時口猜,請牢記下面幾個重要問題:
哪些類型應為類型參數(shù)?一般而言合呐,參數(shù)化的類型越多暮的,泛型類就越靈活欢瞪。然而盯腌,對實際的類型參數(shù)數(shù)量存在一定的限制凌埂,因為類型參數(shù)越多,代碼的可讀性越差拆祈。
應指定什么樣的約束?確定這一點的方式有多種倘感。一種方式是放坏,確保能夠使用希望的類型的情況下,指定盡可能多的約束老玛;另一種方式是淤年,指定盡可能少的約束,以最大限度地提高泛型類的靈活性蜡豹。這兩種方式都可行麸粮,但是也可采取更實用的方式,即根據(jù)泛型類要達到的目的镜廉,指定必要的約束弄诲。例如,如果知道泛型類應只用于引用類型娇唯,就應指定where T : class約束齐遵。這樣既可禁止泛型類用于值類型寂玲,又能使用as運算符并進行null檢查。
行為應在基類還是子類中提供梗摇?泛型類可用作基類拓哟,就像非泛型類一樣。因此伶授,適用于非泛型類的設計選項也可用于泛型類彰檬。
應實現(xiàn)泛型接口嗎?您可能需要實現(xiàn)甚至創(chuàng)建一個或多個泛型接口谎砾,這取決于設計的泛型類是什么樣的逢倍。自定義泛型類的用法也決定了它要實現(xiàn)哪些接口。
非泛型類可繼承具體的非泛型類景图,也可繼承抽象的非泛型類较雕;同樣,泛型類也可繼承非泛型具體類或抽象類挚币,但泛型類還可繼承其他泛型類亮蒋。
ps:泛型結(jié)構(gòu)和泛型接口
結(jié)構(gòu)也可以是泛型的,泛型結(jié)構(gòu)使用的語法和類型約束與泛型類相同妆毕。泛型結(jié)構(gòu)和泛型類之間的差別與非泛型結(jié)構(gòu)和非泛型類之間的差別相同慎玖。
泛型接口使用的類型參數(shù)語法和約束與泛型類相同,其聲明規(guī)則與非泛型接口相同笛粘。一種明顯的差別是趁怔,泛型類型實現(xiàn)的接口對所有可能的構(gòu)造類型來說都必須是唯一的,這意味著如果替換類型參數(shù)后薪前,同一個泛型類實現(xiàn)的兩個泛型接口相同润努,那么該泛型類的聲明將是非法的。
雖然泛型類可以繼承非泛型接口示括,但是最好不要這樣做铺浇,而是繼承泛型接口。
為理解泛型類的繼承垛膝,需要明白開放類型(open type)和封閉類型(closed type)之間的差別鳍侣。開放類型是包含類型參數(shù)的類型,具體地說吼拥,它是這樣的泛型類型倚聚,即沒有給其類型參數(shù)提供類型實參。封閉類型也叫構(gòu)造類型扔罪,是不開放的泛型類型秉沼,即給它的所有類型參數(shù)都提供了類型實參。
泛型類可繼承開放類型,也可繼承封閉類型唬复。派生類可給基類的所有類型參數(shù)都提供類型實參矗积,在這種情況下,派生類為構(gòu)造類型敞咧;如果派生類沒有給基類提供任何類型實參棘捣,它將是開放類型。雖然泛型類可繼承封閉類型和開放類型休建,但非泛型類只能繼承封閉類型乍恐,否則編譯器將無法知道應使用什么樣的類型實參。
如下代碼提供了一些繼承開放類型和封閉類型的示例
abstract class Element { }
class Element<T> : Element { }
class BasicElement<T> : Element<T> { }
class Int32Element : BasicElement<int> { }
在這個示例中测砂, Element<T>繼承了 Element 茵烈,是開放類型;BasicElement<T>繼承了Element<T>砌些,是開放類型呜投;而Int32Element是構(gòu)造類型,因為它繼承了構(gòu)造類型BasicElement<int>存璃。
然而仑荐,派生類可給基類的部分類型參數(shù)提供類型實參,在這種情況下纵东,派生類為開放構(gòu)造類型(open constructed type)粘招。可認為開放構(gòu)造類型位于開放類型和封閉類型之間偎球,即它至少給一個類型參數(shù)提供了實參洒扎,但要成為構(gòu)造類型,還至少有一個類型參數(shù)需要提供實參甜橱。
如下代碼擴展了上述示例逊笆,它創(chuàng)建了一個有兩個類型參數(shù)(T和K)的開放類型(Element)栈戳,還創(chuàng)建了各種可能的開放構(gòu)造類型岂傲。
class Element<T, K> { }
class Element1<T> : Element<T, int> { }
class Element2<K> : Element<string, K> { }
如果是開放類型指定的約束,那么其派生類提供的類型實參必須滿足這些約束子檀,可以指定約束來實現(xiàn)镊掖。子類的約束可以與基類相同,也可以是基類約束的超集褂痰。
如下代碼演示了如何繼承帶約束的開放類型:
class ConstainedElement<T>
where T : IComparable<T>,new()
class ConstainedElement1<T> : ConstainedElement<T>
where T : IComparable<T>,new()
最后亩进,如果泛型類實現(xiàn)了一個接口,那么其所有實例都可轉(zhuǎn)換為該接口缩歪。
四归薛、結(jié)合使用泛型和數(shù)組(泛型數(shù)組)
所有一維數(shù)組的最小索引都為零,且自動實現(xiàn)了IList<T>。因此主籍,可以創(chuàng)建一個對 IList<T>的內(nèi)容進行遍歷泛型方法习贫,而該方法可用于所有集合類型(因為它們都實現(xiàn)了IList<T>)和所有一維數(shù)組。
如下示例演示了使用泛型方法顯示集合的元素
public static class Program
{
public static void PrintCollection<T>(IList<T> collection)
{
StringBuilder builder = new StringBuilder();
foreach (var item in collection)
{
builder.AppendFormat("{0} ", item);
}
Console.WriteLine(builder.ToString());
}
public static void Main()
{
int[] array = { 0, 2, 4, 6, 8 };
List<int> list = new List<int>() { 1, 3, 5, 7, 9 };
PrintCollection(array);
PrintCollection(list);
string[] array2 = { "hello", "world" };
list<string> list2 = new List<string>() { "now", "is", "the", "time" };
PrintCollection(array2);
PrintCollection(list2);
}
}
泛型接口的可變性
類型可變性(variance)指的是可使用不同于指定類型的類型千元。協(xié)變(covariance)能夠使用派生程度比指定類型更高的類型苫昌,而逆變(contravariance)能夠使用派生程度更低的類型。C#對返回類型支持協(xié)變幸海,對參數(shù)支持逆變祟身。
C#泛型集合是不可變的(invariant),這意味著必須使用指定的類型物独。因此袜硫,在需要派生程度低的類型的集合時,不能使用派生程度高的類型的集合挡篓。
ps:實現(xiàn)了可變(variant)泛型接口的類
實現(xiàn)了可變(variant)泛型接口的類總是不變的(invariant)父款。
這里的真正問題是集合是可修改的。如果可對集合進行限制瞻凤,使其只支持只讀行為憨攒,就可將其聲明為協(xié)變的。
在C#中阀参,如果接口的類型參數(shù)被聲明為協(xié)變或逆變的肝集,接口就是可變的。僅當兩種類型之間能夠進行引用轉(zhuǎn)換時蛛壳,才能使用協(xié)變和逆變杏瞻。這意味著可變性不能用于值類型,也不能用于ref和out參數(shù)衙荐。
有多個泛型集合接口支持可變性捞挥,如下所示
接口 | 可變性 |
---|---|
IEnumerable<T> | T是協(xié)變的 |
IEnumerator<T> | T是協(xié)變的 |
IQueryable<T> | T是協(xié)變的 |
IGrouping<TKey, TElement> | TKey和TElement是協(xié)變的 |
IComparer<T> | T是逆變的 |
IEqualityComparer<T> | T是逆變的 |
IComparable<T> | T是逆變的 |
擴展可變的泛型接口
編譯器不會根據(jù)被繼承的接口推斷可變性,因此必須顯式地指定派生接口是否支持可變性忧吟,如下所示:
interface ICovariant<out T>{ }
interface IInvariant<T> : ICovariant<T>{ }
interface IExtendCovariant<out T> : ICovariant<T>{ }
雖然接口 IInvariant<T>和 IExtendedCovariant<out T>擴展的是同一個協(xié)變接口砌函,但只有IExtendedCovariant<out T>也是協(xié)變的×镒澹可以以同樣的方式擴展逆變接口讹俊。
還可在同一個接口中同時擴展協(xié)變接口和逆變接口,條件是派生接口是不可變的煌抒,如下所示同時擴展協(xié)變接口和逆變接口:
interface ICovariant<out T>{ }
interface IInvariant<in T>{ }
interface IInvariant<T> : IContravariant<T>, ICovariant<T>{ }
然而仍劈,不能使用協(xié)變接口擴展逆變接口,反之亦然寡壮,如下代碼所示:
// Generates a compiler error.
interface IInvalidVariance<in T> : ICovariant<T>{ }
ps:創(chuàng)建自定義的可變泛型接口
可以創(chuàng)建自定義的泛型接口贩疙,同樣讹弯,可創(chuàng)建自定義的可變泛型接口,方法是給泛型類型參數(shù)指定關鍵字in和out这溅。
關鍵字out將泛型類型參數(shù)聲明為協(xié)變的闸婴,而關鍵字in將泛型類型參數(shù)聲明為逆變的。同一個接口可同時包含協(xié)變類型參數(shù)和逆變類型參數(shù)芍躏。關鍵字ref和out在聲明和調(diào)用方法時都需指定邪乍,而關鍵字in和out只需在接口聲明中指定。
五对竣、元組
元組是一種數(shù)據(jù)結(jié)構(gòu)庇楞,它包含特定個數(shù)值,而這些值按特定順序排列否纬。元組常用于以下用途:
表示一組數(shù)據(jù)吕晌;
提供一種訪問數(shù)據(jù)集的簡單方法;
輕松地從方法返回多個值
雖然元組最常用于函數(shù)編程語言临燃,如F#睛驳、Ruby和Python,但是.NET Framework也提供了多個Tuple類膜廊,分別用于表示包含1~7個值的元組乏沸。還有一個表示n元組的類,其中n是大于或等于8的任何整數(shù)爪瓜。表示n元組的類與表示1~7元組的類稍有不同蹬跃,其第8個分量也是一個Tuple對象,定義了其他的分量铆铆。