.NET 泛型参数支持 ByRefLike 类型(ref struct)的设计与实现
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
导读
本文基于 docs/design/features/byreflike-generics.md 展开,讲解 .NET 运行时如何让Span<T>、ref struct这类 ByRefLike 类型安全地进入泛型参数(where T : allows ref struct),涵盖 IL 指令影响、元数据扩展(gpAcceptByRefLike)、编译器辅助 API、无效 IL 处理选项以及约束传播的合法/非法用例。读完本文,你将掌握 ByRefLike 泛型的能力边界、各 IL 指令在T为 ByRefLike 时的行为差异,以及该特性在 .NET 仓库中的实际落地形态。
背景:什么是 ByRefLike 类型,为什么它进不了泛型
ByRefLike 类型(俗称 ref struct)是一类只能存活于栈上(或托管引用中)、严禁被装箱到堆上的特殊值类型。它们的典型代表是System.Span<T>与System.ReadOnlySpan<T>,声明为public readonly ref struct Span<T>(见 src/libraries/System.Private.CoreLib/src/System/Span.cs)。编译器通过 src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IsByRefLikeAttribute.cs 中的IsByRefLikeAttribute来标记这类类型,该特性只允许应用于Struct。
由于泛型实例化历史上可能隐式引入装箱、数组等堆分配,ByRefLike 类型长期被排斥在泛型参数之外。在ref字段(reffields)支持落地之后,运行时获得了让 ByRefLike 类型安全参与泛型的基础——本设计文档正是建立在该基础之上的扩展。
最受益的场景:Span<T>
使用 ByRefLike 作为泛型参数,收益最大的是涉及Span<T>的编程模式:
Action<Span<char>>、Func<int, Span<byte>>——允许在基于委托的 API 中直接传递和返回Span<T>,无需再手工定义专用的自定义委托类型。仓库中 src/libraries/System.Private.CoreLib/src/System/Action.cs 已经为这些委托添加了allows ref struct约束(例如public delegate void SpanAction<T, in TArg>(Span<T> span, TArg arg) where T : allows ref struct;)。string.Create<TState>(int length, TState state, SpanAction<char, TState> action)——提供一种安全高效地基于 span 状态创建字符串的途径。该 API 已存在于 src/libraries/System.Private.CoreLib/src/System/String.cs,并被Base64Encoder、Base64UrlEncoder等内部实现直接使用。
潜在的未来扩展
以下属于潜在的未来扩展方向:
Span<TypedReference>——代表"ByRefLike 类型作为泛型参数的通用情形"这一更一般化的场景,未来可能用于构建更高效的反射 API。Span<Span<char>>——嵌套的Span<T>类型,可用于字符串解析结果的表示。
运行时影响:各 IL 指令对 ByRefLike 泛型参数的行为
支持 ByRefLike 类型作为泛型参数后,部分 IL 指令的行为将发生变化。设计文档按"合法"与"抛异常"两类做了明确划分。
合法的指令序列:constrained. callvirt
当目标解析为在 ByRefLike 类型自身(或非默认接口方法)上实现的方法时,constrained. callvirt序列是合法的,因为此时不需要装箱即可完成调度。注意:如果目标解析为在object上实现的方法,或解析为默认接口方法(DIM),则会在调用点抛出NotSupportedException。
抛InvalidProgramException的指令:box
box指令在传入 ByRefLike 类型时会抛出InvalidProgramException,根本原因在于 ByRefLike 类型不能被分配到堆上——装箱在语义上就是一种堆分配。
抛TypeLoadException的指令
stsfld/ldsfld——ByRefLike 泛型参数的类型字段不能被标记为static(静态字段意味着堆上的类型状态)。newarr/stelem/ldelem/ldelema——数组无法容纳 ByRefLike 类型(数组元素在堆上)。newobj——用于多维数组构造时(多维数组同样是堆上的)。
天然失效的指令
以下指令本身就已针对该特性做好"失败"准备,因为其行为在当前定义下就会因无法对 ByRefLike 类型装箱而失败:
throwunbox/unbox.anyisinstcastclass
注意:涉及上述某些指令的序列,无论
T是否为 ByRefLike 都可能保持合法——详见下文"无效 IL 的处理选项"一节。
约束并未放松
ByRefLike 类型作为泛型参数的扩展不会放宽ByRefLike 类型本身的使用限制。当T为 ByRefLike 时,将T用作字段要求包含该字段的外层类型也是 ByRefLike(ref struct)。这保证了栈上引用不会被"泄漏"到堆上。
API 提案:元数据层面的AcceptByRefLike标志
设计文档提出了一个新的GenericParameterAttributes取值,同时对应CorGenericParamAttr枚举中的元数据定义。在当前的 .NET 仓库中,这两处都已经落地实现:
- 托管侧:src/libraries/System.Private.CoreLib/src/System/Reflection/GenericParameterAttributes.cs 中定义了:
namespace System.Reflection { [Flags] public enum GenericParameterAttributes { None = 0x0000, VarianceMask = 0x0003, Covariant = 0x0001, Contravariant = 0x0002, SpecialConstraintMask = 0x001C, ReferenceTypeConstraint = 0x0004, NotNullableValueTypeConstraint = 0x0008, DefaultConstructorConstraint = 0x0010, AllowByRefLike = 0x0020, } }- 原生侧:src/coreclr/inc/corhdr.h 中定义了
CorGenericParamAttr枚举的完整取值:
// Special constraints, applicable to any type parameters gpSpecialConstraintMask = 0x003C, gpNoSpecialConstraint = 0x0000, gpReferenceTypeConstraint = 0x0004, // type argument must be a reference type gpNotNullableValueTypeConstraint = 0x0008, // type argument must be a value type but not Nullable gpDefaultConstructorConstraint = 0x0010, // type argument must have a public default constructor gpAllowByRefLike = 0x0020, // type argument can be ByRefLike0x0020这一取值正好落在现有SpecialConstraintMask = 0x001C之外,因此可以安全地作为新的"特殊约束"位使用,而不会与既有的引用类型约束(0x0004)、非空值类型约束(0x0008)、默认构造函数约束(0x0010)冲突。在 C# 语言层面,这一约束对应allows ref struct约束子句。
元数据扩展的影响面
元数据的扩展至少会影响以下工具链组件(仓库内路径):
- ILDasm / ILAsm——src/coreclr/ildasm/dasm.cpp 与 src/coreclr/ilasm/prebuilt/asmparse.cpp 中均已出现对
gpAllowByRefLike的处理,说明反汇编与汇编工具已能识别该标志; System.Reflection.Metadata/System.Reflection.Emit——src/libraries/System.Reflection.Metadata、src/libraries/System.Reflection.Emit;- IL Trimmer——src/tools/illink;
- 其他外部生态(Cecil、F#、C++/CLI 等)。
向既有 API 添加 ByRefLike 支持的缓解方案
如果既有类型打算加入 ByRefLike 支持,可能包含一些在 ByRefLike 被允许后会变得无效的 API。设计文档提出了一个缓解思路:定义一个属性,告知编译器某些 API 在运行时(而非编译时)进行验证。编译器只会尊重与System.Object所在程序集相同的程序集中定义的这个属性。
namespace System.Runtime.CompilerServices { /// <summary> /// Indicates to the compiler the ByRefLike constraint check should be suppressed. /// </summary> /// <remarks> /// The checking will be suppressed for both the signature and method body. These /// checks are deferred and will be enforced at run-time. /// </remarks> [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = false)] internal sealed class SuppressByRefLikeConstraintChecksAttribute : Attribute { /// <summary>Initializes the attribute.</summary> public SuppressByRefLikeConstraintChecksAttribute() { } } }当前需要应用该属性的 API 示例:
Span<T>(src/libraries/System.Private.CoreLib/src/System/Span.cs)public Span(T[]? array);public Span(T[]? array, int start, int length);public T[] ToArray();public static implicit operator Span<T>(ArraySegment<T> segment);public static implicit operator Span<T>(T[]? array);
ReadOnlySpan<T>public ReadOnlySpan(T[]? array);public ReadOnlySpan(T[]? array, int start, int length);public T[] ToArray();public static implicit operator ReadOnlySpan<T>(ArraySegment<T> segment);public static implicit operator ReadOnlySpan<T>(T[]? array);
为什么这些 API 需要豁免?因为当T被实例化为 ByRefLike 类型时,T[]这类签名天然不可用(数组不能容纳 ByRefLike),但这些 API 又是Span<T>面向普通值类型/引用类型的既有能力。对它们抑制编译期检查后,只有实际用 ByRefLike 实例化并触碰这些成员时,运行时才会抛出TypeLoadException。
语义提案:Type.IsByRefLike成为 JIT 内建函数
为了让泛型方法在 JIT 阶段就能判断类型参数是否为 ByRefLike、从而避开对某些T取值无效的路径,设计文档提出将一个 JIT-time intrinsic 暴露给编译器:把已有的Type.IsByRefLike属性升级为 intrinsic,即typeof(T).IsByRefLike。
在 src/libraries/System.Private.CoreLib/src/System/Type.cs 中可以看到该属性已被标记为[Intrinsic]:
public virtual bool IsByRefLike { [Intrinsic] get => throw new NotSupportedException(SR.NotSupported_SubclassOverride); }- 对于调度到
object实现的方法以及默认接口方法,行为规定为:抛出InvalidProgramException。JIT 在代码生成阶段会插入如下 IL:
newobj instance void System.InvalidProgramException::.ctor() throw- 向泛型参数的元数据中添加
gpAcceptByRefLike被认定为非破坏性的二进制变更(增加约束标志不会破坏既有已编译代码)。 - 对
Span<T>和ReadOnlySpan<T>上的构造函数/方法进行反射枚举时,若T是 ByRefLike 类型,可能会抛出TypeLoadException(具体触发条件见上文"Troublesome API mitigation"列出的 API 清单)。
反射侧的实现佐证
在 src/libraries/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs 的ComputeAndUpdateInvocationFlags中可以看到运行时对 ByRefLike 的既有处理逻辑:当声明类型IsByRefLike(方法所在类型本身是 ref struct)或返回类型IsByRefLike时,会为调用标志加上ContainsStackPointers——这正体现了"ByRefLike 类型涉及栈指针、不能走普通装箱式反射调用"这一核心约束。
无效 IL 的处理选项
当T是 ByRefLike 类型时,C# 的is类型检查与模式匹配(type pattern matching)在 IL 层面会生成box; isinst; unbox.any序列。以下 IL 组合展示了is-type序列与"类型模式匹配"场景:
// Type check ldarg.0 box <Source> isinst <Target> brfalse.s NOT_INST // Unbox and store unboxed instance ldarg.0 box <Source> isinst <Target> unbox.any <Target> stloc.X NOT_INST: ret基于上述 IL 组合,设计文档给出了当前 C# 语义下"类型模式匹配"的预期行为矩阵:
struct S {} struct S<T> {} ref struct RS {} ref struct RS<T> {} interface I {} class C {} class C<T> {} // Not currently valid C# void M<T, U>(T t) where T: allows ref struct { // Valid if (t is int i) if (t is S s) if (t is S<char> sc) if (t is S<U> su) if (t is RS rs) if (t is RS<char> rsc) if (t is RS<U> rsu) if (t is string str) if (t is C c) if (t is C<I> ci) if (t is C<U> cu) // Can be made to work in IL. if (t is I itf) // A new local "I" would not be used for ByRefLike scenarios. // The local would be the ByRefLike type, not "I". // Invalid if (t is object o) // ByRefLike types evaluate "true" for object. if (t is U u) }要点解读:
- 对
object的类型检查在语义上对 ByRefLike 恒为true(因为任何类型都"是" object),但 ByRefLike 又无法装箱,因此if (t is object o)这类写法在 ByRefLike 泛型参数下无效。 if (t is U u)无效,因为U是另一个未约束的泛型参数,无法保证其与 ByRefLike 的兼容关系。if (t is I itf)在 IL 层面可以工作,但按设计,为 ByRefLike 场景引入的局部变量应是ByRefLike 类型本身,而不是接口类型I(因为 ByRefLike 无法装箱为接口引用)。
针对这些无效序列,设计文档给出了两种处理选项。
选项 1:编译器辅助方法(Compiler helpers)
引入两个辅助函数,替代涉及 ByRefLike 类型时非法的is-typeIL 序列。其行为大体定义为"仿佛TFrom与TTo的 ByRefLike 面向并不存在";另一种思路是与 Roslyn 团队协商,按 C# 语言规则定义其语义。
namespace System.Runtime.CompilerServices { public static class RuntimeHelpers { // Replacement for the [box; isinst; brfalse/true] sequence. public static bool IsInstanceOf<TFrom, TTo>(TFrom source) where TFrom: allows ref struct where TTo: allows ref struct; // Replacement for the [box; isinst; unbox.any] sequence. // Would throw InvalidCastException for invalid use at run-time. // For example: // TFrom: RS, TTo: object => always throws // TFrom: RS, TTo: <interface> => always throws public static TTo CastTo<TFrom, TTo>(TFrom source) where TFrom: allows ref struct where TTo: allows ref struct; } }使用示例:
TTo result; if (RuntimeHelpers.IsInstanceOf<TFrom, TTo>(source)) { result = RuntimeHelpers.CastTo<TFrom, TTo>(source); }RuntimeHelpers类在仓库中真实存在(src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs),并且已有带allows ref struct约束的泛型方法先例,例如public static bool IsReferenceOrContainsReferences<T>() where T : allows ref struct——这印证了运行时辅助类完全可以承载 ByRefLike 感知的泛型 API。
根据设计文档,基于与 Roslyn 团队的沟通,选项 1 是 .NET 10 的当前计划(plan of record)。
选项 2:特殊 IL 序列(Special IL sequences)
另一条路径是保留涉及box指令的若干 IL 序列,使其即使面对 ByRefLike 类型也保持合法;当目标类型为 ByRefLike 时这些序列必须合法,并会被加入 ECMA-335 增补(addendum):
box ; isinst ; br_true/false——允许将 ByRefLike 类型作为box参数以完成类型检查(C# 中的x is Y)。注意:ByRefLike 类型与System.Object比较时求值为true。box ; isinst ; unbox.any——为支持"类型模式匹配"(C# 中的x is Y y),该序列允许任何指令使用 ByRefLike 类型,但不允许将泛型参数直接暴露给isinst或unbox.any。box ; unbox.any——对 ByRefLike 类型合法。box ; br_true/false——对 ByRefLike 类型合法。
约束传播的合法与非法用例
设计文档以 7 组示例系统性地刻画了allows ref struct约束在继承、实现、字段与重写场景中的传播规则。
示例 1:合法——基类约束更宽松,派生类可省略
class A<T1> where T1: allows ref struct { public void M(); } // The derived class is okay to lack the 'allows' // because the base permits non-ByRefLike (default) // _and_ ByRefLike types. class B<T2> : A<T2> { public void N() => M(); // Any T2 satisfies the constraints from A<> }示例 2:非法——派生类不能"推高"约束
class A<T1> { public void M(); } // The derived class cannot push up the allows // constraint for ByRefLike types. class B<T2> : A<T2> where T2: allows ref struct { public void N() => M(); // A<> may not permit a T2 }示例 3:合法——非默认接口方法可直接调度
interface IA { void M(); } ref struct A : IA { public void M() { } } class B { // This call is permitted because no boxing is needed // to dispatch to the method - it is implemented on A. public static void C<T>(T t) where T: IA, allows ref struct => t.M(); }示例 4:非法——依赖默认接口方法(DIM)必然触发装箱
interface IA { public void M() { } } ref struct A : IA { // Relies on IA::M() implementation. } class B { // Reliance on a DIM forces the generic parameter // to be boxed, which is invalid for ByRefLike types. public static void C<T>(T t) where T: IA, allows ref struct => t.M(); }示例 5:合法——字段类型约束更宽松,外层泛型参数可省略
class A<T1> where T1: allows ref struct { } class B<T2> { // The type parameter is okay to lack the 'allows' // because the field permits non-ByRefLike (default) // _and_ ByRefLike types. A<T2> Field; }示例 6:非法——字段类型不保证兼容 ByRefLike
class A<T1> { } class B<T2> where T2: allows ref struct { // The type parameter can be passed to // the field type, but will fail if // T2 is a ByRefLike type. A<T2> Field; }示例 7:非法——重写方法的约束必须至少同样严格
class A { virtual void M<T1>() where T1: allows ref struct; } class B : A { // Override methods need to match be at least // as restrictive with respect to constraints. // If a user has an instance of A, they are // not aware they could be calling B. override void M<T2>(); }这些示例背后的核心原则是:约束只能"收窄"不能"放宽"。派生类、重写方法、字段类型在继承/组合关系中,不能比其基类/被重写方法/字段类型要求更宽泛的 ByRefLike 许可——否则调用方拿着基类引用就无法安全地处理派生实例。
总结
ByRefLike 泛型参数是 .NET 在"零堆分配"方向上的一次关键扩展:它以ref字段支持为基础,通过GenericParameterAttributes.AllowByRefLike(0x0020,原生侧gpAllowByRefLike)这一元数据位,配合allows ref struct语言约束、Type.IsByRefLike的 JIT intrinsic 化,以及针对box/数组/静态字段等 IL 指令的运行时异常,让Span<T>等 ref struct 得以安全地进入委托、string.Create<TState>等泛型 API。设计文档同时明确了约束传播只可收窄不可放宽的原则,并为is/模式匹配等"无效 IL"场景规划了编译器辅助方法与特殊 IL 序列两条路线(.NET 10 当前计划采用编译器辅助方法)。对希望深挖的读者,建议继续阅读仓库中的 泛型参数属性定义、原生元数据枚举、Span<T>声明、string.Create<TState>以及 反射调用标志计算逻辑 等实现。
【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考