注意
本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。
功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。
可以在 规范一文中详细了解将功能规范采用 C# 语言标准的过程。
支持者问题:https://github.com/dotnet/csharplang/issues/7608
总结
此建议将扩展 ref struct
的功能,以便它们可以实现接口并作为泛型类型参数参与。
动机
ref struct
无法实现接口意味着它们不能参与 .NET 的基本抽象技术。 Span<T>
,即使具有顺序列表的所有属性,也不能参与采用 IReadOnlyList<T>
、IEnumerable<T>
等的方法。相反,必须为 Span<T>
编写特定的方法,尽管这些方法的实现几乎相同。 允许 ref struct
实现接口,这样就可以像对其他类型一样,对它们的操作进行抽象化。
详细设计
ref 结构体接口
该语言将允许 ref struct
类型实现接口。 语法和规则与普通 struct
相同,但有一些例外情况来说明 ref struct
类型的限制。
实现接口的能力不会影响对装箱 ref struct
实例的现有限制。 这意味着,即使 ref struct
实现了一个特定的接口,也不能直接将其转换为该接口,因为这代表了一个装箱操作。
ref struct File : IDisposable
{
private SafeHandle _handle;
public void Dispose()
{
_handle.Dispose();
}
}
File f = ...;
// Error: cannot box `ref struct` type `File`
IDisposable d = f;
实现接口的能力只有与 ref struct
参与泛型参数的能力(如稍后所述)相结合时才会有用。
为了允许接口涵盖 ref struct
的完整表现力及其存在的生存期问题,该语言将允许 [UnscopedRef]
显示在接口方法和属性上。 这是必需的,因为它允许通过 struct
抽象的接口具有与直接使用 struct
相同的灵活性。 请考虑以下示例:
interface I1
{
[UnscopedRef]
ref int P1 { get; }
ref int P2 { get; }
}
struct S1
{
[UnscopedRef]
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
ref int M<T>(T t, S1 s)
where T : I1, allows ref struct
{
// Error: may return ref to t
return ref t.P1;
// Error: may return ref to t
return ref s.P1;
// Okay
return ref t.P2;
// Okay
return ref s.P2;
}
当 struct
/ ref struct
成员实现具有 [UnscopedRef]
属性的接口成员时,实现成员也可以用 [UnscopedRef]
修饰,但这不是必需的。 但是,带有 [UnscopedRef]
的成员可能无法用于实现缺少此属性的成员(详细信息)。
interface I1
{
[UnscopedRef]
ref int P1 { get; }
ref int P2 { get; }
}
struct S1
{
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
struct S2
{
[UnscopedRef]
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
struct S3 : I1
{
internal ref int P1 { get {...} }
// Error: P2 is marked with [UnscopedRef] and cannot implement I1.P2 as is not marked
// with [UnscopedRef]
[UnscopedRef]
internal ref int P2 { get {...} }
}
class C1 : I1
{
internal ref int P1 { get {...} }
internal ref int P2 { get {...} }
}
默认接口方法给 ref struct
带来了问题,因为没有针对默认实现装箱 this
成员的保护措施。
interface I1
{
void M()
{
// Danger: both of these box if I1 is implemented by a ref struct
I1 local1 = this;
object local2 = this;
}
}
// Error: I1.M cannot implement interface member I1.M() for ref struct S
ref struct S : I1 { }
为了处理这种情况,ref struct
将被强制实现接口的所有成员,即使它们有默认实现。
如果在 ref struct
类型上调用默认接口成员,运行时也将更新为引发异常。
为了避免在运行时出现异常,编译器将会在调用允许 ref 结构体的类型参数上的非虚拟实例方法或属性时报告错误。 下面是一个示例:
public interface I1
{
sealed void M3() {}
}
class C
{
static void Test2<T>(T x) where T : I1, allows ref struct
{
#line 100
x.M3(); // (100,9): error: A non-virtual instance interface member cannot be accessed on a type parameter that allows ref struct.
}
}
还有一个开放的设计问题,即在允许 ref 结构体的类型参数上调用虚拟(非抽象)实例方法(或属性)时,是否要报告警告。
详细说明:
- 一个
ref struct
可以实现一个接口 - 一个
ref struct
无法参与默认接口成员 ref struct
无法被转换到它所实现的接口,因为这是一个装箱操作
ref 结构体泛型参数
type_parameter_constraints_clause
: 'where' type_parameter ':' type_parameter_constraints
;
type_parameter_constraints
: restrictive_type_parameter_constraints
| allows_type_parameter_constraints_clause
| restrictive_type_parameter_constraints ',' allows_type_parameter_constraints_clause
restrictive_type_parameter_constraints
: primary_constraint
| secondary_constraints
| constructor_constraint
| primary_constraint ',' secondary_constraints
| primary_constraint ',' constructor_constraint
| secondary_constraints ',' constructor_constraint
| primary_constraint ',' secondary_constraints ',' constructor_constraint
;
primary_constraint
: class_type
| 'class'
| 'struct'
| 'unmanaged'
;
secondary_constraints
: interface_type
| type_parameter
| secondary_constraints ',' interface_type
| secondary_constraints ',' type_parameter
;
constructor_constraint
: 'new' '(' ')'
;
allows_type_parameter_constraints_clause
: 'allows' allows_type_parameter_constraints
allows_type_parameter_constraints
: allows_type_parameter_constraint
| allows_type_parameter_constraints ',' allows_type_parameter_constraint
allows_type_parameter_constraint
: ref_struct_clause
ref_struct_clause
: 'ref' 'struct'
通过在 where
子句中使用 allows ref struct
语法,语言将允许泛型参数选择支持 ref struct
作为参数:
T Identity<T>(T p)
where T : allows ref struct
=> p;
// Okay
Span<int> local = Identity(new Span<int>(new int[10]));
这类似于 where
子句中的其他项,因为它指定泛型参数的功能。 区别在于,其他语法项限制可满足泛型参数的类型集,而 allows ref struct
则扩展了这些类型集。 这实际上是一个反约束,因为它删除了 ref struct
无法满足泛型参数的隐式约束。 因此,赋予其一个新的语法前缀allows
,以便使其更清晰。
由 allows ref struct
绑定的类型参数具有 ref struct
类型的所有行为:
- 它的实例无法装箱
- 实例像普通
ref struct
一样参与生命周期规则 - 类型参数不能用于
static
字段、数组元素等... - 可以用
scoped
标记实例
这些规则在操作中的示例:
interface I1 { }
I1 M1<T>(T p)
where T : I1, allows ref struct
{
// Error: cannot box potential ref struct
return p;
}
T M2<T>(T p)
where T : allows ref struct
{
Span<int> span = stackalloc int[42];
// The safe-to-escape of the return is current method because one of the inputs is
// current method
T t = M3<int, T>(span);
// Error: the safe-to-escape is current method.
return t;
// Okay
return default;
return p;
}
R M3<T, R>(Span<T> span)
where R : allows ref struct
{
return default;
}
反约束不是从类型参数类型约束“继承”的。
例如,下面的代码中的 S
不能替换为 ref 结构:
class C<T, S>
where T : allows ref struct
where S : T
{}
详细说明:
where T : allows ref struct
泛型参数无法- 具有
where T : U
,其中U
为已知引用类型 - 具有
where T : class
约束 - 除非相应的参数也
where T: allows ref struct
,否则不能用作泛型参数
- 具有
allows ref struct
必须是where
子句中的最后一个约束- 具有
allows ref struct
的类型参数T
与ref struct
类型具有相同的限制。
元数据中的表示形式
允许 ref 结构体的类型参数将按照 类似 byref 的泛型文档中的描述在元数据中编码。具体而言,就是使用 CorGenericParamAttr.gpAllowByRefLike(0x0020)
或 System.Reflection.GenericParameterAttributes.AllowByRefLike(0x0020)
标志值。
运行时是否支持该功能,可以通过检查 System.Runtime.CompilerServices.RuntimeFeature.ByRefLikeGenerics
字段是否存在来确定。
在 https://github.com/dotnet/runtime/pull/98070 中添加了 API。
using
语句
当资源为 ref 结构时,using
语句将识别和使用 IDisposable
接口的实现。
ref struct S2 : System.IDisposable
{
void System.IDisposable.Dispose()
{
}
}
class C
{
static void Main()
{
using (new S2())
{
} // S2.System.IDisposable.Dispose is called
}
}
请注意,优先于实现模式的 Dispose
方法,并且仅当找不到模式时,才使用 IDisposable
实现。
当资源是类型参数,并且 allows ref struct
和 IDisposable
在其有效接口集中的时候,using
语句将识别并使用 IDisposable
接口的实现。
class C
{
static void Test<T>(T t) where T : System.IDisposable, allows ref struct
{
using (t)
{
}
}
}
请注意,模式 Dispose
方法不会在类型参数 allows ref struct
上识别,因为接口(这也是我们可能查找模式的唯一位置)不是 ref 结构体。
interface IMyDisposable
{
void Dispose();
}
class C
{
static void Test<T>(T t, IMyDisposable s) where T : IMyDisposable, allows ref struct
{
using (t) // Error, the pattern is not recognized
{
}
using (s) // Error, the pattern is not recognized
{
}
}
}
await using
语句
当前语言禁止在 await using
语句中使用 ref 结构作为资源。 同样的限制也适用于 allows ref struct
的类型参数。
有人建议取消对异步方法中的 ref 结构的使用一般限制 - https://github.com/dotnet/csharplang/pull/7994。
本部分的其余部分将描述 await using
语句的一般限制取消后的行为(如果/当它被取消时)。
当资源为 ref 结构时,await using
语句将识别和使用 IAsyncDisposable
接口的实现。
ref struct S2 : IAsyncDisposable
{
ValueTask IAsyncDisposable.DisposeAsync()
{
}
}
class C
{
static async Task Main()
{
await using (new S2())
{
} // S2.IAsyncDisposable.DisposeAsync
}
}
请注意,优先于实现模式的 DisposeAsync
方法,并且仅当找不到模式时,才使用 IAsyncDisposable
实现。
模式 DisposeAsync
方法将在类型参数 allows ref struct
上被识别,就像目前在没有该限制的类型参数上被识别一样。
interface IMyAsyncDisposable
{
ValueTask DisposeAsync();
}
class C
{
static async Task Test<T>() where T : IMyAsyncDisposable, new(), allows ref struct
{
await using (new T())
{
} // IMyAsyncDisposable.DisposeAsync
}
}
在资源是类型参数 allows ref struct
且查找 DisposeAsync
模式方法的过程失败时,using
语句将识别并使用 IAsyncDisposable
接口的实现,而 IAsyncDisposable
属于类型参数的有效接口集。
interface IMyAsyncDisposable1
{
ValueTask DisposeAsync();
}
interface IMyAsyncDisposable2
{
ValueTask DisposeAsync();
}
class C
{
static async Task Test<T>() where T : IMyAsyncDisposable1, IMyAsyncDisposable2, IAsyncDisposable, new(), allows ref struct
{
await using (new T())
{
System.Console.Write(123);
} // IAsyncDisposable.DisposeAsync
}
}
foreach
语句
应相应地更新 https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement 部分,以合并以下内容。
当集合为 ref 结构时,foreach
语句将识别和使用 IEnumerable<T>
/IEnumerable
接口的实现。
ref struct S : IEnumerable<int>
{
IEnumerator<int> IEnumerable<int>.GetEnumerator() {...}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {...}
}
class C
{
static void Main()
{
foreach (var i in new S()) // IEnumerable<int>.GetEnumerator
{
}
}
}
模式 GetEnumerator
方法将在类型参数 allows ref struct
上被识别,就像目前在没有该限制的类型参数上被识别一样。
interface IMyEnumerable<T>
{
IEnumerator<T> GetEnumerator();
}
class C
{
static void Test<T>(T t) where T : IMyEnumerable<int>, allows ref struct
{
foreach (var i in t) // IMyEnumerable<int>.GetEnumerator
{
}
}
}
foreach
语句将识别并使用 IEnumerable<T>
/IEnumerable
接口的实现,前提是集合是 allows ref struct
的类型参数,寻找 GetEnumerator
模式方法的过程失败,并且 IEnumerable<T>
/IEnumerable
在类型参数的有效接口集中。
interface IMyEnumerable1<T>
{
IEnumerator<int> GetEnumerator();
}
interface IMyEnumerable2<T>
{
IEnumerator<int> GetEnumerator();
}
class C
{
static void Test<T>(T t) where T : IMyEnumerable1<int>, IMyEnumerable2<int>, IEnumerable<int>, allows ref struct
{
foreach (var i in t) // IEnumerable<int>.GetEnumerator
{
}
}
}
enumerator
模式将在类型参数 allows ref struct
上被识别,就像目前在没有该限制的类型参数上被识别一样。
interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct
{
TEnumerator GetEnumerator();
}
class C
{
static void Test1<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
where TEnumerator : IEnumerator, IDisposable, allows ref struct
{
foreach (var i in t) // IEnumerator.MoveNext/Current
{
}
}
static void Test2<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
where TEnumerator : IEnumerator<int>, allows ref struct
{
foreach (var i in t) // IEnumerator<int>.MoveNext/Current
{
}
}
static void Test3<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>, allows ref struct
where TEnumerator : IMyEnumerator<int>, allows ref struct
{
foreach (var i in t) // IMyEnumerator<int>.MoveNext/Current
{
}
}
}
interface IMyEnumerator<T> : System.IDisposable
{
T Current {get;}
bool MoveNext();
}
当枚举器为 ref 结构时,foreach
语句将识别和使用 IDisposable
接口的实现。
struct S1
{
public S2 GetEnumerator()
{
return new S2();
}
}
ref struct S2 : System.IDisposable
{
public int Current {...}
public bool MoveNext() {...}
void System.IDisposable.Dispose() {...}
}
class C
{
static void Main()
{
foreach (var i in new S1())
{
} // S2.System.IDisposable.Dispose()
}
}
请注意,优先于实现模式的 Dispose
方法,并且仅当找不到模式时,才使用 IDisposable
实现。
当枚举器是类型参数,并且 allows ref struct
和 IDisposable
位于其有效接口集时,foreach
语句将识别并使用 IDisposable
接口的实现。
interface ICustomEnumerator
{
int Current {get;}
bool MoveNext();
}
interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct
{
TEnumerator GetEnumerator();
}
class C
{
static void Test<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>
where TEnumerator : ICustomEnumerator, System.IDisposable, allows ref struct
{
foreach (var i in t)
{
} // System.IDisposable.Dispose()
}
}
请注意,模式 Dispose
方法不会在类型参数 allows ref struct
上被识别,因为接口(这是我们唯一可能寻找模式的地方)不是 ref 结构体。
另外,由于运行时没有提供方法来检查 allows ref struct
的类型参数是否实现了 IDisposable
接口,因此除非 IDisposable
在其有效接口集中,否则 allows ref struct
的类型参数枚举器将被禁止使用。
interface ICustomEnumerator
{
int Current {get;}
bool MoveNext();
}
interface IMyDisposable
{
void Dispose();
}
interface IGetEnumerator<TEnumerator> where TEnumerator : allows ref struct
{
TEnumerator GetEnumerator();
}
class C
{
static void Test<TEnumerable, TEnumerator>(TEnumerable t)
where TEnumerable : IGetEnumerator<TEnumerator>
where TEnumerator : ICustomEnumerator, IMyDisposable, allows ref struct
{
// error CS9507: foreach statement cannot operate on enumerators of type 'TEnumerator'
// because it is a type parameter that allows ref struct and
// it is not known at compile time to implement IDisposable.
foreach (var i in t)
{
}
}
}
await foreach
语句
应相应地更新 https://github.com/dotnet/csharpstandard/blob/standard-v6/standard/statements.md#1295-the-foreach-statement 部分,以合并以下内容。
当集合为 ref 结构时,await foreach
语句将识别和使用 IAsyncEnumerable<T>
接口的实现。
ref struct S : IAsyncEnumerable<int>
{
IAsyncEnumerator<int> IAsyncEnumerable<int>.GetAsyncEnumerator(CancellationToken token) {...}
}
class C
{
static async Task Main()
{
await foreach (var i in new S()) // S.IAsyncEnumerable<int>.GetAsyncEnumerator
{
}
}
}
模式 GetAsyncEnumerator
方法将在类型参数 allows ref struct
上被识别,就像目前在没有该限制的类型参数上被识别一样。
interface IMyAsyncEnumerable<T>
{
IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
class C
{
static async Task Test<T>() where T : IMyAsyncEnumerable<int>, allows ref struct
{
await foreach (var i in default(T)) // IMyAsyncEnumerable<int>.GetAsyncEnumerator
{
}
}
}
当集合是 allows ref struct
类型参数,查找 GetAsyncEnumerator
模式方法的过程失败,且 IAsyncEnumerable<T>
位于类型参数的有效接口集合中时,await foreach
语句将识别并使用 IAsyncEnumerable<T>
接口的实现。
interface IMyAsyncEnumerable1<T>
{
IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
interface IMyAsyncEnumerable2<T>
{
IAsyncEnumerator<int> GetAsyncEnumerator(CancellationToken cancellationToken = default);
}
class C
{
static async Task Test<T>() where T : IMyAsyncEnumerable1<int>, IMyAsyncEnumerable2<int>, IAsyncEnumerable<int>, allows ref struct
{
await foreach (var i in default(T)) // IAsyncEnumerable<int>.GetAsyncEnumerator
{
System.Console.Write(i);
}
}
}
await foreach
语句将继续禁止 ref 结构体枚举器以及 allows ref struct
的类型参数枚举器。 原因是在跨 await MoveNextAsync()
调用时必须保留枚举器。
匿名函数或方法组的委托类型
编译器将来可能会允许更多签名绑定到
System.Action<>
和System.Func<>
类型(例如,如果ref struct
类型允许类型参数)。
类型参数上带有 allows ref struct
约束的 Action<>
和 Func<>
类型将在委托签名中涉及 ref 结构体类型的更多情况下使用。
如果目标运行时支持 allows ref struct
约束,则泛型匿名委托类型将包含其类型参数的 allows ref struct
约束。 这将使那些类型参数能够替换为 ref 结构类型,以及具有 allows ref struct
约束的其他类型参数。
内联数组
语言将为访问内联数组类型的元素提供一种类型安全/引用安全的方法。 访问将以跨度为基础。 这限制了对可用作类型参数的元素类型的内联数组类型的支持。
当范围类型更改为支持引用结构的范围时,应取消对引用结构内联数组的限制。
完整性
我们希望验证 ref struct
反约束的健全性,特别是反约束的概念在一般情况下的健全性。 为此,我们希望利用现有的 C# 类型系统完整性证明。 通过定义类似于 C# 的新语言来简化此任务,但在构造中更常规。 我们将验证该模型的安全性,然后指定此语言的声音翻译。 由于这种新语言以约束为中心,因此我们将此语言称为“constraint-C#”。
必须保留的 ref 结构体安全不变式主要是 ref 结构体类型的变量不得出现在堆中。 我们可以通过约束来编码此限制。 由于约束允许替换,而不是禁止替换,因此我们将从技术上定义反函数约束:heap
。 heap
约束规定一个类型可以出现在堆中。 在“constraint-C#”中,除 ref 结构外,所有类型都满足 heap
约束。 此外,C# 中的所有现有类型参数都将降低到类型参数,并在“constraint-C#”中使用 heap
约束。
现在,假设现有 C# 是安全的,我们可以将 C# ref 结构规则传输到“constraint-C#”。
- 类的字段不能具有 ref 结构体类型。
- 静态字段不能具有 ref 结构类型。
- ref-struct 类型的变量不能转换为非 ref 结构。
- ref-struct 类型的变量不能替换为类型参数。
- ref-struct 类型的变量无法实现接口。
新规则适用于 heap
约束:
- 类的字段类型必须满足
heap
约束条件。 - 静态字段必须具有满足
heap
约束的类型。 - 带有
heap
约束的类型只有标识转换。 - ref-struct 类型的变量只能替换为没有
heap
约束的类型参数。 - Ref-struct 类型只能实现没有默认接口成员的接口。
规则(4)和(5)略有改变。 请注意,规则(4)不需要完全转换,因为我们对类型参数有一个概念,没有 heap
约束。 规则 (5) 很复杂。 接口的实现并非普遍不完整,但默认的接口方法意味着接口类型的接收器,这是一种非值类型,违反了规则 (3)。 因此,默认接口成员是不允许的。
使用这些规则,“constraint-C#”是 ref 结构安全的,支持类型替换,并支持接口实现。 下一步是将此建议中定义的语言(我们可以将其称为“allow-C#”)翻译为“constraint-C#”。 幸运的是,这是微不足道的。 降低是一个简单的语法转换。 “allow-C#“中的语法 where T : allows ref struct
在”constraint-C#“中等同于无约束,而不使用”allow clauses”等同于 heap
约束。 由于抽象语义和类型是等效的,所以“allow-C#”也是合理的。
我们还可以考虑最后一个属性:C# 中的所有类型化术语是否也在“constraint-C#”中类型化。 换句话说,我们想知道,对于 C# 中的所有术语 t
来说,降低为“constraint-C#”后的相应术语 t'
是否类型良好。 这不是一个健全性约束——在我们的目标语言中使用未类型化的术语并不会导致不安全——相反,这关乎向后兼容性。 如果我们决定使用“constraint-C#”类型来验证“allow-C#”,我们希望确认没有使任何现有的 C# 代码变得非法。
由于所有 C# 术语都是以有效的“constraint-C#”术语开始的,因此我们可以通过检查每个新的“constraint-C#”限制来验证保存。 首先,添加 heap
约束。 由于 C# 中的所有类型参数都将获取 heap
约束,因此所有现有术语都必须满足上述约束。 对于除 ref 结构之外的所有具体类型都是如此,这很合适,因为 ref 结构今天可能不会显示为类型参数。 对于所有类型参数也是如此,因为它们本身都会获取 heap
约束。 此外,由于 heap
约束是与其他所有约束的有效组合,因此这不会造成任何问题。 规则(1-5)不会提出任何问题,因为它们直接与现有的 C# 规则相对应,或者放宽了这些规则。 因此,C# 中的所有可键入术语都应在“constraint-C#”中可键入,不应引入任何键入中断性变更。
未结的问题
反约束语法
决定:使用 where T: allows ref struct
此提议选择通过扩充现有的 where
语法,使其包含 allows ref struct
来公开 ref struct
反约束。 这两者都简洁地描述了该功能,并且也是可扩展的,以在将来包括其他反约束,如指针。 还有其他值得讨论的解决方案。
首先,只需在 where
子句中选择另一种语法即可。 其他建议的选项包括:
~ref struct
:~
用作标记,表示后续的语法为反约束。include ref struct
:使用includes
而不是allows
void M<T>(T p)
where T : IDisposable, ~ref struct
{
p.Dispose();
}
第二种方法是使用一个全新的子句,明确说明接下来要扩展允许的类型集。 这种观点的支持者认为,在 where
中使用语法可能会导致阅读时的混淆。 初始建议使用以下语法:allow T: ref struct
:
void M<T>(T p)
where T : IDisposable
allow T : ref struct
{
p.Dispose();
}
where T: allows ref struct
语法在 LDM 讨论中稍微更受偏爱。
协变和逆变
决策:无新问题
为了尽可能有用,allows ref struct
类型参数必须与泛型方差兼容。 具体而言,一个参数必须同时具有协变/逆变特性以及 allows ref struct
时是合法的。 如果缺少它们,那么在 .NET 中的许多最受欢迎的 delegate
和 interface
类型(如 Func<T>
、Action<T>
、IEnumerable<T>
等)将无法使用。
经过讨论后,结论是这不是一个问题。 allows ref struct
约束只是 struct
可用作泛型参数的另一种方式。 正如普通 struct
参数删除 API 的方差一样,ref struct
也是如此。
自动应用于委托成员
决定:不自动应用
对于许多泛型 delegate
成员,语言可以自动应用 allows ref struct
,因为这纯粹是一个颠倒的变化。 考虑到对于 Func<> / Action<>
样式的委托和大多数接口定义,扩展到允许 ref struct
并没有什么坏处。 语言可以概述可安全地自动应用此反约束的规则。 这将删除手动过程,并加快此功能的采用速度。
不过,allows ref struct
的这种自动应用带来了一些问题。 首先是在多目标场景中。 代码可能会在一个目标框架中编译,而在另一个框架中可能会失败,并且没有语法上的指示来说明 API 行为为何不同的原因。
// Works in net9.0 but fails in all other TF
Func<Span<char>> func;
这很可能会导致客户混淆,而且在 net9.0
源中查看 Func<T>
中的变化也不会给客户提供任何有关变化的线索。
另一个问题是,代码中非常微妙的变化可能会导致远程离幽灵行动问题。 请考虑以下代码:
interface I1<T>
{
}
该接口符合自动应用 allows ref struct
的条件。 如果开发人员后来又添加了一个默认接口方法,那么它就突然不是默认接口方法了,而且会破坏任何已经创建了调用(如 I1<Span<char>>
)的使用者。 这是一个非常微妙的变化,很难跟踪。
二进制重大更改
将 allows ref struct
添加到现有 API 不是源中断性变更。 它纯粹是扩展 API 的允许类型集。 需要追查这是否是二进制重大更改。 不清楚更新泛型参数的属性是否构成二进制重大变更。
调用 DIM 时发出警告
编译器是否应在以下调用 M
时发出警告,因为它为运行时异常创造了机会?
interface I1
{
// Virtual method with default implementation
void M() { }
}
// Invocation of a virtual instance method with default implementation in a generic method that has the `allows ref struct`
// anti-constraint
void M<T>(T p)
where T : allows ref struct, I1
{
p.M(); // Warn?
}
但是,这在大多数情况下可能会很吵闹,并且不是非常有用。 C# 需要 ref 结构来实现所有虚拟 API。 因此,假设其他玩家遵循相同的规则,唯一的例外情况是事后添加该方法。 使用代码的作者通常不知道所有这些详细信息,并且通常无法控制代码将使用的 ref 结构。 因此,作者真正可以采取的唯一操作是禁止显示警告。
考虑事项
运行时支持
此功能需要运行时/库团队提供的多项支持:
- 阻止默认接口方法应用于
ref struct
System.Reflection.Metadata
中用于编码gpAcceptByRefLike
值的 API- 支持作为
ref struct
的泛型参数
大部分支持可能已经到位。 作为泛型参数的常规 ref struct
支持已按照此处所述实现。 可能 DIM 实现已经考虑了 ref struct
。 但每一个项目都需要追踪。
API 版本控制
允许 ref 结构体反约束
可以将 allows ref struct
反约束安全地应用于大量没有实现的泛型定义。 这意味着大多数委托、接口和 abstract
方法可以安全地将 allows ref struct
应用于其参数。 这些只是没有实现的 API 定义,因此扩展允许的类型集只会导致错误(如果它们用作不允许 ref struct
的类型参数)。
API 所有者可以依赖一个简单的规则“如果编译通过,可以认为即是安全的”。 编译器会对 allows ref struct
的任何不安全使用出错,就像对其他 ref struct
的使用一样。
同时,API 作者还应考虑版本控制的注意事项。 实质上,API 所有者应避免将 allows ref struct
添加到类型参数,其中拥有类型/成员将来可能会更改,使其与 allows ref struct
不兼容。 例如:
abstract
方法,以后可能会更改为virtual
方法abstract
类型,稍后可能会添加实现
在这种情况下,API 作者在添加 allows ref struct
时应十分谨慎,除非它们确定类型/成员演变不会以违反 ref struct
规则的方式使用 T
。
删除 allows ref struct
反约束始终是一项重大变更:源和二进制都是如此。
默认接口方法
API 作者需要注意,添加 DIMS 会中断 ref struct
实现程序,直到重新编译它们。 这类似于 现有 DIM 行为, 其中向接口添加 DIM 会中断现有实现,直到重新编译这些实现。 这意味着 API 作者在添加 DIM 时需要考虑 ref struct
实现的可能性。
创建这种情况需要三个代码组件:
interface I1
{
// 1. The addition of a DIM method to an _existing_ interface
void M() { }
}
// 2. A ref struct implementing the interface but not explicitly defining the DIM
// method
ref struct S : I1 { }
// 3. The invocation of the DIM method in a generic method that has the `allows ref struct`
// anti-constraint
void M<T>(T p)
where T : allows ref struct, I1
{
p.M();
}
需要这三个组件才能创建此特定问题。 此外,至少 (1) 和 (2) 必须位于不同的程序集中。 如果它们位于同一程序集中,则会发生编译错误。
UnscopedRef
将 [UnscopedRef]
添加到或从 interface
的成员中移除是一项会导致源代码中断的变更(并可能产生运行时问题)。 定义接口成员时,应应用该属性,但以后不应添加或删除。
Span<Span<T>>
这种功能组合不允许创建构造,例如 Span<Span<T>>
。 通过查看 Span<T>
的定义,可以更清楚地理解这一点。
readonly ref struct Span<T>
{
public readonly ref T _data;
public readonly int _length;
public Span(T[] array) { ... }
public static implicit operator Span<T>(T[]? array) { }
public static implicit operator Span<T>(ArraySegment<T> segment) { }
}
如果此类型定义包括 allows ref struct
,则需要将定义中的所有 T
实例视为可能 ref struct
类型。 这提出了两类问题。
首先,对于像 Span(T[] array)
这样的 API 和隐式运算符,T
不能是 ref struct
:它要么用作数组元素,要么用作泛型参数,而泛型参数不能是 allows ref struct
。 Span<T>
上有一些公共 API,其对 T
的使用无法与 ref struct
兼容。 这些是无法删除的公共 API,因此必须由语言合理化。 最有可能的前进道路是编译器将对 Span<T>
进行特殊处理,并在 T
的参数可能是一个 ref struct
时发出一个与这些 API 之一绑定的错误代码。
其次是语言不支持是 ref struct
的 ref
字段。 有一项设计提议允许使用该功能。 目前还不清楚该语言是否会接受这一点,也不清楚该语言的表现力是否足以处理围绕 Span<T>
的所有情景。
这两个问题都超出了这一建议的范围。
UnscopedRef 实现逻辑
在将 this
参数可视化为显式参数而不是隐式参数时,接口实现 [UnscopedRef]
规则背后的原理最容易理解。 例如,以下 struct
将 this
可视化为隐式参数(类似于 Python 如何处理它):
struct S
{
public void M(scoped ref S this) { }
}
接口成员上的 [UnscopedRef]
指明 this
在呼叫站点的生命周期中缺少 scoped
。 在实现成员上允许省略 [UnscopedRef]
,实际上是允许由 scoped ref T
参数实现一个 ref T
参数。 该语言已允许以下操作:
interface I1
{
void M(ref Span<char> span);
}
struct S : I1
{
public void M(scoped ref Span<char> span) { }
}
相关项目
相关项:
- https://github.com/dotnet/csharplang/issues/7608
- https://github.com/dotnet/csharplang/pull/7555
- https://github.com/dotnet/runtime/blob/main/docs/design/features/byreflike-generics.md
- https://github.com/dotnet/runtime/pull/67783
- https://github.com/dotnet/runtime/issues/27229#issuecomment-1537274804
- https://github.com/dotnet/runtime/issues/68002