提高 C# 的生產力:C# 13 更新完全指南
前言
預計在 2024 年 11 月,C# 13 將與 .NET 9 一起正式發布。今年的 C# 更新主要集中在 ref struct 上進行了許多改進,并添加了許多有助于進一步提高生產力的便利功能。
本文將介紹預計將在 C# 13 中添加的功能。
注意:目前 C# 13 還未正式發布,因此以下內容可能會發生變化。
在迭代器和異步方法中使用 ref 和 ref struct
在使用 C# 進行編程時,你是否經常使用 ref 變量和 Span 等 ref struct 類型?然而,這些不能在迭代器和異步方法中使用,于是必須使用局部函數等來避免在迭代器和異步方法中直接使用 ref 變量 ref struct 類型,這非常不方便。
這個缺點在 C# 13 中得到了改善,現在迭代器和異步方法也可以使用 ref 和 ref struct 了!
在迭代器中使用 ref 和 ref struct 的例子:
IEnumerable<float> GetFloatNumberFromIntArray(int[] array)
{
for (int i = 0; i < array.Length; i++)
{
Span<int> span = array.AsSpan();
// 進行一些處理...
ref float v = ref Unsafe.As<int, float>(ref array[i]);
yield return v;
}
}
在異步方法中使用 ref struct 的例子:
async Task ProcessDataAsync(int[] array)
{
Span<int> span = array.AsSpan();
// 進行一些處理...
ref int element = ref span[42];
element++;
await Task.Yield();
}
為了展示功能,我使用了不適當且含糊不清的“一些處理”,不過重要的是現在可以使用 ref 和 ref struct 了!
但是,有一點需要注意,ref 變量和 ref struct 類型的變量不能超出 yield 和 await 的邊界使用。例如,以下示例將導致編譯錯誤。
async Task ProcessDataAsync(int[] array)
{
Span<int> span = array.AsSpan();
// 進行一些處理...
ref int element = ref span[42];
element++;
await Task.Yield();
element++; // 錯誤:對 element 的訪問超出了 await 的邊界
}
雖然我們已經說到這里,但我想可能有人會疑惑,到底 ref 和 ref struct 是什么,所以我稍微解釋一下。
在 C# 中,可以使用 ref 來獲取變量的引用。這樣,就可以通過引用來更改原始變量。以下是一個例子:
void Swap(ref int a, ref int b) // ref 表示引用
{
int temp = a;
a = b;
b = temp; // 到這里,a 和 b 已經交換了
}
int x = 1;
int y = 2;
Swap(ref x, ref y); // 獲取 x 和 y 的引用,調用 Swap 來交換 x 和 y
另一方面,ref struct 是用于定義只能存在于堆棧上的值類型的。這是為了避免垃圾收集的開銷。然而,由于 ref struct 只能存在于堆棧上,所以在 C# 13 之前,它不能在迭代器和異步方法等地方使用。
順便一提,ref struct 之所以帶有 ref,是因為 ref struct 的實例只能存在于堆棧上,其遵循的生命周期規則與 ref 變量相同。
allows ref struct 泛型約束
在以前,ref struct 不能作為泛型類型參數使用,因此,考慮到代碼的可重用性,引入了泛型,但最終 ref struct 不能使用,必須為 Span 或 ReadOnlySpan 重新編寫相同的處理,于是就很麻煩。
在 C# 13 中,泛型類型也可以使用 ref struct 了:
using System;
using System.Numerics;
Process([1, 2, 3, 4], Sum); // 10
Process([1, 2, 3, 4], Multiply); // 24
T Process<T>(ReadOnlySpan<T> span, Func<ReadOnlySpan<T>, T> method)
{
return method(span);
}
T Sum<T>(ReadOnlySpan<T> span) where T : INumberBase<T>
{
T result = T.Zero;
foreach (T value in span)
{
result += value;
}
return result;
}
T Multiply<T>(ReadOnlySpan<T> span) where T : INumberBase<T>
{
T result = T.One;
foreach (T value in span)
{
result *= value;
}
return result;
}
為什么像 ReadOnlySpan<T> 這樣的 ref struct 類型可以作為 Func 的類型參數呢?為了調查這個問題,我查看了 .NET 的 源代碼,發現 Func 類型的泛型參數是這樣定義的:
public delegate TResult Func<in T, out TResult>(T arg)
where T : allows ref struct
where TResult : allows ref struct;
如果在泛型參數上添加 allow ref struct 約束,那么就可以將 ref struct 類型傳遞給該參數。
這確實是一個方便的功能。
ref struct 也可以實現接口
在 C# 13 中,ref struct 可以實現接口。
如果將此功能與 allows ref struct 結合使用,那么也可以通過泛型類型傳遞引用:
using System;
using System.Numerics;
int a = 10;
// 使用 Ref<int> 保存 a 的引用
Ref<int> aRef = new Ref<int>(ref a);
// 傳遞 Ref<int>
Increase<Ref<int>, int>(aRef);
Console.WriteLine(a); // 11
void Increase<T, U>(T data) where T : IRef<U>, allows ref struct where U : INumberBase<U>
{
ref U value = ref data.GetRef();
value++;
}
interface IRef<T>
{
ref T GetRef();
}
// 為 Ref<T> 這樣的 ref struct 實現接口
ref struct Ref<T> : IRef<T>
{
private ref T _value;
public Ref(ref T value)
{
_value = ref value;
}
public ref T GetRef()
{
return ref _value;
}
}
這樣一來,編寫 ref struct 相關的代碼就變得更容易了。另外,也能給各種 ref struct 實現的枚舉器實現 IEnumerator 之類的接口了。
集合類型和 Span 也可以使用 params
在以前,params 只能用于數組類型,但從 C# 13 開始,它也可以用于其他集合類型和 Span。
params 是一種功能,允許在調用方法時直接指定任意數量的參數。
例如,
Test(1, 2, 3, 4, 5, 6);
void Test(params int[] values) { }
如上所示,可以直接指定任意數量的 int 參數。
從 C# 13 開始,除了數組類型外,其他集合類型、Span、ReadOnlySpan 類型以及與集合相關的接口也可以添加 params:
Test(1, 2, 3, 4, 5, 6);
void Test(params ReadOnlySpan<int> values) { }
// 或者
Test(1, 2, 3, 4, 5, 6);
void Test(params List<int> values) { }
// 接口也可以
Test(1, 2, 3, 4, 5, 6);
void Test(params IEnumerable<int> values) { }
這也很方便!
field 關鍵字
在實現 C# 的屬性時,經常需要定義一大堆字段,如下所示...
partial class ViewModel : INotifyPropertyChanged
{
// 定義字段
private int _myProperty;
public int MyProperty
{
get => _myProperty;
set
{
if (_myProperty != value)
{
_myProperty = value;
OnPropertyChanged();
}
}
}
}
因此,從 C# 13 開始,field 關鍵字將派上用場!
partial class ViewModel : INotifyPropertyChanged
{
public int MyProperty
{
// 只需使用 field
get => field;
set
{
if (field != value)
{
field = value;
OnPropertyChanged();
}
}
}
}
不再需要自己定義字段,只需使用 field 關鍵字,字段就會自動生成。
這也非常方便!
部分屬性
在編寫 C# 時,常見的問題之一是:屬性不能添加 partial 修飾符。
在 C# 中,可以在類或方法上添加 partial,以便分別進行聲明和實現。此外,還可以分散類的各個部分。它的主要用途是在使用源代碼生成器等自動生成工具時,指定要生成的內容。
例如:
partial class ViewModel
{
// 這里只聲明方法,實現部分由工具自動生成
partial void OnPropertyChanged(string propertyName);
}
然后自動生成工具會生成以下代碼:
partial class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
partial void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new(propertyName));
}
}
開發者只需要聲明 OnPropertyChanged,其實現將全部由自動生成,從而節省了開發者的時間。
從 C# 13 開始,屬性也支持 partial:
partial class ViewModel
{
// 聲明部分屬性
public partial int MyProperty { get; set; }
}
partial class ViewModel
{
// 部分屬性的實現
public partial int MyProperty
{
get
{
// ...
}
set
{
// ...
}
}
}
這樣,屬性也可以由工具自動生成了。
鎖對象
眾所周知,lock 是一種功能,通過監視器用于線程同步。
object lockObject = new object();
lock (lockObject)
{
// 關鍵區
}
但是,這個功能的開銷其實很大,會影響性能。
為了解決這個問題,C# 13 實現了鎖對象。要使用此功能,只需用 System.Threading.Lock 替換被鎖定的對象即可:
using System.Threading;
Lock lockObject = new Lock();
lock (lockObject)
{
// 關鍵區
}
這樣就可以輕松提高性能了。
初始化器中的尾部索引
索引運算符 ^ 可用于表示集合末尾的相對位置。從 C# 13 開始,初始化器也支持此功能:
var x = new Numbers
{
Values =
{
[1] = 111,
[^1] = 999 // ^1 是從末尾開始的第一個元素
}
// x.Values[1] 是 111
// x.Values[9] 是 999,因為 Values[9] 是最后一個元素
};
class Numbers
{
public int[] Values { get; set; } = new int[10];
}
ESCAPE 字符
在 Unicode 字符串中,可以使用 \e 代替 \u001b 和 \x1b。\u001b、\x1b 和 \e 都表示 ESCAPE 字符。它們通常用于表示控制字符。
\u001b表示 Unicode 轉義序列,\u后面的 4 位十六進制數表示 Unicode 代碼點\x1b表示十六進制轉義序列,\x后面的 2 位十六進制數表示 ASCII 代碼\e表示 ESCAPE 字符本身
推薦使用 \e 的原因是,可以避免在十六進制中的混淆。
例如,如果 \x1b 后面跟著 3,則變為 \x1b3,由于 \x1b 和 3 之間沒有明確的分隔,因此不清楚應該分別解釋成 \x1b 和 3,還是放在一起解釋。
如果使用 \e,則可以避免混淆。
其他
除了上述功能外,方法組中的自然類型和方法重載中的優先級也有一些改進,但在本文中省略。如果想了解更多信息,請參閱文檔。
結語
C# 正在年復一年地進化,對我來說 C# 13 的更新中實現了許多非常實用且方便的功能,解決了不少實際的痛點。期待 .NET 9 和 C# 13 的正式發布~

浙公網安備 33010602011771號