C#新特性
匿名对象的 with
可以用 with
来根据已有的匿名对象创建新的匿名对象了:
var x = new { A = 1, B = 2 };
var y = x with { A = 3 };
常量字符串插值
你可以给 const string 使用字符串插值了,非常方便:
const string x = "hello";
const string y = $"{x}, world!";
CallerArgumentExpression
现在,CallerArgumentExpression
这个 attribute 终于有用了。借助这个 attribute,编译器会自动填充调用参数的表达式字符串,例如:
void Foo(int value, [CallerArgumentExpression("value")] string? expression = null)
{
Console.WriteLine(expression + " = " + value);
}
当你调用 Foo(4 + 5)
时,会输出 4 + 5 = 9
。这对测试框架极其有用,因为你可以输出 assert 的原表达式了:
static void Assert(bool value, [CallerArgumentExpression("value")] string? expr = null)
{
if (!value) throw new AssertFailureException(expr);
}
条件 ref
表达式
条件表达式可能生成 ref 结果而不是值。 例如,你将编写以下内容以检索对两个数组之一中第一个元素的引用
ref var r = ref (arr != null ? ref arr[0] : ref otherArr[0]);
ref struct
ref
结构类型的实例在堆栈上分配,并且不能转义到托管堆,因此不会给 GC 带来任何的压力。相对的,使用中就会有不能逃逸出栈的强制限制。
编译器将 ref
结构类型的使用限制如下:
ref
结构不能是数组的元素类型。ref
结构不能是类或非ref
结构的字段的声明类型。ref
结构不能实现接口。ref
结构不能被装箱为 System.ValueType 或 System.Object。ref
结构不能是类型参数。ref
结构变量不能由 lambda 表达式或本地函数捕获。ref
结构变量不能在async
方法中使用。 但是,可以在同步方法中使用ref
结构变量,例如,在返回 Task 或 Task<TResult> 的方法中。ref
结构变量不能在迭代器中使用。
Span<T> 和 System.ReadOnlySpan<T>
本身是一个 readonly ref struct
,成功的封装出了安全且高性能的内存访问操作,且可在大多数情况下代替指针而不损失任何的性能。
ref struct MyStruct { public int Value { get; set; } } class RefStructGuide { static void Test() { MyStruct x = new MyStruct(); x.Value = 100; Foo(x); // ok Bar(x); // error, x cannot be boxed } static void Foo(MyStruct x) { } static void Bar(object x) { } }
通过 stackalloc
直接在栈上分配内存,并使用 Span<T>
来安全的访问,同样的,这么做可以做到 0 GC 压力。
stackalloc
允许任何的值类型结构,但是要注意,Span<T>
目前不支持 ref struct
作为泛型参数,因此在使用 ref struct
时需要直接使用指针。
static unsafe void RefStructAlloc() { MyStruct* x = stackalloc MyStruct[10]; for (int i = 0; i < 10; i++) { *(x + i) = new MyStruct { Value = i }; } } static void StructAlloc() { Span<int> x = stackalloc int[10]; for (int i = 0; i < x.Length; i++) { x[i] = i; } }
性能敏感时对于频繁调用的函数使用 SkipLocalsInit
C# 为了确保代码的安全会将所有的局部变量在声明时就进行初始化,无论是否必要。一般情况下这对性能并没有太大影响,但是如果你的函数在操作很多栈上分配的内存,并且该函数还是被频繁调用的,那么这一消耗的副作用将会被放大变成不可忽略的损失。
因此你可以使用 SkipLocalsInit
这一特性禁用自动初始化局部变量的行为。
[SkipLocalsInit] unsafe static void Main() { Guid g; Console.WriteLine(*&g); }
上述代码将输出不可预期的结果,因为 g
并没有被初始化为 0。另外,访问未初始化的变量需要在 unsafe
上下文中使用指针进行访问。
表达式体
成员函数、只读属性、构造函数、get
和 set
访问器。
// Expression-bodied constructor public ExpressionMembersExample(string label) => this.Label = label; private string label; // Expression-bodied get / set accessors. public string Label { get => label; set => this.label = value ?? "Default label"; }
throw
表达式
throw
可以用作表达式和语句。 这允许在以前不支持的上下文中引发异常。 这些方法包括条件表达式、null 合并表达式和一些 lambda 表达式。
string arg = args.Length >= 1 ? args[0] : throw new ArgumentException("You must supply an argument"); name = value ?? throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null"); DateTime ToDateTime(IFormatProvider provider) => throw new InvalidCastException("Conversion to a DateTime is not supported.");
ValueTask
从异步方法返回 Task
对象可能在某些路径中导致性能瓶颈。 Task
是引用类型,因此使用它意味着分配对象。
ValueTask
此增强功能对于库作者最有用,可避免在性能关键型代码中分配 Task
。
需要添加 NuGet 包 System.Threading.Tasks.Extensions
才能使用 ValueTask<TResult> 类型。
数字文本语法改进
二进制文本和数字分隔符
public const int OneHundredTwentyEight = 0b1000_0000; public const long BillionsAndBillions = 100_000_000_000; public const double AvogadroConstant = 6.022_140_857_747_474e23;
int binaryValue = 0b_0101_0101;
默认文本表达式
针对默认值表达式的一项增强功能。 这些表达式将变量初始化为默认值。 过去会这么编写:
Func<string, bool> whereClause = default(Func<string, bool>);
现在,可以省略掉初始化右侧的类型:
Func<string, bool> whereClause = default;
增强的泛型约束
可以将类型 System.Enum 或 System.Delegate 指定为类型参数的基类约束。
现在也可以使用新的 unmanaged 约束来指定类型参数必须是不可为 null 的“非托管类型”。
非托管类型 :
• sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal 或 bool
• 任何枚举类型
• 任何指针类型
• 任何用户定义的 struct 类型,只包含非托管类型的字段
public struct Coords<T> where T : unmanaged { public T X; public T Y; } var c = new Coords<int>();
默认接口方法
可以将成员添加到接口,并为这些成员提供实现。
将方法添加到以后版本的接口中,而不会破坏与该接口当前实现的源或二进制文件兼容性。
异步流
public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence() { for (int i = 0; i < 20; i++) { await Task.Delay(100); yield return i; } } await foreach (var number in GenerateSequence()) { Console.WriteLine(number); }
同步流在迭代集合的过程中会阻塞调用线程,而且需要给所有数据分配存储空间后再返回
异步流解决了上述两个问题
异步可释放
实现 System.IAsyncDisposable 接口的异步可释放类型。 可使用 await using
语句来处理异步可释放对象。
public
无参数的 DisposeAsync()
方法在 await using
语句中隐式调用,其用途是释放非托管资源,执行常规清理,以及指示终结器(如果存在)不必运行。 释放与托管对象关联的内存始终是垃圾回收器的域。 因此,它具有标准实现:
public class ExampleAsyncDisposable : IAsyncDisposable, IDisposable { private Utf8JsonWriter? _jsonWriter = new(new MemoryStream()); public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } public async ValueTask DisposeAsync() { await DisposeAsyncCore().ConfigureAwait(false); Dispose(disposing: false); #pragma warning disable CA1816 // Dispose methods should call SuppressFinalize GC.SuppressFinalize(this); #pragma warning restore CA1816 // Dispose methods should call SuppressFinalize } protected virtual void Dispose(bool disposing) { if (disposing) { _jsonWriter?.Dispose(); _jsonWriter = null; } } protected virtual async ValueTask DisposeAsyncCore() { if (_jsonWriter is not null) { await _jsonWriter.DisposeAsync().ConfigureAwait(false); } _jsonWriter = null; } }
DisposeAsyncCore()
方法将异步释放托管资源,因此不希望也同步释放这些资源。 因此,调用 Dispose(false)
而非 Dispose(true)
。
class ExampleConfigureAwaitProgram { static async Task Main() { var exampleAsyncDisposable = new ExampleAsyncDisposable(); await using (exampleAsyncDisposable.ConfigureAwait(false)) { // Interact with the exampleAsyncDisposable instance. } Console.ReadLine(); } }
在创建和使用实现 IAsyncDisposable 的多个对象的情况下,残存错误条件中具有 ConfigureAwait 的堆叠 await using
语句可能会阻止调用 DisposeAsync()。 若要确保始终调用 DisposeAsync(),应避免堆叠。
class DoNotDoThisProgram { static async Task Main() { var objOne = new ExampleAsyncDisposable(); // Exception thrown on .ctor var objTwo = new AnotherAsyncDisposable(); await using (objOne.ConfigureAwait(false)) await using (objTwo.ConfigureAwait(false)) { // Neither object has its DisposeAsync called. } Console.ReadLine(); } }
如果从 AnotherAsyncDisposable
构造函数引发异常,则 objOne 不会正确释放。因此对象应该各自await using
索引和范围
0
索引与 sequence[0]
相同。 ^0
索引与 sequence[sequence.Length]
相同。 请注意,sequence[^0]
不会引发异常
对于任何数字 n
,索引 ^n
与 sequence.Length - n
相同。^1
索引最后
[..]表示整个范围,相当于范围 [0..^0]
words[1..4]包括 words[1]
到 words[3]
。 元素 words[4]
不在该范围内
var allWords = words[..]; // contains "The" through "dog". var firstPhrase = words[..4]; // contains "The" through "fox" var lastPhrase = words[6..]; // contains "the", "lazy" and "dog"
Null 合并赋值
List<int> numbers = null; int? i = null; numbers ??= new List<int>(); numbers.Add(i ??= 17); numbers.Add(i ??= 20); Console.WriteLine(string.Join(" ", numbers)); // output: 17 17 Console.WriteLine(i); // output: 17
已知创建对象的类型时,可在 new
表达式中省略该类型
private List<WeatherObservation> _observations = new(); var forecast = station.ForecastFor(DateTime.Now.AddDays(2), new());
从 C# 9.0 开始,可将 static
修饰符添加到 Lambda 表达式或匿名方法。 静态 Lambda 表达式类似于 static
局部函数:静态 Lambda 或匿名方法无法捕获局部变量或实例状态。 static
修饰符可防止意外捕获其他变量。