内联数组
注意
本文是特性规范。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 ECMA 规范。
功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。
可以在有关规范的文章中了解更多有关将功能规范子块纳入 C# 语言标准的过程。
支持者问题:https://github.com/dotnet/csharplang/issues/7431
总结
提供利用 InlineArrayAttribute 功能的结构类型的一种通用且安全的消费机制。 为在 C# 类、结构体和接口中声明内联数组提供了一种通用而安全的机制。
注意:此规范的早期版本使用了术语“ref-safe-to-escape”和“safe-to-escape”,这些术语是在跨度安全功能规范中引入的。 ECMA 标准委员会分别将名称改为“ref-safe-context”和“安全上下文”。 对安全上下文的值进行了改进,以便更加一致地使用“声明块”、“函数成员”和“调用者上下文”。 小规范对这些术语使用了不同的措辞,还使用了“safe-to-return”作为“caller-context”的同义词。 此规范已更新为使用 C# 7.3 标准中的术语。
动机
此提案旨在解决 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#238-fixed-size-buffers的诸多限制。 具体来说,它的目标是允许:
- 使用 InlineArrayAttribute 特性访问结构体类型的元素;
- 在
struct
、class
或interface
中声明针对托管和非托管类型的内联数组。
并为他们提供语言安全验证。
详细设计
最近,运行时环境添加了 InlineArrayAttribute 的功能。 简而言之,用户可以声明如下结构类型:
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer
{
private object _element0;
}
运行时为 Buffer
类型提供了一种特殊的类型布局:
- 类型的大小扩展为适合包含 10 个
object
类型的元素(该数字来自 InlineArray 属性)(该类型来自结构体中唯一实例字段的类型,在本示例中为_element0
)。 - 第一个元素与实例字段和结构开头对齐
- 这些元素在内存中按顺序排列,就像数组中的元素一样。
运行时为结构中的所有元素提供常规 GC 跟踪。
本提案将此类型称为“内联数组类型”。
可以通过指针或通过 System.Runtime.InteropServices.MemoryMarshal.CreateSpan/System.Runtime.InteropServices.MemoryMarshal.CreateReadOnlySpan API 返回的 Span 实例访问内联数组类型的元素。 但是,无论是指针方法还是 API 接口,都不提供开箱即用的类型和边界检查。
语言将提供一种类型安全和引用安全的方法,用于访问内联数组类型的元素。 访问将以跨度为基础。 这限制了对可用作类型参数的元素类型的内联数组类型的支持。 例如,指针类型不能用作元素类型。 范围类型的其他示例。
获取用于内联数组类型的跨度类型实例
由于可以保证内联数组类型中的第一个元素在类型开头对齐(无间隙),编译器将使用下面的代码获取 Span
值:
MemoryMarshal.CreateSpan(ref Unsafe.As<TBuffer, TElement>(ref buffer), size);
要获取 ReadOnlySpan
值,请使用以下代码:
MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<TBuffer, TElement>(ref Unsafe.AsRef(in buffer)), size);
为了减少使用点的 IL 大小,编译器应能够将两个通用可重用的辅助工具添加到私有实现细节类型中,并在同一程序中的所有使用点中使用它们。
public static System.Span<TElement> InlineArrayAsSpan<TBuffer, TElement>(ref TBuffer buffer, int size) where TBuffer : struct
{
return MemoryMarshal.CreateSpan(ref Unsafe.As<TBuffer, TElement>(ref buffer), size);
}
public static System.ReadOnlySpan<TElement> InlineArrayAsReadOnlySpan<TBuffer, TElement>(in TBuffer buffer, int size) where TBuffer : struct
{
return MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<TBuffer, TElement>(ref Unsafe.AsRef(in buffer)), size);
}
元素访问
元素访问将被扩展以支持内联数组元素访问。
element_access 由一个 primary_no_array_creation_expression 组成,后跟一个 “[
” 标记,后跟一个 argument_list,后跟一个 “]
” 标记。 argument_list 由一个或多个参数组成,用逗号分隔。
element_access
: primary_no_array_creation_expression '[' argument_list ']'
;
不允许 element_access 的 argument_list 中包含 ref
或 out
参数。
如果以下情况至少有一种成立,则动态绑定 element_access (§11.3.3):
- primary_no_array_creation_expression 具有编译时类型
dynamic
。 - argument_list 至少有一个表达式具有编译时类型
dynamic
,并且 primary_no_array_creation_expression 不是数组类型,而且 primary_no_array_creation_expression 不是内联数组类型,或者参数列表中有多个项。
在这种情况下,编译器将 element_access 分类为类型 dynamic
的值。 运行时将应用以下规则以确定 element_access 的含义。此过程中使用的是运行时类型,而不是 primary_no_array_creation_expression 的编译时类型,以及 argument_list 表达式中的那些具有编译时类型的类型 dynamic
。 如果 primary_no_array_creation_expression 没有编译时类型 dynamic
,则如 §11.6.5中所述,元素访问将会经过有限的编译时检查。
如果 element_access 的 primary_no_array_creation_expression 是 array_type 的值,则 element_access 是数组访问 (§12.8.12.2)。 如果 element_access 的 primary_no_array_creation_expression 是内联数组类型的变量或值,并且 argument_list 由单个参数组成,则 element_access 是内联数组元素访问。 否则,primary_no_array_creation_expression 应是具有一个或多个索引器成员的类、结构或接口类型的变量或值,在这种情况下,element_access 是索引器访问 (§12.8.12.3)。
内联数组元素访问
对于内联数组元素访问,element_access 的 primary_no_array_creation_expression 必须是内联数组类型的变量或值。 此外,内联数组元素访问的 argument_list 不允许包含命名参数。 argument_list 必须包含一个表达式,且表达式必须是
- 类型为
int
,或 - 可隐式转换为
int
,或 - 可隐式转换为
System.Index
,或 - 可隐式转换为
System.Range
。
当表达式类型为 int 时
如果 primary_no_array_creation_expression 是一个可写变量,则内联数组元素访问的计算结果是一个可写变量,这与在 public ref T this[int index] { get; }
上调用 System.Span<T>
方法后由 System.Span<T> InlineArrayAsSpan
实例使用该整数值调用 等效。 为了进行 ref-safety 分析,访问的 ref-safe-context/safe-context 等效于具有签名 static ref T GetItem(ref InlineArrayType array)
的方法调用的相同上下文。
如果且仅当 primary_no_array_creation_expression 可移动时,生成的变量被视为可移动变量。
如果 primary_no_array_creation_expression 是一个只读变量,则内联数组元素访问的计算结果是一个只读变量,这与在 public ref readonly T this[int index] { get; }
上调用 System.ReadOnlySpan<T>
方法后由 System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan
实例使用该整数值调用 等效。 为了进行 ref-safety 分析,访问的 ref-safe-context/safe-context 等效于具有签名 static ref readonly T GetItem(in InlineArrayType array)
的方法调用的相同上下文。
如果且仅当 primary_no_array_creation_expression 可移动时,生成的变量被视为可移动变量。
如果 primary_no_array_creation_expression 是一个值,则内联数组元素访问的计算结果是一个值,这与在 public ref readonly T this[int index] { get; }
上调用 System.ReadOnlySpan<T>
方法后由 System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan
实例使用该整数值调用 等效。 为了进行 ref-safety 分析,访问的 ref-safe-context/safe-context 等效于具有签名 static T GetItem(InlineArrayType array)
的方法调用的相同上下文。
例如:
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10<T>
{
private T _element0;
}
void M1(Buffer10<int> x)
{
ref int a = ref x[0]; // Ok, equivalent to `ref int a = ref InlineArrayAsSpan<Buffer10<int>, int>(ref x, 10)[0]`
}
void M2(in Buffer10<int> x)
{
ref readonly int a = ref x[0]; // Ok, equivalent to `ref readonly int a = ref InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10)[0]`
ref int b = ref x[0]; // An error, `x` is a readonly variable => `x[0]` is a readonly variable
}
Buffer10<int> GetBuffer() => default;
void M3()
{
int a = GetBuffer()[0]; // Ok, equivalent to `int a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(GetBuffer(), 10)[0]`
ref readonly int b = ref GetBuffer()[0]; // An error, `GetBuffer()[0]` is a value
ref int c = ref GetBuffer()[0]; // An error, `GetBuffer()[0]` is a value
}
在声明内联数组边界之外使用常量表达式编制内联数组索引是编译时错误。
在表达式可以隐式转换为 int
时
表达式将转换为 int,然后按照 当表达式类型为 int 时部分的表达式类型时解释元素访问。
当表达式可以隐式转换为 System.Index
时
表达式被转换为 System.Index
,然后根据 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-index-support中所述转换为一个基于 int 的索引值,假设集合的长度在编译时是已知的,并且等于 primary_no_array_creation_expression 中的内联数组类型的元素个数。 然后,按照 标记所述,当表达式类型为 int 时,解释元素访问如 节所述。
当表达式可以隐式转换为 System.Range
时
如果 primary_no_array_creation_expression 是一个写入变量,则内联数组元素访问的计算结果是一个值,这与在 public Span<T> Slice (int start, int length)
上调用 System.Span<T>
方法后由 System.Span<T> InlineArrayAsSpan
实例调用 等效。
为了进行 ref-safety 分析,访问的 ref-safe-context/safe-context 等效于具有签名 static System.Span<T> GetSlice(ref InlineArrayType array)
的方法调用的相同上下文。
如果 primary_no_array_creation_expression 是一个只读变量,则内联数组元素访问的计算结果是一个值,这与在 public ReadOnlySpan<T> Slice (int start, int length)
上调用 System.ReadOnlySpan<T>
方法后由 System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan
实例调用 等效。
为了进行 ref-safety 分析,访问的 ref-safe-context/safe-context 等效于具有签名 static System.ReadOnlySpan<T> GetSlice(in InlineArrayType array)
的方法调用的相同上下文。
如果 primary_no_array_creation_expression 为值,则报告错误。
Slice
方法调用的参数是根据在 System.Range
所述的索引表达式转换为 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-range-support 后进行计算的,假定集合的长度在编译时已知,并且等于 primary_no_array_creation_expression 中内联数组类型的元素数量。
如果编译时已知 start
为 0,且 length
小于或等于内联数组类型中的元素数量,则编译器可以省略 Slice
调用。 如果在编译时知道切片超出了内联数组的边界,则编译器也会报错。
例如:
void M1(Buffer10<int> x)
{
System.Span<int> a = x[..]; // Ok, equivalent to `System.Span<int> a = InlineArrayAsSpan<Buffer10<int>, int>(ref x, 10).Slice(0, 10)`
}
void M2(in Buffer10<int> x)
{
System.ReadOnlySpan<int> a = x[..]; // Ok, equivalent to `System.ReadOnlySpan<int> a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10).Slice(0, 10)`
System.Span<int> b = x[..]; // An error, System.ReadOnlySpan<int> cannot be converted to System.Span<int>
}
Buffer10<int> GetBuffer() => default;
void M3()
{
_ = GetBuffer()[..]; // An error, `GetBuffer()` is a value
}
转换
将从表达式中添加新的转换(内联数组转换)。 内联数组的转换是标准转换。
从内联数组类型的表达式到以下类型有隐式转换:
System.Span<T>
System.ReadOnlySpan<T>
但是,将只读变量转换为 System.Span<T>
或将值转换为任一类型都是错误的。
例如:
void M1(Buffer10<int> x)
{
System.ReadOnlySpan<int> a = x; // Ok, equivalent to `System.ReadOnlySpan<int> a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10)`
System.Span<int> b = x; // Ok, equivalent to `System.Span<int> b = InlineArrayAsSpan<Buffer10<int>, int>(ref x, 10)`
}
void M2(in Buffer10<int> x)
{
System.ReadOnlySpan<int> a = x; // Ok, equivalent to `System.ReadOnlySpan<int> a = InlineArrayAsReadOnlySpan<Buffer10<int>, int>(in x, 10)`
System.Span<int> b = x; // An error, readonly mismatch
}
Buffer10<int> GetBuffer() => default;
void M3()
{
System.ReadOnlySpan<int> a = GetBuffer(); // An error, ref-safety
System.Span<int> b = GetBuffer(); // An error, ref-safety
}
为了进行 ref-safety 分析,转换的 safe-context 等同于在方法签名为 或 static System.Span<T> Convert(ref InlineArrayType array)
的调用中的 static System.ReadOnlySpan<T> Convert(in InlineArrayType array)
。
列表模式
内联数组类型的实例将不支持列表模式。
明确分配检查
常规定值赋值规则适用于具有内联数组类型的变量。
集合文本
内联数组类型的实例是 spread_element 中的有效表达式。
C# 12 中没有提供以下功能。 这仍是一个未决提案。 本例中的代码会生成 CS9174:
内联数组类型是集合表达式的有效可构造集合目标类型。 例如:
Buffer10<int> b = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // initializes user-defined inline array
集合字面量的长度必须与目标内联数组类型的长度一致。 如果字面量的长度在编译时已知,但与目标长度不匹配,则会报错。 否则,在运行时一旦遇到不匹配,将会引发异常。 确切的异常类型待定。 一些候选项包括:System.NotSupportedException、System.InvalidOperationException。
验证 InlineArrayAttribute 应用程序
编译器将对 InlineArrayAttribute 应用程序的以下方面进行验证:
- 目标类型是非记录结构体
- 目标类型只有一个字段
- 指定的长度 > 0
- 目标结构未指定显式布局
内联数组元素在对象初始化器中
默认情况下,形式 中的 '[' argument_list ']'
不支持元素初始化(请参阅 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#128173-object-initializers):
static C M2() => new C() { F = {[0] = 111} }; // error CS1913: Member '[0]' cannot be initialized. It is not a field or property.
class C
{
public Buffer10<int> F;
}
但是,如果内联数组类型显式地定义了合适的索引器,对象初始值设定项将会使用它:
static C M2() => new C() { F = {[0] = 111} }; // Ok, indexer is invoked
class C
{
public Buffer10<int> F;
}
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10<T>
{
private T _element0;
public T this[int i]
{
get => this[i];
set => this[i] = value;
}
}
Foreach 语句
foreach 语句 将进行调整,以允许在 foreach 语句中使用内联数组类型作为集合。
例如:
foreach (var a in getBufferAsValue())
{
WriteLine(a);
}
foreach (var b in getBufferAsWritableVariable())
{
WriteLine(b);
}
foreach (var c in getBufferAsReadonlyVariable())
{
WriteLine(c);
}
Buffer10<int> getBufferAsValue() => default;
ref Buffer10<int> getBufferAsWritableVariable() => default;
ref readonly Buffer10<int> getBufferAsReadonlyVariable() => default;
等效于:
Buffer10<int> temp = getBufferAsValue();
foreach (var a in (System.ReadOnlySpan<int>)temp)
{
WriteLine(a);
}
foreach (var b in (System.Span<int>)getBufferAsWritableVariable())
{
WriteLine(b);
}
foreach (var c in (System.ReadOnlySpan<int>)getBufferAsReadonlyVariable())
{
WriteLine(c);
}
我们将支持 foreach
在内联数组上的使用,即使由于跨距类型的参与,这在 async
方法中受到限制。
开放式设计问题
替代方案
内联数组类型语法
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/types.md#821-general 处的语法将进行如下调整:
array_type
: non_array_type rank_specifier+
;
rank_specifier
: '[' ','* ']'
+ | '[' constant_expression ']'
;
constant_expression 的类型必须隐式转换为 int
类型,且值必须是非零的正整数。
https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/arrays.md#1721-general 部分的相关内容将进行如下调整。
数组类型的语法生产在 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/types.md#821-general中提供。
数组类型表示为 non_array_type,并跟随一个或多个 rank_specifier。
non_array_type 是任何不属于 array_type 的类型。
数组类型的秩由 array_type 中最左边的 rank_specifier 给出:rank_specifier 表示数组的秩为 1 加上 rank_specifier 中的“,
”标记数。
数组类型的元素类型是对该类型删除最左侧 rank_specifier 后得到的类型。
- 形式为
T[ constant_expression ]
的数组类型是匿名内联数组类型,其长度由 constant_expression 和非数组元素类型T
表示。 - 形式为
T[ constant_expression ][R₁]...[Rₓ]
的数组类型是匿名内联数组类型,其长度由 constant_expression 和元素类型T[R₁]...[Rₓ]
表示。 - 一个形如
T[R]
的数组类型(其中 R 不是 constant_expression)是一个具有R
个维度和非数组元素类型T
的常规数组类型。 - 形式为
T[R][R₁]...[Rₓ]
的数组类型(其中 R 不是 constant_expression)是具有维度R
的规则数组类型,其元素类型为T[R₁]...[Rₓ]
。
实际上,rank_specifier 从左到右读取 ,然后 最终的非数组元素类型。
示例:
int[][,,][,]
中的类型是int
二维数组的三维数组的单维数组。 结束示例
在运行时,常规数组类型的值可以 null
或对该数组类型的实例的引用。
注意:按照 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/arrays.md#176-array-covariance 的规则,值也可以是对协变数组类型的引用。 尾注
匿名内联数组类型是由编译器合成的内联数组类型,具有内部可访问性。 元素类型必须是可用作类型参数的类型。 与显式声明的内联数组类型不同,匿名内联数组类型不能用名称引用,只能用 array_type 语法来引用。 在同一程序上下文中,任何两个 array_type 表示相同元素类型且具有相同长度的内联数组类型,均指向相同的匿名内联数组类型。
除了内部访问性之外,编译器还将通过在签名中对匿名内联数组类型引用应用一个必要的自定义修饰符(具体类型待定),来防止在跨程序集边界使用匿名内联数组类型的 API。
数组创建表达式
array_creation_expression
: 'new' non_array_type '[' expression_list ']' rank_specifier*
array_initializer?
| 'new' array_type array_initializer
| 'new' rank_specifier array_initializer
;
鉴于当前语法,使用 constant_expression 代替 expression_list 已经具有分配指定长度的普通单维数组类型的意义。 因此,array_creation_expression 将继续表示常规数组的分配。
但是,rank_specifier 的新形式可用于将匿名内联数组类型纳入已分配数组的元素类型中。
例如,下面的表达式创建了一个长度为 2 的正则表达式数组,其元素类型为元素类型为 int、长度为 5 的匿名内联数组类型:
new int[2][5];
new int[][5] {default, default};
new [] {default(int[5]), default(int[5])};
数组初始值设定项
C# 12 中未实现数组初始化器。 本部分仍是一个积极的提案。
将调整 数组初始化器 部分,以允许使用 array_initializer 初始化内联数组类型(无需对语法进行更改)。
array_initializer
: '{' variable_initializer_list? '}'
| '{' variable_initializer_list ',' '}'
;
variable_initializer_list
: variable_initializer (',' variable_initializer)*
;
variable_initializer
: expression
| array_initializer
;
内联数组的长度必须由目标类型显式提供。
例如:
int[5] a = {1, 2, 3, 4, 5}; // initializes anonymous inline array of length 5
Buffer10<int> b = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // initializes user-defined inline array
var c = new int[][] {{11, 12}, {21, 22}, {31, 32}}; // An error for the nested array initializer
var d = new int[][2] {{11, 12}, {21, 22}, {31, 32}}; // An error for the nested array initializer
详细设计(选项 2)
请注意,在本提案中,“固定大小缓冲区”指的是建议的“安全固定大小缓冲区”功能,而不是 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#238-fixed-size-buffers 中描述的缓冲区。
在此设计中,固定大小的缓冲区类型不会受到语言的一般特殊处理。 有一种特殊的语法来声明表示固定大小的缓冲区的成员,以及有关使用这些成员的新规则。 从语言的角度来看,它们并不是字段。
将扩展 https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#155-fields 中 variable_declarator 的语法,以允许指定缓冲区的大小:
field_declaration
: attributes? field_modifier* type variable_declarators ';'
;
field_modifier
: 'new'
| 'public'
| 'protected'
| 'internal'
| 'private'
| 'static'
| 'readonly'
| 'volatile'
| unsafe_modifier // unsafe code support
;
variable_declarators
: variable_declarator (',' variable_declarator)*
;
variable_declarator
: identifier ('=' variable_initializer)?
+ | fixed_size_buffer_declarator
;
fixed_size_buffer_declarator
: identifier '[' constant_expression ']'
;
fixed_size_buffer_declarator 引入了给定元素类型的固定大小缓冲区。
缓冲区元素类型是 field_declaration
中指定的 type 类型。 固定大小缓冲区声明符引入了一个新成员,由一个命名该成员的标识符和一个用 [
和 ]
符号括起来的常量表达式组成。 常量表达式表示由固定大小缓冲区声明符引入的成员中的元素数量。 常量表达式的类型必须隐式转换为 int
类型,且值必须是非零的正整数。
固定大小缓冲区的元素应在内存中按顺序排列,就像数组中的元素一样。
接口中包含 fixed_size_buffer_declarator 的 field_declaration 必须有 static
修饰符。
根据情况(下面详细说明),对固定大小缓冲区成员的访问被归类为 System.ReadOnlySpan<S>
或 System.Span<S>
的值(绝不是变量),其中 S 是固定大小缓冲区的元素类型。 这两种类型都提供索引器,用于返回对具有适当“只读性”的特定元素的引用,这可以防止在语言规则不允许时直接分配给元素。
这样就会将可用作固定大小缓冲区元素类型的类型集合限制为可用作类型参数的类型。 例如,指针类型不能用作元素类型。
生成的跨度实例的长度等于固定大小缓冲区上声明的大小。 声明的固定大小缓存区之外使用常量表达式对范围进行索引会导致编译时错误。
值的安全上下文将与容器的 安全上下文相等,这就像在作为字段访问支持数据时一样。
表达式中的固定大小缓冲区
固定大小缓冲区成员的成员查找与字段成员的成员查找完全相同。
可以在表达式中使用 simple_name 或 member_access 引用固定大小的缓冲区。
当以简单名称引用固定大小缓冲区成员实例时,其效果与成员访问形式 this.I
相同,其中 I
是固定大小缓冲区成员。 当以简单名称引用静态固定大小缓冲区成员时,其效果与成员访问形式 E.I
相同,其中 I
是固定大小缓冲区成员,E
是声明类型。
非只读固定大小的缓冲区
在形如 E.I
的成员访问中,如果 E
是结构类型,并且在该结构类型中查找 I
的成员时标识出一个非只读的实例固定大小成员,那么 E.I
会被计算并按如下方式分类:
- 如果
E
被归类为值,那么E.I
只能用作 primary_no_array_creation_expression,当进行 元素访问 时,索引是System.Index
类型或隐式转换为 int 的类型。元素访问的结果是位于指定位置的固定大小成员的元素,归类为值。 - 否则,如果
E
被归类为只读变量,并且表达式的结果被归类为System.ReadOnlySpan<S>
类型的值,其中 S 是I
的元素类型。 该值可用于访问成员的元素。 - 否则,
E
将被归类为可写变量,表达式的结果将被归类为System.Span<S>
类型的值,其中 S 是I
的元素类型。 该值可用于访问成员的元素。
在形式为 E.I
的成员访问中,如果 E
是一个类类型,并且在该类类型中对 I
的成员查找识别出一个非只读的实例固定大小成员,那么会计算 E.I
,并将其分类为类型 System.Span<S>
的值,其中 S 是 I
的元素类型。
在表单 E.I
的成员访问中,如果成员查找 I
标识非只读静态固定大小成员,则计算 E.I
并将其分类为类型 System.Span<S>
的值,其中 S 是 I
的元素类型。
只读固定大小的缓冲区
当 field_declaration 包含 readonly
修饰符时,fixed_size_buffer_declarator引入的成员是只读固定大小的缓冲区。
对只读固定大小缓冲区元素的直接赋值只能在同一类型的实例构造函数、init 成员或静态构造函数中进行。
具体而言,只有在以下情况下允许将值直接赋给只读固定大小缓冲区的元素:
- 对于实例成员,在包含成员声明的类型的实例构造函数或 init 成员中;对于静态成员,在包含成员声明的类型的静态构造函数中。 在这些情况下,它是唯一有效的场景,可以将只读固定大小的缓冲区元素作为
out
或ref
参数传递。
尝试将一个值赋给只读的固定大小缓冲区的元素,或者在其它上下文中将其作为 out
或 ref
参数传递时,会导致编译时错误。
这是通过以下方式实现的。
对只读固定大小的缓冲区的成员访问进行评估并分类如下:
- 在形式
E.I
的成员访问中,如果E
是结构类型,且E
被归类为值,那么E.I
只能用作元素访问 的 primary_no_array_creation_expression,其索引为System.Index
类型或能隐式转换为 int。元素访问的结果是位于指定位置、固定大小的成员的元素,并被归类为值。 - 如果访问发生在允许对只读固定大小缓冲区的元素直接赋值的上下文中,表达式的结果将被归类为
System.Span<S>
类型的值,其中 S 是固定大小缓冲区的元素类型。 该值可用于访问成员的元素。 - 否则,表达式将被归类为
System.ReadOnlySpan<S>
类型的值,其中 S 是固定大小缓冲区的元素类型。 该值可用于访问成员的元素。
明确分配检查
固定大小的缓冲区不受定值赋值检查的限制,在对结构类型变量进行定值赋值检查时,固定大小的缓冲区成员将被忽略。
如果固定大小缓冲区成员是静态的,或者固定大小缓冲区成员的最外层包含结构变量是静态变量、类实例的实例变量或数组元素,则固定大小缓冲区的元素会自动初始化为默认值。 在所有其他情况下,固定大小缓冲区的初始内容都是未定义的。
元数据
元数据输出和代码生成
编译器将在元数据编码中依赖最近添加的 System.Runtime.CompilerServices.InlineArrayAttribute
。
固定大小的缓冲区,如下面的伪代码:
// Not valid C#
public partial class C
{
public int buffer1[10];
public readonly int buffer2[10];
}
将作为特殊修饰结构类型的字段发出。
等效的 C# 代码如下:
public partial class C
{
public Buffer10<int> buffer1;
public readonly Buffer10<int> buffer2;
}
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10<T>
{
private T _element0;
[UnscopedRef]
public System.Span<T> AsSpan()
{
return System.Runtime.InteropServices.MemoryMarshal.CreateSpan(ref _element0, 10);
}
[UnscopedRef]
public readonly System.ReadOnlySpan<T> AsReadOnlySpan()
{
return System.Runtime.InteropServices.MemoryMarshal.CreateReadOnlySpan(
ref System.Runtime.CompilerServices.Unsafe.AsRef(in _element0), 10);
}
}
类型及其成员的实际命名约定尚待确定。 该框架可能会包含一组预定义的“缓冲区”类型,涵盖一组有限的缓冲区大小。 如果不存在预定义类型,编译器将在构建模块时合成该类型。 生成的类型名称将是“易于表述的”,以支持其他语言的使用。
为访问生成的代码,例如:
public partial class C
{
void M1(int val)
{
buffer1[1] = val;
}
int M2()
{
return buffer2[1];
}
}
将等效于:
public partial class C
{
void M1(int val)
{
buffer.AsSpan()[1] = val;
}
int M2()
{
return buffer2.AsReadOnlySpan()[1];
}
}
元数据导入
当编译器导入 T 类型的字段声明,并且满足以下所有条件时:
- T 是使用
InlineArray
属性修饰的结构类型,并且 - T 中声明的第一个实例字段的类型是 F,
- 在
public System.Span<F> AsSpan()
T中有一个 ,并且 - T 内有
public readonly System.ReadOnlySpan<T> AsReadOnlySpan()
或public System.ReadOnlySpan<T> AsReadOnlySpan()
。
字段将被视为元素类型为 F 的 C# 固定大小缓冲区。否则,字段将被视为 T 类型的常规字段。
方法或属性组(如语言中的方法)
一个想法是像对待方法组那样对待这些成员,因为它们本身并非自动成为一个值,但如果需要可以转换为一个值。 下面是其工作原理:
- 安全的固定大小缓冲区访问具有自己的分类(例如方法组和 lambda)
- 它们可以作为语言操作直接索引(而不是通过跨度类型)来生成变量(如果缓冲区位于只读上下文中,则该变量是只读的,就像结构的字段一样)
- 它们具有从表达式到
Span<T>
和ReadOnlySpan<T>
的隐式转换,但如果前者处于只读上下文中,则使用前者是错误的 - 他们的自然类型是
ReadOnlySpan<T>
,因此,如果他们参与类型推理(如 var、best-common-type 或 generic),这也是它们的贡献。
C/C++ 固定大小缓冲区
C/C++ 对固定大小缓冲区的概念有所不同。 例如,有一种“零长度固定大小缓冲区”的概念,通常用来表示数据是“可变长度”的。 这项提案的目标并不是为了实现与其的互操作性。
LDM 会议
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-04-03.md#fixed-size-buffers
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-04-10.md#fixed-size-buffers
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-01.md#fixed-size-buffers
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#inline-arrays
- https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-17.md#inline-arrays
- https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-06-17.md#inline-arrays-as-record-structs