C#中的泛型

一、沒有泛型之前

在沒有泛型之前,我們是怎么處理不同類型的相同操作的:

示例1
 //下面是一個處理string類型的集合類型
 public class MyStringList
    {
        string[] _list;
        public void Add(string x)
        {
            //將x添加到_list中,省略實現(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中,省略實現(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中,省略實現(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];

從上面這三段代碼中,我們可以看出一些問題:

  1. int和string集合類型的代碼大量重復(fù)(維護(hù)難度大)。
  2. object集合類型發(fā)生了裝箱和拆箱(損耗性能)。
  3. object集合類型是存在安全隱患的(類型不安全)。

問題1,雖然代碼重復(fù)但是沒有裝箱、拆箱而且類型是安全的
問題2,發(fā)生了裝箱和拆箱,是損耗性能影響執(zhí)行效率的。
問題3,如果add的類型不是int類型,在編譯器是不會檢查出來的(編譯通過),運行期就會報錯,MyObjList類似于我們熟知的ArrayList

運行期報錯

現(xiàn)在,我們必須解決如下問題
1、避免代碼重復(fù)
2、避免裝箱和拆箱
3、保證類型安全

范型為我們提供了完美的解決方案

二、什么是泛型

如果你理解類是對象的模板(類是具有相同屬性和行為的對象的抽象),那么泛型就很好理解了。
泛型:generic paradigm(通用的范式),generic這個單詞也很好的說明了模板這個概念:通用的,標(biāo)準(zhǔn)的。
泛型是類型的模板
不同的是:作為模板的類是通過實例化產(chǎn)生不同的對象,而泛型是通過不同的類型實參產(chǎn)生不同的類型
泛型的基本概念介紹完,我們來看看泛型到底是怎么幫我們解決問題的

如何解決代碼重復(fù):提取代碼相同的部分,封裝變換的部分——封裝變化,而示例1和示例2中變換的部分就是int和string類型本身,如何將類型抽象呢

示例4
    //將示例3改裝下
    public class MyList<T>
    {
        T [] _list;
        public void Add(T x)
        {
            //將x添加到_list中,省略實現(xiàn)
        }
        public T this[int index]
        {
            get { return _list[index]; }
        }
    }

類型參數(shù) T
類型參數(shù)可以理解為泛型的"形參"("形參"一般用來形容方法的),有“形參”就會有實參。如我們聲明的List<string>,string就是實參;List<int> ,int就是實參,而List<string>和List<int>是兩種不同的類型。

不同的類型

通過類型參數(shù)解決了代碼重復(fù)的問題

如何解決裝箱、拆箱以及類型安全的問題:

 //示例5
       List<int> list = new List<int>();
       list.Add(100);//強(qiáng)類型無需裝箱
       //list.Add("ABC"); 編譯期安全檢查報錯
       int num = list[0];//無需拆箱   
編譯期安全檢查報錯

聲明泛型類型時,因為確定了類型實參,所以操作泛型類型不需要裝箱、拆箱,而且泛型將大量安全檢查從運行時轉(zhuǎn)移到了編譯時進(jìn)行,保證了類型安全。
注:C#為我們提供了5種泛型:類、結(jié)構(gòu)、接口、委托和方法。

在示例4中,自定義泛型集合只是添加和獲取類型參數(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);//報錯
        }
    }
示例6:T未包含“Name”的定義

如果我們可以將類型參數(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)驗的開發(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);
 }

不是說,不同類型實參構(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說明了泛型的逆變性(聽起來很唬人)
其實協(xié)變和逆變只要弄清楚兩個概念一切就非常清晰了

  1. 類型參數(shù)分為輸入?yún)?shù)(in)、輸出參數(shù)(out)和不變參數(shù)(沒有關(guān)鍵字)
  2. 設(shè)計原則:里氏替換原則——派生類(子類)對象能夠替換其基類(超類)對象被使用

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)鍵字只適用于接口和委托類型

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容