Compartilhar via


Matrizes embutidas

Nota

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.

Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de idioma (LDM).

Você pode saber mais sobre o processo de adoção de speclets de recursos no padrão de linguagem C# no artigo sobre as especificações de .

Problema do especialista: https://github.com/dotnet/csharplang/issues/7431

Resumo

Forneça um mecanismo seguro e de finalidade geral para consumir tipos de struct utilizando o recurso de InlineArrayAttribute. Forneça um mecanismo de uso geral e seguro para declarar matrizes embutidas em classes, structs e interfaces em C#.

Observação: versões anteriores desta especificação usavam os termos "ref-safe-to-escape" e "safe-to-escape", que foram introduzidos na especificação do recurso de segurança de intervalo na seção . O comitê padrão ECMA alterou os nomes para "ref-safe-context" e "safe-context", respectivamente. Os valores do contexto seguro foram refinados para utilizar "declaration-block", "function-member" e "caller-context" consistentemente. Os speclets haviam usado frases diferentes para esses termos e também usaram "safe-to-return" como sinônimo de "caller-context". Essa especificação foi atualizada para usar os termos do padrão C# 7.3.

Motivação

Esta proposta planeja resolver as muitas limitações de https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#238-fixed-size-buffers. Especificamente, ele pretende permitir:

  • acessando elementos de tipos de struct utilizando o recurso inlineArrayAttribute;
  • a declaração de matrizes embutidas para tipos gerenciados e não gerenciados em um struct, class ou interface.

E forneça a eles a verificação de segurança de linguagem.

Design detalhado

Recentemente, o runtime adicionou o recurso InlineArrayAttribute. Em suma, um usuário pode declarar um tipo de estrutura como o seguinte:

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

O runtime fornece um layout de tipo especial para o tipo Buffer:

  • O tamanho do tipo é estendido para comportar 10 elementos (o número vem do atributo InlineArray) do tipo object (que vem do tipo do único campo de instância na estrutura, _element0 neste exemplo).
  • O primeiro elemento é alinhado com o campo de instância e com o início do struct
  • Os elementos são dispostos sequencialmente na memória como se fossem elementos de uma matriz.

O runtime fornece controle GC regular para todos os elementos no struct.

Essa proposta se referirá a tipos como este como "tipos de matriz embutida".

Elementos de um tipo de matriz embutida podem ser acessados por meio de ponteiros ou instâncias de intervalo retornadas por APIs System.Runtime.InteropServices.MemoryMarshal.CreateSpan/System.Runtime.InteropServices.MemoryMarshal.CreateReadOnlySpan. No entanto, nem a abordagem de ponteiro nem as APIs fornecem verificação de tipo e limites pronta para uso.

A linguagem fornecerá uma maneira segura para tipo e referência para acessar elementos de tipos de matriz embutida. O acesso será baseado em intervalos. Isso limita o suporte a tipos de matriz embutida com tipos de elemento que podem ser usados como um argumento de tipo. Por exemplo, um tipo de ponteiro não pode ser usado como um tipo de elemento. Outros exemplos dos tipos de intervalo.

Obtendo instâncias de tipos de intervalo para um tipo de matriz embutida

Como há uma garantia de que o primeiro elemento em um tipo de matriz embutida está alinhado no início do tipo (sem lacuna), o compilador usará o seguinte código para obter um valor Span:

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

E o seguinte código para obter um valor ReadOnlySpan:

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

Para reduzir o tamanho da IL em sites de uso, o compilador deve ser capaz de adicionar dois auxiliares reutilizáveis genéricos ao tipo de detalhes de implementação privada e usá-los em todos os sites de uso no mesmo programa.

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

Acesso a elementos

O de acesso ao elemento será estendido para dar suporte ao acesso ao elemento de matriz embutida.

Um element_access consiste em uma primary_no_array_creation_expression, seguida por um token "[", uma argument_list e um token "]". O argument_list consiste em um ou mais argumentos , separados por vírgulas.

element_access
    : primary_no_array_creation_expression '[' argument_list ']'
    ;

O argument_list de um element_access não pode conter argumentos ref ou out.

Um element_access será associado dinamicamente (§11.3.3) se pelo menos uma das seguintes condições for aplicável:

  • A primary_no_array_creation_expression tem um tipo de tempo de compilação dynamic.
  • Pelo menos uma expressão da argument_list tem tipo compile-time dynamic e a primary_no_array_creation_expression não tem um tipo de matriz, e a primary_no_array_creation_expression não tem tipo de matriz embutida ou há mais de um item na lista de argumentos.

Nesse caso, o compilador classifica o element_access como um valor do tipo dynamic. As regras abaixo para determinar o significado do element_access são aplicadas em run-time, usando o tipo de run-time em vez do tipo de tempo de compilação das expressões primary_no_array_creation_expression e argument_list que têm o tipo de tempo de compilação dynamic. Se a primary_no_array_creation_expression não tiver tipo de tempo de compilação dynamic, o acesso do elemento passará por uma verificação de tempo de compilação limitada, conforme descrito em §11.6.5.

Se primary_no_array_creation_expression de um element_access for um valor de um array_type, o element_access será um acesso de matriz (§12.8.12.2). Se a primary_no_array_creation_expression de um element_access for uma variável ou valor de um tipo de matriz embutida e a argument_list consistir em um único argumento, o element_access será um acesso de elemento de matriz embutida. Caso contrário, o primary_no_array_creation_expression será uma variável ou valor de uma classe, struct ou tipo de interface que tenha um ou mais membros indexadores. Nesse caso, o element_access é um acesso de indexador (§12.8.12.3).

Acesso a elementos de matriz embutida

Para o acesso de um elemento de matriz embutida, a primary_no_array_creation_expression do element_access deve ser uma variável ou um valor de um tipo de matriz embutida. Além disso, a argument_list de um acesso de elemento de matriz embutida não tem permissão para conter argumentos nomeados. O argument_list deve conter uma única expressão e a expressão deve ser

  • do tipo int ou
  • implicitamente conversível para intou
  • implicitamente conversível para System.Indexou
  • implicitamente conversível para System.Range.
Quando o tipo de expressão é int

Se primary_no_array_creation_expression for uma variável gravável, o resultado da avaliação de um acesso de elemento de matriz embutida será uma variável gravável equivalente a invocar public ref T this[int index] { get; } com esse valor inteiro em uma instância de System.Span<T> retornada pelo método System.Span<T> InlineArrayAsSpan em primary_no_array_creation_expression. Para fins de análise de segurança de referência, o ref-safe-context/safe-context do acesso são equivalentes ao mesmo usado para uma invocação de um método com a assinatura static ref T GetItem(ref InlineArrayType array). A variável resultante será considerada móvel se e somente se primary_no_array_creation_expression for móvel.

Se primary_no_array_creation_expression for uma variável somente leitura, o resultado da avaliação de um acesso de elemento de matriz embutida será uma variável somente leitura equivalente a invocar public ref readonly T this[int index] { get; } com esse valor inteiro em uma instância de System.ReadOnlySpan<T> retornada pelo método System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan em primary_no_array_creation_expression. Para fins de análise de segurança de referência, o ref-safe-context/safe-context do acesso são equivalentes ao mesmo usado para uma invocação de um método com a assinatura static ref readonly T GetItem(in InlineArrayType array). A variável resultante é considerada móvel se e somente se expressão de criação sem matriz primária for móvel.

Se primary_no_array_creation_expression for um valor, o resultado da avaliação de um acesso de elemento de matriz embutida será um valor equivalente a invocar public ref readonly T this[int index] { get; } com esse valor inteiro em uma instância de System.ReadOnlySpan<T> retornada pelo método System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan em primary_no_array_creation_expression. Para fins de análise de segurança de referência, o ref-safe-context/safe-context do acesso são equivalentes ao mesmo usado para uma invocação de um método com a assinatura static T GetItem(InlineArrayType array).

Por exemplo:

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

Indexar em uma matriz embutida com uma expressão constante fora dos limites declarados da matriz embutida é um erro de tempo de compilação.

Quando a expressão é implicitamente conversível para int

A expressão é convertida em int e, em seguida, o acesso ao elemento é interpretado conforme descrito na seção "Quando o tipo de expressão é int".

Quando a expressão implicitamente conversível para System.Index

A expressão é convertida em System.Index, que é então transformada em um valor de índice baseado em int, conforme descrito em https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-index-support, supondo que o comprimento da coleção seja conhecido em tempo de compilação e seja igual à quantidade de elementos no tipo de matriz embutida do primary_no_array_creation_expression. Em seguida, o acesso de elemento é interpretado conforme descrito na seção Quando o tipo de expressão é int seção.

Quando a expressão é implicitamente conversível para System.Range

Se primary_no_array_creation_expression for uma variável gravável, o resultado da avaliação de um acesso de elemento de matriz embutida será um valor equivalente a invocar public Span<T> Slice (int start, int length) em uma instância de System.Span<T> retornada pelo método System.Span<T> InlineArrayAsSpan em primary_no_array_creation_expression. Para fins de análise de segurança de referência, o ref-safe-context/safe-context do acesso são equivalentes ao mesmo usado para uma invocação de um método com a assinatura static System.Span<T> GetSlice(ref InlineArrayType array).

Se primary_no_array_creation_expression for uma variável somente leitura, o resultado da avaliação de um acesso de elemento de matriz embutida será um valor equivalente a invocar public ReadOnlySpan<T> Slice (int start, int length) em uma instância de System.ReadOnlySpan<T> retornada pelo método System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan em primary_no_array_creation_expression. Para fins de análise de segurança de referência, o ref-safe-context/safe-context do acesso são equivalentes ao mesmo usado para uma invocação de um método com a assinatura static System.ReadOnlySpan<T> GetSlice(in InlineArrayType array).

Se primary_no_array_creation_expression for um valor, um erro será relatado.

Os argumentos para a invocação do método Slice são calculados a partir da expressão de índice convertida em System.Range conforme descrito em https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-range-support, supondo que o comprimento da coleção seja conhecido em tempo de compilação e seja igual à quantidade de elementos no tipo de matriz embutida do primary_no_array_creation_expression.

O compilador poderá omitir a chamada Slice se for conhecido no momento da compilação que start é 0 e length é menor ou igual à quantidade de elementos no tipo de matriz embutida. O compilador também poderá relatar um erro se souber durante a compilação que o fatiamento sai dos limites de uma matriz embutida.

Por exemplo:

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
}

Conversões

Uma nova conversão, uma conversão de matriz embutida, da expressão será adicionada. A conversão de matriz embutida é uma conversão padrão.

Há uma conversão implícita da expressão de um tipo de matriz embutida para os seguintes tipos:

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

No entanto, converter uma variável somente leitura em System.Span<T> ou converter um valor em qualquer tipo é um erro.

Por exemplo:

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
}

Para fins de análise de segurança de referência safe-context da conversão é equivalente a safe-context para uma invocação de um método com a assinatura static System.Span<T> Convert(ref InlineArrayType array) ou static System.ReadOnlySpan<T> Convert(in InlineArrayType array).

Padrões de lista

Padrões de lista não terão suporte para instâncias de tipos de matriz embutida.

Verificação de atribuição definida

Regras de atribuição definidas regulares são aplicáveis a variáveis que têm um tipo de matriz embutida.

Literais de coleção

Uma instância de um tipo de matriz embutida é uma expressão válida em um spread_element.

O recurso a seguir não foi enviado no C# 12. Continua sendo uma proposta aberta. O código neste exemplo gera CS9174:

Um tipo de matriz embutida é um tipo de destino de coleção construível válido em uma expressão de coleção. Por exemplo:

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

O comprimento do literal da coleção deve corresponder ao comprimento do tipo de matriz embutida de destino. Se o comprimento do literal for conhecido no tempo de compilação e ele não corresponder ao comprimento do destino, um erro será relatado. Caso contrário, uma exceção será lançada em tempo de execução ao ser encontrada a incompatibilidade. O tipo de exceção exato é TBD. Alguns candidatos são: System.NotSupportedException, System.InvalidOperationException.

Validação dos aplicativos InlineArrayAttribute

O compilador validará os seguintes aspectos dos aplicativos InlineArrayAttribute:

  • O tipo de destino é um struct não registrado
  • O tipo de destino tem apenas um campo
  • Comprimento especificado > 0
  • A estrutura de destino não tem um layout explícito especificado

Elementos de matriz em linha em um inicializador de objeto

Por padrão, a inicialização de elementos não terá suporte por meio de initializer_target da forma '[' argument_list ']' (consulte 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;
}

No entanto, se o tipo de matriz embutida definir explicitamente o indexador adequado, o inicializador de objeto o usará:

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

A instrução foreach

A instrução foreach será ajustada para permitir o uso de um tipo de matriz embutida como uma coleção em uma instrução foreach.

Por exemplo:

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;

é equivalente a:

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

Daremos suporte a foreach em matrizes embutidas, mesmo que ela comece como restrita em métodos de async devido ao envolvimento dos tipos de intervalo na tradução.

Abrir perguntas de design

Alternativas

Sintaxe de tipo de matriz embutida

A gramática em https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/types.md#821-general será ajustada da seguinte maneira:

array_type
    : non_array_type rank_specifier+
    ;

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

O tipo da constant_expression deve ser implicitamente conversível para digitar int, e o valor deve ser um inteiro positivo diferente de zero.

A parte relevante da seção https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/arrays.md#1721-general será ajustada da seguinte maneira.

As produções gramaticais para tipos de matriz são fornecidas em https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/types.md#821-general.

Um tipo de matriz é escrito como um non_array_type seguido por um ou mais rank_specifiers.

Um non_array_type é qualquer tipo de que não seja um array_type.

A classificação de um tipo de matriz é fornecida pelo rank_specifier mais à esquerda no array_type: um rank_specifier indica que a matriz tem classificação de um mais o número de tokens "," no rank_specifier.

O tipo de elemento de um tipo de matriz é o tipo que resulta da remoção do especificador de classificação mais à esquerda:

  • Um tipo de matriz do formulário T[ constant_expression ] é um tipo de matriz embutida anônima com comprimento indicado por constant_expression e um tipo de elemento não matriz T.
  • Um tipo de matriz do formulário T[ constant_expression ][R₁]...[Rₓ] é um tipo de matriz embutida anônima com comprimento indicado por constant_expression e um tipo de elemento T[R₁]...[Rₓ].
  • Um tipo de matriz do formulário T[R] (em que R não é uma constant_expression) é um tipo de matriz regular com classificação de R e um tipo de elemento que não é uma matriz de T.
  • Um tipo de matriz do tipo T[R][R₁]...[Rₓ] (onde R não é uma constant_expression) é uma matriz regular com classificação R e um tipo de elemento T[R₁]...[Rₓ].

Na verdade, os rank_specifiers são lidos da esquerda para a direita antes de o tipo de elemento não matriz final.

Exemplo: o tipo em int[][,,][,] é uma matriz unidimensional de matrizes tridimensionais de matrizes bidimensionais de int. fim do exemplo

Em tempo de execução, um valor de um tipo de matriz regular pode ser null ou uma referência a uma instância desse tipo de matriz.

Observação: seguindo as regras de https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/arrays.md#176-array-covariance, o valor também pode ser uma referência a um tipo de matriz covariante. nota final

Um tipo de matriz embutida anônima é um tipo de matriz embutida sintetizada pelo compilador, com acessibilidade interna. O tipo de elemento deve ser um tipo que pode ser usado como um argumento de tipo. Ao contrário de um tipo de matriz embutida explicitamente declarado, um tipo de matriz embutida anônima não pode ser referenciado por nome, ele só pode ser referenciado por array_type sintaxe. No contexto do mesmo programa, quaisquer dois array_types que denotam tipos de matriz embutida do mesmo tipo de elemento e do mesmo comprimento referem-se ao mesmo tipo de matriz embutida anônima.

Além da acessibilidade interna, o compilador impedirá o consumo de APIs que utilizam tipos de matriz embutida anônima nos limites do assembly usando um modificador personalizado necessário (tipo exato a ser definido) aplicado a uma referência de tipo de matriz embutida anônima na assinatura.

Expressões de criação de matriz

Expressões de criação de matriz

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

Considerando a gramática atual, o uso de uma constant_expression no lugar do expression_list já tem o significado de alocar um tipo de matriz unidimensional regular do comprimento especificado. Portanto, array_creation_expression continuará a representar uma alocação de uma matriz regular.

No entanto, a nova forma do rank_specifier pode ser usada para incorporar um tipo de matriz embutida anônima no tipo de elemento da matriz alocada.

Por exemplo, as expressões a seguir criam uma matriz regular de comprimento 2 com um tipo de elemento de um tipo de matriz embutida anônimo com tipo de elemento int e comprimento 5:

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

Inicializadores de matriz

Inicializadores de matriz não foram implementados no C# 12. Esta seção continua sendo uma proposta ativa.

A seção Inicializadores de matriz será ajustada para permitir o uso de array_initializer para inicializar tipos de matrizes embutidas (sem necessidade de alterações na gramática).

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

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

O comprimento da matriz embutida deve ser fornecido explicitamente pelo tipo alvo.

Por exemplo:

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

Design Detalhado (Opção 2)

Observe que, para a finalidade desta proposta, um termo "buffer de tamanho fixo" refere-se a um recurso de "buffer de tamanho fixo seguro" proposto em vez de a um buffer descrito em https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#238-fixed-size-buffers.

Nesse design, os tipos de buffer de tamanho fixo não recebem tratamento especial geral pelo idioma. Há uma sintaxe especial para declarar membros que representam buffers de tamanho fixo e novas regras em torno do consumo desses membros. Eles não são campos do ponto de vista da linguagem.

A gramática para variable_declarator em https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#155-fields será estendida para permitir a especificação do tamanho do 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 ']'
    ;    

Um fixed_size_buffer_declarator introduz um buffer de tamanho fixo de um determinado tipo de elemento.

O tipo de elemento buffer é o tipo especificado em field_declaration. Um declarador de buffer de tamanho fixo introduz um novo membro e consiste em um identificador que nomeia o membro, seguido por uma expressão constante entre tokens [ e ]. A expressão constante indica o número de elementos no membro introduzido pelo declarador de buffer de tamanho fixo. O tipo da expressão constante deve ser implicitamente conversível para o tipo int, e o valor deve ser um inteiro positivo não nulo.

Os elementos de um buffer de tamanho fixo devem ser dispostos sequencialmente na memória como se fossem elementos de uma matriz.

Uma field_declaration com um fixed_size_buffer_declarator em uma interface deve ter o modificador static.

Dependendo da situação (detalhes são especificados abaixo), um acesso a um membro de buffer de tamanho fixo é classificado como um valor (nunca uma variável) de System.ReadOnlySpan<S> ou System.Span<S>, em que S é o tipo de elemento do buffer de tamanho fixo. Ambos os tipos fornecem indexadores que retornam uma referência para um elemento específico com "readonly-ness" apropriado, o que impede a atribuição direta aos elementos quando as regras de linguagem não permitem isso.

Isso limita o conjunto de tipos que podem ser usados como um tipo de elemento de buffer de tamanho fixo a tipos que podem ser usados como argumentos de tipo. Por exemplo, um tipo de ponteiro não pode ser usado como um tipo de elemento.

A instância de intervalo resultante terá um comprimento igual ao tamanho declarado no buffer de tamanho fixo. A indexação no intervalo com uma expressão constante fora dos limites do buffer de tamanho fixo declarado é um erro de tempo de compilação.

O safe-context do valor será igual ao safe-context do contêiner, assim como seria se os dados subjacentes fossem acessados como um campo.

Buffers de tamanho fixo em expressões

A busca por um membro de um buffer de tamanho fixo ocorre exatamente como a busca por um campo.

Um buffer de tamanho fixo pode ser referenciado em uma expressão usando um simple_name ou um member_access .

Quando um membro de um buffer de tamanho fixo de instância é referenciado como um nome único, o efeito é o mesmo que um acesso ao membro na forma de this.I, em que I é o membro do buffer de tamanho fixo. Quando um membro de buffer de tamanho fixo estático é referenciado como um nome simples, o efeito é o mesmo que um acesso de membro na forma E.I, em que I é o membro do buffer de tamanho fixo e E é o tipo declarativo.

Buffers de tamanho fixo que não são somente leitura

Em um acesso de membro da forma E.I, se E for de um tipo de struct e uma pesquisa de membro de I nesse tipo de struct identificar uma instância de membro de tamanho fixo não readonly, E.I será avaliado e classificado da seguinte maneira:

  • Se E for classificado como um valor, E.I poderá ser usado apenas como um primary_no_array_creation_expression de um acesso de elemento com índice de tipo System.Index ou de um tipo implicitamente conversível para int. O resultado do acesso ao elemento é um elemento de membro de tamanho fixo na posição especificada, classificado como um valor.
  • Caso contrário, se E for classificado como uma variável somente leitura e o resultado da expressão for classificado como um valor do tipo System.ReadOnlySpan<S>, em que S é o tipo de elemento de I. O valor pode ser usado para acessar os elementos do membro.
  • Caso contrário, E é classificado como uma variável gravável e o resultado da expressão é classificado como um valor do tipo System.Span<S>, em que S é o tipo de elemento de I. O valor pode ser usado para acessar os elementos do membro.

Em um acesso de membro da forma E.I, se E for de um tipo de classe e uma pesquisa de membro de I nesse tipo de classe identificar um membro de instância de tamanho fixo que não é somente leitura, E.I será avaliado e classificado como um valor do tipo System.Span<S>, onde S é o tipo de elemento de I.

Em um acesso de membro do formulário E.I, se a pesquisa de membro de I identificar um membro de tamanho fixo não estático, E.I será avaliada e classificada como um valor do tipo System.Span<S>, em que S é o tipo de elemento de I.

Buffers de tamanho fixo somente leitura

Quando uma field_declaration inclui um modificador de readonly, o membro introduzido pelo fixed_size_buffer_declarator é um buffer somente leitura de tamanho fixo. Atribuições diretas a elementos de um buffer de tamanho fixo e somente leitura podem ocorrer apenas em um construtor de instância, em um membro init ou em um construtor estático do mesmo tipo. Especificamente, as atribuições diretas a um elemento de buffer de tamanho fixo somente são permitidas nos seguintes contextos:

  • Para um membro de instância, a declaração ocorre nos construtores de instância ou no membro 'init' do tipo que o contém. Para um membro estático, a declaração se dá no construtor estático do tipo que o contém. Esses também são os únicos contextos nos quais é válido passar um elemento de buffer de tamanho fixo somente leitura como um parâmetro out ou ref.

Tentar atribuir a um elemento de um buffer de tamanho fixo somente leitura ou transmiti-lo como um parâmetro out ou ref em qualquer outro contexto é um erro de tempo de compilação. Isso é obtido pelo seguinte.

Um acesso de um membro para um buffer somente leitura e de tamanho fixo é avaliado e classificado da seguinte maneira:

  • Em um acesso de membro do formulário E.I, se E for de um tipo de struct e E for classificado como um valor, E.I poderá ser usado apenas como um primary_no_array_creation_expression de um de acesso de elemento com índice de tipo System.Index ou de um tipo implicitamente conversível para int. O resultado do acesso ao elemento é um elemento de membro de tamanho fixo na posição especificada, classificado como um valor.
  • Se o acesso ocorrer em um contexto em que as atribuições diretas a um elemento de buffer de tamanho fixo são permitidas, o resultado da expressão é classificado como um valor do tipo System.Span<S>, em que S é o tipo de elemento do buffer de tamanho fixo. O valor pode ser usado para acessar os elementos do membro.
  • Caso contrário, a expressão é classificada como um valor do tipo System.ReadOnlySpan<S>, em que S é o tipo de elemento do buffer de tamanho fixo. O valor pode ser usado para acessar os elementos do membro.

Verificação de atribuição definida

Os buffers de tamanho fixo não estão sujeitos à verificação de atribuição definida e os membros do buffer de tamanho fixo são ignorados para fins de verificação de atribuição definitiva de variáveis de tipo de struct.

Quando um membro de buffer de tamanho fixo é estático ou a variável struct mais externa que contém um membro de buffer de tamanho fixo é uma variável estática, uma variável de instância de classe ou um elemento de matriz, os elementos do buffer de tamanho fixo são inicializados automaticamente para seus valores padrão. Em todos os outros casos, o conteúdo inicial de um buffer de tamanho fixo é indefinido.

Metadados

Emissão de metadados e geração de código

O compilador, ao codificar metadados, dependerá do System.Runtime.CompilerServices.InlineArrayAttributeadicionado recentemente.

Buffers de tamanho fixo, como por exemplo o seguinte pseudocódigo:

// Not valid C#
public partial class C
{
    public int buffer1[10];
    public readonly int buffer2[10];
}

serão emitidos como campos de um tipo de estrutura especialmente decorado.

O código C# equivalente será:

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

As convenções de nomenclatura do tipo e seus membros ainda serão definidas. A estrutura provavelmente incluirá um conjunto de tipos de "buffer" predefinidos que abrangem um conjunto limitado de tamanhos de buffer. Quando um tipo predefinido não existir, o compilador o sintetizará no módulo que está sendo criado. Os nomes dos tipos gerados serão de fácil pronúncia para facilitar a utilização por outros idiomas.

Um código gerado para um acesso como:

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

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

será equivalente a:

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

    int M2()
    {
        return buffer2.AsReadOnlySpan()[1];
    }
}
Importação de metadados

Quando o compilador importa uma declaração de campo do tipo T e todas as seguintes condições são atendidas:

  • T é um tipo de estrutura decorado com o atributo InlineArray e
  • O campo de primeira instância declarado em T tem o tipo Fe
  • Há um public System.Span<F> AsSpan() dentro de T e
  • Há um public readonly System.ReadOnlySpan<T> AsReadOnlySpan() ou public System.ReadOnlySpan<T> AsReadOnlySpan() dentro de T.

o campo será tratado como buffer de tamanho fixo C# com o tipo de elemento F. Caso contrário, o campo será tratado como um campo regular do tipo T.

Método ou grupo de propriedades semelhante à abordagem na linguagem

Um pensamento é tratar esses membros mais como grupos de métodos, pois eles não são automaticamente um valor em si mesmos, mas podem ser transformados em um, se necessário. Veja como isso funcionaria:

  • Os acessos seguros de buffer de tamanho fixo têm sua própria classificação (assim como grupos de métodos e lambdas)
  • Eles podem ser indexados diretamente como uma operação de linguagem (não por meio de tipos de intervalo) para produzir uma variável (que é somente leitura se o buffer estiver em um contexto somente leitura, da mesma forma que os campos de um struct)
  • Eles têm conversions-from-expression implícitas para Span<T> e ReadOnlySpan<T>, mas o uso do primeiro será um erro se eles estiverem em um contexto somente leitura
  • Seu tipo natural é ReadOnlySpan<T>, portanto, é com isso que eles contribuirão se participarem da inferência de tipo (por exemplo, var, best-common-type ou generic)

Buffers de tamanho fixo C/C++

C/C++ tem uma noção diferente de buffers de tamanho fixo. Por exemplo, há uma noção de "buffers de tamanho fixo de comprimento zero", que geralmente são usados como uma forma de indicar que os dados são de "comprimento variável". Não é uma meta desta proposta ser capaz de interoperar com isso.

Reuniões do LDM