【科普】.NET6 泛型

sogeisetsu發表於2021-12-23

本文內容來自我寫的開源電子書《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 國際許可協議進行許可。

相關文章