小提示
剛開始開發軟體嗎? 先從 入門 教學開始。 一旦你使用像 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.Enum、 where 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>,因為Dog由Animal衍生。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
泛型型別不限於類別。 你可以定義通用 interface、 struct、 和 record 類型。 欲了解更多關於設計通用演算法與複雜約束組合的資訊,請參閱 .NET 中的