类型系统:值类型 vs 引用类型
C# 的类型系统分为两大类:值类型存储在栈上(或内联在包含对象中),赋值时复制整个值;引用类型在堆上分配,变量存储的是指向堆的引用,赋值时复制引用。
| 维度 | 值类型(struct / enum) | 引用类型(class / interface / delegate) |
|---|---|---|
| 存储位置 | 栈(局部变量)/ 内联堆 | 堆(托管堆) |
| 赋值语义 | 深拷贝(复制整个数据) | 浅拷贝(复制引用地址) |
| 默认值 | 0 / false / \0(按字段) | null |
| 继承 | 不能继承类(可实现接口) | 支持单继承,可实现多接口 |
| GC 压力 | 无(不在托管堆上) | 有(需 GC 回收) |
| 典型场景 | 小型数据(Point、Color) | 大型对象、有行为的实体 |
struct vs class vs record
// struct — 值类型,适合小型不可变数据
public struct Vector2
{
public float X, Y;
public float Length => MathF.Sqrt(X * X + Y * Y);
}
// class — 引用类型,有行为、有状态
public class Order
{
public Guid Id { get; init; } = Guid.NewGuid();
public List<OrderItem> Items { get; } = [];
public void AddItem(OrderItem item) => Items.Add(item);
}
// record — 引用类型,值语义,不可变 DTO
public record ProductDto(int Id, string Name, decimal Price);
// readonly record struct — 值类型 + 值语义 + 零堆分配
public readonly record struct Money(decimal Amount, string Currency);
泛型:约束与变体
泛型允许编写适用于多种类型的代码,同时保持类型安全和高性能(避免装箱)。
常用泛型约束
// where T : class — T 必须是引用类型
// where T : struct — T 必须是值类型(不可为 null)
// where T : new() — T 必须有无参构造函数
// where T : IComparable<T> — T 必须实现接口
// where T : notnull — T 不可为 null(值类型或不可空引用类型)
public class Repository<T> where T : class, IEntity, new()
{
private readonly List<T> _store = [];
public void Add(T entity) => _store.Add(entity);
public T CreateNew() => new T(); // 需要 new() 约束
}
// 泛型方法:最小值(需要 IComparable 约束)
public static T Min<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) <= 0 ? a : b;
协变(out)与逆变(in)
协变(Covariance)— out 关键字
接口/委托的类型参数只用于输出(返回值),则可以将
IEnumerable<Cat> 赋值给 IEnumerable<Animal>。IEnumerable<out T> 就是协变接口——因为 T 只在 GetEnumerator 的 Current 属性中被"读出"。逆变(Contravariance)— in 关键字
类型参数只用于输入(方法参数),则可以将
Action<Animal> 赋值给 Action<Cat>。IComparer<in T> 就是逆变接口——一个能比较 Animal 的比较器,也能用来比较 Cat。// 协变示例:IEnumerable<out T>
IEnumerable<Cat> cats = new List<Cat> { new() };
IEnumerable<Animal> animals = cats; // 合法,Cat is-a Animal
// 逆变示例:Action<in T>
Action<Animal> feedAnimal = a => Console.WriteLine($"喂 {a.Name}");
Action<Cat> feedCat = feedAnimal; // 合法
feedCat(new Cat { Name = "小花" });
LINQ:延迟执行与常用操作符
LINQ(Language Integrated Query)是 C# 最强大的特性之一,允许以声明式风格查询任意数据源(集合、数据库、XML、JSON 等)。
Warning — 延迟执行陷阱
LINQ 查询默认是懒惰的(Lazy):调用
Where/Select 等方法时并不执行查询,只有在枚举时(foreach、ToList()、Count() 等)才真正运行。若数据源在枚举前已发生变化,结果可能出乎意料。对于多次枚举的场景,应先调用 ToList() 物化结果。
var orders = new List<Order> { /* ... */ };
// 方法语法(推荐)
var result = orders
.Where(o => o.Status == "paid")
.OrderByDescending(o => o.CreatedAt)
.Take(10)
.Select(o => new { o.Id, o.Total });
// 分组聚合
var monthly = orders
.GroupBy(o => o.CreatedAt.Month)
.Select(g => new
{
Month = g.Key,
TotalSales = g.Sum(o => o.Total),
Count = g.Count()
});
// .NET 9 新增方法
var countByStatus = orders.CountBy(o => o.Status);
var totalByUser = orders.AggregateBy(
keySelector: o => o.UserId,
seed: 0m,
func: (acc, o) => acc + o.Total
);
// 带索引枚举
foreach (var (i, order) in orders.Index())
Console.WriteLine($"[{i}] {order.Id}");
模式匹配(Pattern Matching)
C# 的模式匹配从 C# 7 开始引入,经过多个版本迭代,已成为非常强大的功能,可以替代大量 if-else 和类型转换代码。
// 类型模式(is + 变量)
object obj = "hello";
if (obj is string s && s.Length > 3)
Console.WriteLine(s.ToUpper());
// switch 表达式(C# 8+)
string GetDiscount(Customer c) => c switch
{
{ IsPremium: true, YearsActive: >= 5 } => "20%",
{ IsPremium: true } => "10%",
{ YearsActive: >= 2 } => "5%",
_ => "0%"
};
// 位置模式(解构)
string Quadrant(Point p) => p switch
{
(0, 0) => "原点",
(var x, 0) when x > 0 => "正 X 轴",
(> 0, > 0) => "第一象限",
(< 0, > 0) => "第二象限",
(< 0, < 0) => "第三象限",
(> 0, < 0) => "第四象限",
_ => "坐标轴上"
};
// 列表模式(C# 11+)
string DescribeList(int[] nums) => nums switch
{
[] => "空列表",
[var x] => $"只有一个元素: {x}",
[var x, var y] => $"两个元素: {x}, {y}",
[1, 2, ..] => "以 1,2 开头",
_ => $"多个元素,共 {nums.Length} 个"
};
可空引用类型(Nullable Reference Types)
C# 8 引入了可空引用类型(NRT),通过静态分析在编译时警告潜在的 NullReferenceException。在 .csproj 中设置 <Nullable>enable</Nullable> 后,所有引用类型默认不可为 null,必须显式添加 ? 表示可为 null。
// 不可空(默认):编译器确保不为 null
string name = "Alice"; // 不可为 null
// name = null; // 警告!
// 可空:必须在使用前检查
string? maybeNull = null;
int len = maybeNull?.Length ?? 0; // ?. 空条件运算符
// 空合并赋值 ??= (C# 8)
string? config = null;
config ??= "default_value"; // 仅当 null 时赋值
// null! 抑制警告(确信不为 null 时使用,谨慎)
string definitelyNotNull = GetMaybeNull()!;
// 模式匹配检查 null
if (maybeNull is not null)
Console.WriteLine(maybeNull.Length); // 已检查,无警告
Span<T> 与 Memory<T>:零分配内存操作
Span<T>
对连续内存区域的栈上引用,不拥有内存所有权,不产生堆分配。可以包装数组的切片、栈分配的内存(stackalloc)或非托管内存。由于是 ref struct,只能在同步方法中使用,不能存储在字段中、不能跨越 await 边界。
Memory<T>
Span<T> 的"堆版本",可以存储在字段中,可以跨 await 使用。适合需要在异步上下文中操作内存切片的场景。通过
.Span 属性获取 Span<T>。ArrayPool<T>
数组对象池,避免频繁分配和 GC 回收大数组。通过
ArrayPool<byte>.Shared.Rent(size) 借用,使用后 Return() 归还。与 Span<T> 配合是高性能 .NET 代码的标准模式。// 零分配字符串解析
static int ParseFirstNumber(ReadOnlySpan<char> text)
{
int comma = text.IndexOf(',');
if (comma < 0) return int.Parse(text);
return int.Parse(text[..comma]); // 切片,无新字符串分配
}
ParseFirstNumber("42,100,200"); // 返回 42,无堆分配
// stackalloc + Span(栈上分配小缓冲区)
Span<byte> buffer = stackalloc byte[256];
int bytesRead = ReadData(buffer);
ProcessData(buffer[..bytesRead]);
// ArrayPool 避免大数组分配
byte[] rented = ArrayPool<byte>.Shared.Rent(4096);
try
{
Span<byte> span = rented.AsSpan(0, 4096);
// 使用 span...
}
finally
{
ArrayPool<byte>.Shared.Return(rented);
}
本章小结
C# 类型系统的 struct/class/record 三角组合让你可以精确控制值语义、内存分配和可变性。泛型约束与协变/逆变提供了类型安全的多态。LINQ 的延迟执行是强大但需谨慎的特性。可空引用类型在编译期消灭 NullReferenceException。Span<T> 是高性能代码的利器——零分配内存切片让解析和缓冲处理无需产生任何 GC 压力。下一章深入异步编程:async/await 状态机与并发控制。