目录
ref 和 unsafe 在 async 方法和迭代器中的使用
前言
微软即将在 2024年11月12日发布 .NET 9 的最终版本,而08月09日发布的.NET 9 Preview 7 是最终发布前的最后一个预览版。这个版本将与.NET Conf 2024一同亮相,并已与 Visual Studio 2022 17.12 预览版1一同发布,可以直接通过Visual Studio安装。同时Visual Studio Code 和 C# Dev Kit 扩展也支持.NET 9。
C# 13 作为 .NET 9 的一部分,将带来一系列新特性,提升开发灵活性和性能,让编程体验更加流畅。
尽管C# 13 尚未正式发布,但我们可以在 .NET 9 Preview 7 中尝试这些新特性,需要下载最新的 Visual Studio 2022 17.11 预览版。
注意: 目前 C# 13 尚未正式发布,因此功能细节可能会有所调整。
新特性
1、params 集合增强,以提高灵活性
在 C# 13 中,params关键字的使用已经扩展到不仅仅是数组,还可以应用于任何可识别的集合类型,包括System.Span< T>、System.ReadOnlySpan< T >和实现了System.Collections.Generic.IEnumerable< T >的类型。
2、锁对象
.NET 9 运行时引入了System.Threading.Lock类型,提供了改进的线程同步机制。Lock类型通过其 API 支持更高效的线程同步操作,例如Lock.EnterScope()方法可以进入一个独占作用域
3、索引器改进
索引器的使用变得更加直观和灵活,能够更高效地操作集合。
4、转义序列\e
使用\e 的好处是它可以避免与十六进制转义序列混淆。
5、部分属性
部分属性的引入使得属性的定义和实现可以分布在不同的文件中,提高了代码的组织性和可维护性。
6、方法组自然类型改进
方法组的自然类型得到了改进,使得调用变得更简单,减少了不必要的转换。
7、ref 和 unsafe 在 async 方法和迭代器中的使用
现在 async 方法和迭代器可以使用ref变量和不安全代码,可以在更多情况下使用这些特性,尽管仍然有一些限制。
8、关于扩展类型(Extension Types)的更新
C# 13 中一个非常重大的特性,它允许向现有类添加新的方法、属性、甚至静态成员,而无需修改原始类代码。
9、LINQ 新方法
新增了CountBy和AggregateBy方法,允许按键聚合状态而无需通过GroupBy分配中间分组,这为数据聚合提供了更灵活的方式
10、Foreach 支持 Index
引入了Index< TSource >(IEnumerable< TSource >),使得在 foreach 循环中可以快速提取可枚举项的索引
11、序列化改进
System.Text.Json在 .NET 9 中进行了改进,提供了新的选项用于 JSON 序列化,并引入了 JsonSerializerOptions.Web 单例,简化了使用 Web 默认值进行序列化的过程。
12、性能改进
.NET 9 在异常处理、环路性能、动态 PGO(按配置文件优化)、RyuJIT 编译器以及 Arm64 指令集支持方面进行了优化,显著提升了应用程序的性能。
Params 集合
params关键字允许方法接受一个参数列表,这个列表可以是任何实现了IEnumerable< T >接口的集合类型。
意味着可以使用方法参数来传递数组、列表、元组等集合,而不必显式地创建集合实例。
以下是一个使用 params关键字的简单示例:
using System;
using System.Collections.Generic;
using System.Linq;
public class Program
{
// 这个方法可以接受任意数量的字符串参数
public static void PrintNames(params string[] names)
{
Console.WriteLine("Names provided:");
foreach (var name in names)
{
Console.WriteLine(name);
}
}
public static void Main()
{
// 直接传递字符串参数
PrintNames("Alice", "Bob", "Charlie");
// 使用数组
string[] namesArray = new string[] { "Dave", "Eve", "Frank" };
PrintNames(namesArray);
// 使用列表
List<string> namesList = new List<string> { "Grace", "Heidi", "Ivan" };
PrintNames(namesList);
// 使用 LINQ 表达式
var query = from person in new List<Person> {
new Person("Judy", "Walker"),
new Person("Kevin", "Smith")
}
select person.FirstName;
PrintNames(query);
// 使用从集合中选择的属性
var persons = new List<Person>
{
new Person("Leonard", "Nimoy"),
new Person("Morgan", "Freeman")
};
PrintNames(from p in persons select p.FirstName);
}
}
public class Person
{
public string FirstName { get; }
public string LastName { get; }
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}
在这个示例中,PrintNames 方法使用 params 关键字来接受任意数量的字符串参数。可以使用多种方式调用这个方法:
-
直接传递字符串字面量。
-
传递一个字符串数组。
-
传递一个字符串列表。
-
使用 LINQ 查询来传递查询结果。
-
使用 LINQ 从Person对象的集合中选择FirstName属性。
这个示例展示了params集合的灵活性,允许以多种不同的集合类型传递参数,而方法内部的实现保持不变。
锁对象
众所周知,lock 是一种功能,通过监视器用于线程同步。
object lockObject = new object();
lock (lockObject)
{
// 关键区
}
但是,这个功能的开销其实很大,会影响性能。为了解决这个问题,C# 13 实现了锁对象。要使用此功能,只需用System.Threading.Lock替换被锁定的对象即可:
using System.Threading;
Lock lockObject = new Lock();
lock (lockObject)
{
// 关键区
}
这样就可以轻松提高性能了。
索引器改进
对索引器的改进,其中包括在对象初始化器中使用”尾部索引"(也称为“从末尾开始的索引”)的能力。这种索引方式允许从集合的末尾开始计数,使用^符号来指定元素的位置。
以下是 C# 13 中索引器改进的示例:
using System;
public class Demo
{
public static void Main()
{
// 定义一个可索引的类型
var data = new IndexedData
{
// 使用传统的索引器初始化
Items = { [2] = "Second", [3] = "Third" },
// 使用尾部索引初始化
[^1] = "First", // 从末尾开始的第一个元素
[^2] = "Fourth" // 从末尾开始的第二个元素
};
// 打印初始化后的数据
for (int i = 0; i < data.Items.Length; i++)
{
Console.WriteLine($"Index {i}: {data.Items[i]}");
}
}
}
public class IndexedData
{
public string[] Items { get; set; } = new string[5];
}
在这个示例中,IndexedData 类有一个名为 Items 的字符串数组属性。 在初始化 data 对象时,我们使用了两种索引方式:
-
传统的索引器,通过指定索引位置(例如[2] 和[3])来初始化数组元素。
-
尾部索引器,使用^符号后跟数字来指定从数组末尾开始的位置(例如[^1] 和[^2])。
当运行Main方法时,它将打印出数组中每个元素的索引和值,包括使用尾部索引初始化的元素。
输出结果将是:
Index 0:
Index 1:
Index 2: Second
Index 3: Third
Index 4: First
请注意,尾部索引 [^1] 被分配给了数组的最后一个位置(索引4),而 [^2]被分配给了倒数第二个位置(索引3),这是因为它们是从末尾开始计数的。这种特性在初始化数组或集合时特别有用,尤其是当你需要在已知末尾元素的情况下进行初始化时。
转义序列 \e在 Unicode 字符串中,可以使用\e 来代表 ESCAPE 字符,它等同于传统的\u001b 或\x1b。
-
\u001b 是一个 Unicode 转义序列,其中 \u 后跟的四位十六进制数代表一个 Unicode 点。
-
\x1b 是一个十六进制转义序列,\x 后面跟的两位十六进制数代表一个 ASCII 字符。
-
\e 直接表示 ESCAPE 字符,它避免了可能的混淆。
推荐使用 \e 是因为它提供了一种清晰无歧义的方式来表示 ESCAPE 字符。例如,\x1b 后如果紧跟数字可能会造成混淆,如 \x1b3 可能被误解为单一的转义序列。使用 \e 就可以清楚地表达 ESCAPE 字符,避免了这种混淆。
部分属性
在 C# 13 之前,属性不支持使用partial修饰符,这意味着属性的声明和实现必须在同一个位置完成。这在自动生成代码或分离关注点时可能会带来限制。
C# 13 改进了这一点,允许属性跨越多个部分进行声明和实现。特性特别适用于与源代码生成器等工具结合使用的场景,可以更灵活地生成和管理属性代码。
以下是 C# 13 中属性支持partial的示例:
public class DemoModel
{
//声明部分属性
public partial int MyProperty { get; set; }
}
public class DemoModel
{
// 部分属性的实现
public partial int MyProperty
{
get { return GetValue(); }
set { SetValue(value); }
}
}
这种方式可以专注于属性的业务逻辑部分,而将具体的实现细节留给自动化工具处理,从而提高开发效率并减少重复性编码工作。
方法组自然类型
方法组的自然类型改进允许编译器更精确地确定方法的自然类型,特别是在重载解析时。这意味着编译器可以更有效地识别应该使用哪个重载版本,尤其是在涉及委托和方法组的情况下。
以下是一个示例,展示了 C# 13 中方法组自然类型的改进:
using System;
public class Program
{
public static void Main()
{
// 声明一个委托类型,它指向一个接受 Action 作为参数的方法
Action<string> action = PrintMessage;
// 调用 PrintMessage 方法,使用方法组作为参数
action("Hello, World!");
}
// 这是原始的重载版本
public static void PrintMessage(string message)
{
Console.WriteLine($"Original: {message}");
}
// C# 13 允许更精确的自然类型推断
public static void PrintMessage(Action<string> messagePrinter, string message)
{
messagePrinter(message);
Console.WriteLine("Improved natural type inference in C# 13.");
}
}
在这个示例中,PrintMessage方法有两个重载。第一个重载接受一个string参数,而第二个重载接受一个Action< string >和一个string参数。
在 C# 13 之前,如果尝试使用方法组调用action委托,编译器可能会在重载解析时产生模糊性,因为它需要确定使用哪个重载。
C# 13 中的方法组自然类型改进允许编译器更准确地推断出应该使用第一个 PrintMessage 重载,因为它更匹配传递的参数类型(一个字符串)。第二个重载虽然也能接受字符串,但它期望的是一个Action< string >类型的参数,这在方法组调用中是不匹配的。
请注意,这个示例仅用于说明 C# 13 中方法组自然类型改进的概念。在实际代码中,可能需要根据具体情况调整方法签名和调用方式。
ref 和 unsafe 在 async 方法和迭代器中的使用
在 C# 13 之前,ref 和 unsafe 关键字在异步方法(使用 async和 await 修饰的方法)和迭代器中有一些限制。
然而,C# 13 放宽了这些限制,可以在这些上下文中使用 ref 和 unsafe。
以下是一些示例,展示在 C# 13 中如何在异步方法和迭代器中使用 ref 和 unsafe:
1、在异步方法中使用ref
async Task RefInAsyncMethod()
{
int value = 0;
await Task.Yield();
ref int local = ref ModifyValue(ref value);
local++; // 修改原始变量的值
Console.WriteLine(value); // 输出修改后的值
}
ref int ModifyValue(ref int x)
{
return ref x;
}
在这个示例中,ModifyValue方法返回对传入引用的引用。在异步方法RefInAsyncMethod中,我们使用await Task.Yield();来切换到另一个上下文,然后通过ref返回的引用来修改原始变量的值。
2、在迭代器中使用ref
IEnumerable<int> GetNumbers()
{
int number = 0;
yield return number; // 返回第一个值
number++; // 修改状态
yield return number; // 返回修改后的值
}
// 使用迭代器
foreach (int num in GetNumbers())
{
Console.WriteLine(num);
}
在这个示例中,迭代器GetNumbers使用yield return来返回序列中的值。
在两次yield调用之间,迭代器的状态(number 变量)被保持,允许在第二次迭代时返回修改后的值。
3、在异步方法中使用unsafe
async Task UnsafeInAsyncMethod()
{
unsafe
{
int* p = stackalloc int[10];
for (int i = 0; i < 10; i++)
{
p[i] = i;
}
await Task.Yield(); // 切换上下文
// 继续使用 p
for (int i = 0; i < 10; i++)
{
Console.WriteLine(p[i]);
}
}
}
在这个示例中,unsafe上下文被用在异步方法UnsafeInAsyncMethod中。
我们使用stackalloc在栈上分配内存,并在await之前和之后访问这个内存。
这展示了即使在异步方法中,也可以执行不安全操作。
4、注意事项
-
在异步方法中使用ref和unsafe需要谨慎,因为await会导致方法的执行上下文被挂起和恢复,这可能会影响对ref局部变量和unsafe代码的预期行为。
-
确保在使用 ref和unsafe代码时,遵守 C# 的安全和并发规则。
C# 13 的这些改进提供了更大的灵活性,可以在异步编程和迭代器中使用ref和unsafe代码,但同时也需要更多的注意来确保代码的正确性和安全性。
总结
C# 13 带来的新特性和改进,如扩展类型的灵活性、params关键字的增强、在异步方法中使用ref和unsafe的能力,以及对序列化性能的优化等,都极大地提升了我们开发效率,解决了很多实际开发中遇到的问题。
对.NET 9 和 C# 13 的正式发布充满期待,相信将为社区带来更加强大和便捷的工具,进一步推动技术的更新和发展。下载最新的 Visual Studio 2022-17.11 预览版,可以亲自体验这些新特性。
下载地址
Download .NET 9.0 (Linux, macOS, and Windows)
最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号[DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!