.NET 泛型参数支持 ByRefLike 类型(ref struct)的设计与实现
.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 运行时如何让SpanT、ref struct这类 ByRefLike 类型安全地进入泛型参数where T : allows ref struct涵盖 IL 指令影响、元数据扩展gpAcceptByRefLike、编译器辅助 API、无效 IL 处理选项以及约束传播的合法/非法用例。读完本文你将掌握 ByRefLike 泛型的能力边界、各 IL 指令在T为 ByRefLike 时的行为差异以及该特性在 .NET 仓库中的实际落地形态。背景什么是 ByRefLike 类型为什么它进不了泛型ByRefLike 类型俗称 ref struct是一类只能存活于栈上或托管引用中、严禁被装箱到堆上的特殊值类型。它们的典型代表是System.SpanT与System.ReadOnlySpanT声明为public readonly ref struct SpanT见 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 类型安全参与泛型的基础——本设计文档正是建立在该基础之上的扩展。最受益的场景SpanT使用 ByRefLike 作为泛型参数收益最大的是涉及SpanT的编程模式ActionSpanchar、Funcint, Spanbyte——允许在基于委托的 API 中直接传递和返回SpanT无需再手工定义专用的自定义委托类型。仓库中 src/libraries/System.Private.CoreLib/src/System/Action.cs 已经为这些委托添加了allows ref struct约束例如public delegate void SpanActionT, in TArg(SpanT span, TArg arg) where T : allows ref struct;。string.CreateTState(int length, TState state, SpanActionchar, TState action)——提供一种安全高效地基于 span 状态创建字符串的途径。该 API 已存在于 src/libraries/System.Private.CoreLib/src/System/String.cs并被Base64Encoder、Base64UrlEncoder等内部实现直接使用。潜在的未来扩展以下属于潜在的未来扩展方向SpanTypedReference——代表ByRefLike 类型作为泛型参数的通用情形这一更一般化的场景未来可能用于构建更高效的反射 API。SpanSpanchar——嵌套的SpanT类型可用于字符串解析结果的表示。运行时影响各 IL 指令对 ByRefLike 泛型参数的行为支持 ByRefLike 类型作为泛型参数后部分 IL 指令的行为将发生变化。设计文档按合法与抛异常两类做了明确划分。合法的指令序列constrained. callvirt当目标解析为在 ByRefLike 类型自身或非默认接口方法上实现的方法时constrained. callvirt序列是合法的因为此时不需要装箱即可完成调度。注意如果目标解析为在object上实现的方法或解析为默认接口方法DIM则会在调用点抛出NotSupportedException。抛InvalidProgramException的指令boxbox指令在传入 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用作字段要求包含该字段的外层类型也是 ByRefLikeref 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.EmitIL 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 { /// summaryInitializes the attribute./summary public SuppressByRefLikeConstraintChecksAttribute() { } } }当前需要应用该属性的 API 示例SpanTsrc/libraries/System.Private.CoreLib/src/System/Span.cspublic Span(T[]? array);public Span(T[]? array, int start, int length);public T[] ToArray();public static implicit operator SpanT(ArraySegmentT segment);public static implicit operator SpanT(T[]? array);ReadOnlySpanTpublic ReadOnlySpan(T[]? array);public ReadOnlySpan(T[]? array, int start, int length);public T[] ToArray();public static implicit operator ReadOnlySpanT(ArraySegmentT segment);public static implicit operator ReadOnlySpanT(T[]? array);为什么这些 API 需要豁免因为当T被实例化为 ByRefLike 类型时T[]这类签名天然不可用数组不能容纳 ByRefLike但这些 API 又是SpanT面向普通值类型/引用类型的既有能力。对它们抑制编译期检查后只有实际用 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 在代码生成阶段会插入如下 ILnewobj instance void System.InvalidProgramException::.ctor() throw向泛型参数的元数据中添加gpAcceptByRefLike被认定为非破坏性的二进制变更增加约束标志不会破坏既有已编译代码。对SpanT和ReadOnlySpanT上的构造函数/方法进行反射枚举时若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 ST {} ref struct RS {} ref struct RST {} interface I {} class C {} class CT {} // Not currently valid C# void MT, U(T t) where T: allows ref struct { // Valid if (t is int i) if (t is S s) if (t is Schar sc) if (t is SU su) if (t is RS rs) if (t is RSchar rsc) if (t is RSU rsu) if (t is string str) if (t is C c) if (t is CI ci) if (t is CU 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 IsInstanceOfTFrom, 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 CastToTFrom, TTo(TFrom source) where TFrom: allows ref struct where TTo: allows ref struct; } }使用示例TTo result; if (RuntimeHelpers.IsInstanceOfTFrom, TTo(source)) { result RuntimeHelpers.CastToTFrom, TTo(source); }RuntimeHelpers类在仓库中真实存在src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs并且已有带allows ref struct约束的泛型方法先例例如public static bool IsReferenceOrContainsReferencesT() 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 增补addendumbox ; 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 AT1 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 BT2 : AT2 { public void N() M(); // Any T2 satisfies the constraints from A }示例 2非法——派生类不能推高约束class AT1 { public void M(); } // The derived class cannot push up the allows // constraint for ByRefLike types. class BT2 : AT2 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 CT(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 CT(T t) where T: IA, allows ref struct t.M(); }示例 5合法——字段类型约束更宽松外层泛型参数可省略class AT1 where T1: allows ref struct { } class BT2 { // The type parameter is okay to lack the allows // because the field permits non-ByRefLike (default) // _and_ ByRefLike types. AT2 Field; }示例 6非法——字段类型不保证兼容 ByRefLikeclass AT1 { } class BT2 where T2: allows ref struct { // The type parameter can be passed to // the field type, but will fail if // T2 is a ByRefLike type. AT2 Field; }示例 7非法——重写方法的约束必须至少同样严格class A { virtual void MT1() 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 MT2(); }这些示例背后的核心原则是约束只能收窄不能放宽。派生类、重写方法、字段类型在继承/组合关系中不能比其基类/被重写方法/字段类型要求更宽泛的 ByRefLike 许可——否则调用方拿着基类引用就无法安全地处理派生实例。总结ByRefLike 泛型参数是 .NET 在零堆分配方向上的一次关键扩展它以ref字段支持为基础通过GenericParameterAttributes.AllowByRefLike0x0020原生侧gpAllowByRefLike这一元数据位配合allows ref struct语言约束、Type.IsByRefLike的 JIT intrinsic 化以及针对box/数组/静态字段等 IL 指令的运行时异常让SpanT等 ref struct 得以安全地进入委托、string.CreateTState等泛型 API。设计文档同时明确了约束传播只可收窄不可放宽的原则并为is/模式匹配等无效 IL场景规划了编译器辅助方法与特殊 IL 序列两条路线.NET 10 当前计划采用编译器辅助方法。对希望深挖的读者建议继续阅读仓库中的 泛型参数属性定义、原生元数据枚举、SpanT声明、string.CreateTState以及 反射调用标志计算逻辑 等实现。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考