C#泛型的使用及示例詳解

這篇文章主要講解C#中的泛型,泛型在C#中有很重要的地位,尤其是在搭建項目框架的時候。

一、什麼是泛型

泛型是C#2.0推出的新語法,不是語法糖,而是2.0由框架升級提供的功能。

我們在編程程序時,經常會遇到功能非常相似的模塊,隻是它們處理的數據不一樣。但我們沒有辦法,隻能分別寫多個方法來處理不同的數據類型。這個時候,那麼問題來瞭,有沒有一種辦法,用同一個方法來處理傳入不同種類型參數的辦法呢?泛型的出現就是專門來解決這個問題的。

二、為什麼使用泛型

先來看下面一個例子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    public class CommonMethod
    {
        /// <summary>
        /// 打印個int值
        /// </summary>
        /// <param name="iParameter"></param>
        public static void ShowInt(int iParameter)
        {
            Console.WriteLine("This is {0},parameter={1},type={2}",
                typeof(CommonMethod).Name, iParameter.GetType().Name, iParameter);
        }

        /// <summary>
        /// 打印個string值
        /// </summary>
        /// <param name="sParameter"></param>
        public static void ShowString(string sParameter)
        {
            Console.WriteLine("This is {0},parameter={1},type={2}",
                typeof(CommonMethod).Name, sParameter.GetType().Name, sParameter);
        }

        /// <summary>
        /// 打印個DateTime值
        /// </summary>
        /// <param name="oParameter"></param>
        public static void ShowDateTime(DateTime dtParameter)
        {
            Console.WriteLine("This is {0},parameter={1},type={2}",
                typeof(CommonMethod).Name, dtParameter.GetType().Name, dtParameter);
        }
    }
}

結果:

從上面的結果中我們可以看出這三個方法,除瞭傳入的參數不同外,其裡面實現的功能都是一樣的。在1.0版的時候,還沒有泛型這個概念,那麼怎麼辦呢。相信很多人會想到瞭OOP三大特性之一的繼承,我們知道,C#語言中,object是所有類型的基類,將上面的代碼進行以下優化:

public static void ShowObject(object oParameter)
{
      Console.WriteLine("This is {0},parameter={1},type={2}",
         typeof(CommonMethod), oParameter.GetType().Name, oParameter);
}

結果:

從上面的結果中我們可以看出,使用Object類型達到瞭我們的要求,解決瞭代碼的可復用。可能有人會問定義的是object類型的,為什麼可以傳入int、string等類型呢?原因有二:

1、object類型是一切類型的父類。

2、通過繼承,子類擁有父類的一切屬性和行為,任何父類出現的地方,都可以用子類來代替。

但是上面object類型的方法又會帶來另外一個問題:裝箱和拆箱,會損耗程序的性能。

微軟在C#2.0的時候推出瞭泛型,可以很好的解決上面的問題。

三、泛型類型參數

在泛型類型或方法定義中,類型參數是在其實例化泛型類型的一個變量時,客戶端指定的特定類型的占位符。 泛型類(GenericList<T>)無法按原樣使用,因為它不是真正的類型;它更像是類型的藍圖。 若要使用GenericList<T>,客戶端代碼必須通過指定尖括號內的類型參數來聲明並實例化構造類型。 此特定類的類型參數可以是編譯器可識別的任何類型。 可創建任意數量的構造類型實例,其中每個使用不同的類型參數。

上面例子中的代碼可以修改如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    public class GenericMethod
    {
        /// <summary>
        /// 泛型方法
        /// </summary>
        /// <typeparam name="T"></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());
        }
    }
}

調用:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    class Program
    {
        static void Main(string[] args)
        {

            int iValue = 123;
            string sValue = "456";
            DateTime dtValue = DateTime.Now;

            Console.WriteLine("***********CommonMethod***************");
            CommonMethod.ShowInt(iValue);
            CommonMethod.ShowString(sValue);
            CommonMethod.ShowDateTime(dtValue);
            Console.WriteLine("***********Object***************");
            CommonMethod.ShowObject(iValue);
            CommonMethod.ShowObject(sValue);
            CommonMethod.ShowObject(dtValue);
            Console.WriteLine("***********Generic***************");
            GenericMethod.Show<int>(iValue);
            GenericMethod.Show<string>(sValue);
            GenericMethod.Show<DateTime>(dtValue);
            Console.ReadKey();
        }
    }
}

顯示結果:

為什麼泛型可以解決上面的問題呢?

泛型是延遲聲明的:即定義的時候沒有指定具體的參數類型,把參數類型的聲明推遲到瞭調用的時候才指定參數類型。 延遲思想在程序架構設計的時候很受歡迎。例如:分佈式緩存隊列、EF的延遲加載等等。

泛型究竟是如何工作的呢?

控制臺程序最終會編譯成一個exe程序,exe被點擊的時候,會經過JIT(即時編譯器)的編譯,最終生成二進制代碼,才能被計算機執行。泛型加入到語法以後,VS自帶的編譯器又做瞭升級,升級之後編譯時遇到泛型,會做特殊的處理:生成占位符。再次經過JIT編譯的時候,會把上面編譯生成的占位符替換成具體的數據類型。請看下面一個例子:

Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));

結果:

從上面的截圖中可以看出:泛型在編譯之後會生成占位符。

註意:占位符需要在英文輸入法狀態下才能輸入,隻需要按一次波浪線(數字1左邊的鍵位)的鍵位即可,不需要按Shift鍵。

1、泛型性能問題

請看一下的一個例子,比較普通方法、Object參數類型的方法、泛型方法的性能。

添加一個Monitor類,讓三種方法執行同樣的操作,比較用時長短:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    public class Monitor
    {
        public static void Show()
        {
            Console.WriteLine("****************Monitor******************");
            {
                int iValue = 12345;
                long commonSecond = 0;
                long objectSecond = 0;
                long genericSecond = 0;

                {
                    Stopwatch watch = new Stopwatch();
                    watch.Start();
                    for (int i = 0; i < 100000000; i++)
                    {
                        ShowInt(iValue);
                    }
                    watch.Stop();
                    commonSecond = watch.ElapsedMilliseconds;
                }
                {
                    Stopwatch watch = new Stopwatch();
                    watch.Start();
                    for (int i = 0; i < 100000000; i++)
                    {
                        ShowObject(iValue);
                    }
                    watch.Stop();
                    objectSecond = watch.ElapsedMilliseconds;
                }
                {
                    Stopwatch watch = new Stopwatch();
                    watch.Start();
                    for (int i = 0; i < 100000000; i++)
                    {
                        Show<int>(iValue);
                    }
                    watch.Stop();
                    genericSecond = watch.ElapsedMilliseconds;
                }
                Console.WriteLine("commonSecond={0},objectSecond={1},genericSecond={2}"
                    , commonSecond, objectSecond, genericSecond);
            }
        }

        #region PrivateMethod
        private static void ShowInt(int iParameter)
        {
            //do nothing
        }
        private static void ShowObject(object oParameter)
        {
            //do nothing
        }
        private static void Show<T>(T tParameter)
        {
            //do nothing
        }
        #endregion

    }
}

Main()方法調用:

Monitor.Show();

結果:

從結果中可以看出:泛型方法的性能最高,其次是普通方法,object方法的性能最低。

四、泛型類

除瞭方法可以是泛型以外,類也可以是泛型的,例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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

Main()方法中調用:

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

除瞭可以有泛型類,也可以有泛型接口,例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    /// <summary>
    /// 泛型接口
    /// </summary>
    public interface IGenericInterface<T>
    {
        //泛型類型的返回值
        T GetT(T t);
    }
}

也可以有泛型委托:

public delegate void SayHi<T>(T t);//泛型委托

註意:

1、泛型在聲明的時候可以不指定具體的類型,但是在使用的時候必須指定具體類型,例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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

如果子類也是泛型的,那麼繼承的時候可以不指定具體類型,例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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

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

    }
}

2、類實現泛型接口也是這種情況,例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    /// <summary>
    /// 必須指定具體類型
    /// </summary>
    public class Common : IGenericInterface<string>
    {
        public string GetT(string t)
        {
            throw new NotImplementedException();
        }
    }

    /// <summary>
    /// 可以不知道具體類型,但是子類也必須是泛型的
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class CommonChild<T> : IGenericInterface<T>
    {
        public T GetT(T t)
        {
            throw new NotImplementedException();
        }
    }
}

五、泛型約束

先來看看下面的一個例子:

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

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    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("打乒乓球...");
        }
    }
}

在Main()方法裡面實例化:

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 = "werwer"
};

這時有一個需求:需要打印出Id和Name屬性的值,將ShowObject()方法修改如下:

但是這樣修改報錯瞭:object類裡面沒有Id和Name屬性,可能會有人說,強制類型轉換一下就行瞭啊:

public static void ShowObject(object oParameter)
{
         Console.WriteLine("This is {0},parameter={1},type={2}",
         typeof(CommonMethod), oParameter.GetType().Name, oParameter);

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

這樣修改以後,代碼不會報錯瞭,這時我們在Main()方法裡面調用:

CommonMethod.ShowObject(people);
 CommonMethod.ShowObject(chinese);
 CommonMethod.ShowObject(hubei);
 CommonMethod.ShowObject(japanese);

結果:

可以看出程序報錯瞭,因為Japanese沒有繼承自People,這裡類型轉換的時候失敗瞭。這樣會造成類型不安全的問題。那麼怎麼解決類型不安全的問題呢?那就是使用泛型約束。

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

泛型約束總共有五種。

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

1、基類約束

上面打印的方法約束T類型必須是People類型。

/// <summary>
/// 基類約束:約束T必須是People類型或者是People的子類
/// </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>
/// 引用類型約束
/// </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>
/// 值類型類型約束
/// </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()

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

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

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()約束一定是在最後。

六、泛型的協變和逆變

協變和逆變是在.NET 4.0的時候出現的,隻能放在接口或者委托的泛型參數前面,out 協變covariant,用來修飾返回值;in:逆變contravariant,用來修飾傳入參數。

先看下面的一個例子:

定義一個Animal類:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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

然後在定義一個Cat類繼承自Animal類:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

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

在Main()方法可以這樣調用:

// 直接聲明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>();

那麼問題來瞭:下面的一句代碼是不是正確的呢?

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

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

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

// 協變
 IEnumerable<Animal> List1 = new List<Animal>();
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>();//逆變+協變

七、泛型緩存

在前面我們學習過,類中的靜態類型無論實例化多少次,在內存中隻會有一個。靜態構造函數隻會執行一次。在泛型類中,T類型不同,每個不同的T類型,都會產生一個不同的副本,所以會產生不同的靜態屬性、不同的靜態構造函數,請看下面的例子:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyGeneric
{
    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;
        }
    }
}

然後新建一個測試類,用來測試GenericCache類的執行順序:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace MyGeneric
{
    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()方法裡面調用:

GenericCacheTest.Show();

結果:

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

註意:隻能為不同的類型緩存一次。泛型緩存比字典緩存效率高。泛型緩存不能主動釋放

到此這篇關於C#泛型的使用及示例的文章就介紹到這瞭,更多相關C#泛型內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: