Skip to content

截至2024年12月。以下是C#各版本主要新特性的设计动机解决的痛点分析,按版本从新到旧组织:


C# 13 (.NET 9) - 最新

1. 增强的 params 集合支持

为什么:传统 params 只允许数组,限制了性能优化空间。 解决痛点:高频调用方法时,数组分配造成GC压力。

csharp
// 之前:params只能接受数组,调用时必然分配内存
void Print(params int[] numbers) { }

// C# 13:支持Span/List,调用方可避免堆分配
void Print(params ReadOnlySpan<int> numbers) { }

// 调用:Span直接指向栈上或数组内存,零分配
int[] data = [1,2,3];
Print(data.AsSpan());

2. 新型锁 System.Threading.Lock

为什么:传统 lock 在异步场景易死锁,且无法配置超时。 解决痛点:异步编程中需要更灵活的同步原语。

csharp
private readonly Lock _lock = new();

async Task UpdateAsync()
{
    // 之前:await在lock内会导致死锁风险
    // lock(_obj) { await Task.Delay(100); } // ❌ 危险
    
    // C# 13:支持异步获取锁,避免死锁
    using (await _lock.AcquireAsync())
    {
        await Task.Delay(100); // ✅ 安全
    }
}

C# 12 (.NET 8)

3. 主构造函数(扩展到所有 class/struct)

为什么record 的主构造函数太香,但普通类还得写样板代码。 解决痛点:减少构造函数和字段声明的重复劳动。

csharp
// 之前:普通类需要手动声明字段和构造函数
class Circle
{
    private readonly double _radius;
    public Circle(double radius) => _radius = radius;
    public double Area => Math.PI * _radius * _radius;
}

// C# 12:主构造函数参数自动成为类成员
class Circle(double radius)
{
    public double Area => Math.PI * radius * radius; // 参数可在全类使用
}

4. 集合表达式

为什么:数组、列表、Span初始化语法不统一,合并集合啰嗦。 解决痛点:提供统一、简洁的集合创建语法。

csharp
// 之前:多种语法混用
int[] arr = new[] { 1, 2, 3 };
List<int> list = new List<int> { 1, 2, 3 };
Span<int> span = stackalloc[] { 1, 2, 3 };

// C# 12:统一语法 + 展开运算
int[] arr = [1, 2, 3];
List<int> list = [1, 2, 3];
int[] combined = [..arr, 4, ..list]; // [1,2,3,4,1,2,3]

5. 内联数组(Inline Arrays)

为什么:高性能场景需要固定大小数组,但结构体嵌套数组性能差。 解决痛点:提供零开销的固定大小数组。

csharp
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer
{
    private int _element0; // 编译器展开为10个连续字段
}

Buffer buf = new();
buf[^1] = 42; // 支持索引,无堆分配
// 内存布局:struct内直接包含10个int,无引用跳转

C# 11 (.NET 7)

6. 原始字符串字面量(Raw String Literals)

为什么:JSON/SQL等多行字符串需要大量转义,可读性差。 解决痛点:消除转义符,保持原始格式。

csharp
// 之前:JSON里的引号需要转义,惨不忍睹
string json = "{\"name\":\"Alice\",\"age\":30}";

// C# 11:使用"""包裹,字符串内容原样输出
string json = """
{
    "name": "Alice",
    "age": 30
}
""";
// 编译器自动处理缩进和引号,无需转义

7. 列表模式匹配

为什么:检查数组/列表的特定结构需要写大量循环和条件。 解决痛点:声明式匹配集合结构。

csharp
// 之前:检查数组是否以1,2开头并以10结尾
bool Check(int[] arr)
{
    if (arr.Length < 3) return false;
    if (arr[0] != 1 || arr[1] != 2) return false;
    return arr[^1] == 10;
}

// C# 11:模式匹配
bool Check(int[] arr) => arr is [1, 2, .., 10];

// 还能捕获切片
if (arr is [1, .. var middle, 10])
{
    Console.WriteLine($"Middle: {string.Join(",", middle)}");
}

8. required 成员

为什么:对象初始化器允许漏设关键属性,导致运行时错误。 解决痛点:编译时强制初始化必备成员。

csharp
// 之前:无法强制用户必须初始化某些属性
public class Person
{
    public string Name { get; init; } // 可省略,导致对象不完整
}

// C# 11:标记为required,编译器强制初始化
public class Person
{
    public required string Name { get; init; }
}

var p = new Person(); // ❌ 编译错误:未初始化Name
var p = new Person { Name = "Alice" }; // ✅ 正确

C# 10 (.NET 6)

9. 全局 Using

为什么:每个文件重复相同的 using 声明,样板代码冗余。 解决痛点:项目级统一命名空间引用。

csharp
// GlobalUsings.cs(只需一个文件)
global using System.Collections.Generic;
global using System.Linq;

// 其他文件无需再写上述using,代码更简洁

10. 文件范围的命名空间

为什么:单文件命名空间必须用大括号,增加缩进层级。 解决痛点:减少缩进,提升可读性。

csharp
// 之前:namespace增加一层缩进
namespace MyApp
{
    class MyClass { } // 多一层缩进
}

// C# 10:文件范围命名空间
namespace MyApp; // 直接作用于整个文件

class MyClass { } // 少一层缩进

C# 9 (.NET 5)

11. 记录(Record)类型

为什么:DTO/Data Model需要手动实现相等性、ToString、不可变性等功能。 解决痛点:一键生成不可变数据类型。

csharp
// 之前:手写DTO需要50行代码实现Equals、GetHashCode、ToString等
public class Person : IEquatable<Person>
{
    public string Name { get; }
    public int Age { get; }
    // 手动实现相等性... 太啰嗦
}

// C# 9:一行搞定
public record Person(string Name, int Age); // 自动生成相等性、ToString、With表达式

var p1 = new Person("Alice", 30);
var p2 = p1 with { Age = 31 }; // 非破坏性修改

12. Init-Only 属性

为什么:对象初始化器允许后续修改,无法保证不可变性。 解决痛点:初始化后不再可写。

csharp
// 之前:初始化后仍可修改
public class Person { public string Name { get; set; } }
var p = new Person { Name = "Alice" };
p.Name = "Bob"; // ❌ 意外修改

// C# 9:init后不可再赋值
public class Person { public string Name { get; init; } }
var p = new Person { Name = "Alice" };
// p.Name = "Bob"; // ❌ 编译错误

C# 8 (.NET Core 3.0)

13. 可空引用类型(Nullable Reference Types)

为什么:空引用异常是历史上最常见的Bug,没有编译时检查。 解决痛点:编译时防空引用异常。

csharp
#nullable enable

// 之前:所有引用类型默认可空,运行时才能发现错误
string s = null; // 不报错,但后续s.Length会崩溃

// C# 8:声明意图,编译器帮你检查
string? s = null; // 明确可为空
Console.WriteLine(s.Length); // ⚠️ 编译警告:可能为空

string s2 = ""; // 不可空,赋值null会警告

14. 异步流(IAsyncEnumerable)

为什么IEnumerable<T> 无法支持异步数据流(如网络数据分页)。 解决痛点:异步生成数据序列。

csharp
// 之前:无法异步yield return
async Task<IEnumerable<int>> GetDataAsync()
{
    await Task.Delay(100); // 无法边下载边返回数据
    return new[] { 1, 2, 3 };
}

// C# 8:支持异步生成
async IAsyncEnumerable<int> GetDataAsync()
{
    for (int i = 0; i < 3; i++)
    {
        await Task.Delay(100); // 每生成一个元素前都可await
        yield return i;        // 边下载边返回
    }
}

// 调用:await foreach
await foreach (var item in GetDataAsync())
{
    Console.WriteLine(item);
}

15. 范围和索引(Ranges & Indices)

为什么:取数组子集需要写 Array.Copy 或手动计算索引,易出错。 解决痛点:提供直观语法访问集合片段。

csharp
// 之前:取最后3个元素
var arr = new[] { 0, 1, 2, 3, 4, 5 };
var last3 = arr.Skip(arr.Length - 3).ToArray(); // 啰嗦且分配内存

// C# 8:语法糖
var last = arr[^1];    // 最后一个元素
var slice = arr[^3..]; // 最后3个元素,零分配返回Span

设计哲学总结

C# 新特性的演进始终围绕三个核心目标:

  1. 提升开发者生产力:减少样板代码(主构造函数、记录、全局using)
  2. 增强类型安全:编译期捕获更多错误(可空引用类型、required成员)
  3. 兼顾性能与抽象:零分配内存访问(Span/Memory)、高性能集合(内联数组)

这些特性共同让 C# 在保持高级语言易用性的同时,能胜任系统级高性能编程。


更早版本(简要)

  • C# 7.0-7.3 (2017-2019):元组、模式匹配、ref 局部变量、ValueTask
  • C# 6 (2015):字符串内插、表达式体成员、nameof、null 传播运算符
  • C# 5 (2012):async/await
  • C# 4 (2010):动态类型、可选参数、命名参数
  • C# 3 (2007):LINQ、Lambda 表达式、匿名类型
  • C# 2 (2005):泛型、迭代器、yieldnull 可空类型
  • C# 1 (2002):基础面向对象特性

使用建议

  • 新项目:优先使用 C# 12-13 特性(主构造函数、集合表达式)
  • 异步编程:使用 IAsyncEnumerable (C# 8) 和 Lock (C# 13)
  • 高性能:利用 Span<T>InlineArray (C# 12)
  • 数据模型:使用记录 (C# 9) 和 required 属性 (C# 11)