一、沒有泛型之前
在沒有泛型之前,我們是怎么處理不同類型的相同操作的:
示例1
//下面是一個處理string類型的集合類型
public class MyStringList
{
string[] _list;
public void Add(string x)
{
//將x添加到_list中谭企,省略實(shí)現(xiàn)
}
public string this[int index]
{
get { return _list[index]; }
}
}
//調(diào)用
MyStringList myStringList = new MyStringList();
myStringList.Add("abc");
var str = myStringList[0];
示例2
//如果我們需要處理int類型就需要復(fù)制粘貼然后把string類型替換為int類型:
public class MyIntList
{
int [] _list;
public void Add(int x)
{
//將x添加到_list中廓译,省略實(shí)現(xiàn)
}
public int this[int index]
{
get { return _list[index]; }
}
}
//調(diào)用
MyIntList myIntList = new MyIntList();
myIntList.Add(100);
var num = myIntList[0];
可以看得出我們的代碼大部分是重復(fù)的,而作為有追求的程序員是不允許發(fā)生這樣的事情的债查。
于是乎非区,我們做了如下改變:
示例3
public class MyObjList
{
object[] _list;
public void Add(object x)
{
//將x添加到_list中,省略實(shí)現(xiàn)
}
public object this[int index]
{
get { return _list[index]; }
}
}
//調(diào)用
MyObjList myObjList = new MyObjList();
myObjList.Add(100);
var num = (int)myObjList[0];
從上面這三段代碼中攀操,我們可以看出一些問題:
- int和string集合類型的代碼大量重復(fù)(維護(hù)難度大)院仿。
- object集合類型發(fā)生了裝箱和拆箱(損耗性能)。
- object集合類型是存在安全隱患的(類型不安全)速和。
問題1歹垫,雖然代碼重復(fù)但是沒有裝箱、拆箱而且類型是安全的
問題2颠放,發(fā)生了裝箱和拆箱排惨,是損耗性能影響執(zhí)行效率的。
問題3碰凶,如果add的類型不是int類型暮芭,在編譯器是不會檢查出來的(編譯通過),運(yùn)行期就會報(bào)錯,MyObjList類似于我們熟知的ArrayList
現(xiàn)在欲低,我們必須解決如下問題
1辕宏、避免代碼重復(fù)
2、避免裝箱和拆箱
3砾莱、保證類型安全
范型為我們提供了完美的解決方案
二瑞筐、什么是泛型
如果你理解類是對象的模板(類是具有相同屬性和行為的對象的抽象),那么泛型就很好理解了腊瑟。
泛型:generic paradigm(通用的范式)聚假,generic這個單詞也很好的說明了模板這個概念:通用的,標(biāo)準(zhǔn)的闰非。
泛型是類型的模板
不同的是:作為模板的類是通過實(shí)例化產(chǎn)生不同的對象膘格,而泛型是通過不同的類型實(shí)參產(chǎn)生不同的類型
泛型的基本概念介紹完,我們來看看泛型到底是怎么幫我們解決問題的
如何解決代碼重復(fù):提取代碼相同的部分财松,封裝變換的部分——封裝變化瘪贱,而示例1和示例2中變換的部分就是int和string類型本身,如何將類型抽象呢
示例4
//將示例3改裝下
public class MyList<T>
{
T [] _list;
public void Add(T x)
{
//將x添加到_list中辆毡,省略實(shí)現(xiàn)
}
public T this[int index]
{
get { return _list[index]; }
}
}
類型參數(shù) T
類型參數(shù)可以理解為泛型的"形參"("形參"一般用來形容方法的)政敢,有“形參”就會有實(shí)參。如我們聲明的List<string>胚迫,string就是實(shí)參;List<int> ,int就是實(shí)參,而List<string>和List<int>是兩種不同的類型唾那。
通過類型參數(shù)解決了代碼重復(fù)的問題
如何解決裝箱访锻、拆箱以及類型安全的問題:
//示例5
List<int> list = new List<int>();
list.Add(100);//強(qiáng)類型無需裝箱
//list.Add("ABC"); 編譯期安全檢查報(bào)錯
int num = list[0];//無需拆箱
聲明泛型類型時褪尝,因?yàn)榇_定了類型實(shí)參,所以操作泛型類型不需要裝箱期犬、拆箱河哑,而且泛型將大量安全檢查從運(yùn)行時轉(zhuǎn)移到了編譯時進(jìn)行,保證了類型安全龟虎。
注:C#為我們提供了5種泛型:類璃谨、結(jié)構(gòu)、接口鲤妥、委托和方法佳吞。
在示例4中,自定義泛型集合只是添加和獲取類型參數(shù)的實(shí)例棉安,除此之外底扳,沒有對類型參數(shù)實(shí)例的成員做任何操作。C#的所有類型都繼承自O(shè)bject類型贡耽,也就是說衷模,我們目前只能操作Object中的成員(Equals,GetType,ToString等)。但是蒲赂,我自定義的泛型很多時候是需要操作類型更多的成員
新需求阱冶,打印員工的信息
示例6
public class Person
{
public string Name { get; set; }
public int Age{ get; set; }
}
public class Employee : Person { }
public class PrintEmployeeInfo<T>
{
public void Print(T t)
{
Console.WriteLine(t.Name);//報(bào)錯
}
}
如果我們可以將類型參數(shù)T限定為Person類型,那么在泛型內(nèi)部就可以操作Person類型的成員了滥嘴。
三木蹬、泛型的約束
表格來至微軟官方文檔
約束 | 描述 |
---|---|
where T:結(jié)構(gòu) | 類型參數(shù)必須是值類型。 可以指定除 Nullable 以外的任何值類型氏涩。 |
where T:類 | 類型參數(shù)必須是引用類型届囚;這同樣適用于所有類、接口是尖、委托或數(shù)組類型意系。 |
where T:new() | 類型參數(shù)必須具有公共無參數(shù)構(gòu)造函數(shù)。 與其他約束一起使用時饺汹,new() 約束必須最后指定蛔添。 |
where T:<基類名稱> | 類型參數(shù)必須是指定的基類或派生自指定的基類。 |
where T:<接口名稱> | 類型參數(shù)必須是指定的接口或?qū)崿F(xiàn)指定的接口兜辞。 可指定多個接口約束迎瞧。 約束接口也可以是泛型。 |
where T:U | 為 T 提供的類型參數(shù)必須是為 U 提供的參數(shù)或派生自為 U 提供的參數(shù)逸吵。 |
示例7
public class PrintEmployeeInfo<T> where T:Person
{
public void Print(T t)
{
Console.WriteLine(t.Name);
}
}
四凶硅、協(xié)變和逆變很簡單
有一定工作經(jīng)驗(yàn)的開發(fā)人員一定遇到過下面這樣的情況:
示例8
List<Employee> list = new List<Employee>();
list.Add(new Employee() { Age = 20, Name = "小明" });
IEnumerable<Person> perList;
perList = list;
foreach (var item in perList)
{
Console.WriteLine("名字:" + item.Name + ",年齡:" + item.Age);
}
不是說,不同類型實(shí)參構(gòu)造的泛型也是不同的嗎扫皱,為啥可以將List<Employee>對象賦值給IEnumerable<Person>呢?
再看下面的示例
示例9
public static void PrintEmployee(Person item)
{
Console.WriteLine("名字:" + item.Name + ",年齡:" + item.Age);
}
Action<Employee> empAction = PrintEmployee;
empAction(new Employee() { Age = 20, Name = "小明" });
Action<Person> perAction = PrintEmployee;
perAction(new Employee() { Age = 20, Name = "小明" });
執(zhí)行結(jié)果正常輸出
為什么可以將參數(shù)類型為Person的方法分別賦值給Action<Person>和Action<Employee>呢足绅?
示例8說明了泛型的協(xié)變性捷绑,示例9說明了泛型的逆變性(聽起來很唬人)
其實(shí)協(xié)變和逆變只要弄清楚兩個概念一切就非常清晰了
- 類型參數(shù)分為輸入?yún)?shù)(in)、輸出參數(shù)(out)和不變參數(shù)(沒有關(guān)鍵字)
- 設(shè)計(jì)原則:里氏替換原則——派生類(子類)對象能夠替換其基類(超類)對象被使用
IEnumerable的定義
public interface IEnumerable<out T> : IEnumerable
示例8中IEnumerable<Person>輸出參數(shù)類型需要的Person類型氢妈,而List類型參數(shù)給的是Employee(Employee繼承了Person)——里氏替換原則
委托Action的定義
public delegate void Action<in T>(T obj);
示例9中方法PrintEmployee需要的參數(shù)類型是Person粹污,而Action<Employee>輸入類型參數(shù)是Employee(Employee繼承了Person)——里氏替換原則
如果將PrintEmployee的參數(shù)類型變?yōu)镋mployee,示例9中其他代碼不變首量,會怎樣壮吩?
方法PrintEmployee需要的參數(shù)類型是Employee,而Action的輸入?yún)?shù)是Person加缘,顯然Person不一定是Employee
注:in和out關(guān)鍵字只適用于接口和委托類型