衔接上一篇文章:text
面试如闯关,装备要精良。
本文核心要义,是让你能够深刻理解C#各个版本的技术要点,从而在面试时,能够游刃有余地回答面试官的各种刁钻问题。同时,当你对技术要点有深刻理解后,有一个全局的把控,从而在面试和工作开发中,能够更好地选择技术特性,给出更加优雅的方案。
本文主要内容点
C# 8.0 - 可空引用类型、异步流和默认接口方法
C# 9.0 - 记录和模式匹配增强
C# 10.0 - 文件范围命名空间声明和全局 Using 指令
C# 11.0 - 泛型属性和 Required 修饰符
C# 12.0 - 主构造函数、集合字面量、内联数组和实验性状态
C# 13.0 - 字段关键字、新的锁类型和参数集合
C# 14.0 - 部分构造函数和事件、Null 条件赋值
C# 8.0 主要更新要点
C# 8.0 于 2019 年随 .NET Core 3.0 和 Visual Studio 2019 发布。
1. 可空引用类型 (Nullable Reference Types)
引入原因和好处:
空引用被其发明者称为"十亿美元的错误"。可空引用类型是C#在类型安全方面的一次重大改进,通过静态分析帮助开发人员在编译时发现潜在的空引用异常。
解决的问题:
- 在编译时检测潜在的NullReferenceException
- 明确表达API的意图(哪些参数/返回值可以为null)
- 减少运行时的空引用异常
- 提高代码质量和可靠性
示例代码:
// 启用可空引用类型后
string name; // 不可为空
string? nickname; // 可以为 null
name = null; // 编译器警告
nickname = null; // 正常
// 安全访问
if (nickname != null)
{
int length = nickname.Length; // 安全访问
}
// 使用空合并运算符
int length = nickname?.Length ?? 0;
// 在API设计中的应用
public class Person
{
public string FirstName { get; } // 不可为空
public string? MiddleName { get; } // 可以为空
public string LastName { get; } // 不可为空
// 构造函数中确保非空属性被正确初始化
public Person(string firstName, string lastName, string? middleName = null)
{
FirstName = firstName ?? throw new ArgumentNullException(nameof(firstName));
LastName = lastName ?? throw new ArgumentNullException(nameof(lastName));
MiddleName = middleName; // 可以为 null
}
}
2. 异步流 (Async Streams)
引入原因和好处:
传统的迭代模式(IEnumerable)是同步的,无法很好地处理需要异步获取的数据源。异步流结合了异步编程和迭代模式的优点,适用于需要逐步异步获取数据的场景。
解决的问题:
- 支持异步数据源的迭代处理
- 提高处理大量数据时的内存效率
- 在数据到达时逐步处理,而不是等待所有数据准备就绪
- 支持实时数据流处理
示例代码:
// 异步返回多个值
public async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(1000);
yield return i;
}
}
// 使用异步流
public async Task ProcessNumbersAsync()
{
await foreach (var number in GetNumbersAsync())
{
Console.WriteLine(number);
}
}
// 实际应用场景:从API分页获取数据
public async IAsyncEnumerable<Product> GetProductsAsync()
{
int page = 1;
bool hasMore = true;
while (hasMore)
{
var response = await _httpClient.GetAsync($"/api/products?page={page}");
var result = await response.Content.ReadFromJsonAsync<PagedResult<Product>>();
foreach (var product in result.Items)
{
yield return product;
}
hasMore = result.HasMore;
page++;
}
}
3. 默认接口方法 (Default Interface Methods)
引入原因和好处:
在接口中添加新方法会破坏现有实现。默认接口方法允许在接口中提供方法的默认实现,从而在不破坏现有实现的情况下扩展接口。
解决的问题:
- 允许在不破坏现有实现的情况下扩展接口
- 提供接口方法的默认实现
- 支持接口的演化
示例代码:
public interface ICalculator
{
int Add(int x, int y);
// 默认实现
int Multiply(int x, int y) => x * y;
// 带有私有辅助方法的默认实现
int Power(int baseValue, int exponent)
{
if (exponent == 0) return 1;
if (exponent == 1) return baseValue;
return PowerHelper(baseValue, exponent);
}
// 接口中的私有方法
private int PowerHelper(int baseValue, int exponent)
{
int result = 1;
for (int i = 0; i < exponent; i++)
{
result = Multiply(result, baseValue);
}
return result;
}
}
// 实现类可以选择是否重写默认方法
public class BasicCalculator : ICalculator
{
public int Add(int x, int y) => x + y;
// 可以选择使用默认实现
// public int Multiply(int x, int y) => x * y;
// 也可以提供自己的实现
public int Multiply(int x, int y) => x * y * 2; // 自定义实现
}
C# 9.0 主要更新要点
C# 9.0 于 2020 年随 .NET 5 发布。
1. 记录 (Records)
引入原因和好处:
在数据传输对象(DTO)和不可变数据类型场景中,开发人员需要编写大量样板代码来实现相等比较、哈希码计算和字符串表示。记录类型通过简洁的语法自动生成这些常用功能。
解决的问题:
- 大幅减少不可变数据类型的样板代码
- 提供基于值的相等比较而不是引用比较
- 自动实现常用的数据方法(ToString、Equals、GetHashCode)
- 支持非破坏性修改(with表达式)
示例代码:
// 旧方式定义不可变类
public class Person
{
public string FirstName { get; }
public string LastName { get; }
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public override bool Equals(object obj) => // 复杂的相等比较实现
public override int GetHashCode() => // 哈希码实现
public override string ToString() => $"{FirstName} {LastName}";
}
// 新方式使用记录
public record Person(string FirstName, string LastName);
// 使用
var person1 = new Person("张", "三");
var person2 = new Person("张", "三");
bool equal = person1 == person2; // true - 基于值的相等比较
var person3 = person1 with { FirstName = "李" }; // 非破坏性修改
// 记录的继承
public record Student(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
// with 表达式的应用
var student1 = new Student("王", "五", 90);
var student2 = student1 with { Grade = 95 }; // 只修改 Grade
2. 模式匹配增强 (Pattern Matching Enhancements)
引入原因和好处:
C# 8.0 引入了模式匹配,但功能有限。C# 9.0 进一步增强了模式匹配能力,使其更加强大和灵活。
解决的问题:
- 提供更丰富的模式匹配选项
- 简化复杂对象的解构和匹配
- 提高代码的表达能力
示例代码:
// 类型模式
public static decimal CalculateArea(object shape)
{
return shape switch
{
Rectangle r => r.Width * r.Height,
Circle c => Math.PI * c.Radius * c.Radius,
Triangle t => 0.5 * t.Base * t.Height,
null => throw new ArgumentNullException(nameof(shape)),
_ => throw new ArgumentException("未知的形状类型", nameof(shape))
};
}
// 关系模式
public static string GetAgeGroup(int age) => age switch
{
< 18 => "未成年",
>= 18 and < 60 => "成年人",
>= 60 => "老年人",
_ => "未知"
};
// 逻辑模式
public static string AnalyzeValue(int value) => value switch
{
> 0 and <= 100 => "正常范围",
> 100 or < 0 => "超出范围",
0 => "零值",
_ => "其他"
};
// 属性模式
public static bool IsUsAdult(Person person) => person switch
{
{ Age: >= 18, Country: "US" } => true,
_ => false
};
C# 10.0 主要更新要点
C# 10.0 于 2021 年随 .NET 6 发布。
1. 文件范围命名空间声明 (File-scoped namespace declaration)
引入原因和好处:
传统的命名空间声明需要使用花括号,导致额外的缩进层级,降低了代码的可读性。文件范围命名空间声明简化了语法,减少了不必要的缩进。
解决的问题:
- 减少代码文件中的嵌套层级
- 提高代码的可读性和整洁性
- 减少花括号的使用,使代码更简洁
- 与现代代码风格保持一致
示例代码:
// 旧方式
namespace MyApplication.Models
{
public class Person
{
// 类内容
public string Name { get; set; }
public void DoWork()
{
// 方法内容
}
}
}
// 新方式
namespace MyApplication.Models;
public class Person
{
public string Name { get; set; }
public void DoWork()
{
// 方法内容
}
}
2. 全局 Using 指令 (Global using directives)
引入原因和好处:
在大型项目中,许多using指令在多个文件中重复出现,增加了代码冗余。全局using指令允许在一处定义常用的命名空间,减少重复代码。
解决的问题:
- 减少项目中重复的using指令
- 简化单个代码文件的头部声明
- 更好地管理项目级别的命名空间引用
- 提高代码的一致性和可维护性
示例代码:
// 在 Program.cs 或单独的 Imports.cs 文件中
global using System.Text;
global using System.Net.Http;
global using System.Text.Json;
// 在其他文件中,无需再次声明 using System.Text;
// 可以使用别名
global using static System.Console;
global using Env = System.Environment;
// 在其他文件中直接使用
WriteLine("Hello World"); // 来自 System.Console
var machineName = Env.MachineName; // 来自 System.Environment
C# 11.0 主要更新要点
C# 11.0 于 2022 年随 .NET 7 发布。
1. 泛型属性 (Generic attributes)
引入原因和好处:
在C# 11之前,属性类不能接受泛型类型参数,这限制了属性的灵活性和复用性。泛型属性允许创建更通用、更灵活的属性类。
解决的问题:
- 提高属性类的灵活性和复用性
- 减少为不同类型的值创建重复的属性类
- 提供类型安全的属性参数传递
- 支持更复杂的元数据场景
示例代码:
public class GenericAttribute<T> : Attribute
{
public T Value { get; }
public GenericAttribute(T value)
{
Value = value;
}
}
// 使用泛型属性
[GenericAttribute<string>("示例值")]
[GenericAttribute<int>(42)]
public class MyClass
{
// 类内容
}
// 在运行时获取泛型属性
var attributes = typeof(MyClass).GetCustomAttributes();
foreach (var attr in attributes)
{
if (attr is GenericAttribute<string> stringAttr)
{
Console.WriteLine($"字符串值: {stringAttr.Value}");
}
else if (attr is GenericAttribute<int> intAttr)
{
Console.WriteLine($"整数值: {intAttr.Value}");
}
}
2. Required 修饰符
引入原因和好处:
对象初始化时遗漏必需属性是常见的错误来源。Required修饰符通过编译时检查确保对象初始化时设置所有必需的属性。
解决的问题:
- 在编译时强制检查必需属性的初始化
- 减少运行时因缺少必需属性导致的错误
- 明确表达对象初始化的约束条件
- 提高API的可用性和健壮性
示例代码:
public class Person
{
public required string FirstName { get; set; }
public required string LastName { get; set; }
public int Age { get; set; } // 非必需
// 构造函数中也可以初始化 required 属性
public Person(string firstName)
{
FirstName = firstName;
// LastName 仍然需要在对象初始化器中设置
}
}
// 使用
var person = new Person
{
FirstName = "张",
LastName = "三"
// Age 不是必需的,可以不设置
};
// 编译错误示例
// var person2 = new Person(); // 编译错误:缺少 FirstName 和 LastName
// var person3 = new Person { FirstName = "李" }; // 编译错误:缺少 LastName
C# 12.0 主要更新要点
C# 12.0 于 2023 年随 .NET 8 发布。
1. 主构造函数 (Primary Constructors)
引入原因和好处:
在类和结构体中,构造函数经常需要初始化只读字段或属性。主构造函数允许在类型声明中直接指定构造函数参数,简化了类的定义。
解决的问题:
- 减少样板代码,特别是对于简单的数据类
- 提高代码的可读性和简洁性
- 简化字段和属性的初始化过程
示例代码:
// 旧方式
public class Person
{
private readonly string firstName;
private readonly string lastName;
private readonly int age;
public Person(string firstName, string lastName, int age)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public string FirstName => firstName;
public string LastName => lastName;
public int Age => age;
}
// 新方式 - 使用主构造函数
public class Person(string firstName, string lastName, int age)
{
public string FirstName { get; } = firstName;
public string LastName { get; } = lastName;
public int Age { get; } = age;
public string FullName => $"{firstName} {lastName}";
}
// 结构体也可以使用主构造函数
public struct Point(int x, int y)
{
public int X { get; } = x;
public int Y { get; } = y;
}
2. 集合字面量 (Collection Literals)
引入原因和好处:
创建集合对象并初始化其内容是一个常见操作,但语法相对冗长。集合字面量提供了一种更简洁的语法来创建和初始化集合。
解决的问题:
- 简化集合的创建和初始化语法
- 提高代码的可读性和简洁性
- 统一不同类型集合的初始化语法
示例代码:
// 旧方式
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var names = new string[] { "张三", "李四", "王五" };
var dictionary = new Dictionary<string, int>
{
{ "one", 1 },
{ "two", 2 },
{ "three", 3 }
};
// 新方式 - 使用集合字面量
var numbers = [1, 2, 3, 4, 5];
var names = ["张三", "李四", "王五"];
var dictionary = new Dictionary<string, int> { ["one"] = 1, ["two"] = 2, ["three"] = 3 };
// 可以直接使用接口类型
IEnumerable<int> enumerable = [1, 2, 3];
ICollection<string> collection = ["a", "b", "c"];
IList<int> list = [1, 2, 3];
3. 内联数组 (Inline Arrays)
引入原因和好处:
System.Runtime.CompilerServices.InlineArrayAttribute 允许创建固定大小的结构体数组,这些数组存储在栈上而不是堆上,提高了性能。
解决的问题:
- 提供高性能的固定大小数组
- 减少堆内存分配
- 提高数值计算和游戏开发等场景的性能
示例代码:
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer
{
private int _element0;
}
// 使用
Buffer buffer = new();
for (int i = 0; i < 10; i++)
{
buffer[i] = i * i;
}
foreach (var item in buffer)
{
Console.WriteLine(item);
}
4. 实验性状态 (Experimental Attribute)
引入原因和好处:
允许开发者标记某些API为实验性,使用者需要添加特定特性才能使用这些API,避免在API稳定前被广泛使用。
解决的问题:
- 允许发布实验性功能
- 防止实验性API被意外广泛使用
- 提供明确的实验性标记
示例代码:
[Experimental("EXPERIMENTAL001")]
public void ExperimentalMethod()
{
// 实验性方法实现
}
// 使用实验性方法需要添加相应的特性
[Experimental("EXPERIMENTAL001", UrlFormat = "https://learn.microsoft.com/dotnet/csharp/language-reference/compiler-messages/experimental")]
public class MyClass
{
public void UseExperimentalMethod()
{
ExperimentalMethod();
}
}
C# 13.0 主要更新要点
C# 13.0 于 2024 年随 .NET 9 发布。
1. 字段关键字 (Field Keyword)
引入原因和好处:
在属性访问器中,有时需要访问编译器生成的后备字段。以前需要手动声明字段,现在可以使用[field]关键字直接访问。
解决的问题:
- 简化属性中后备字段的访问
- 减少手动声明字段的样板代码
- 提高代码的可读性和简洁性
示例代码:
// 旧方式
private string _name;
public string Name
{
get => _name;
set => _name = value ?? throw new ArgumentNullException(nameof(value));
}
// 新方式 - 使用 field 关键字
public string Name
{
get => field;
set => field = value ?? throw new ArgumentNullException(nameof(value));
}
2. 新的锁类型 (New Lock Type)
引入原因和好处:
引入了新的 System.Threading.Lock 类型,提供了更高效和安全的线程同步机制。
解决的问题:
- 提供更高效的线程同步机制
- 简化锁的使用方式
- 提高多线程程序的性能
示例代码:
// 使用新的 Lock 类型
private readonly Lock _lock = new();
public void ThreadSafeMethod()
{
lock (_lock)
{
// 线程安全的代码
}
}
3. 参数集合 (Params Collections)
引入原因和好处:
扩展了 params 关键字,支持更多的集合类型,而不仅仅是数组。
解决的问题:
- 提供更灵活的参数传递方式
- 支持更多类型的集合参数
- 提高API的易用性
示例代码:
// 支持 List<T> 等集合类型
public void ProcessItems(params List<string> items)
{
foreach (var item in items)
{
Console.WriteLine(item);
}
}
// 调用
ProcessItems(["item1", "item2", "item3"]);
C# 14.0 主要更新要点
C# 14.0 是即将发布的版本,随 .NET 10 发布。
1. 部分构造函数和事件 (Partial Constructors and Events)
引入原因和好处: