1. 程式人生 > WINDOWS開發 >C#基礎篇——泛型

C#基礎篇——泛型

前言

在開發程式設計中,我們經常會遇到功能非常相似的功能模組,只是他們的處理的資料不一樣,所以我們會分別採用多個方法來處理不同的資料型別。但是這個時候,我們就會想一個問題,有沒有辦法實現利用同一個方法來傳遞不同種類型的引數呢?

這個時候,泛型也就因運而生,專門來解決這個問題的。

泛型是在C#2.0就推出的一個新語法,由框架升級提供的功能。

說明

泛型通過引數化型別實現在同一份程式碼上操作多種資料型別。例如使用泛型的型別引數T,定義一個類Stack

可以用Stack、Stack或者Stack例項化它,從而使類Stack可以處理int、string、Person型別資料。這樣可以避免執行時型別轉換或封箱操作的代價和風險。泛型提醒的是將具體的東西模糊化。

同時使用泛型型別可以最大限度地重用程式碼、保護型別安全以及提高效能。

可以建立:泛型介面泛型類泛型方法泛型事件泛型委託

開始

泛型類

泛型類封裝不特定於特定資料型別的操作。 泛型類最常見用法是用於連結列表、雜湊表、堆疊、佇列和樹等集合。 無論儲存資料的型別如何,新增項和從集合刪除項等操作的執行方式基本相同。

    static void Main(string[] args)
    {

        // T是int型別
        GenericClass<int> genericInt = new GenericClass<int>();
        genericInt._T = 123;
        // T是string型別
        GenericClass<string> genericString = new GenericClass<string>();
        genericString._T = "123";

    }

新建一個GenericClass類

    /// <summary>
    /// 泛型類
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class GenericClass<T>
    {
        public T _T;
    }

泛型方法

泛型方法是通過型別引數宣告的方法,解決用一個方法,滿足不同引數型別

    static void Main(string[] args)
    {
        #region 泛型方法
        Console.WriteLine("************Generic**************");
        int iValue = 123;
        string sValue = "456";
        DateTime dtValue = DateTime.Now;
        object oValue = "MrValue";
        GenericMethod.Show<int>(iValue);//需要指定型別引數
        //GenericMethod.Show<string>(iValue);//必須吻合
        GenericMethod.Show(iValue);//能省略,自動推算
        GenericMethod.Show<string>(sValue);
        GenericMethod.Show<DateTime>(dtValue);
        GenericMethod.Show<object>(oValue);
        #endregion

    }

新建一個GenericMethod

/// <summary>
/// 泛型方法
/// </summary>
public class GenericMethod
{
    /// <summary>
    /// 2.0推出的新語法
    /// 泛型方法解決用一個方法,滿足不同引數型別;做相同的事兒
    /// 沒有寫死引數型別,呼叫的時候才指定的型別
    /// 延遲宣告:把引數型別的宣告推遲到呼叫
    /// 推遲一切可以推遲的~~  延遲思想
    /// 不是語法糖,而是2.0由框架升級提供的功能
    /// 需要編譯器支援+JIT支援
    /// </summary>
    /// <typeparam name="T">T/S 不要用關鍵字  也不要跟別的型別衝突 </typeparam>
    /// <param name="tParameter"></param>
    public static void Show<T>(T tParameter)
    {
        Console.WriteLine("This is {0},parameter={1},type={2}",typeof(GenericMethod),tParameter.GetType().Name,tParameter.ToString());
    }
}

泛型介面

為泛型集合類或表示集合中的項的泛型類定義介面通常很有用處。在c#中,通過尖括號“<>”將型別引數括起來,表示泛型。宣告泛型介面時,與宣告一般介面的唯一區別是增加了一個。一般來說,宣告泛型介面與宣告非泛型介面遵循相同的規則。

泛型介面定義完成之後,就要定義此介面的子類。定義泛型介面的子類有以下兩種方法。

(1)直接在子類後宣告泛型。

(2)在子類實現的介面中明確的給出泛型型別。

    static void Main(string[] args)
    {
        #region 泛型介面
        CommonInterface commonInterface = new CommonInterface();
        commonInterface.GetT("123");
        #endregion
    }

新建GenericInterface.cs類檔案

        /// <summary>
        /// 泛型類
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public class GenericClass<T>
        {
            public T _T;
        }

        /// <summary>
        /// 泛型介面
        /// </summary>
        public interface IGenericInterface<T>
        {
            //泛型型別的返回值
            T GetT(T t);
        }


        /// <summary>
        /// 使用泛型的時候必須指定具體型別,
        /// 這裡的具體型別是int
        /// </summary>
        public class CommonClass : GenericClass<int>
        {

        }

        /// <summary>
        /// 必須指定具體型別
        /// </summary>
        public class CommonInterface : IGenericInterface<string>
        {
            public string GetT(string t)
            {
                return t;
            }
        }

        /// <summary>
        /// 子類也是泛型的,繼承的時候可以不指定具體型別
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public class CommonClassChild<T> : GenericClass<T>
        {

        }

泛型委託

泛型委託主要是想講一下Action和Func兩個委託,因為這兩個在Linq中是經常見到的。

Action只能委託必須是無返回值的方法

Fun只是委託必須有返回值的方法

不管是不是泛型委託,只要是委託委託那能用Lamdba表示式,因為不管Lamdba表示式還是匿名函式其實都是將函式變數化。

下面簡單的來做的demo說下兩個的用法,這個會了基本linq會了一半了。

    static void Main(string[] args)
    {
        #region 泛型委託
        Action<string> action = s => {
            Console.WriteLine(s);
        };
        action("i3yuan");
        Func<int,int,int> func = (int a,int b) => {
            return a + b;
        };
        Console.WriteLine("sum:{0}",func(1,1));
        Console.ReadLine();
        #endregion
    }

上面其實都是將函式做為變數,這也是委託的思想。action是例項化了一個只有一個字串引數沒有返回值得函式變數。func是例項化了一個有兩個int型別的引數返回值為int的函式變數。

可以看到通過Lamdba表示式和泛型的結合,算是又方便了開發者們,更加方便實用。

引入委託常用的另一方式

無論是在類定義內還是類定義外,委託可以定義自己的型別引數。引用泛型委託的程式碼可以指定型別引數來建立一個封閉構造型別,這和例項化泛型類或呼叫泛型方法一樣,如下例所示:

public delegate void MyDelegate<T>(T item);
public void Notify(int i){}
//...
 
MyDelegate<int> m = new MyDelegate<int>(Notify);
 
C#2.0版有個新特性稱為方法組轉換(method group conversion),具體代理和泛型代理型別都可以使用。用方法組轉換可以把上面一行寫做簡化語法:
MyDelegate<int> m = Notify;
 
在泛型類中定義的委託,可以與類的方法一樣地使用泛型類的型別引數。
class Stack<T>
{
T[] items;
      int index
//...
public delegate void StackDelegate(T[] items);
}
 
引用委託的程式碼必須要指定所在類的型別引數,如下:
 
Stack<float> s = new Stack<float>();
Stack<float>.StackDelegate myDelegate = StackNotify;
 
 
泛型委託在定義基於典型設計模式的事件時特別有用。因為sender[JX2] ,而再也不用與Object相互轉換。
public void StackEventHandler<T,U>(T sender,U eventArgs);
class Stack<T>
{
    //…
    public class StackEventArgs : EventArgs{...}
    public event StackEventHandler<Stack<T>,StackEventArgs> stackEvent;
    protected virtual void OnStackChanged(StackEventArgs a)
    {
      stackEvent(this,a);
    }
}
class MyClass
{
  public static void HandleStackChange<T>(Stack<T> stack,StackEventArgs args){...};
}
Stack<double> s = new Stack<double>();
MyClass mc = new MyClass();
s.StackEventHandler += mc.HandleStackChange;


泛型約束

所謂的泛型約束,實際上就是約束的型別T。使T必須遵循一定的規則。比如T必須繼承自某個類,或者T必須實現某個介面等等。那麼怎麼給泛型指定約束?其實也很簡單,只需要where關鍵字,加上約束的條件。

定義一個People類,裡面有屬性和方法:

    public interface ISports
    {
        void Pingpang();
    }
    public interface IWork
    {
        void Work();
    }
    public class People
    {
        public int Id { get; set; }
        public string Name { get; set; }
    
        public void Hi()
        {
            Console.WriteLine("Hi");
        }
    
    }
    public class Chinese : People,ISports,IWork
    {
        public void Tradition()
        {
            Console.WriteLine("仁義禮智信,溫良恭儉讓");
        }
        public void SayHi()
        {
            Console.WriteLine("吃了麼?");
        }
    
        public void Pingpang()
        {
            Console.WriteLine("打乒乓球...");
        }
    
        public void Work()
        {
            throw new NotImplementedException();
        }
    } 
    public class Hubei : Chinese
    {
        public Hubei(int version)
        { }
    
        public string Changjiang { get; set; }
        public void Majiang()
        {
            Console.WriteLine("打麻將啦。。");
        }
    }
    public class Japanese : ISports
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public void Hi()
        {
            Console.WriteLine("Hi");
        }
        public void Pingpang()
        {
            Console.WriteLine("打乒乓球...");
        }
    }

列印方法

    /// <summary>
    /// 列印個object值
    /// 1 object型別是一切型別的父類
    /// 2 通過繼承,子類擁有父類的一切屬性和行為;任何父類出現的地方,都可以用子類來代替
    /// object引用型別  加入傳個值型別int  會有裝箱拆箱  效能損失
    /// 型別不安全
    /// </summary>
    /// <param name="oParameter"></param>
    public static void ShowObject(object oParameter)
    {
        Console.WriteLine("This is {0},typeof(Constraint),oParameter.GetType().Name,oParameter);

        Console.WriteLine($"{((People)oParameter).Id}_{((People)oParameter).Name}");

    }

在main方法中

    static void Main(string[] args)
    {
        #region  Constraint 介面約束
        Console.WriteLine("************Constraint*****************");
        {
            People people = new People()
            {
                Id = 123,Name = "走自己的路"
            };
            Chinese chinese = new Chinese()
            {
                Id = 234,Name = "晴天"
            };
            Hubei hubei = new Hubei(123)
            {
                Id = 345,Name = "流年"
            };
            Japanese japanese = new Japanese()
            {
                Id = 7654,Name = "i3yuan"//
            };
            CommonMethod.ShowObject(people);
            CommonMethod.ShowObject(chinese);
            CommonMethod.ShowObject(hubei);
            CommonMethod.ShowObject(japanese);
  
            Console.ReadLine();
        }
        #endregion
    }

泛型約束總共有五種。

約束 說明
T:結構 型別引數必須是值型別
T:類 型別引數必須是引用型別;這一點也適用於任何類、介面、委託或陣列型別。
T:new() 型別引數必須具有無引數的公共建構函式。 當與其他約束一起使用時,new() 約束必須最後指定。
T:<基類名> 型別引數必須是指定的基類或派生自指定的基類。
T:<介面名稱> 型別引數必須是指定的介面或實現指定的介面。 可以指定多個介面約束。 約束介面也可以是泛型的。

1、基類約束

上面列印的方法約束T型別必須是People型別。

///


/// 基類約束:約束T必須是People型別或者是People的子類
/// 1 可以使用基類的一切屬性方法---權利
/// 2 強制保證T一定是People或者People的子類---義務

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="tParameter"></param>
        public static void Show<T>(T tParameter) where T : People
        {
            Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
            tParameter.Hi();
        }

注意:

基類約束時,基類不能是密封類,即不能是sealed類。sealed類表示該類不能被繼承,在這裡用作約束就無任何意義,因為sealed類沒有子類。

2、介面約束

        /// <summary>
        /// 介面約束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : ISports
        {
            t.Pingpang();
            return t;
        }

3、引用型別約束 class

引用型別約束保證T一定是引用型別的。

        /// 引用型別約束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : class
        {
            return t;
        }

4、值型別約束 struct

值型別約束保證T一定是值型別的。

        /// 值型別型別約束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : struct
        {
            return t;
        }

5、無引數建構函式約束 new()

        /// new()約束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : new()
        {
            return t;
        }

泛型約束也可以同時約束多個,例如:

        /// <summary>
        ///  泛型:不同的引數型別都能進來;任何型別都能過來,你知道我是誰?
        /// 沒有約束,也就沒有自由
        ///  泛型約束--基類約束(不能是sealed):
        /// 1 可以使用基類的一切屬性方法---權利
        /// 2  強制保證T一定是People或者People的子類---義務
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="tParameter"></param>
        public static void Show<T>(T tParameter)
        where T : People,IWork,new()
        {
            Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
            tParameter.Hi();
            tParameter.Pingpang();
            tParameter.Work();
        }

注意:有多個泛型約束時,new()約束一定是在最後。

泛型的協變和逆變

    public class Animal
    {
        public int Id { get; set; }
    }

    public class Cat : Animal
    {
        public string Name { get; set; }
    }
    

 static void Main(string[] args)
 {
    #region 協變和逆變

    // 直接宣告Animal類
    Animal animal = new Animal();
    // 直接宣告Cat類
    Cat cat = new Cat();
    // 宣告子類物件指向父類
    Animal animal2 = new Cat();
    // 宣告Animal類的集合
    List<Animal> listAnimal = new List<Animal>();
    // 宣告Cat類的集合
    List<Cat> listCat = new List<Cat>();

    #endregion
 }

那麼問題來了:下面的一句程式碼是不是正確的呢?

1 List<Animal> list = new List<Cat>();

可能有人會認為是正確的:因為一隻Cat屬於Animal,那麼一群Cat也應該屬於Animal啊。但是實際上這樣宣告是錯誤的:因為List和List之間沒有父子關係。

技術分享圖片

這時就可以用到協變和逆變了。

1 // 協變
2 IEnumerable<Animal> List1 = new List<Animal>();
3 IEnumerable<Animal> List2 = new List<Cat>();

F12檢視定義:

技術分享圖片

可以看到,在泛型介面的T前面有一個out關鍵字修飾,而且T只能是返回值型別,不能作為引數型別,這就是協變。使用了協變以後,左邊宣告的是基類,右邊可以宣告基類或者基類的子類。

協變除了可以用在介面上面,也可以用在委託上面:

 Func<Animal> func = new Func<Cat>(() => null);

除了使用.NET框架定義好的以為,我們還可以自定義協變,例如:

    /// <summary>
    /// out 協變 只能是返回結果
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ICustomerListOut<out T>
    {
        T Get();
    }

    public class CustomerListOut<T> : ICustomerListOut<T>
    {
        public T Get()
        {
            return default(T);
        }
    }

使用自定義的協變:

 // 使用自定義協變
 ICustomerListOut<Animal> customerList1 = new CustomerListOut<Animal>();
 ICustomerListOut<Animal> customerList2 = new CustomerListOut<Cat>();

在來看看逆變。

在泛型介面的T前面有一個In關鍵字修飾,而且T只能方法引數,不能作為返回值型別,這就是逆變。請看下面的自定義逆變:

    /// <summary>
    /// 逆變 只能是方法引數
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ICustomerListIn<in T>
    {
        void Show(T t);
    }

    public class CustomerListIn<T> : ICustomerListIn<T>
    {
        public void Show(T t)
        {
        }
    }

使用自定義逆變:

 // 使用自定義逆變
 ICustomerListIn<Cat> customerListCat1 = new CustomerListIn<Cat>();
 ICustomerListIn<Cat> customerListCat2 = new CustomerListIn<Animal>();

協變和逆變也可以同時使用,看看下面的例子:

    /// <summary>
    /// inT 逆變
    /// outT 協變
    /// </summary>
    /// <typeparam name="inT"></typeparam>
    /// <typeparam name="outT"></typeparam>
    public interface IMyList<in inT,out outT>
    {
        void Show(inT t);
        outT Get();
        outT Do(inT t);
    }

    public class MyList<T1,T2> : IMyList<T1,T2>
    {

        public void Show(T1 t)
        {
            Console.WriteLine(t.GetType().Name);
        }

        public T2 Get()
        {
            Console.WriteLine(typeof(T2).Name);
            return default(T2);
        }

        public T2 Do(T1 t)
        {
            Console.WriteLine(t.GetType().Name);
            Console.WriteLine(typeof(T2).Name);
            return default(T2);
        }
    }

使用:

 IMyList<Cat,Animal> myList1 = new MyList<Cat,Animal>();
 IMyList<Cat,Animal> myList2 = new MyList<Cat,Cat>();//協變
 IMyList<Cat,Animal> myList3 = new MyList<Animal,Animal>();//逆變
 IMyList<Cat,Animal> myList4 = new MyList<Animal,Cat>();//逆變+協變

有關可變性的注意事項

  • 變化只適用於引用型別,因為不能直接從值型別派生其他型別
  • 顯示變化使用in和out關鍵字只適用於委託和介面,不適用於類、結構和方法
  • 不包括in和out關鍵字的委託和介面型別引數叫做不變

泛型快取

在前面我們學習過,類中的靜態型別無論例項化多少次,在記憶體中只會有一個。靜態建構函式只會執行一次。在泛型類中,T型別不同,每個不同的T型別,都會產生一個不同的副本,所以會產生不同的靜態屬性、不同的靜態建構函式,請看下面的例子:

public class GenericCache<T>
{
    static GenericCache()
    {
        Console.WriteLine("This is GenericCache 靜態建構函式");
        _TypeTime = string.Format("{0}_{1}",typeof(T).FullName,DateTime.Now.ToString("yyyyMMddHHmmss.fff"));
    }

    private static string _TypeTime = "";

    public static string GetCache()
    {
        return _TypeTime;
    }
}
public class GenericCacheTest
{
    public static void Show()
    {
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine(GenericCache<int>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<long>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<DateTime>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<string>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<GenericCacheTest>.GetCache());
            Thread.Sleep(10);
        }
    }
}

Main()方法裡面呼叫:

 static void Main(string[] args)
 {
    #region 泛型快取
	GenericCacheTest.Show();
    #endregion
 }

結果:

技術分享圖片

從上面的截圖中可以看出,泛型會為不同的型別都建立一個副本,所以靜態建構函式會執行5次。 而且每次靜態屬性的值都是一樣的。利用泛型的這一特性,可以實現快取。

注意:只能為不同的型別快取一次。泛型快取比字典快取效率高。泛型快取不能主動釋放。

注意

1.泛型程式碼中的 default 關鍵字

在泛型類和泛型方法中會出現的一個問題是,如何把預設值賦給引數化型別,此時無法預先知道以下兩點:

  • T將是值型別還是引用型別

  • 如果T是值型別,那麼T將是數值還是結構

對於一個引數化型別T的變數t,僅當T是引用型別時,t = null語句才是合法的; t = 0只對數值的有效,而對結構則不行。這個問題的解決辦法是用default關鍵字,它對引用型別返回空,對值型別的數值型返回零。而對於結構,它將返回結構每個成員,並根據成員是值型別還是引用型別,返回零或空。下面GenericList類的例子顯示瞭如何使用default關鍵字。

    static void Main(string[] args)
    {
        #region 泛型程式碼預設關鍵字default
        // 使用非空的整數列表進行測試.
        GenericList<int> gll = new GenericList<int>();
        gll.AddNode(5);
        gll.AddNode(4);
        gll.AddNode(3);
        int intVal = gll.GetLast();
        // 下面一行顯示5.
        Console.WriteLine(intVal);

        // 用一個空的整數列表進行測試.
        GenericList<int> gll2 = new GenericList<int>();
        intVal = gll2.GetLast();
        // 下面一行顯示0.
        Console.WriteLine(intVal);

        // 使用非空字串列表進行測試.
        GenericList<string> gll3 = new GenericList<string>();
        gll3.AddNode("five");
        gll3.AddNode("four");
        string sVal = gll3.GetLast();
        // 下面一行顯示five.
        Console.WriteLine(sVal);

        // 使用一個空字串列表進行測試.
        GenericList<string> gll4 = new GenericList<string>();
        sVal = gll4.GetLast();
        // 下面一行顯示一條空白行.
        Console.WriteLine(sVal);
        #endregion
        Console.ReadKey();
    }
    public class GenericList<T>
    {
        private class Node
        {
            // 每個節點都有一個指向列表中的下一個節點的引用.
            public Node Next;
            // 每個節點都有一個T型別的值.
            public T Data;
        }

        // 這個列表最初是空的.
        private Node head = null;

        // 在列表開始的時候新增一個節點,用t作為它的資料值.
        public void AddNode(T t)
        {
            Node newNode = new Node();
            newNode.Next = head;
            newNode.Data = t;
            head = newNode;
        }

        // 下面的方法返回儲存在最後一個節點中的資料值列表. 如果列表是空的,返回型別T的預設值.
        public T GetLast()
        {
            // 臨時變數的值作為方法的值返回. 
            // 下面的宣告初始化了臨時的溫度 
            // 型別T的預設值. 如果該列表為空返回預設值.
            T temp = default(T);

            Node current = head;
            while (current != null)
            {
                temp = current.Data;
                current = current.Next;
            }
            return temp;
        }
    }

2.泛型集合

通常情況下,建議您使用泛型集合,因為這樣可以獲得型別安全的直接優點而不需要從基集合型別派生並實現型別特定的成員。下面的泛型型別對應於現有的集合型別:

1、List 是對應於 ArrayList 的泛型類。
2、Dictionary 是對應於 Hashtable 的泛型類。
3、Collection 是對應於 CollectionBase 的泛型類。
4、ReadOnlyCollection 是對應於 ReadOnlyCollectionBase 的泛型類。
5、QueueStackSortedList 泛型類分別對應於與其同名的非泛型類。
6、LinkedList 是一個通用連結列表,它提供運算複雜度為 O(1) 的插入和移除操作。
7、SortedDictionary 是一個排序的字典,其插入和檢索操作的運算複雜度為 O(log n),這使得它成為 SortedList 的十分有用的替代型別。
8、KeyedCollection 是介於列表和字典之間的混合型別,它提供了一種儲存包含自己鍵的物件的方法。

總結

  1. 作為一個開發人員,當我們程式程式碼有相同的邏輯,有可能是方法、介面、類或者委託,只是某些引數型別不同,我們希望程式碼可以通用、複用,甚至是說為了偷懶,也可以說是在不確定型別的情況下,就應該考慮用泛型的思維去實現。
  2. 在非泛型程式設計中,雖然所有的東西都可以作為Object傳遞,但是在傳遞的過程中免不了要進行型別轉換。而型別轉換在執行時是不安全的。使用泛型程式設計將可以減少不必要的型別轉換,從而提高安全性。不僅是值型別,引用型別也存在這樣的問題,因此有必要的儘量的去使用泛型集合。
  3. 在非泛型程式設計中,將簡單型別作為Object傳遞時會引起裝箱和拆箱的操作,這兩個過程都是具有很大開銷的。使用泛型程式設計就不必進行裝箱和拆箱操作了。

參考 文件 《C#圖解教程》

注:搜尋關注公眾號【DotNet技術谷】--回覆【C#圖解】,可獲取 C#圖解教程檔案