为什么值得认真学 C#

C# 是一门被低估的语言:它同时具备现代类型系统的表达能力、成熟的异步模型、可预测的垃圾回收,以及覆盖桌面、服务端、游戏与云原生的统一工具链。用一门语言就能写出 WPF 桌面程序、ASP.NET Core 接口服务和 Unity 游戏逻辑,这在主流语言里并不多见。

学 C# 的性价比在于:它的语言特性大多带着明确的设计动机,学会之后理解其他语言的同类特性会轻松很多。
核心语法骨架
  • 类型系统静态强类型,支持泛型、可空引用类型与模式匹配。
  • LINQ统一的查询语法,可以作用于集合、数据库与 XML。
  • async/await语言级异步,编译器生成状态机,不额外占线程。
  • 内存管理托管堆自动回收,非托管资源仍需显式释放。
C# 语言核心配图:抽象视窗轮廓与缺口圆环标记、代码卡片
语言特性与平台能力共同构成开发基石,两者缺一不可。

值类型与引用类型

结构体、枚举与基础数值类型是值类型,赋值时按内容复制;类与接口是引用类型,赋值只复制地址。string 是特殊的引用类型——不可变,每次拼接都会产生新对象,因此循环里做字符串累加应当改用 StringBuilder,否则会生成大量中间对象给回收器加压。

可空引用类型是现代 C# 最重要的一个开关。开启后编译器会跟踪引用是否可能为 null,并提示你在解引用前做检查。它不改变运行时行为,但能把大量空引用异常提前暴露在编译期,这也是升级旧项目时收益最大的一项改动。

public readonly record struct Point(double X, double Y);

public sealed class Order
{
    public required string Code { get; init; }
    public string? Note { get; init; }        // 明确可能为空
    public decimal Total { get; set; }
}

var order = new Order { Code = "A-1001", Total = 128.5m };
Console.WriteLine($"{order.Code}: {order.Note?.Length ?? 0}");

这段代码用了三个现代特性:record struct 自动生成相等比较,required 强制初始化关键字段,? 与 ?? 把空值处理写进表达式。它们共同的效果是——把「这个字段一定存在」和「这个字段可能没有」在类型层面区分开。

LINQ:把集合操作写成声明式

LINQ 把筛选、投影、分组、排序抽象成一组延迟执行的操作符。延迟执行意味着查询在真正遍历时才计算,因此可以在链式调用中自由组合而不会产生中间集合。它是 C# 里最能提升代码可读性的特性:一段嵌套循环加上条件判断,往往能压缩成三行表达式。

var report = orders
    .Where(o => o.Total > 0)
    .GroupBy(o => o.Code[..2])
    .Select(g => new
    {
        Prefix = g.Key,
        Count = g.Count(),
        Amount = g.Sum(o => o.Total)
    })
    .OrderByDescending(x => x.Amount)
    .ToList();

需要留意的是边界:Count() 会遍历整个序列,在已实现 ICollection 的集合上应优先用 Count 属性;多次遍历同一个延迟查询会重复计算,必要时先 ToList() 固化结果。这些差别在数据量小时无关紧要,在百万级数据上就是秒级差异。

异步编程:async 与 await

async 方法在遇到 await 时把控制权交还给调用方,等待期间不占用线程,这正是它相对多线程的优势所在——I/O 密集场景下用极少的线程支撑大量并发请求。要记住几条纪律:不要在异步方法里用 .Result 或 .Wait() 同步等待,在有同步上下文的环境下会造成死锁;async void 只用于事件处理,其他场合一律返回 Task,否则异常无法被捕获。

异步实践要点
  • 取消支持对外接口接收 CancellationToken 并向下传递。
  • 并发聚合互不依赖的任务先启动再 Task.WhenAll 统一等待。
  • ConfigureAwait库代码中用 ConfigureAwait(false) 避免不必要的上下文切换。
  • 异常处理WhenAll 只抛第一个异常,需要全部结果时逐个捕获。

内存管理与资源释放

托管堆由垃圾回收器自动管理,但它只懂托管内存。文件句柄、网络连接、数据库连接、图形资源都属于非托管资源,必须显式释放——做法是实现 IDisposable 并配合 using 声明确保离开作用域就释放,而不是等回收器在不确定的时刻处理。

回收器的分代假设是「大多数对象很快就不再使用」。据此推出的实践建议是:避免大对象频繁分配(大对象进入大对象堆,回收代价高)、减少不必要的装箱(值类型转成 object 会在堆上分配)、用 Span<T> 与 ArrayPool 处理大块缓冲区以避免反复申请。这些优化在服务端吞吐场景下效果显著。