什么是定义站点中的引用结构

What is ref struct in definition site

我想我前段时间在GitHub中听过一个词"ref like struct"。

既然我已经掌握了最新的 C# 版本 (7.3),我终于可以自己测试它了。所以这似乎是一个有效的代码:

public ref struct MyStruct
{
    int x;
}

我知道什么是 ref locals 和 ref returns 因为有相关文档。但是我找不到关于 ref struct 的文档。


Ref 结构不能用于自动属性或字段。它们也不能转换为对象。这些是实证研究结果。

根据最近新 c# 给我的 "Span" 背景,我猜 ref struct 是一个仅堆栈结构。这是一个永远不会进入堆的结构。但我不是 100% 确定。

我很确定应该有关于此的文档,但我找不到它。

经过一些研究,我在 Compile time enforcement of safety for ref-like types in C# 7.2 上偶然发现了这篇文章。

This C# feature is also known as “interior pointer” or “ref-like types”. The proposal is to allow the compiler to require that certain types such as Span<T> only appear on the stack.

该站点还说明了这样做的好处,主要涉及垃圾收集和堆栈分配。


使用类似 ref 的类型也会带来一些限制,例如:

  • ref-like 类型不能是数组元素的类型
  • ref-like 类型不能用作泛型类型参数
  • ref-like 变量不能装箱
  • ref-like类型不能是普通非ref-like类型的字段
  • ref-like 类型不能实现接口
  • 间接限制,例如不允许在异步方法中使用类引用类型,这实际上是不允许类引用类型字段的结果。

这限制了它们用于参数、局部变量,并且在某些情况下 return 值。


还有一个 official documentation from Microsoft,正如@UnholySheep 在评论中指出的那样。

只是在另一个答案中添加了一点。基本上,他们创建了一个 ref 结构,能够将托管指针作为成员保存。这意味着它不能被垃圾回收,如果它最终落在堆上,GC 就会崩溃。关于你能做什么和不能做什么的奇怪限制都与此有关(如此处的微软文档中所述):

Microsoft docs on reference semantics in C# 7.2

所有这些都非常吸引人,但并不能真正解释为什么他们提供了这个功能。真正的原因是允许处理托管和非托管内存的 api 有一个共同的接口(即消除无限重载的需要)。

这在这篇博客中有详细解释:

Adam Sitnik on Span<T>

C# 7.2 的这个添加在添加的意义上并不是真正的 feature或者在如此标记的值类型本身中启用任何新功能,相反,它允许开发人员声明或发布特定的 restriction 来管理允许在其他任何地方使用该类型。

[edit: see span-safety at the github/dotnet site]

因此,与其考虑 ref struct 指定给结构的最终用户什么,不如考虑它如何使作者受益。在逻辑上添加任何对外部使用的限制都需要 ref struct 假定的相关保证,因此关键字的效果是授权或 "license" ref struct做需要这些特定保证的事情。

关键是它是一个 间接 的好处,因为通常认为 ref struct 许可的操作类型基本上是关键字的 none关注,并且可以在任何地方通过狡猾的代码实施和尝试,甚至可能成功,而不管 ref struct 标记(或不标记)。

理论部分到此为止。实际上,什么是“狡猾的代码”用例 如此依赖额外的保证,甚至达到接受所有伴随限制的极端程度?从本质上讲,它是 struct 能够 公开对其自身 或其字段之一的托管引用的能力。

通常,C#this 引用从 struct:

的任何实例方法中泄漏实施严格限制

error CS8170: Struct members cannot return 'this' or other instance members by reference

编译器必须确定结构的 this 指针几乎不可能从它“所在”的上下文中泄漏出来。对于存在于 GC 对象中的任何结构实例,或者为了调用其实例方法之一而暂时装箱的任何结构实例,这是可能的、很可能的,甚至是不可避免的。

note: When anticipated, such cases can be controlled-for in advance by associating a GetPinnableReference GC instance relative to which the managed pointer to the struct (or its interior) might be taken. Explicit pinning is the strictest and best-known technique, but there are also less heavy-handed options such as so-called managed tracking references, which are one of the most amazing under-recognized features of .NET... But these all add overhead, and in most situations, given that you're using value types in the first place, reducing the amount of cruft is desirable.

随着近年来 ref 的所有增强,C# 现在更进一步检测和禁止 this 转义。例如,除了上述之外,我们现在还有:

error CS8157: Cannot return 'x' by reference because it was initialized to a value that cannot be returned by reference

及相关错误,例如...

error CS8374: Cannot ref-assign 'foo' to 'p' because 'foo' has a narrower escape scope than 'p'.

有时编译器断言 CS8157 的根本原因可能令人费解或难以看清,但编译器坚持保守的“安全胜于遗憾”的方法,这有时会导致误报,其中,例如,您有额外的特殊知识,转义最终包含在堆栈中。

对于 CS8157 确实没有根据的情况(即,考虑到编译器无法推断的信息),这些代表了我提到的“'perhaps even successful' 狡猾的代码”示例更早,通常没有简单的解决方法,尤其是通过 ref struct。这是因为复杂的误报通常只出现在更高级别的 ref 传递代码场景中,这些场景永远无法采用首先对 ref struct 强制实施的极端限制。

相反,ref struct 用于非常简单的值类型。通过向他们保证他们的 this 引用将始终锚定在上层堆栈框架中——因此至关重要的是,永远不会淹没在 GC 堆中——这样的类型因此获得了发布指向自己或他们的托管指针的信心内饰。

但是请记住,我说过 ref struct 对于它提供的放松方式、原因和用途是不可知的。我特别提到的是,不幸的是,使用 ref struct 并不能使 CS8157 消失(我认为这是一个错误,请参阅 here and here)。

因为 ref struct 应该允许 return 自己 this 的代码仍然被编译器阻止这样做,你必须求助于一些相当粗暴的技术来在本应被释放的 ref struct 实例方法中编码时绕过致命错误。也就是说,用 C# 编写的值类型实例方法合法地需要覆盖致命错误 CS8170 / CS8157 可以 不透明 'this' 指针 通过 IntPtr 来回传递它。这留作 reader 的练习,但一种方法是通过 System.​Runtime.​CompilerServices.​Unsafe 包。