.NET 零開銷抽象指南( 三 )

ref 或者 ref readonly 使用 scoped
Span<int> a = stackalloc[] { 1, 2, 3, 4, 5 };scoped ref int x = ref a[0];scoped ref readonly int y = ref a[1];foreach (scoped ref int i in a) i++;foreach (scoped ref readonly int i in a) Console.WriteLine(i); // 2 3 4 5 6x++;Console.WriteLine(a[0]); // 3a[1]++;Console.WriteLine(y); // 4當然,上面這個例子中即使不加 scoped,也是默認 scoped 的,這里標出來只是為了演示,實際上與下面的代碼等價:
Span<int> a = stackalloc[] { 1, 2, 3, 4, 5 };ref int x = ref a[0];ref readonly int y = ref a[1];foreach (ref int i in a) i++;foreach (ref readonly int i in a) Console.WriteLine(i); // 2 3 4 5 6x++;Console.WriteLine(a[0]); // 3a[1]++;Console.WriteLine(y); // 4對于 ref struct 而言,由于其自身就是一種可以保存引用的“類引用”類型,因此我們的 scoped 也可以用于 ref struct,表明該 ref struct 的生命周期就是當前函數:
Span<int> Foo(Span<int> s){return s;}Span<int> Bar(scoped Span<int> s){return s; // 錯誤}有時候我們希望在 struct 中返回 this 上成員的引用,但是由于 structthis 有著默認的 scoped 生命周期,因此此時無法通過編譯 。這個時候我們可以借助 [UnscopedRef] 來將 this 的生命周期從當前函數延長到調用函數上:
Foo foo = new Foo();foo.RefX = 42;Console.WriteLine(foo.X); // 42struct Foo{public int X;[UnscopedRef]public ref int RefX => ref X;}這對 out 也是同理的,因為 out 也是默認有 scoped 生命周期:
ref int Foo(out int i){i = 42;return ref i; // 錯誤}但是我們同樣可以添加 [UnscopedRef] 來擴展生命周期:
ref int Foo([UnscopedRef] out int i){i = 42;return ref i; // 錯誤}Unsafe、Marshal、MemoryMarshal、CollectionsMarshal、NativeMemoryBuffer在 .NET 中 , 我們有著非常多的工具函數,分布在 Unsafe.*、Marshal.*、MemoryMarshal.*、CollectionsMarshal.*、NativeMemory.*Buffer.* 中 。利用這些工具函數,我們可以非常高效地在幾乎不直接使用指針的情況下,操作各類內存、引用和數組、集合等等 。當然,使用的前提是你有相關的知識并且明確知道你在干什么,不然很容易寫出不安全的代碼 , 畢竟這里面大多數 API 就是 unsafe 的 。
例如消除掉邊界檢查的訪問:
void Foo(Span<int> s){Console.WriteLine(Unsafe.Add(ref MemoryMarshal.GetReference(s), 3));}Span<int> s = new[] { 1, 2, 3, 4, 5, 6 };Foo(s); // 4查看生成的代碼驗證:
G_M000_IG02:;; offset=0004Hmovrcx, bword ptr [rcx]movecx, dword ptr [rcx+0CH]call[System.Console:WriteLine(int)]可以看到,邊界檢查確實被消滅了,對比直接訪問的情況:
void Foo(Span<int> s){Console.WriteLine(s[3]);}G_M000_IG02:;; offset=0004Hcmpdword ptr [rcx+08H], 3 ; <-- range checkjbeSHORT G_M000_IG04movrcx, bword ptr [rcx]movecx, dword ptr [rcx+0CH]call[System.Console:WriteLine(int)]nopG_M000_IG04:;; offset=001CHcallCORINFO_HELP_RNGCHKFAILint3再比如,直接獲取字典中成員的引用:
Dictionary<int, int> dict = new(){[1] = 7,[2] = 42};// 如果存在則獲取引用,否則添加一個 default 進去然后再返回引用ref int value = https://www.huyubaike.com/biancheng/ref CollectionsMarshal.GetValueRefOrAddDefault(dict, 3, out bool exists);value++;Console.WriteLine(exists); // falseConsole.WriteLine(dict[3]); // 1如此一來,我們便不需要先調用 ContainsKey 再操作,只需要一次查找即可完成我們需要的操作,而不是 ContainsKey 查找一次,后續操作再查找一次 。
我們還可以用 Buffer.CopyMemory 來實現與 memcpy 等價的高效率數組拷貝;再有就是前文中出現過的 NativeMemory,借助此 API,我們可以手動分配非托管內存,并指定對齊方式、是否清零等參數 。
顯式布局、字段重疊和定長數組C# 的 struct 允許我們利用 [StructLayout] 按字節手動指定內存布局,例如:
unsafe{Console.WriteLine(sizeof(Foo)); // 10}[StructLayout(LayoutKind.Explicit, Pack = 1)]struct Foo{[FieldOffset(0)] public int X;[FieldOffset(4)] public float Y;[FieldOffset(0)] public long XY;[FieldOffset(8)] public byte Z;[FieldOffset(9)] public byte W;}

推薦閱讀