Inline Arrays

Note

This article is a feature specification. The specification serves as the design document for the feature. It includes proposed specification changes, along with information needed during the design and development of the feature. These articles are published until the proposed spec changes are finalized and incorporated in the current ECMA specification.

There may be some discrepancies between the feature specification and the completed implementation. Those differences are captured in the pertinent language design meeting (LDM) notes.

You can learn more about the process for adopting feature speclets into the C# language standard in the article on the specifications.

Summary

Provide a general-purpose and safe mechanism for consuming struct types utilizing InlineArrayAttribute feature. Provide a general-purpose and safe mechanism for declaring inline arrays within C# classes, structs, and interfaces.

Note: Previous versions of this spec used the terms "ref-safe-to-escape" and "safe-to-escape", which were introduced in the Span safety feature specification. The ECMA standard committee changed the names to "ref-safe-context" and "safe-context", respectively. The values of the safe context have been refined to use "declaration-block", "function-member", and "caller-context" consistently. The speclets had used different phrasing for these terms, and also used "safe-to-return" as a synonym for "caller-context". This speclet has been updated to use the terms in the C# 7.3 standard.

Motivation

This proposal plans to address the many limitations of https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#228-fixed-size-buffers. Specifically it aims to allow:

  • accessing elements of struct types utilizing InlineArrayAttribute feature;
  • the declaration of inline arrays for managed and unmanaged types in a struct, class, or interface.

And provide language safety verification for them.

Detailed Design

Recently runtime added InlineArrayAttribute feature. In short, a user can declare a structure type like the following:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer
{
    private object _element0;
}

Runtime provides a special type layout for the Buffer type:

  • The size of the type is extended to fit 10 (the number comes from the InlineArray attribute) elements of object type (the type comes from the type of the only instance field in the struct, _element0 in this example).
  • The first element is aligned with the instance field and with the beginning of the struct
  • The elements are laid out sequentially in memory as though they are elements of an array.

Runtime provides regular GC tracking for all elements in the struct.

This proposal will refer to types like this as "inline array types".

Elements of an inline array type can be accessed through pointers or through span instances returned by System.Runtime.InteropServices.MemoryMarshal.CreateSpan/System.Runtime.InteropServices.MemoryMarshal.CreateReadOnlySpan APIs. However, neither the pointer approach, nor the APIs provide type and bounds checking out of the box.

Language will provide a type-safe/ref-safe way for accessing elements of inline array types. The access will be span based. This limits support to inline array types with element types that can be used as a type argument. For example, a pointer type cannot be used as an element type. Other examples the span types.

Obtaining instances of span types for an inline array type

Since there is a guarantee that the first element in an inline array type is aligned at the beginning of the type (no gap), compiler will use the following code to get a Span value:

MemoryMarshal.CreateSpan(ref Unsafe.As<TBuffer, TElement>(ref buffer), size)

And the following code to get a ReadOnlySpan value:

MemoryMarshal.CreateReadOnlySpan(ref Unsafe.As<TBuffer, TElement>(ref Unsafe.AsRef(in buffer)), size)

In order to reduce IL size at use sites compiler should be able to add two generic reusable helpers into private implementation detail type and use them across all use sites in the same program.

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

The Element access will be extended to support inline array element access.

An element_access consists of a primary_no_array_creation_expression, followed by a “[” token, followed by an argument_list, followed by a “]” token. The argument_list consists of one or more arguments, separated by commas.

element_access
    : primary_no_array_creation_expression '[' argument_list ']'
    ;

The argument_list of an element_access is not allowed to contain ref or out arguments.

An element_access is dynamically bound (§11.3.3) if at least one of the following holds:

  • The primary_no_array_creation_expression has compile-time type dynamic.
  • At least one expression of the argument_list has compile-time type dynamic and the primary_no_array_creation_expression does not have an array type, and the primary_no_array_creation_expression does not have an inline array type or there is more than one item in the argument list.

In this case, the compiler classifies the element_access as a value of type dynamic. The rules below to determine the meaning of the element_access are then applied at run-time, using the run-time type instead of the compile-time type of those of the primary_no_array_creation_expression and argument_list expressions which have the compile-time type dynamic. If the primary_no_array_creation_expression does not have compile-time type dynamic, then the element access undergoes a limited compile-time check as described in §11.6.5.

If the primary_no_array_creation_expression of an element_access is a value of an array_type, the element_access is an array access (§11.7.10.2). If the primary_no_array_creation_expression of an element_access is a variable or value of an inline array type and the argument_list consists of a single argument, the element_access is an inline array element access. Otherwise, the primary_no_array_creation_expression shall be a variable or value of a class, struct, or interface type that has one or more indexer members, in which case the element_access is an indexer access (§11.7.10.3).

Inline array element access

For an inline array element access, the primary_no_array_creation_expression of the element_access must be a variable or value of an inline array type. Furthermore, the argument_list of an inline array element access is not allowed to contain named arguments. The argument_list must contain a single expression, and the expression must be

  • of type int, or
  • implicitly convertible to int, or
  • implicitly convertible to System.Index, or
  • implicitly convertible to System.Range.
When the expression type is int

If primary_no_array_creation_expression is a writable variable, the result of evaluating an inline array element access is a writable variable equivalent to invoking public ref T this[int index] { get; } with that integer value on an instance of System.Span<T> returned by System.Span<T> InlineArrayAsSpan method on primary_no_array_creation_expression. For the purpose of ref-safety analysis the ref-safe-context/safe-context of the access are equivalent to the same for an invocation of a method with the signature static ref T GetItem(ref InlineArrayType array). The resulting variable is considered movable if and only if primary_no_array_creation_expression is movable.

If primary_no_array_creation_expression is a readonly variable, the result of evaluating an inline array element access is a readonly variable equivalent to invoking public ref readonly T this[int index] { get; } with that integer value on an instance of System.ReadOnlySpan<T> returned by System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan method on primary_no_array_creation_expression. For the purpose of ref-safety analysis the ref-safe-context/safe-context of the access are equivalent to the same for an invocation of a method with the signature static ref readonly T GetItem(in InlineArrayType array). The resulting variable is considered movable if and only if primary_no_array_creation_expression is movable.

If primary_no_array_creation_expression is a value, the result of evaluating an inline array element access is a value equivalent to invoking public ref readonly T this[int index] { get; } with that integer value on an instance of System.ReadOnlySpan<T> returned by System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan method on primary_no_array_creation_expression. For the purpose of ref-safety analysis the ref-safe-context/safe-context of the access are equivalent to the same for an invocation of a method with the signature static T GetItem(InlineArrayType array).

For example:

[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
}

Indexing into an inline array with a constant expression outside of the declared inline array bounds is a compile time error.

When the expression is implicitly convertible to int

The expression is converted to int and then the element access is interpreted as described in When the expression type is int section.

When the expression implicitly convertible to System.Index

The expression is converted to System.Index, which is then transformed to an int-based index value as described at https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-index-support, assuming that the length of the collection is known at compile time and is equal to the amount of elements in the inline array type of the primary_no_array_creation_expression. Then the element access is interpreted as described in When the expression type is int section.

When the expression implicitly convertible to System.Range

If primary_no_array_creation_expression is a writable variable, the result of evaluating an inline array element access is a value equivalent to invoking public Span<T> Slice (int start, int length) on an instance of System.Span<T> returned by System.Span<T> InlineArrayAsSpan method on primary_no_array_creation_expression. For the purpose of ref-safety analysis the ref-safe-context/safe-context of the access are equivalent to the same for an invocation of a method with the signature static System.Span<T> GetSlice(ref InlineArrayType array).

If primary_no_array_creation_expression is a readonly variable, the result of evaluating an inline array element access is a value equivalent to invoking public ReadOnlySpan<T> Slice (int start, int length) on an instance of System.ReadOnlySpan<T> returned by System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan method on primary_no_array_creation_expression. For the purpose of ref-safety analysis the ref-safe-context/safe-context of the access are equivalent to the same for an invocation of a method with the signature static System.ReadOnlySpan<T> GetSlice(in InlineArrayType array).

If primary_no_array_creation_expression is a value, an error is reported.

The arguments for the Slice method invocation are calculated from the index expression converted toSystem.Range as described at https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-range-support, assuming that the length of the collection is known at compile time and is equal to the amount of elements in the inline array type of the primary_no_array_creation_expression.

Compiler can omit the Slice call if it is known at compile time that start is 0 and length is less or equal to the amount of elements in the inline array type. Compiler can also report an error if it is known at compile time that slicing goes out of inline array bounds.

For example:

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
}

Conversions

A new conversion, an inline array conversion, from expression will be added. The inline array conversion is a standard conversion.

There is an implicit conversion from expression of an inline array type to the following types:

  • System.Span<T>
  • System.ReadOnlySpan<T>

However, converting a readonly variable to System.Span<T> or converting a value to either type is an error.

For example:

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
}

For the purpose of ref-safety analysis the safe-context of the conversion is equivalent to safe-context for an invocation of a method with the signature static System.Span<T> Convert(ref InlineArrayType array), or static System.ReadOnlySpan<T> Convert(in InlineArrayType array).

List patterns

List patterns will not be supported for instances of inline array types.

Definite assignment checking

Regular definite assignment rules are applicable to variables that have an inline array type.

Collection literals

An inline array type is a valid constructible collection target type for a collection expression.

For example:

Buffer10<int> b = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; // initializes user-defined inline array

The length of the collection literal must match the length of the target inline array type. If the length of the literal is known at compile time and it doesn't match the target length, an error is reported. Otherwise, an exception is going to be thrown at runtime once the mismatch is encountered. The exact exception type is TBD. Some candidates are: System.NotSupportedException, System.InvalidOperationException.

An instance of an inline array type is a valid expression in a spread_element.

Validation of the InlineArrayAttribute applications

Compiler will validate the following aspects of the InlineArrayAttribute applications:

  • The target type is a non-record struct
  • The target type has only one field
  • Specified length > 0
  • The target struct doesn't have an explicit layout specified

Inline Array elements in an object initializer

By default, element initialization will not be supported via initializer_target of form '[' argument_list ']' (see https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#128163-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;
}

However, if the inline array type explicitly defines suitable indexer, object initializer will use it:

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;
    }
}

The foreach statement

The foreach statement will be adjusted to allow usage of an inline array type as a collection in a foreach statement.

For example:

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;

is equivalent to:

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);
}

We will support foreach over inline arrays, even if it starts as restricted in async methods due to involvement of the span types into the translation.

Open design questions

Alternatives

Inline array type syntax

The grammar at https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/types.md#821-general will be adjusted as follows:

array_type
    : non_array_type rank_specifier+
    ;

rank_specifier
    : '[' ','* ']'
+   | '[' constant_expression ']' 
    ;

The type of the constant_expression must be implicitly convertible to type int, and the value must be a non-zero positive integer.

The relevant part of the https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/arrays.md#1621-general section will be adjusted as follows.

The grammar productions for array types are provided in https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/types.md#821-general.

An array type is written as a non_array_type followed by one or more rank_specifiers.

A non_array_type is any type that is not itself an array_type.

The rank of an array type is given by the leftmost rank_specifier in the array_type: A rank_specifier indicates that the array is an array with a rank of one plus the number of “,” tokens in the rank_specifier.

The element type of an array type is the type that results from deleting the leftmost rank_specifier:

  • An array type of the form T[ constant_expression ] is an anonymous inline array type with length denoted by constant_expression and a non-array element type T.
  • An array type of the form T[ constant_expression ][R₁]...[Rₓ] is an anonymous inline array type with length denoted by constant_expression and an element type T[R₁]...[Rₓ].
  • An array type of the form T[R] (where R is not a constant_expression) is a regular array type with rank R and a non-array element type T.
  • An array type of the form T[R][R₁]...[Rₓ] (where R is not a constant_expression) is a regular array type with rank R and an element type T[R₁]...[Rₓ].

In effect, the rank_specifiers are read from left to right before the final non-array element type.

Example: The type in int[][,,][,] is a single-dimensional array of three-dimensional arrays of two-dimensional arrays of int. end example

At run-time, a value of a regular array type can be null or a reference to an instance of that array type.

Note: Following the rules of https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/arrays.md#166-array-covariance, the value may also be a reference to a covariant array type. end note

An anonymous inline array type is a compiler synthesized inline array type with internal accessibility. The element type must be a type that can be used as a type argument. Unlike an explicitly declared inline array type, an anonymous inline array type cannot be referenced by name, it can be referenced only by array_type syntax. In context of the same program, any two array_types denoting inline array types of the same element type and of the same length, refer to the same anonymous inline array type.

Besides internal accessibility, compiler will prevent consumption of APIs utilizing anonymous inline array types across assembly boundaries by using a required custom modifier (exact type TBD) applied to an anonymous inline array type reference in the signature.

Array creation expressions

Array creation expressions

array_creation_expression
    : 'new' non_array_type '[' expression_list ']' rank_specifier*
      array_initializer?
    | 'new' array_type array_initializer
    | 'new' rank_specifier array_initializer
    ;

Given the current grammar, use of a constant_expression in place of the expression_list already has meaning of allocating a regular single-dimensional array type of the specified length. Therefore, array_creation_expression will continue to represent an allocation of a regular array.

However, the new form of the rank_specifier could be used to incorporate an anonymous inline array type into the element type of the allocated array.

For example, the following expressions create a regular array of length 2 with an element type of an anonymous inline array type with element type int and length 5:

new int[2][5];
new int[][5] {default, default};
new [] {default(int[5]), default(int[5])};

Array initializers

The Array initializers section will be adjusted to allow use of array_initializer to initialize inline array types (no changes to the grammar necessary).

array_initializer
    : '{' variable_initializer_list? '}'
    | '{' variable_initializer_list ',' '}'
    ;

variable_initializer_list
    : variable_initializer (',' variable_initializer)*
    ;
    
variable_initializer
    : expression
    | array_initializer
    ;

The length of the inline array must be explicitly provided by the target type.

For example:

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

Detailed Design (Option 2)

Note, that for the purpose of this proposal a term "fixed-size buffer" refers to a the proposed "safe fixed-size buffer" feature rather than to a buffer described at https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#228-fixed-size-buffers.

In this design, fixed-size buffer types do not get general special treatment by the language. There is a special syntax to declare members that represent fixed-size buffers and new rules around consuming those members. They are not fields from the language point of view.

The grammar for variable_declarator in https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#145-fields will be extended to allow specifying the size of the buffer:

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 ']'
    ;    

A fixed_size_buffer_declarator introduces a fixed-size buffer of a given element type.

The buffer element type is the type specified in field_declaration. A fixed-size buffer declarator introduces a new member and consists of an identifier that names the member, followed by a constant expression enclosed in [ and ] tokens. The constant expression denotes the number of elements in the member introduced by that fixed-size buffer declarator. The type of the constant expression must be implicitly convertible to type int, and the value must be a non-zero positive integer.

The elements of a fixed-size buffer shall be laid out sequentially in memory as though they are elements of an array.

A field_declaration with a fixed_size_buffer_declarator in an interface must have static modifier.

Depending on the situation (details are specified below), an access to a fixed-size buffer member is classified as a value (never a variable) of either System.ReadOnlySpan<S> or System.Span<S>, where S is the element type of the fixed-size buffer. Both types provide indexers returning a reference to a specific element with appropriate "readonly-ness", which prevents direct assignment to the elements when language rules don't permit that.

This limits the set of types that can be used as a fixed-size buffer element type to types that can be used as type arguments. For example, a pointer type cannot be used as an element type.

The resulting span instance will have a length equal to the size declared on the fixed-size buffer. Indexing into the span with a constant expression outside of the declared fixed-size buffer bounds is a compile time error.

The safe-context of the value will be equal to the safe-context of the container, just as it would if the backing data was accessed as a field.

Fixed-size buffers in expressions

Member lookup of a fixed-size buffer member proceeds exactly like member lookup of a field.

A fixed-size buffer can be referenced in an expression using a simple_name or a member_access .

When an instance fixed-size buffer member is referenced as a simple name, the effect is the same as a member access of the form this.I, where I is the fixed-size buffer member. When a static fixed-size buffer member is referenced as a simple name, the effect is the same as a member access of the form E.I, where I is the fixed-size buffer member and E is the declaring type.

Non-readonly fixed-size buffers

In a member access of the form E.I, if E is of a struct type and a member lookup of I in that struct type identifies a non-readonly instance fixed-size member, then E.I is evaluated and classified as follows:

  • If E is classified as a value, then E.I can be used only as a primary_no_array_creation_expression of an element access with index of System.Index type, or of a type implicitly convertible to int. Result of the element access is a fixed-size member's element at the specified position, classified as a value.
  • Otherwise, if E is classified as a readonly variable and the result of the expression is classified as a value of type System.ReadOnlySpan<S>, where S is the element type of I. The value can be used to access member's elements.
  • Otherwise, E is classified as a writable variable and the result of the expression is classified as a value of type System.Span<S>, where S is the element type of I. The value can be used to access member's elements.

In a member access of the form E.I, if E is of a class type and a member lookup of I in that class type identifies a non-readonly instance fixed-size member, then E.I is evaluated and classified as a value of type System.Span<S>, where S is the element type of I.

In a member access of the form E.I, if member lookup of I identifies a non-readonly static fixed-size member, then E.I is evaluated and classified as a value of type System.Span<S>, where S is the element type of I.

Readonly fixed-size buffers

When a field_declaration includes a readonly modifier, the member introduced by the fixed_size_buffer_declarator is a readonly fixed-size buffer. Direct assignments to elements of a readonly fixed-size buffer can only occur in an instance constructor, init member or static constructor in the same type. Specifically, direct assignments to an element of readonly fixed-size buffer are permitted only in the following contexts:

  • For an instance member, in the instance constructors or init member of the type that contains the member declaration; for a static member, in the static constructor of the type that contains the member declaration. These are also the only contexts in which it is valid to pass an element of readonly fixed-size buffer as an out or ref parameter.

Attempting to assign to an element of a readonly fixed-size buffer or pass it as an out or ref parameter in any other context is a compile-time error. This is achieved by the following.

A member access for a readonly fixed-size buffer is evaluated and classified as follows:

  • In a member access of the form E.I, if E is of a struct type and E is classified as a value, then E.I can be used only as a primary_no_array_creation_expression of an element access with index of System.Index type, or of a type implicitly convertible to int. Result of the element access is a fixed-size member's element at the specified position, classified as a value.
  • If access occurs in a context where direct assignments to an element of readonly fixed-size buffer are permitted, the result of the expression is classified as a value of type System.Span<S>, where S is the element type of the fixed-size buffer. The value can be used to access member's elements.
  • Otherwise, the expression is classified as a value of type System.ReadOnlySpan<S>, where S is the element type of the fixed-size buffer. The value can be used to access member's elements.

Definite assignment checking

Fixed-size buffers are not subject to definite assignment-checking, and fixed-size buffer members are ignored for purposes of definite-assignment checking of struct type variables.

When a fixed-size buffer member is static or the outermost containing struct variable of a fixed-size buffer member is a static variable, an instance variable of a class instance, or an array element, the elements of the fixed-size buffer are automatically initialized to their default values. In all other cases, the initial content of a fixed-size buffer is undefined.

Metadata

Metadata emit and code generation

For metadata encoding compiler will rely on recently added System.Runtime.CompilerServices.InlineArrayAttribute.

Fixed-size buffers like:

public partial class C
{
    public int buffer1[10];
    public readonly int buffer2[10];
}

will be emitted as fields of a specially decorated struct type.

Equivalent C# code will be:

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);
    }
}

The actual naming conventions for the type and its members are TBD. The framework will likely include a set of predefined "buffer" types that cover a limited set of buffer sizes. When a predefined type doesn't exist, compiler will synthesize it in the module being built. Names of the generated types will be "speakable" in order to support consumption from other languages.

A code generated for an access like:

public partial class C
{
    void M1(int val)
    {
        buffer1[1] = val;
    }

    int M2()
    {
        return buffer2[1];
    }
}

will be equivalent to:

public partial class C
{
    void M1(int val)
    {
        buffer.AsSpan()[1] = val;
    }

    int M2()
    {
        return buffer2.AsReadOnlySpan()[1];
    }
}
Metadata import

When compiler imports a field declaration of type T and the following conditions are all met:

  • T is a struct type decorated with the InlineArray attribute, and
  • The first instance field declared within T has type F, and
  • There is a public System.Span<F> AsSpan() within T, and
  • There is a public readonly System.ReadOnlySpan<T> AsReadOnlySpan() or public System.ReadOnlySpan<T> AsReadOnlySpan() within T.

the field will be treated as C# fixed-size buffer with element type F. Otherwise, the field will be treated as a regular field of type T.

Method or property group like approach in the language

One thought is to treat these members more like method groups, in that they aren't automatically a value in and of themselves, but can be made into one if necessary. Here’s how that would work:

  • Safe fixed-size buffer accesses have their own classification (just like e.g. method groups and lambdas)
  • They can be indexed directly as a language operation (not via span types) to produce a variable (which is readonly if the buffer is in a readonly context, just the same as fields of a struct)
  • They have implicit conversions-from-expression to Span<T> and ReadOnlySpan<T>, but use of the former is an error if they are in a readonly context
  • Their natural type is ReadOnlySpan<T>, so that’s what they contribute if they participate in type inference (e.g., var, best-common-type or generic)

C/C++ fixed-size buffers

C/C++ has a different notion of fixed-size buffers. For example, there is a notion of "zero-length fixed sized buffers", which is often used as a way to indicate that the data is "variable length". It is not a goal of this proposal to be able to interop with that.

LDM meetings