.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; // 错误}UnsafeMarshalMemoryMarshalCollectionsMarshalNativeMemoryBuffer在 .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;}

推荐阅读