泛型類型和方法

小提示

剛開始開發軟體嗎? 先從 入門 教學開始。 一旦你使用像 List<T> 這樣的集合時,就會遇到泛型。

有其他語言的經驗嗎? C# 通用代碼類似於 Java 中的通用代碼或 C++ 中的模板,但具備完整的執行時型別資訊且無型別擦除。 瀏覽 集合運算式協變與逆變 章節,以尋找 C# 特定的模式。

Generics 讓你能寫出能在任何類型運作的程式碼,同時保持完整型別安全。 與其為 、 int以及你需要的其他類型分別寫類別或方法string,不如寫一個版本,包含一個或多個型別參數(例如 T、 和 TKeyTValue),並在使用時指定實際型別。 編譯器會在編譯時檢查類型,所以你不需要執行時的類型轉換或承擔風險 InvalidCastException

你在日常 C# 裡會不斷遇到泛用詞。 集合、非同步回傳類型、代理式與 LINQ 皆依賴通用型別:

List<int> scores = [95, 87, 72, 91];
Dictionary<string, decimal> prices = new()
{
    ["Widget"] = 19.99m,
    ["Gadget"] = 29.99m
};
Task<string> greeting = Task.FromResult("Hello, generics!");
Func<int, bool> isPositive = n => n > 0;

Console.WriteLine($"First score: {scores[0]}");
Console.WriteLine($"Widget price: {prices["Widget"]:C}");
Console.WriteLine($"Greeting: {await greeting}");
Console.WriteLine($"Is 5 positive? {isPositive(5)}");

在每種情況下,括號內的<int>型別參數(, <string><Product>)告訴一般型別它所持有或操作的資料類型。 編譯器會強制執行型別安全。 你不可能不小心把 a string 加到 List<int>.

食用仿製藥

更常見的是,你會從.NET類別庫消費的通用類型,而不是自己創建。 以下章節展示你最常用的通用藥型。

泛型集合

名稱空間 System.Collections.Generic 提供具有型別安全的集合類別。 一定要使用這些集合,而不是像 ArrayList 這樣的非泛型集合:

// A strongly typed list of strings
List<string> names = ["Alice", "Bob", "Carol"];
names.Add("Dave");
// names.Add(42); // Compile-time error: can't add an int to List<string>

// A dictionary mapping string keys to int values
var inventory = new Dictionary<string, int>
{
    ["Apples"] = 50,
    ["Oranges"] = 30
};
inventory["Bananas"] = 25;

// A set that prevents duplicates
HashSet<int> uniqueIds = [1, 2, 3, 1, 2];
Console.WriteLine($"Unique count: {uniqueIds.Count}"); // 3

// A FIFO queue
Queue<string> tasks = new();
tasks.Enqueue("Build");
tasks.Enqueue("Test");
Console.WriteLine($"Next task: {tasks.Dequeue()}"); // Build

通用集合在執行時防止型別錯誤,因為錯誤會在編譯時出現。 這些集合也避免了價值類型的盒裝,提升了效能。

通用方法

一般方法會宣告自己的型別參數。 編譯器通常會根據你傳遞的值 推導 型別參數,因此你不需要明確指定:

static void Print<T>(T value) =>
    Console.WriteLine($"Value: {value}");

Print(42);        // Compiler infers T as int
Print("hello");   // Compiler infers T as string
Print(3.14);      // Compiler infers T as double

在調用 Print(42)中,編譯器從參數推 T 論為 int 。 你可以明確寫入 Print<int>(42) ,但型別推論能讓程式碼更乾淨。

集合表達式

集合表達式(C# 12)提供簡潔的語法來建立集合。 使用方括號代替建構呼叫或初始化器語法:

// Create a list with a collection expression
List<string> fruits = ["Apple", "Banana", "Cherry"];

// Create an array
int[] numbers = [1, 2, 3, 4, 5];

// Works with any supported collection type
IReadOnlyList<double> temperatures = [72.0, 68.5, 75.3];

Console.WriteLine($"Fruits: {string.Join(", ", fruits)}");
Console.WriteLine($"Numbers: {string.Join(", ", numbers)}");
Console.WriteLine($"Temps: {string.Join(", ", temperatures)}");

擴散算符..)將一個集合的元素串聯到另一個集合,這對於組合序列非常有用:

List<int> first = [1, 2, 3];
List<int> second = [4, 5, 6];

// Spread both lists into a new combined list
List<int> combined = [.. first, .. second];
Console.WriteLine(string.Join(", ", combined));
// Output: 1, 2, 3, 4, 5, 6

// Add extra elements alongside spreads
List<int> withExtras = [0, .. first, 99, .. second];
Console.WriteLine(string.Join(", ", withExtras));
// Output: 0, 1, 2, 3, 99, 4, 5, 6

集合表達式可處理陣列、 List<T>Span<T>ImmutableArray<T>及任何支援集合建構模式的型別。 完整語法參考請參見 集合表達式

字典初始化

你可以用索引器初始化器簡潔地初始化字典。 此語法使用方括號來設定鍵值對:

Dictionary<string, int> scores = new()
{
    ["Alice"] = 95,
    ["Bob"] = 87,
    ["Carol"] = 92
};

foreach (var (name, score) in scores)
{
    Console.WriteLine($"{name}: {score}");
}

你可以透過複製一個字典並套用覆寫來合併:

Dictionary<string, int> defaults = new()
{
    ["Timeout"] = 30,
    ["Retries"] = 3
};
Dictionary<string, int> overrides = new()
{
    ["Timeout"] = 60
};

// Merge defaults and overrides into a new dictionary
Dictionary<string, int> config = new(defaults);
foreach (var (key, value) in overrides)
{
    config[key] = value;
}

Console.WriteLine($"Timeout: {config["Timeout"]}");  // 60
Console.WriteLine($"Retries: {config["Retries"]}");   // 3

型別限制

限制 限制一般型別或方法接受哪些型別參數。 限制條件使你能夠呼叫方法或存取型別參數上的屬性,而這些在僅用object無法實現:

static T Max<T>(T a, T b) where T : IComparable<T> =>
    a.CompareTo(b) >= 0 ? a : b;

Console.WriteLine(Max(3, 7));          // 7
Console.WriteLine(Max("apple", "banana")); // banana

static T CreateDefault<T>() where T : new() => new T();

var list = CreateDefault<List<int>>(); // Creates an empty List<int>
Console.WriteLine($"Empty list count: {list.Count}"); // 0

最常見的限制條件包括:

Constraint Meaning
where T : class T 必須是參考型別
where T : struct T 必須是非空值型別
where T : new() T 必須有一個公開的無參數建構子
where T : BaseClass T 必須由 衍生 BaseClass
where T : IInterface T 必須實作 IInterface

你可以結合約束條件: where T : class, IComparable<T>, new()。 較少見的限制包括 where T : System.Enumwhere T : System.Delegate以及 where T : unmanaged 針對專門情境的限制。 完整列表請參見 型別參數限制

共變性和逆變性

協變性逆變 性描述了泛型類型在繼承中的行為。 它們決定你是否可以使用比原本指定更導出或較少導出的型別參數:

// Covariance: IEnumerable<Dog> can be used as IEnumerable<Animal>
// because IEnumerable<out T> is covariant
List<Dog> dogs = [new("Rex"), new("Buddy")];
IEnumerable<Animal> animals = dogs; // Allowed because Dog derives from Animal

foreach (var animal in animals)
{
    Console.WriteLine(animal.Name);
}

// Contravariance: Action<Animal> can be used as Action<Dog>
// because Action<in T> is contravariant
Action<Animal> printAnimal = a => Console.WriteLine($"Animal: {a.Name}");
Action<Dog> printDog = printAnimal; // Allowed because any Animal handler can handle Dog

printDog(new Dog("Spot"));
  • 協方差out T):可在預期 IEnumerable<Animal> 的地方使用 IEnumerable<Dog>,因為 DogAnimal 衍生。 out型態參數上的關鍵字可實現此功能。 協變型參數只能出現在輸出位置(返回型態)。
  • 逆變性in T):可在需要 Action<Dog> 的地方使用 Action<Animal>,因為任何能處理 Animal 的動作也可以處理 Dog。 關鍵字使 in 這一切成為可能。 逆變型參數只能出現在輸入位置(參數)。

許多內建介面與代理已經有變體:IEnumerable<out T>IReadOnlyList<out T>Func<out TResult>Action<in T>。 使用這些類型時,你會自動受益於變異性。 關於設計變體介面與代理的深入說明,請參見 協變性與逆變性

建立你自己的通用類型

你可以自己定義通用類別、結構、介面和方法。 以下範例展示了一個簡單的泛型鏈結串表作為說明。 實務上,使用 List<T> 或其他內建集合:

public class GenericList<T>
{
    private class Node(T data)
    {
        public T Data { get; set; } = data;
        public Node? Next { get; set; }
    }

    private Node? head;

    public void AddHead(T data)
    {
        var node = new Node(data) { Next = head };
        head = node;
    }

    public IEnumerator<T> GetEnumerator()
    {
        var current = head;
        while (current is not null)
        {
            yield return current.Data;
            current = current.Next;
        }
    }
}
var list = new GenericList<int>();
for (var i = 0; i < 5; i++)
{
    list.AddHead(i);
}

foreach (var item in list)
{
    Console.Write($"{item} ");
}
Console.WriteLine();
// Output: 4 3 2 1 0

泛型型別不限於類別。 你可以定義通用 interfacestruct、 和 record 類型。 欲了解更多關於設計通用演算法與複雜約束組合的資訊,請參閱 .NET 中的 Generics。

另請參閱