C# 泛型

XY·源發表於2022-04-13

C# 泛型

泛型允許開發人員建立演算法和模式,併為不同資料型別重用程式碼

定義簡單泛型類

在類名之後,需要在一對尖括號中指定型別引數

public class Stock<T>
{
    private T[] InternalItems { get; }

    public void Push(T data)
    {
		...
    }
    public void Pop() { ... }
}
設計規範

要為型別引數選擇有意義的名稱,併為名稱附加“T”字首,例如 EntityCollection<TEntity>

考慮在型別引數的名稱中指明約束

泛型約束

泛型允許為型別引數定義約束 ,強迫作為型別實參提供的型別遵守各種規則。

介面約束

public interface IPerson
{
    string Name { get; set; }
}
public class Stock<T>
    where T : IPerson
    {
        public Stock(T data)
        {
            data.Name = "張三";
        }
    }

新增了介面約束後,編譯器會確保每次使用Stock類的時候,所提供的型別引數都實現了IPerson介面。可以顯示訪問介面成員

類型別約束

public class Stock<T>
    where T : EntityBase

與介面約束相似,要求所有型別實參都能隱式轉換為EntityBase類。同樣可以顯式呼叫約束類成員

與介面約束不同的是不允許出現多個類型別約束,因為不可能從多個不同的類派生

struct/class 約束

約束泛型型別必須struct或者class型別

where T : class   //T必須是一個類(class)型別
where T : struct   //T必須是一個類struct型別

如果約束了泛型型別struct型別,可空值同樣會不符合條件

建構函式約束

並非所有物件都肯定有公共預設建構函式,編譯器不允許未約束的型別引數呼叫預設建構函式。因此,可以通過在其他所有約束之後新增new()。這就是所謂的建構函式約束,它要求實參必須有預設建構函式

public class EntityDictionary<TKey,TValue>
    where TKey : new()
    {
        TKey key = new TKey();
    }

只能對預設建構函式進行約束。不能為有參建構函式指定約束

多個約束

可為任意型別引數指定任意數量的介面型別約束,但類型別約束只能指定一個。where 後面的所有約束都以逗號分隔。如果有多個型別引數,則每個型別引數前面都要使用where關鍵字

public class EntityDictionary<TKey,TValue>
    where TKey : IComparable<TKey>,IFormattable
    where TValue : EntityBase,new()

注意,兩個where子句之間並不存在逗號

泛型方法

與泛型類一樣,泛型方法要使用泛型別引數。在泛型或非泛型型別中都能宣告泛型方法。在泛型型別中宣告,其型別引數要和泛型型別的型別引數有區別

泛型方法宣告

通過在方法名之後新增型別引數來宣告泛型方法

 public class Cat
 {
     public void Say<T>(T entity)
         where T : IComparable
         {

         }
 }

指定約束

泛型方法的型別引數可使用同泛型類的型別引數同樣的方式指定約束,可以約束型別引數必須實現某個介面,或者是某個類的子類

public class Cat
{
    public void Say<T>(EntityBase<T> entity)
        where T : IComparable
        {

        }
}

注意:如果引數型別是由泛型類組成,那麼泛型類上的約束 泛型方法也必須同樣宣告

public class EntityBase<T>
   where T : IComparable
   {
       public T Key { get; set; }
   }

泛型繼承

泛型型別引數與它們的約束都不會被派生類所繼承(因為泛型型別引數不知類的成員),但是派生類的型別引數必須具有等同或更強於基類的約束

internal class EntityDictionary<TKey,TValue>:Dictionary<TKey,TValue>
        where TKey : IComparable<TKey>
        where TValue : EntityBase<TKey>,new()
    {

    }

error:型別TKey不能用作泛型型別或方法EntityBase<T>中的型別引數“T”。沒有從TKeySystem.IComparable的裝箱轉換或型別引數轉換。

泛型方法的繼承

當虛泛型方法被繼承並重寫時或顯示實現介面方法時,約束是隱式繼承的,不可以重新宣告

public class EntityBase<T>
    where T : IComparable
    {
        public virtual void Run<T>(T t)
        {

        }
    }
public class Base:EntityBase
{
    public override void Run<T>(T t)
        where T :IComparable //error:重寫和顯式介面實現方法的約束是從基方法繼承的,因此不能直接指定這些約束,除非指定 "class" 或 "struct" 約束。
        {
            base.Run(t);
        }
}

協變性 與逆變性

協變性

假定兩個型別X和Y具有特殊關係,即每個X型別的值都能轉換成Y型別。如果I<X>I<Y> 也具有同樣的關係,那麼稱為“I<X>對I<Y>協變”

由於I<X>I<Y>並不是父子型別所以無法進行自由轉換

List<Cat> cat = new List<Cat>();
List<Animal> animal = new List<Cat>();//error:無法將型別“System.Collections.Generic.List<ConsoleApp.Cat>”隱式轉換為“System.Collections.Generic.List<ConsoleApp.Animal>”

不過,從C#4.0 起使用out型別引數修飾符可以允許協變性

public interface IEntity<out T>
{
    public T Build();
}
public class Entity<T> : IEntity<T>
{
    public T Build()
    {...}
}
internal class Program
{
    static void Main(string[] args)
    {
        IEntity<Cat> entity = new Entity<Cat>();
        IEntity<Animal> animal = new Entity<Cat>();
    }
}

協變轉換存在一些重要限制

  1. 只有泛型介面和泛型委託才可以協變。泛型類和結構永遠不是協變的
  2. 被out修飾的引數型別只能做方法返回值,不能做引數
  3. 提供給“來源”和“目標”的型別實參必須是引用型別

逆變性

協變性的反方向即被成為逆變性 ,我們可以通過in修飾符,將介面標記為逆變

class Fruit { }
class Apple: Fruit { }
class Orange : Fruit { }
interface ICompareThings<in T>
{
    bool Compare(T x, T y);
}
class FruitCompare : ICompareThings<Fruit>
{
    public bool Compare(Fruit x, Fruit y)
    {...}
}
public static void Main(string[] args)
{
    ICompareThings<Fruit> compare = new FruitCompare();
    Apple apple1 = new Apple();
    Apple apple2 = new Apple();
    Orange orange = new Orange();

    //因為Apple和Orange都是Fruit的子類所以可以進行比較
    compare.Compare(apple1, apple2);
    compare.Compare(apple1, orange);
    //將ICompareThings<Fruit>逆變成ICompareThings<Apple>型別
    ICompareThings<Apple> ac = compare;
    //此時由於泛型型別變成了Apple,因此Orange無法再參與比較
    ac.Compare(apple1, orange);//error:無法從“ConsoleApp.Orange”轉換為“ConsoleApp.Apple”

}

相關文章