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

,我們便能在 ref struct 中保存引用,而無需擔心 ref struct 的實例因為生命周期被意外延長而導致出現無效引用 。
Span<T>、ReadOnlySpan<T>從 .NET Core 2.1 開始,.NET 引入了 Span<T>ReadOnlySpan<T> 這兩個類型來表示對一段連續內存的引用和只讀引用 。
Span<T>ReadOnlySpan<T> 都是 ref struct,因此他們絕對不可能被裝箱,這確保了只要在他們自身的生命周期內 , 他們所引用的內存絕對都是有效的,因此借助這兩個類型 , 我們可以代替指針來安全地操作任何連續內存 。
Span<int> x = new[] { 1, 2, 3, 4, 5 };x[2] = 0;void* ptr = NativeMemory.Alloc(1024);Span<int> y = new Span<int>(ptr, 1024 / sizeof(int));y[4] = 42;NativeMemory.Free(ptr);我們還可以在 foreach 中使用 refref readonly 來以引用的方式訪問各成員:
Span<int> x = new[] { 1, 2, 3, 4, 5 };foreach (ref int i in x) i++;foreach (int i in x) Console.WriteLine(i); // 2 3 4 5 6stackalloc在 C# 中,除了 new 之外,我們還有一個關鍵字 stackalloc,允許我們在棧內存上分配數組:
Span<int> array = stackalloc[] { 1, 2, 3, 4, 5 };這樣我們就成功在棧上分配出了一個數組,這個數組的生命周期就是所在代碼塊的生命周期 。
ref field我們已經能夠在局部變量中使用 refref readonly 了,自然,我們就想要在字段中也使用這些東西 。因此我們在 C# 11 中迎來了 refref readonly 字段 。
字段的生命周期與包含該字段的類型的實例相同,因此 , 為了確保安全 , refref readonly 必須在 ref struct 中定義,這樣才能確保這些字段引用的東西一定是有效的:
int x = 1;Foo foo = new Foo(ref x);foo.X = 2;Console.WriteLine(x); // 2Bar bar = new Bar { X = ref foo.X };x = 3;Console.WriteLine(bar.X); // 3bar.X = 4; // 錯誤ref struct Foo{public ref int X;public Foo(ref int x){X = ref x;}}ref struct Bar{public ref readonly int X;}當然,上面的 Bar 里我們展示了對只讀內容的引用,但是字段本身也可以是只讀的 , 于是我們就還有:
ref struct Bar{public ref int X; // 引用可變內容的可變字段public ref readonly int Y; // 引用只讀內容的可變字段public readonly ref int Z; // 引用可變內容的只讀字段public readonly ref readonly int W; // 引用只讀內容的只讀字段}scopedUnscopedRef我們再看看上面這個例子的 Foo,這個 ref struct 中有接收引用作為參數的構造函數,這次我們不再在字段中保存引用:
Foo Test(){Span<int> x = stackalloc[] { 1, 2, 3, 4, 5 };Foo foo = new Foo(ref x[0]); // 錯誤return foo;}ref struct Foo{public Foo(ref int x){x++;}}你會發現這時代碼無法編譯了 。
因為 stackalloc 出來的東西僅在 Test 函數的生命周期內有效 , 但是我們有可能在 Foo 的構造函數中將 ref int x 這一引用存儲到 Foo 的字段中,然后由于 Test 方法返回了 foo,這使得 foo 的生命周期被擴展到了調用 Test 函數的函數上,有可能導致本身應該在 Test 結束時就釋放的 x[0] 的生命周期被延長,從而出現無效引用 。因此編譯器拒絕編譯了 。
你可能會好奇,編譯器在理論上明明可以檢測到底有沒有實際的代碼在字段中保存了引用 , 為什么還是直接報錯了?這是因為,如果需要檢測則需要實現復雜度極其高的過程分析,不僅會大幅拖慢編譯速度,而且還存在很多無法靜態處理的邊緣情況 。
那要怎么處理呢?這個時候 scoped 就出場了:
Foo Test(){Span<int> x = stackalloc[] { 1, 2, 3, 4, 5 };Foo foo = new Foo(ref x[0]);return foo;}ref struct Foo{public Foo(scoped ref int x){x++;}}我們只需要在 ref 前加一個 scoped,顯式標注出 ref int x 的生命周期不會超出該函數,這樣我們就能通過編譯了 。
此時,如果我們試圖在字段中保存這個引用的話 , 編譯器則會有效的指出錯誤:
ref struct Foo{public ref int X;public Foo(scoped ref int x){X = ref x; // 錯誤}}同樣的,我們還可以在局部變量中配合

推薦閱讀