泛型(三)

風靈使發表於2019-01-12

泛型類(C# 程式設計指南)

泛型類封裝不特定於特定資料型別的操作。 泛型類最常見用法是用於連結列表、雜湊表、堆疊、佇列和樹等集合。 無論儲存資料的型別如何,新增項和從集合刪除項等操作的執行方式基本相同。

對於大多數需要集合類的方案,推薦做法是使用 .NET 類庫中提供的集合類。 有關使用這些類的詳細資訊,請參閱 .NET 中的泛型集合。

通常,建立泛型類是從現有具體類開始,然後每次逐個將型別更改為型別引數,直到泛化和可用性達到最佳平衡。 建立自己的泛型類時,需要考慮以下重要注意事項:

  • 要將哪些型別泛化為型別引數。

    通常,可引數化的型別越多,程式碼就越靈活、其可重用性就越高。 但過度泛化會造成其他開發人員難以閱讀或理解程式碼。

  • 要將何種約束(如有)應用到型別引數(請參閱型別引數的約束)。

    其中一個有用的規則是,應用最大程度的約束,同時仍可處理必須處理的型別。 例如,如果知道泛型類僅用於引用型別,則請應用類約束。 這可防止將類意外用於值型別,並使你可在 T 上使用 as 運算子和檢查 null 值。

  • 是否將泛型行為分解為基類和子類。

    因為泛型類可用作基類,所以非泛型類的相同設計注意事項在此也適用。 請參閱本主題後文有關從泛型基類繼承的規則。

  • 實現一個泛型介面還是多個泛型介面。

    例如,如果要設計用於在基於泛型的集合中建立項的類,則可能必須實現一個介面,例如 IComparable<T>,其中 T 為類的型別。

有關簡單泛型類的示例,請參閱泛型介紹。

型別引數和約束的規則對於泛型類行為具有多種含義,尤其是在繼承性和成員可訪問性方面。 應當瞭解一些術語,然後再繼續。 對於泛型類 Node<T>,客戶端程式碼可通過指定型別引數來引用類,建立封閉式構造型別 (Node<int>)。 或者,可以不指定型別引數(例如指定泛型基類時),建立開放式構造型別 (Node<T>)。 泛型類可繼承自具體的封閉式構造或開放式構造基類:

class BaseNode { }
class BaseNodeGeneric<T> { }

// 具體型別
class NodeConcrete<T> : BaseNode { }

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

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

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

//No error
class Node1 : BaseNodeGeneric<int> { }

//Generates an error
//class Node2 : BaseNodeGeneric<T> {}

//Generates an error
//class Node3 : T {}

繼承自開放式構造型別的泛型類必須對非此繼承類共享的任何基類型別引數提供型別引數,如下方程式碼所示:

class BaseNodeMultiple<T, U> { }

//No error
class Node4<T> : BaseNodeMultiple<T, int> { }

//No error
class Node5<T, U> : BaseNodeMultiple<T, U> { }

//Generates an error
//class Node6<T> : BaseNodeMultiple<T, U> {} 

繼承自開放式構造型別的泛型類必須指定作為基型別上約束超集或表示這些約束的約束:

class NodeItem<T> where T : System.IComparable<T>, new() { }
class SpecialNodeItem<T> : NodeItem<T> where T : System.IComparable<T>, new() { }

泛型型別可使用多個型別引數和約束,如下所示:

class SuperKeyType<K, V, U>
    where U : System.IComparable<U>
    where V : new()
{ }

開放式構造和封閉式構造型別可用作方法引數:


void Swap<T>(List<T> list1, List<T> list2)
{
    //code to swap items
}

void Swap(List<int> list1, List<int> list2)
{
    //code to swap items
}

如果一個泛型類實現一個介面,則該類的所有例項均可強制轉換為該介面。

泛型類是不變數。 換而言之,如果一個輸入引數指定 List<BaseClass>,且你嘗試提供 List<DerivedClass>,則會出現編譯時錯誤。


泛型介面(C# 程式設計指南)

為泛型集合類或表示集合中的項的泛型類定義介面通常很有用處。 為避免對值型別的裝箱和取消裝箱操作,泛型類的首選項使用泛型介面,例如 IComparable<T>而不是 IComparable.NET Framework 類庫定義多個泛型介面,以將其用於 System.Collections.Generic 名稱空間中的集合類。

介面被指定為型別引數上的約束時,僅可使用實現介面的型別。 如下程式碼示例演示一個派生自 GenericList<T> 類的 SortedList<T> 類。 有關詳細資訊,請參閱泛型介紹。 SortedList<T> 新增約束 where T : IComparable<T>。 這可使 SortedList<T> 中的 BubbleSort 方法在列表元素上使用泛型 CompareTo 方法。 在此示例中,列表元素是一個實現 IComparable<Person> 的簡單類 Person

//在尖括號中鍵入引數T
public class GenericList<T> : System.Collections.Generic.IEnumerable<T>
{
    protected Node head;
    protected Node current = null;

    //巢狀類在T上也是通用的
    protected class Node
    {
        public Node next;
        private T data;  //T作為私有成員資料型別

        public Node(T t)  //T用於非泛型建構函式
        {
            next = null;
            data = t;
        }

        public Node Next
        {
            get { return next; }
            set { next = value; }
        }

        public T Data  //T作為返回型別的屬性
        {
            get { return data; }
            set { data = value; }
        }
    }

    public GenericList()  //建構函式
    {
        head = null;
    }

    public void AddHead(T t)  //T作為方法引數型別
    {
        Node n = new Node(t);
        n.Next = head;
        head = n;
    }

    //迭代器的實現
    public System.Collections.Generic.IEnumerator<T> GetEnumerator()
    {
        Node current = head;
        while (current != null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }

    //IEnumerable <T>繼承自IEnumerable,因此該類必須實現GetEnumerator的泛型和非泛型版本。 在大多數情況下,非泛型方法可以簡單地呼叫泛型方法。
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

public class SortedList<T> : GenericList<T> where T : System.IComparable<T>
{
    //一種簡單的,未經優化的排序演算法,可以從最低到最高對列表元素進行排序:

    public void BubbleSort()
    {
        if (null == head || null == head.Next)
        {
            return;
        }
        bool swapped;

        do
        {
            Node previous = null;
            Node current = head;
            swapped = false;

            while (current.next != null)
            {
                //因為我們需要呼叫此方法,所以SortedList類受IEnumerable<T>約束
                if (current.Data.CompareTo(current.next.Data) > 0)
                {
                    Node tmp = current.next;
                    current.next = current.next.next;
                    tmp.next = current;

                    if (previous == null)
                    {
                        head = tmp;
                    }
                    else
                    {
                        previous.next = tmp;
                    }
                    previous = tmp;
                    swapped = true;
                }
                else
                {
                    previous = current;
                    current = current.next;
                }
            }
        } while (swapped);
    }
}

//一個簡單的類,它使用自身作為型別引數來實現IComparable<T>。這是儲存在通用列表中的物件中的常見設計模式。
public class Person : System.IComparable<Person>
{
    string name;
    int age;

    public Person(string s, int i)
    {
        name = s;
        age = i;
    }

    //這將導致列表元素按年齡值排序
    public int CompareTo(Person p)
    {
        return age - p.age;
    }

    public override string ToString()
    {
        return name + ":" + age;
    }

    // 必須實現Equals
    public bool Equals(Person p)
    {
        return (this.age == p.age);
    }
}

class Program
{
    static void Main()
    {
        //宣告並例項化一個新的通用SortedList類。
        //Person是型別引數。
        SortedList<Person> list = new SortedList<Person>();

        //建立名稱和年齡值以初始化Person物件
        string[] names = new string[] 
        { 
            "Franscoise", 
            "Bill", 
            "Li", 
            "Sandra", 
            "Gunnar", 
            "Alok", 
            "Hiroyuki", 
            "Maria", 
            "Alessandro", 
            "Raul" 
        };

        int[] ages = new int[] { 45, 19, 28, 23, 18, 9, 108, 72, 30, 35 };

        //填充列表
        for (int x = 0; x < 10; x++)
        {
            list.AddHead(new Person(names[x], ages[x]));
        }

        //列印未排序的列表
        foreach (Person p in list)
        {
            System.Console.WriteLine(p.ToString());
        }
        System.Console.WriteLine("Done with unsorted list");

        //對列表進行排序
        list.BubbleSort();

        //列印出排序列表
        foreach (Person p in list)
        {
            System.Console.WriteLine(p.ToString());
        }
        System.Console.WriteLine("Done with sorted list");
    }
}

可將多個介面指定為單個型別上的約束,如下所示:

class Stack<T> where T : System.IComparable<T>, IEnumerable<T>
{
}

一個介面可定義多個型別引數,如下所示:

interface IDictionary<K, V>
{
}

適用於類的繼承規則也適用於介面:

interface IMonth<T> { }

interface IJanuary     : IMonth<int> { }  //No error
interface IFebruary<T> : IMonth<int> { }  //No error
interface IMarch<T>    : IMonth<T> { }    //No error
//interface IApril<T>  : IMonth<T, U> {}  //Error

泛型介面如為逆變(即,僅使用自身的型別引數作為返回值),則可繼承自非泛型介面。 在 .NET Framework 類庫中,IEnumerable<T> 繼承自 IEnumerable,因為 IEnumerable<T>GetEnumerator 的返回值和 Current 屬性 Getter 中僅使用 T

具體類可實現封閉式構造介面,如下所示:

interface IBaseInterface<T> { }

class SampleClass : IBaseInterface<string> { }

只要類形參列表提供介面所需的所有實參,泛型類即可實現泛型介面或封閉式構造介面,如下所示:

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

class SampleClass1<T> : IBaseInterface1<T> { }          //No error
class SampleClass2<T> : IBaseInterface2<T, string> { }  //No error

控制方法過載的規則對泛型類、泛型結構或泛型介面內的方法一樣。 有關詳細資訊,請參閱泛型方法。


泛型方法(C# 程式設計指南)

泛型方法是通過型別引數宣告的方法,如下所示:

static void Swap<T>(ref T lhs, ref T rhs)
{
    T temp;
    temp = lhs;
    lhs = rhs;
    rhs = temp;
}

如下示例演示使用型別引數的 int 呼叫方法的一種方式:

public static void TestSwap()
{
    int a = 1;
    int b = 2;

    Swap<int>(ref a, ref b);
    System.Console.WriteLine(a + " " + b);
}

還可省略型別引數,編譯器將推斷型別引數。 如下 Swap 呼叫等效於之前的呼叫:

Swap(ref a, ref b);

型別推理的相同規則適用於靜態方法和例項方法。 編譯器可基於傳入的方法引數推斷型別引數;而無法僅根據約束或返回值推斷型別引數。 因此,型別推理不適用於不具有引數的方法。 型別推理髮生在編譯時,之後編譯器嘗試解析過載的方法簽名。 編譯器將型別推理邏輯應用於共用同一名稱的所有泛型方法。 在過載解決方案步驟中,編譯器僅包含在其上型別推理成功的泛型方法。

在泛型類中,非泛型方法可訪問類級別型別引數,如下所示:

class SampleClass<T>
{
    void Swap(ref T lhs, ref T rhs) { }
}

如果定義一個具有與包含類相同的型別引數的泛型方法,則編譯器會生成警告 CS0693,因為在該方法範圍內,向內 T 提供的引數會隱藏向外 T 提供的引數。 如果需要使用型別引數(而不是類例項化時提供的引數)呼叫泛型類方法所具備的靈活性,請考慮為此方法的型別引數提供另一識別符號,如下方示例中 GenericList2<T> 所示。

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

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

使用約束在方法中的型別引數上實現更多專用操作。 此版 Swap<T> 現名為 SwapIfGreater<T>,僅可用於實現 IComparable<T> 的型別引數。

void SwapIfGreater<T>(ref T lhs, ref T rhs) where T : System.IComparable<T>
{
    T temp;
    if (lhs.CompareTo(rhs) > 0)
    {
        temp = lhs;
        lhs = rhs;
        rhs = temp;
    }
}

泛型方法可過載在數個泛型引數上。 例如,以下方法可全部位於同一類中:

void DoWork() { }
void DoWork<T>() { }
void DoWork<T, U>() { }


相關文章