小窍门
开发软件的新手? 首先开始 学习入门 教程。 一旦使用类似List<T>这样的集合,就会立即遇到泛型。
是否在其他语言中有经验? C# 泛型类似于 Java 中的泛型或 C++ 中的模板,但具有完整的运行时类型信息,并且没有类型擦除。 针对特定于 C# 的模式,浏览集合表达式和协变和逆变部分。
泛型 允许编写适用于任何类型的代码,同时保持完整类型安全性。 与其为int、string及其他每种所需类型编写单独的类或方法,不如通过使用一个或多个类型参数(例如T,或者TKey和TValue)来编写一个版本,并在使用时指定实际类型。 编译器在编译时检查类型,因此不需要运行时强制转换或风险 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>)中的类型参数指示泛型类型所持有或操作的数据类型。 编译器会强制执行类型安全性。 你不能意外地把一个 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)中,编译器从自变量推断Tint。 可以显式编写 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
最常见的约束包括:
| 约束 | 含义 |
|---|---|
where T : class |
T 必须是引用类型 |
where T : struct |
T 必须是不可为 null 的值类型 |
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>,因为任何能够处理Action<Dog>的操作也能够处理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 中的泛型。
另请参阅
- .NET 中的泛型
- 集合表达式
- 类型参数的约束
- 协变和逆变
- System.Collections.Generic