,我们便能在 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
中使用 ref
和 ref 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 6
stackalloc
在 C# 中,除了 new
之外,我们还有一个关键字 stackalloc
,允许我们在栈内存上分配数组:
Span<int> array = stackalloc[] { 1, 2, 3, 4, 5 };
这样我们就成功在栈上分配出了一个数组,这个数组的生命周期就是所在代码块的生命周期 。
ref field
我们已经能够在局部变量中使用 ref
和 ref readonly
了,自然,我们就想要在字段中也使用这些东西 。因此我们在 C# 11 中迎来了 ref
和 ref readonly
字段 。
字段的生命周期与包含该字段的类型的实例相同,因此 , 为了确保安全 , ref
和 ref 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; // 引用只读内容的只读字段}
scoped
和 UnscopedRef
我们再看看上面这个例子的 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; // 错误}}
同样的,我们还可以在局部变量中配合
推荐阅读
- 二、.Net Core搭建Ocelot
- 创建.NET程序Dump的几种姿势
- C# 8.0 添加和增强的功能【基础篇】
- .NET性能系列文章二:Newtonsoft.Json vs. System.Text.Json
- 某 .NET RabbitMQ SDK 有采集行为,你怎么看?
- .net core Blazor+自定义日志提供器实现实时日志查看器
- 学习ASP.NET Core Blazor编程系列九——服务器端校验
- 快读《ASP.NET Core技术内幕与项目实战》WebApi3.1:WebApi最佳实践
- 重新整理 .net core 实践篇 ———— linux上排查问题 [外篇]
- .NET API 接口数据传输加密最佳实践