内联数组

注意

本文是特性规范。 该规范充当该功能的设计文档。 它包括建议的规范更改,以及功能设计和开发过程中所需的信息。 这些文章将发布,直到建议的规范更改最终确定并合并到当前的 ECMA 规范中。

功能规范与已完成的实现之间可能存在一些差异。 这些差异记录在相关的语言设计会议 (LDM) 说明中。

可以在 规范一文中详细了解将功能规范采用 C# 语言标准的过程。

总结

提供利用 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 特性访问结构体类型的元素;
  • structclassinterface中声明针对托管和非托管类型的内联数组。

并为它们提供语言安全验证。

详细设计

最近,运行时环境添加了 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_accessargument_list 中包含 refout 参数。

如果以下情况至少有一种成立,则动态绑定 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_accessprimary_no_array_creation_expressionarray_type 的值,则 element_access 是数组访问 (§12.8.12.2)。 如果 element_accessprimary_no_array_creation_expression 是内联数组类型的变量或值,并且 argument_list 由单个参数组成,则 element_access 是内联数组元素访问。 否则,primary_no_array_creation_expression 应是具有一个或多个索引器成员的类、结构或接口类型的变量或值,在这种情况下,element_access 是索引器访问 (§12.8.12.3)。

内联数组元素访问

对于内联数组元素访问,element_accessprimary_no_array_creation_expression 必须是内联数组类型的变量或值。 此外,内联数组元素访问的 argument_list 不允许包含命名参数。 argument_list 必须包含单个表达式,并且表达式必须是

  • 类型为 int,或
  • 可隐式转换为 int,或
  • 可隐式转换为 System.Index,或
  • 可隐式转换为 System.Range
当表达式类型为 int 时

如果 primary_no_array_creation_expression 是一个可写变量,则内联数组元素访问的计算结果是一个可写变量,这与在 primary_no_array_creation_expression 上调用 System.Span<T> InlineArrayAsSpan 方法后由 System.Span<T> 实例使用该整数值调用 public ref T this[int index] { get; } 等效。 为了进行 ref-safety 分析,访问的 ref-safe-context/safe-context 等效于具有签名 static ref T GetItem(ref InlineArrayType array)的方法调用的相同上下文。 如果且仅当 primary_no_array_creation_expression 可移动时,生成的变量被视为可移动变量。

如果 primary_no_array_creation_expression 是一个只读变量,则内联数组元素访问的计算结果是一个只读变量,这与在 primary_no_array_creation_expression 上调用 System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan 方法后由 System.ReadOnlySpan<T> 实例使用该整数值调用 public ref readonly T this[int index] { get; } 等效。 为了进行 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 是一个值,则内联数组元素访问的计算结果是一个值,这与在 primary_no_array_creation_expression 上调用 System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan 方法后由 System.ReadOnlySpan<T> 实例使用该整数值调用 public ref readonly T this[int index] { get; } 等效。 为了进行 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 是一个写入变量,则内联数组元素访问的计算结果是一个值,这与在 primary_no_array_creation_expression 上调用 System.Span<T> InlineArrayAsSpan 方法后由 System.Span<T> 实例调用 public Span<T> Slice (int start, int length) 等效。 为了进行 ref-safety 分析,访问的 ref-safe-context/safe-context 等效于具有签名 static System.Span<T> GetSlice(ref InlineArrayType array)的方法调用的相同上下文。

如果 primary_no_array_creation_expression 是一个只读变量,则内联数组元素访问的计算结果是一个值,这与在 primary_no_array_creation_expression 上调用 System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan 方法后由 System.ReadOnlySpan<T> 实例调用 public ReadOnlySpan<T> Slice (int start, int length) 等效。 为了进行 ref-safety 分析,访问的 ref-safe-context/safe-context 等效于具有签名 static System.ReadOnlySpan<T> GetSlice(in InlineArrayType array)的方法调用的相同上下文。

如果 primary_no_array_creation_expression 为值,则报告错误。

Slice 方法调用的参数是根据在 https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-range-support 所述的索引表达式转换为 System.Range 后进行计算的,假定集合的长度在编译时已知,并且等于 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) 的调用中的 safe-context

列表模式

内联数组类型的实例将不支持列表模式

明确分配检查

常规明确赋值规则适用于具有内联数组类型的变量。

集合文本

内联数组类型的实例是 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 ']' 中的 initializer_target 不支持元素初始化(请参阅 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 指示数组是一个数组,其排名为一,加上 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-fieldsvariable_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中指定的 类型。 固定大小的缓冲区声明符引入了一个新成员,由一个标识符组成,该标识符命名成员,后跟一个包含在 [] 标记中的常量表达式。 常量表达式表示该固定大小的缓冲区声明符引入的成员中的元素数。 常量表达式的类型必须隐式转换为类型 int,并且该值必须是非零正整数。

固定大小的缓冲区的元素应按顺序在内存中布局,就像它们是数组的元素一样。

接口中包含 fixed_size_buffer_declaratorfield_declaration 必须有 static 修饰符。

根据情况(下面详细说明),对固定大小缓冲区成员的访问被归类为 System.ReadOnlySpan<S>System.Span<S>的值(绝不是变量),其中 S 是固定大小缓冲区的元素类型。 这两种类型都提供索引器,用于返回对具有适当“只读性”的特定元素的引用,这可以防止在语言规则不允许时直接分配给元素。

这会将可用作固定大小的缓冲区元素类型的类型集限制为可用作类型参数的类型。 例如,指针类型不能用作元素类型。

生成的跨度实例的长度将等于固定大小缓冲区上声明的大小。 声明的固定大小缓存区之外使用常量表达式对范围进行索引会导致编译时错误。

值的安全上下文将与容器的 安全上下文相等,这就像在作为字段访问支持数据时一样。

表达式中的固定大小缓冲区

固定大小的缓冲区成员的成员查找与字段的成员查找完全相同。

可以使用 simple_namemember_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 成员中;对于静态成员,在包含成员声明的类型的静态构造函数中。 在这些情况下,它是唯一有效的场景,可以将只读固定大小的缓冲区元素作为 outref 参数传递。

尝试将一个值赋给只读的固定大小缓冲区的元素,或者在其它上下文中将其作为 outref 参数传递时,会导致编译时错误。 这是通过以下实现的。

对只读固定大小的缓冲区的成员访问进行评估并分类如下:

  • 在形式 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()

字段将被视为 C# 固定大小的缓冲区,其元素类型 F。否则,字段将被视为类型为 T的常规字段。

方法或属性组(如语言中的方法)

一个想法是像对待方法组那样对待这些成员,因为它们本身并非自动成为一个值,但如果需要可以转换为一个值。 下面是工作原理:

  • 安全的固定大小缓冲区访问具有自己的分类(例如方法组和 lambda)
  • 它们可以作为语言操作直接索引(而不是通过跨度类型)来生成变量(如果缓冲区位于只读上下文中,则该变量是只读的,就像结构的字段一样)
  • 它们具有从表达式到 Span<T>ReadOnlySpan<T>的隐式转换,但如果前者处于只读上下文中,则使用前者是错误的
  • 他们的自然类型是 ReadOnlySpan<T>,因此,如果他们参与类型推理(例如 var,最佳通用类型或泛型),这就是他们的贡献。

C/C++固定大小的缓冲区

C/C++有不同的固定大小缓冲区概念。 例如,有一种“零长度固定大小的缓冲区”的概念,它通常用作指示数据为“可变长度”的方法。 这项提案的目标并不是为了实现与其的互操作性。

LDM 会议