1. 程式人生 > >C#泛型入門學習泛型類、泛型集合、泛型方法、泛型約束、泛型委託

C#泛型入門學習泛型類、泛型集合、泛型方法、泛型約束、泛型委託

本章閱讀列表

  • 泛型很難理解?不然
  • 泛型集合和ArrayList的裝箱拆箱
  • 常見的泛型型別
  • 泛型類和泛型方法
  • 泛型約束
  • 泛型委託

    泛型很難理解?不然

    在接觸的一個新的概念的時候,總會感覺難以理解,當你掌握並能熟練地使用的時候,發現這個概念其實簡單的,我相信大多數碼農都會有這種似曾相識的感覺。可能大多數人剛學習泛型的時候覺得很難理解,當然我也是這樣的,所以便寫下這篇文章加深一下對泛型的印象。
    第一次接觸泛型那還是在大二上學期的時候,那會是學c#面向物件的時候接觸過泛型集合,但尷尬的是那會還沒有“泛型”這個概念,僅僅只停留在泛型集合的使用。關於泛型入門的文章csdn和部落格園有很多,這裡我也寫一篇關於我對泛型學習的一個總結,如果出現錯誤表達不當的地方,還希望評論指出。

    泛型優點

    官方文件:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/introduction-to-generics
    簡介:
    泛型是.NET Framework2.0新增的一個特性,在名稱空間System.Collections.Generic,包含了幾個新的基於泛型的集合類,官方建議.net 2.0 及更高版本的應用程式使用心得泛型集合類,而不使用非泛型集合類,例如ArrayList。
    官方解釋:
    泛型是程式設計語言的一種特性。允許程式設計師在強型別程式設計語言中編寫程式碼時定義一些可變部分,那些部分在使用前必須作出指明。各種程式設計語言和其編譯器、執行環境對泛型的支援均不一樣。將型別引數化以達到程式碼複用提高軟體開發工作效率的一種資料型別。泛型類是引用型別,是堆物件,主要是引入了型別引數這個概念。
    泛型的定義主要有以下兩種:
    1.在程式編碼中一些包含型別引數的型別,也就是說泛型的引數只可以代表類,不能代表個別物件。(這是當今較常見的定義)
    2.在程式編碼中一些包含引數的類。其引數可以代表類或物件等等。(人們大多把這稱作模板)不論使用哪個定義,泛型的引數在真正使用泛型時都必須作出指明
    官方的解釋雖然很難理解,用我的話來解釋那就是,宣告類和方法時一般都需要定義是什麼類,class Brid ,Class Food…… 宣告泛型類和方法時只需要傳入型別的地方用 ,有點類似於佔位符的作用,用的時候傳入具體的型別。當針對不同型別具有相同行為的時候,也就是泛型發揮作用的時候。
    優點:
    1.使用泛型類、方法,我們可以極大提高程式碼的重用性,不需要對型別不同程式碼相同(僅型別引數不同)的程式碼寫多次。
    2.建立泛型類,可在編譯時建立型別安全的集合
    3.避免裝箱和拆箱操作降低效能,在大型集合中裝箱和拆箱的影響非常大.

泛型集合和ArrayList的裝箱拆箱

裝箱:是指從值型別轉換成引用型別
拆箱:是指從引用型別轉換成值型別
下面的例子是借鑑官方的一段程式碼:

    System.Collections.ArrayList list1 = new System.Collections.ArrayList();
            list1.Add(3);
            list1.Add(105);

            System.Collections.ArrayList list2 = new System.Collections.ArrayList();
            list2.Add
("科比"); list2.Add("詹姆斯");

ArrayList是一個極為方便的集合類,可以用於儲存任何引用或值型別。但是缺點也很明顯,第一個缺點是編譯的時候不會檢查型別,例如

  System.Collections.ArrayList list1 = new System.Collections.ArrayList();
            list1.Add(3);
            list1.Add(105);
            list1.Add("sd");
            foreach (int item in list1)
            {
                Console.WriteLine(item.ToString());
            }

編譯正常,執行的時候會出現轉換型別錯誤。
至於ArrayList第二個缺點就是裝箱拆箱的時候回造成效能的損失。我們看看ArrayList的Add方法的定義。
這裡寫圖片描述
引數是一個object型別,也就是說ArrayList新增任何引用型別或值型別都會隱士轉換成Object,這個時候便發生裝箱操作,在遍歷檢索它們時必須從object 型別轉換成指定的型別,這個時候便發生拆箱操作。這兩種操作會大大降低效能。所以.net 2.0的程式時應該放棄使用ArrayList,推薦使用使用List《T》 泛型集合。這也是我們為什麼要使用泛型的原因之一。

常見的泛型型別

在泛型型別的定義中,出現的每個T(一個展位變數而已叫別的名字也行)在執行時都會被替換成實際的型別引數。下面是一些基礎的泛型型別
1.泛型類

           class MyGenericClass<T>
        {
          //......
        }

2.泛型介面

        interface  GenericInterface<T>
        {
           void  GenericMethod(T t);
        }

3.泛型方法

        public void MyGenericMethod<T>()
        {
          //......
        }

4.泛型陣列

public T[] GenericArray;

5.泛型委託

 public delegate TOutput GenericDelagete<TInput, TOutput>(TInput input);

6.泛型結構

   struct MyGenericStruct<T>
        {

        }

在使用時所有的T的都要替換成具體的型別。
型別引數命名指南,參見官方文件
這裡寫圖片描述

泛型類和泛型方法

我們先來看看泛型方法,這個方法的用途是來交換兩個變數的

        static void Main(string[] args)
        {
            int a = 1;
            int b = 2;
            SwapInt(ref a,ref b);
            Console.WriteLine($"a={a}b={b}");
        }
        public static void SwapInt(ref int a, ref int b)
        {
            int temp;
            temp = a;
            a = b;
            b = temp;
        }

結果是a=2,b=1,但是我們現在要換成string型別呢,是不是得再寫一個string引數的方法呢,如果是char、double………..,這每個不同型別的引數都要寫一個引數,的確太麻煩並且沒有這個必要,Object ?當然可以

        static void Main(string[] args)
        {
            object a = 1;
            object b = 2;
            SwapObject(ref a,ref b);
            Console.WriteLine($"a={a}b={b}");
        }
        public static void SwapObject(ref object a, ref object b)
        {
            object temp;
            temp = a;
            a = b;
            b = temp;
        }

這確實能解決程式碼複用的需求,但是上面我們已經知道使用Object型別會發生裝箱拆箱的操作,會降低效能。所以我們可以使用泛型方法解決這個缺點。

    static void Main(string[] args)
        {
            int a = 1;
            int b = 2;
            SwapGeneric(ref a,ref b);
            Console.WriteLine($"a={a}b={b}");
        }
        //交換兩個變數的方法
        public static void SwapGeneric<T>(ref T a, ref T b)
        {
            T temp;
            temp = a;
            a = b;
            b = temp;
        }

泛型類:這個泛型類常用api通用介面的泛型類。

    class Program
    {
        static void Main(string[] args)
        {
            List<Product> data = new List<Client.Product>() {
                              new Client.Product() { Id=12,Name="土豆"},
                              new Client.Product() { Id=12,Name="茄子"},
                              new Client.Product() { Id=12,Name="黃瓜"}
           };
           var resultProduct = Result<Product>.Success(data);
            var resultUser = Result<User>.Error("沒有資料");
            foreach (var item in resultProduct.Data)
            {
                Console.WriteLine(item.Id+","+item.Name);
            }
            Console.WriteLine(resultUser.IsSuccess+resultUser.Message);
        }

    }
    public class Result<T> { //泛型類,宣告T變數型別
        public bool IsSuccess { get; set; }
        public List<T> Data { get; set;}//未定義具體型別的泛型集合
        public string Message { get; set; }
        public static Result<T> Error(string message) 
        {
            return new Client.Result<T> { IsSuccess = false, Message = message };
        }
        //泛型方法,初始化資料
        public static Result<T> Success(List<T> data)
        {
            return new Client.Result<T> { IsSuccess =true,Data =data}; //成功就沒有提示訊息的原則
        }
    }
    public class Product {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    public class User {
        public int Age { get; set; }
        public string Name { get; set; }
    }

使用該通用的泛型類的好處在於,獲取不同的物件集合不需要寫多個方法,獲取Product資料集合、獲取User資料集………。只需要呼叫Success方法既可,使程式碼變得可複用。

泛型型別引數約束

為什麼要使用型別引數的約束呢,簡單點說就是篩選型別引數,在使用泛型的程式碼中如果違反了某個約束不允許的型別來例項化則會產生編譯錯誤,型別引數的約束是使用關鍵字where。 下面列出了6中型別的約束

  1. where T: struct
    型別引數必須是值型別。可以指定除 Nullable 以外的任何值型別。有關更多資訊,請參見使用可以為 null 的型別(C# 程式設計指南)。
  2. where T : class
    型別引數必須是引用型別;這一點也適用於任何類、介面、委託或陣列型別。
  3. where T:new()
    型別引數必須具有無引數的公共建構函式。當與其他約束一起使用時,new() 約束必須最後指定。
  4. where T:<基類名>
    型別引數必須是指定的基類或派生自指定的基類。
  5. where T:<介面名稱>
    型別引數必須是指定的介面或實現指定的介面。可以指定多個介面約束。約束介面也可以是泛型的。
    1. where T:U
      為 T 提供的型別引數必須是為 U 提供的引數或派生自為 U 提供的引數。
      我們在看看上面那個交換兩個變數的方法SwapGeneric,加上必須是值型別的約束
  public static void SwapGeneric<T>(ref T a,ref T b) where T :struct 
        {
            T temp;
            temp = a;
            a = b;
            b = a;
        }
        //例項化
            Product p = new Product() { Id=1,Name="土豆"};
            Product p1 = new Product() { Id=2,Name ="茄子"};
            SwapGeneric<Product>(ref p,ref p1);

我們在使用的時候編譯給我們提示了以下的錯誤:
這裡寫圖片描述
“型別Product必須是不可以為NUll值得型別”,引用型別的預設值就是NULL,所以該房型方法的型別引數不能是引用型別,這就是使用型別引數約束的好處。
約束多個引數

    class List<TLive,U> where TLive:User where U:struct
    {

    }

泛型委託

泛型委託可以自己定義自己的型別引數,宣告的時候還是和泛型類、泛型方法一樣加個<坑> 站個坑,其實泛型委託使用的時候不是很多,要慎用。,如以下事例

public delegate T DelegateGeneric<T>(T item);
DelegateGeneric<string> test = StringMethod;
        public static string StringMethod(string name)
        {
            return "你好" + name;
        }

將上面的交換兩個變數的方法改成委託是這樣的

        public delegate void DelegateGenericSwap<T>(ref T a,ref T b);
            string a = "張林";
            string b = "科比";
            DelegateGenericSwap<string> swap = GenericSwap;
            Console.WriteLine($"a:{a}b:{b}");
           public static void GenericSwap<T>(ref T a,ref T b)
        {
            T temp;
            temp = a;
            a = b;
            b = a;
        }