Chapter 02

C# 语言核心

深入类型系统、泛型约束、LINQ 惰性求值、模式匹配、可空引用类型与 Span<T> 零分配操作。

类型系统:值类型 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 等方法时并不执行查询,只有在枚举时(foreachToList()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 状态机与并发控制。