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},parameter={1},type={2}",
            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, ISports, 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 之間沒有父子關系。

image-2020053023015097

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

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
 }

結果:

20200530232600809

從上面的截圖中可以看出,泛型會為不同的類型都創建一個副本,所以靜態構造函數會執行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#圖解教程文件


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM