1. 1 線性表的順序表示
這篇文章主要講解C#中的泛型,泛型在C#中有很重要的地位,尤其是在搭建專案框架的時候。
一、什麼是泛型
泛型是C#2.0推出的新語法,不是語法糖,而是2.0由框架升級提供的功能。
我們在程式設計程式時,經常會遇到功能非常相似的模組,只是它們處理的資料不一樣。但我們沒有辦法,只能分別寫多個方法來處理不同的資料型別。這個時候,那麼問題來了,有沒有一種辦法,用同一個方法來處理傳入不同種類型引數的辦法呢?泛型的出現就是專門來解決這個問題的。
二、為什麼使用泛型
先來看下面一個例子:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace MyGeneric
{
public class CommonMethod
{
/// <summary>
/// 列印個int值
///
/// 因為方法宣告的時候,寫死了引數型別
/// 已婚的男人 Eleven San
/// </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編譯的時候,會把上面編譯生成的佔位符替換成具體的資料型別。請看下面一個例子:
1 Console.WriteLine(typeof(List<>)); 2 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);
}
}
也可以有泛型委託:
1 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()方法裡面呼叫:
1 CommonMethod.ShowObject(people); 2 CommonMethod.ShowObject(chinese); 3 CommonMethod.ShowObject(hubei); 4 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>();
那麼問題來了:下面的一句程式碼是不是正確的呢?
1 List<Animal> list = new List<Cat>();
可能有人會認為是正確的:因為一隻Cat屬於Animal,那麼一群Cat也應該屬於Animal啊。但是實際上這樣宣告是錯誤的:因為List<Cat>和List<Animal>之間沒有父子關係。
這時就可以用到協變和逆變了。
1 // 協變 2 IEnumerable<Animal> List1 = new List<Animal>(); 3 IEnumerable<Animal> List2 = new List<Cat>();
F12檢視定義:
可以看到,在泛型介面的T前面有一個out關鍵字修飾,而且T只能是返回值型別,不能作為引數型別,這就是協變。使用了協變以後,左邊宣告的是基類,右邊可以宣告基類或者基類的子類。
協變除了可以用在介面上面,也可以用在委託上面:
1 Func<Animal> func = new Func<Cat>(() => null);
除了使用.NET框架定義好的以為,我們還可以自定義協變,例如:
1 /// <summary> 2 /// out 協變 只能是返回結果 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 public interface ICustomerListOut<out T> 6 { 7 T Get(); 8 } 9 10 public class CustomerListOut<T> : ICustomerListOut<T> 11 { 12 public T Get() 13 { 14 return default(T); 15 } 16 }
使用自定義的協變:
1 // 使用自定義協變 2 ICustomerListOut<Animal> customerList1 = new CustomerListOut<Animal>(); 3 ICustomerListOut<Animal> customerList2 = new CustomerListOut<Cat>();
在來看看逆變。
在泛型介面的T前面有一個In關鍵字修飾,而且T只能方法引數,不能作為返回值型別,這就是逆變。請看下面的自定義逆變:
1 /// <summary> 2 /// 逆變 只能是方法引數 3 /// </summary> 4 /// <typeparam name="T"></typeparam> 5 public interface ICustomerListIn<in T> 6 { 7 void Show(T t); 8 } 9 10 public class CustomerListIn<T> : ICustomerListIn<T> 11 { 12 public void Show(T t) 13 { 14 } 15 }
使用自定義逆變:
1 // 使用自定義逆變 2 ICustomerListIn<Cat> customerListCat1 = new CustomerListIn<Cat>(); 3 ICustomerListIn<Cat> customerListCat2 = new CustomerListIn<Animal>();
協變和逆變也可以同時使用,看看下面的例子:
1 /// <summary> 2 /// inT 逆變 3 /// outT 協變 4 /// </summary> 5 /// <typeparam name="inT"></typeparam> 6 /// <typeparam name="outT"></typeparam> 7 public interface IMyList<in inT, out outT> 8 { 9 void Show(inT t); 10 outT Get(); 11 outT Do(inT t); 12 } 13 14 public class MyList<T1, T2> : IMyList<T1, T2> 15 { 16 17 public void Show(T1 t) 18 { 19 Console.WriteLine(t.GetType().Name); 20 } 21 22 public T2 Get() 23 { 24 Console.WriteLine(typeof(T2).Name); 25 return default(T2); 26 } 27 28 public T2 Do(T1 t) 29 { 30 Console.WriteLine(t.GetType().Name); 31 Console.WriteLine(typeof(T2).Name); 32 return default(T2); 33 } 34 }
使用:
1 IMyList<Cat, Animal> myList1 = new MyList<Cat, Animal>(); 2 IMyList<Cat, Animal> myList2 = new MyList<Cat, Cat>();//協變 3 IMyList<Cat, Animal> myList3 = new MyList<Animal, Animal>();//逆變 4 IMyList<Cat, Animal> myList4 = new MyList<Animal, Cat>();//逆變+協變
七、泛型快取
在前面我們學習過,類中的靜態型別無論例項化多少次,在記憶體中只會有一個。靜態建構函式只會執行一次。在泛型類中,T型別不同,每個不同的T型別,都會產生一個不同的副本,所以會產生不同的靜態屬性、不同的靜態建構函式,請看下面的例子:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace MyGeneric 8 { 9 public class GenericCache<T> 10 { 11 static GenericCache() 12 { 13 Console.WriteLine("This is GenericCache 靜態建構函式"); 14 _TypeTime = string.Format("{0}_{1}", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff")); 15 } 16 17 private static string _TypeTime = ""; 18 19 public static string GetCache() 20 { 21 return _TypeTime; 22 } 23 } 24 }
然後新建一個測試類,用來測試GenericCache類的執行順序:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading; 6 using System.Threading.Tasks; 7 8 namespace MyGeneric 9 { 10 public class GenericCacheTest 11 { 12 public static void Show() 13 { 14 for (int i = 0; i < 5; i++) 15 { 16 Console.WriteLine(GenericCache<int>.GetCache()); 17 Thread.Sleep(10); 18 Console.WriteLine(GenericCache<long>.GetCache()); 19 Thread.Sleep(10); 20 Console.WriteLine(GenericCache<DateTime>.GetCache()); 21 Thread.Sleep(10); 22 Console.WriteLine(GenericCache<string>.GetCache()); 23 Thread.Sleep(10); 24 Console.WriteLine(GenericCache<GenericCacheTest>.GetCache()); 25 Thread.Sleep(10); 26 } 27 } 28 } 29 }
Main()方法裡面呼叫:
1 GenericCacheTest.Show();
結果:
從上面的截圖中可以看出,泛型會為不同的型別都建立一個副本,所以靜態建構函式會執行5次。 而且每次靜態屬性的值都是一樣的。利用泛型的這一特性,可以實現快取。
注意:只能為不同的型別快取一次。泛型快取比字典快取效率高。泛型快取不能主動釋放