【科普】.NET6 泛型

本文內容來自我寫的開源電子書《WoW C#》,現在正在編寫中,可以去WOW-Csharp/學習路徑總結.md at master · sogeisetsu/WOW-Csharp (github.com)來查看編寫進度。預計2021年年底會完成編寫,2022年2月之前會完成所有的校對和轉制電子書工作,爭取能夠在2022年將此書上架亞馬遜。編寫此書的目的是因為目前.NET市場相對低迷,很多優秀的書都是基於.NET framework框架編寫的,與現在的.NET 6相差太大,正規的.NET 5學習教程現在幾乎只有MSDN,可是MSDN雖然準確優美但是太過瑣碎,沒有過閱讀開發文檔的同學容易一頭霧水,於是,我就編寫了基於.NET 5的《WoW C#》。本人水平有限,歡迎大家去本書的開源倉庫sogeisetsu/WOW-Csharp關注、批評、建議和指導。

泛型(Generic) 允許您延遲編寫類或方法中的編程元素的數據類型的規範,直到實際在程序中使用它的時候。換句話說,泛型允許您編寫一個可以與任何數據類型一起工作的類或方法。

Why?

泛型將類型參數的概念引入 .NET,這樣可以設計一個或多個類型的規範。泛型有更好的性能,並且可以達到類型安全,還能夠提升代碼復用率

性能

比如說兩個數組的實現方式,一個是ArrayList,一個是List<T>

看一下MSDN的ArrayList的一段反編譯代碼:

public virtual int Add(object? value)
{
    throw null;
}

可以看到ArrayList的Add方法的參數是默認值為null的object類型。當我們從ArrayList中取出數據時,

ArrayList arrayList = new ArrayList();
for (int i = 0; i < 10000; i++)
{
    arrayList.Add(i);
}
Console.WriteLine(arrayList[1].GetType());// System.Int32

我們可以清楚的看到存入的數據和取出的數據都是設置好的數據類型(System.Int32),也就是說在存入和取出數據的時候會存在裝箱和拆箱的操作,這勢必會使性能下降。

類型安全

一個ArrayList實例化對象可以接受任何的數據類型,可是List<T>的實例化對象只能夠接受指定好的數據類型。這樣就保證了傳入數據類型的一致,這就是所謂類型安全。

List<int> list = new List<int>();
list.Add(12);
//list.Add("12") error

泛型提升代碼復用率

如果沒有泛型,那麼一個普通類類每涉及一個類型,就要重寫類。這個可能說起來比較抽象,可以看一下下面這個demo:

class A
{
    public void GetTAndTest(int value)
    {
        Console.WriteLine(value.GetType());
    }
}

類型A的GetTAndTest()的參數類型僅僅是int類型,如果想要參數為string類型,方法的主體不變,如果沒有泛型的話就只能重新寫一個方法,如果想參數類型為double呢?那麼就必須再重寫一個方法……,方法主體沒有改變,卻因為參數類型的不同而一遍又一遍的重寫,這是不合理的。所以要使用泛型,使用了泛型之後就不用再重寫這麼多次,demo如下:

class A<T>
{
    public void GetTAndTest(T value)
    {
        Console.WriteLine(value.GetType());
    }
}

有了泛型之後,當面對不同的參數類型有無限多,方法主體不變的情況時,使用泛型能夠有效的提升代碼復用率。

泛型類

泛型類封裝不特定於特定數據類型的操作。 所謂泛型類就是在創建一個類的時候在後面加一個類似於<T>的標誌。T就是該泛型類能夠接受的數據類型。

下面定義一個泛型類:

class A<T>
{
    public void GetTAndTest(T value)
    {
        Console.WriteLine(value.GetType());
        Console.WriteLine(typeof(T) == value.GetType());
        // System.Int32
        // True
    }
}

採取類似下面的方法來實例化泛型類A<T>

A<int> a = new A<int>();
a.GetTAndTest(12);

繼承規則

在將泛型類的繼承之前,先說幾個名詞:

中文 英文 形式
具體類 concrete type BaseNode
封閉構造類型 closed constructed type BaseNodeGeneric<int>
開方式構造類型 open constructed type BaseNodeGeneric<T>

泛型類可繼承自具體的封閉式構造或開放式構造基類。

下面這些都是正確泛型類繼承自基類的方式:

class BaseNode { }
class BaseNodeGeneric<T> { }

// concrete type
class NodeConcrete<T> : BaseNode { }

//closed constructed type
class NodeClosed<T> : BaseNodeGeneric<int> { }

//open constructed type
class NodeOpen<T> : BaseNodeGeneric<T> { }

非泛型類(即,具體類)可繼承自封閉式構造基類,但不可繼承自開放式構造類或類型參數,因為運行時客戶端代碼無法提供實例化基類所需的類型參數。

//正確
class Node1 : BaseNodeGeneric<int> { }

//錯誤
//class Node2 : BaseNodeGeneric<T> {}

//錯誤
//class Node3 : T {}

繼承自開放式構造類型的泛型類必須對非此繼承類共享的任何基類類型參數提供類型參數。關於泛型的描述總是十分抽象,不易於理解,這裡用不嚴謹的方式來進行解釋:在泛型類繼承的過程中,基類不能出現不包含在繼承類且無具體意義的類型參數。

class BaseNodeMultiple<T, U> { }

//正確
class Node4<T> : BaseNodeMultiple<T, int> { }

//正確
class Node5<T, U> : BaseNodeMultiple<T, U> { }

//錯誤,U既不是泛型類的泛型參數,也無具體指向某一個類
//class Node6<T> : BaseNodeMultiple<T, U> {}

泛型方法

泛型方法是通過類型參數聲明的方法,demo如下:

class FanXing
{
    public List<Object> ListObj { get; set; }
 
    /// <summary>
    /// 泛型方法
    /// </summary>
    /// <typeparam name="T">類型參數,示意任意類型</typeparam>
    /// <param name="value">類型參數的實例化對象</param>
    public void A<T>(T value)
    {
        ListObj.Add(value);
    }
 
}

下面顯式當類型參數為string時,調用泛型方法A<T>(T value)

// 實例化類
FanXing fanXing = new FanXing()
{
    ListObj = new List<object>()
};
// 調用泛型方法
fanXing.A<string>("1234");
// 打印
fanXing.ListObj.ForEach(item =>
                        {
                            Console.WriteLine(item);
                        });

還可省略類型參數,編譯器將推斷類型參數。比如fanXing.A("1234")fanXing.A<string>("1234")是等效的。

如果泛型類的類型參數和泛型方法的類型參數是同一個字母,也就是說如果定義一個具有與包含類相同的類型參數的泛型方法,則編譯器會生成警告 CS0693,請考慮為此方法的類型參數提供另一標識符。

class GenericList<T>
{
    // CS0693
    void SampleMethod<T>() { }
}

class GenericList2<T>
{
    //No warning
    void SampleMethod<U>() { }
}

泛型接口

泛型也可以用於接口:

public interface IJK<T>
{
    void One(T value);
 
    T Two();
 
    public int MyProperty { get; set; }
}

可以用和接口有相同泛型參數的類來實現接口:

/// <summary>
/// 實現泛型接口
/// </summary>
/// <typeparam name="T">泛型參數</typeparam>
public class Jk<T> : IJK<T>
{
    public int MyProperty { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

    public void One(T value)
    {
        throw new NotImplementedException();
    }

    public T Two()
    {
        throw new NotImplementedException();
    }
}

實現規則

具體類可實現封閉式構造接口。

interface IBaseInterface<T> { }

class SampleClass : IBaseInterface<string> { }

只要類形參列表提供接口所需的所有實參,泛型類即可實現泛型接口或封閉式構造接口

interface IBaseInterface1<T> { }
interface IBaseInterface2<T, U> { }

class SampleClass1<T> : IBaseInterface1<T> { }          //正確
class SampleClass2<T> : IBaseInterface2<T, string> { }  //正確

錯誤實現:

// 錯誤
public class Jk: IJK<T>
{
}
//錯誤
class SampleClass2<T> : IBaseInterface2<T, U> { }

繼承規則

泛型類的繼承規則也適用於接口。

interface IMonth<T> { }

interface IJanuary     : IMonth<int> { }  //正確
interface IFebruary<T> : IMonth<int> { }  //正確
interface IMarch<T>    : IMonth<T> { }    //正確
//interface IApril<T>  : IMonth<T, U> {}  //錯誤,U既不是派生接口IApril的泛型參數,也沒有具體指向哪一個類型

泛型約束關鍵字

在定義泛型類時,可以對代碼能夠在實例化類時用於類型參數的類型種類施加限制。如果代碼嘗試使用某個約束所不允許的類型來實例化類,則會產生編譯時錯誤。這些限制稱為約束。約束是使用 where 上下文關鍵字指定的。

new

new 約束指定泛型類聲明中的類型實參必須有公共的無參數構造函數。 也就是說若要使用 new 約束,則該類型不能為抽象類型。

使用方式如下:

class B<T> where T : new()
{
    public B()
    {
    }
    public B(T value)
    {
        Console.WriteLine(value);
    }
}

假設現在有一個接口類AbB和接口類的實現類AbBExtend,如果某泛型類使用了new約束,則AbB無法作為該泛型類的類型參數。

public void Four()
{
    // AdB為抽象類
    //B<AbB>(); error

    // AbBExtend為AdB抽象類的實現類
    new B<AbBExtend>(); // right

    // C為接口
    //new B<C>(); error
}

where

泛型定義中的 where 子句指定對用作泛型類型、方法、委託或本地函數中類型參數的參數類型的約束。 約束可指定接口、基類或要求泛型類型為引用、值或非託管類型。 它們聲明類型參數必須具備的功能。


來源:where(泛型類型約束)- C# 參考 | Microsoft Docs

說白了,where約束泛型參數是誰的派生類,是誰的實現類,即約束泛型參數來自哪裡

比如說,現在有一個抽象類AdB,想要泛型類D<T>的類型參數T必須是AdB的抽象類,可以這樣做:

/// <summary>
/// 類型參數必須來自AbB
/// </summary>
/// <typeparam name="T">抽象類AbB的派生類</typeparam>
public class D<T> where T : AbB
{

}

where的用法是where T: 約束,下表列出了5種類型的約束:

約束 說明
T:struct 類型參數必須是值類型。可以指定除 Nullable 以外的任何值類型。
T:class 類型參數必須是引用類型,包括任何類、接口、委託或數組類型。
T:new () 類型參數必須具有無參數的公共構造函數。當與其他約束一起使用時,new() 約束必須最後指定。
T:<基類名> 類型參數必須是指定的基類或派生自指定的基類。
T:<接口名稱> 類型參數必須是指定的接口或實現指定的接口。可以指定多個接口約束。約束接口也可以是泛型的。
T:U 為 T 提供的類型參數必須是為 U 提供的參數或派生自為 U 提供的參數。這稱為裸類型約束.
T:notnull 約束將類型參數限制為不可為 null 的類型。
T : default 泛型方法的override或泛型接口的實現中使用default表明沒有泛型約束,即使用 default 約束來指定派生類在派生類中沒有約束的情況下重寫方法,或指定顯式接口實現。此約束極少用到。
T : unmanaged 類型參數為「非指針、不可為 null 的非託管類型」。

來源:C# 泛型約束 xxx Where T:約束(二) – 趙青青 – 博客園 (cnblogs.com)

下面講解幾個比較不容易理解的約束:

引用類型約束

/// <summary>
/// 泛型參數必須為引用數據類型
/// </summary>
/// <typeparam name="T">引用數據類型</typeparam>
public class D<T> where T : class
{

}

裸類型約束

用作約束的泛型類型參數稱為裸類型約束。當具有自己的類型參數的成員函數需要將該參數約束為包含類型的類型參數時,裸類型約束很有用。

class List<T>
{
void Add<U>(List<U> items) where U : T {/*...*/}
}

泛型類的裸類型約束的作用非常有限,因為編譯器除了假設某個裸類型約束派生自 System.Object 以外,不會做其他任何假設。在希望強制兩個類型參數之間的繼承關係的情況下,可對泛型類使用裸類型約束。

new 組合約束

可以將其他的約束類型和new約束進行組合約束。

public class D<T> where T : class, new()
{
 
}

用途

泛型作為一個概念,是一個不指定特定類型的規範,在日常開發中的用途會因為開發者的需求不同而創造出不同的用途。在.NET BCL(基本類庫)中,常見的用途是創建集合類。

關於如何創建集合類,請參考Generic classes and methods | Microsoft Docs,筆者不否認實現一個集合類的作用,但是在筆者並不豐富的開發經驗中,極少自己創建一個集合類,原因是對集合沒有過高的性能要求,認為.NET BCL所提供的泛型集合類已經滿足了性能需求,關於對功能的需求,更多的是創造拓展方法

使用泛型類型可最大限度地提高代碼重用率、類型安全性和性能。

  • 泛型最常見的用途是創建集合類。
  • .NET 類庫包含System.Collections.Generic命名空間中的多個泛型集合類。應儘可能使用泛型集合,而不是System.Collections命名空間中的ArrayList等類。
  • 您可以創建自己的泛型接口、類、方法、事件和委託。
  • 泛型類可能受到限制,以允許訪問特定數據類型上的方法。
  • 有關泛型數據類型中使用的類型的信息可以在運行時使用反射獲得。

來源:Generic classes and methods | Microsoft Docs

LICENSE

已將所有引用其他文章之內容清楚明白地標註,其他部分皆為作者勞動成果。對作者勞動成果做以下聲明:

copyright © 2021 蘇月晟,版權所有。

知識共享許可協議
作品蘇月晟採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

Tags: