Compartilhar via


Matrizes em linha

Observação

Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui mudanças de especificação propostas, juntamente com as informações necessárias durante o design e desenvolvimento do recurso. Estes artigos são publicados até que as alterações de especificações 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 Language Design Meeting (LDM).

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

Resumo

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

Nota: As versões anteriores desta especificação usavam os termos "ref-safe-to-escape" e "safe-to-escape", que foram introduzidos na especificação de recursos de segurança Span. O comité padrão ECMA alterou os nomes para "ref-safe-context" e "safe-context", respectivamente. Os valores do contexto seguro foram refinados para usar "declaration-block", "function-member" e "caller-context" de forma consistente. Os speclets utilizaram formulações diferentes para esses termos, além de usarem "safe-to-return" como sinónimo de "caller-context". Este speclet foi atualizado para usar os termos no padrão C# 7.3.

Motivação

A presente proposta pretende abordar as muitas limitações da https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#238-fixed-size-buffers. Especificamente, visa permitir:

  • acessando elementos de tipos struct utilizando o recurso de InlineArrayAttribute;
  • A declaração de matrizes embutidas para tipos gerenciados e não gerenciados em um struct, classou interface.

E providenciar verificação de segurança linguística para os mesmos.

Detalhamento do Projeto

Recentemente foi adicionada ao tempo de execução a funcionalidade InlineArrayAttribute. Em resumo, 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 especial de tipo para o tipo Buffer:

  • O tamanho do tipo é estendido para acomodar 10 elementos do tipo object (o número vem do atributo InlineArray, e o tipo 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 da estrutura
  • Os elementos são dispostos sequencialmente na memória como se fossem elementos de uma matriz.

O runtime fornece rastreamento regular do GC para todos os elementos na struct.

Esta proposta referir-se-á a tipos como este como "tipos de matriz em linha".

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

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

Obtenção de instâncias de tipos de intervalo para um tipo de matriz em linha

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 locais de uso, o compilador deve ser capaz de adicionar dois auxiliares reutilizáveis genéricos no tipo de detalhe 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 acesso ao elemento será estendido para oferecer suporte ao acesso diretamente ao elemento de matriz.

Um element_access consiste em um primary_no_array_creation_expression, seguido por um “[” token, seguido por uma argument_list, seguido por um “]” token. O argument_list consiste em um ou mais argumentos s, separados por vírgulas.

element_access
    : primary_no_array_creation_expression '[' argument_list ']'
    ;

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

Um element_access está dinamicamente vinculado (§11.3.3) se pelo menos uma das seguintes situações se mantiver:

  • O primary_no_array_creation_expression tem o tipo de tempo de compilação dynamic.
  • Pelo menos uma expressão da argument_list tem tipo de tempo de compilação dynamic e a primary_no_array_creation_expression não tem um tipo de array, e a primary_no_array_creation_expression não tem um tipo de array embutido ou existe mais de um item na lista de argumentos.

Neste 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 então aplicadas em tempo de execução, usando o tipo de tempo de execução 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 o primary_no_array_creation_expression não tiver o tipo de tempo de compilação dynamic, o acesso ao elemento passa por uma verificação limitada em tempo de compilação, conforme descrito no §11.6.5.

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

Acesso a elementos de matriz em linha

Para um acesso a um elemento de matriz embutido, a primary_no_array_creation_expression do element_access deve ser uma variável ou valor de um tipo de matriz embutida. Além disso, o argument_list de acesso a um elemento de matriz embutido 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 convertível em int, ou
  • implicitamente convertível em System.Index, ou
  • implicitamente conversível em System.Range.
Quando o tipo de expressão é int

Na avaliação de um acesso a um elemento de matriz embutido, se primary_no_array_creation_expression for uma variável que possa ser gravada, o resultado será uma variável que possa ser gravada equivalente a invocar public ref T this[int index] { get; } com esse valor inteiro sobre uma instância de System.Span<T> retornada pelo método System.Span<T> InlineArrayAsSpan sobre primary_no_array_creation_expression. Para efeitos de análise de segurança de referência, o contexto ref-seguro /e o contexto seguro do acesso são equivalentes aos mesmos contextos para uma invocação de um método com a assinatura static ref T GetItem(ref InlineArrayType array). A variável resultante é considerada móvel somente se primary_no_array_creation_expression também for móvel.

Se primary_no_array_creation_expression for uma variável somente leitura, o resultado de avaliar um acesso a um elemento de matriz em linha será uma variável somente leitura equivalente a chamar public ref readonly T this[int index] { get; } com esse valor inteiro numa instância de System.ReadOnlySpan<T> retornada pelo método System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan em primary_no_array_creation_expression. Para efeitos de análise de segurança de referência, o contexto seguro /do acesso é equivalente a 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 primary_no_array_creation_expression for móvel.

Se primary_no_array_creation_expression for um valor, o resultado da avaliação de um acesso a um elemento de matriz embutido será um valor equivalente a invocar public ref readonly T this[int index] { get; } com esse valor inteiro numa instância de System.ReadOnlySpan<T>, que é retornada pelo método System.ReadOnlySpan<T> InlineArrayAsReadOnlySpan em primary_no_array_creation_expression. Para a análise da segurança de referência, o contexto ref-safe-context/safe-context do acesso é equivalente ao mesmo para a 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
}

A indexação 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 convertível em 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 convertível em 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 é conhecido em tempo de compilação e é igual à quantidade de elementos no tipo de matriz embutida do primary_no_array_creation_expression. Em seguida, o acesso ao elemento é interpretado como descrito em Quando o tipo de expressão é int seção.

Quando a expressão implicitamente convertível em System.Range

Se primary_no_array_creation_expression for uma variável de escrita, o resultado da avaliação de acesso a um elemento de matriz em linha 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 efeitos de análise de segurança de referência, o contexto de segurança de referência / do acesso são equivalentes ao mesmo 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 é uma variável readonly, o resultado da avaliação de um acesso a um elemento de matriz inline 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 no primary_no_array_creation_expression. Para efeitos de análise de segurança de referência, os de contextode contexto seguro do acesso são equivalentes aos mesmos para uma invocação de um método com a assinatura .

Se primary_no_array_creation_expression é um valor, será gerado um erro.

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, assumindo que o comprimento da coleção é conhecido em tempo de compilação e é igual à quantidade de elementos no tipo de matriz embutida do primary_no_array_creation_expression.

O compilador pode omitir a chamada Slice se for conhecido em tempo de compilação que start é 0 e length é menor ou igual à quantidade de elementos no tipo de matriz embutida. O compilador também pode relatar um erro se for conhecido, no momento da compilação, que o fatiamento sai dos limites da 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

Será adicionada uma nova conversão, uma conversão de matriz embutida, a partir de uma expressão. A conversão de matriz embutida é uma conversão padrão .

Há uma conversão implícita da expressão de um tipo de array em linha para os seguintes tipos:

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

No entanto, converter uma variável somente leitura para System.Span<T> ou converter um valor para 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 análise de segurança de referências, o contexto seguro da conversão é equivalente ao contexto seguro para a 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

Os padrões de lista não serão suportados para instâncias de tipos de matriz embutida.

Verificação de atribuição definitiva

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 inline é uma expressão válida em uma spread_element.

O recurso a seguir não foi fornecido em C# 12. Continua a ser uma proposta em aberto. O código neste exemplo gera CS9174:

Um tipo de matriz embutida é uma coleção construível válida tipo de destino para 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 de coleção deve corresponder ao comprimento do tipo de matriz alvo em linha. Se o comprimento do literal for conhecido em tempo de compilação e não corresponder ao comprimento de destino, um erro será relatado. Caso contrário, uma exceção será lançada em tempo de execução assim que a incompatibilidade for encontrada. 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 aspetos dos aplicativos InlineArrayAttribute:

  • O tipo de destino é um não registro struct
  • 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 embutidos em um inicializador de objeto

Por padrão, a inicialização de elementos não será suportada por meio do inicializador initializer_target de formulário '[' 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 declaração foreach

A instrução foreach será ajustada para permitir o uso de um tipo de array em linha como uma coleção numa 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);
}

Suportaremos foreach sobre matrizes em linha, mesmo que comece como restrito nos métodos async devido ao envolvimento dos tipos de intervalo na tradução.

Perguntas de design abertas

Alternativas

Sintaxe do 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 forma:

array_type
    : non_array_type rank_specifier+
    ;

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

O tipo do constant_expression deve ser implicitamente conversível para o tipo inte o valor deve ser um inteiro positivo diferente de zero.

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

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 de um ou mais rank_specifiers.

Um non_array_type é qualquer tipo que não é em si um array_type.

A classificação de um tipo de matriz é dada pela rank_specifier mais à esquerda na array_type: Uma rank_specifier indica que a matriz é uma matriz com uma classificação de um mais o número de tokens "," na rank_specifier.

O tipo de elemento de um tipo de matriz é o tipo que resulta da exclusã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] (onde R não é um constant_expression) é um tipo de matriz regular com rank R e um tipo de elemento não-array T.
  • Um tipo de matriz do formulário T[R][R₁]...[Rₓ] (onde R não é um constant_expression) é um tipo de matriz regular com classificação R e um tipo de elemento T[R₁]...[Rₓ].

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

Exemplo: O tipo em int[][,,][,] é uma matriz unidimensional que contém matrizes tridimensionais de matrizes bidimensionais de int. exemplo final

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.

Nota: 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 em linha anónima é um tipo de matriz em linha sintetizado 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 pelo nome, ele pode ser referenciado apenas por array_type sintaxe. No contexto do mesmo programa, quaisquer dois array_types denotando 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 evitará o consumo de APIs que utilizam tipos de array anônimos em linha através das fronteiras de assembly, usando um modificador personalizado necessário (tipo exato a definir) aplicado a uma referência anônima de tipo de array em linha 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
    ;

Dada a gramática atual, o uso de um constant_expression no lugar do expression_list já tem 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 inline 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 inline 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

Os inicializadores de matriz não foram implementados no C# 12. Esta secção continua a ser uma proposta ativa.

A seção de inicializadores de matriz será ajustada para permitir o uso de array_initializer para inicializar tipos de matriz inline (sem alterações necessárias 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 explicitamente fornecido 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

Projeto detalhado (opção 2)

Note-se que, para efeitos da presente proposta, o termo "buffer de tamanho fixo" refere-se à característica proposta de "buffer de tamanho fixo seguro" e não a um buffer descrito em https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/unsafe-code.md#238-fixed-size-buffers.

Neste projeto, os tipos de buffer de tamanho fixo não recebem tratamento especial geral pela linguagem. Há uma sintaxe especial para declarar membros que representam buffers de tamanho fixo e novas regras sobre o consumo desses membros. Não são campos do ponto de vista linguístico.

A gramática para variable_declarator em https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/classes.md#155-fields será estendida para permitir especificar o 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 incluída em tokens [ e ]. A expressão constante denota o número de elementos no membro introduzidos por esse 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 diferente de zero.

Os elementos de uma memória intermédia de tamanho fixo devem ser dispostos sequencialmente na memória como se fossem elementos de uma matriz.

Um field_declaration com um fixed_size_buffer_declarator numa interface deve ter static modificador.

Dependendo da situação (os 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>, onde S é o tipo de elemento do buffer de tamanho fixo. Ambos os tipos fornecem indexadores que retornam uma referência a 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 aos 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 span resultante terá um comprimento igual ao tamanho declarado no buffer de tamanho fixo. A indexação de um span com uma expressão constante fora dos limites declarados de um buffer de tamanho fixo é um erro de tempo de compilação.

O de contexto seguro do valor será igual ao de contexto seguro do contêiner, assim como seria se os dados de backup fossem acessados como um campo.

Buffers de tamanho fixo em expressões

A procura de um membro num buffer de tamanho fixo é feita exatamente como a procura de um membro de 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 do buffer de tamanho fixo de instância é referenciado como um nome simples, o efeito é o mesmo que um acesso a membro na forma this.I, onde I é o membro do buffer de tamanho fixo. Quando um membro de buffer estático de tamanho fixo é referenciado como um nome simples, o efeito é o mesmo que um acesso de membro na forma E.I, onde I é o membro do buffer de tamanho fixo e E é o tipo de declaração.

Buffers de tamanho fixo não de somente leitura

Em um acesso de membro do formulário E.I, se E for de um tipo struct e uma pesquisa de membro de I nesse tipo struct identificar um membro de tamanho fixo de instância não somente leitura, E.I será avaliada e classificada da seguinte forma:

  • Se E é classificado como um valor, então E.I pode ser usado apenas como um primary_no_array_creation_expression de um elemento acesso com índice de System.Index tipo, 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 de leitura apenas 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>, onde S é o tipo de elemento de I. O valor pode ser usado para acessar os elementos do membro.

Em um acesso de membro no formulário 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 é apenas de leitura, então E.I é 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 consulta de membros de I identificar um membro estático de tamanho fixo não readonly, então E.I é avaliado e classificado como um valor do tipo System.Span<S>, onde S é o tipo de elemento de I.

Buffers de tamanho fixo somente de leitura

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

  • Para um membro de instância, nos construtores de instância ou no membro de inicialização do tipo que contém a declaração de membro; para um membro estático, no construtor estático do tipo que contém a declaração de membro. Estes também são os únicos contextos em que é 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 passá-lo como um parâmetro out ou ref em qualquer outro contexto é um erro em tempo de compilação. Isto é conseguido através do seguinte:

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

  • No acesso a um membro da forma E.I, se E for de um tipo struct e E for classificado como um valor, então E.I pode ser usado apenas como um primary_no_array_creation_expression de um acesso ao elemento com índice do 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 onde atribuições diretas a um elemento de buffer de tamanho fixo somente leitura são permitidas, o resultado da expressão é classificado como um valor do tipo System.Span<S>, onde 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>, onde 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 definitiva

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 definida de variáveis de tipo struct.

Quando um membro de buffer de tamanho fixo é estático ou a variável struct mais externa de um membro de buffer de tamanho fixo é uma variável estática, uma variável de instância de uma instância de classe ou um elemento de matriz, os elementos do buffer de tamanho fixo são automaticamente inicializados com 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

Para a codificação de metadados, o compilador contará com o recentemente adicionado System.Runtime.CompilerServices.InlineArrayAttribute.

Buffers de tamanho fixo, como no pseudocódigo a seguir:

// 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 reais para o tipo e seus membros estão por definir. A estrutura provavelmente incluirá um conjunto de tipos predefinidos de "buffer" que abrangem um conjunto limitado de tamanhos de buffer. Quando um tipo predefinido não existe, o compilador irá sintetizá-lo no módulo que está sendo construído. Os nomes dos tipos gerados serão "pronunciáveis" para facilitar a utilização noutras linguagens.

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 as seguintes condições são todas atendidas:

  • T é um tipo struct decorado com o atributo InlineArray, e
  • O campo de primeira instância declarado dentro T tem o tipo F, e
  • Existe um public System.Span<F> AsSpan() dentro de T, e
  • Existe 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 tipo de elemento F. Caso contrário, o campo será tratado como um campo regular do tipo T.

Método ou grupo de propriedades como abordagem na linguagem

Um pensamento é tratar esses membros mais como grupos de método, na medida em que 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, por exemplo, grupos de métodos e lambdas)
  • Eles podem ser indexados diretamente através de uma operação linguística (não através de tipos de span) para produzir uma variável (que é apenas leitura se o buffer estiver em um contexto de apenas leitura, da mesma forma que os campos de uma estrutura)
  • Eles têm conversões implícitas de expressão para Span<T> e ReadOnlySpan<T>, mas o uso deste último é um erro se eles estiverem em um contexto somente leitura.
  • Seu tipo natural é ReadOnlySpan<T>, então é isso que eles contribuem se participarem da inferência de tipo (por exemplo, var, best-common-type ou genérico)

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 é frequentemente usada como uma forma de indicar que os dados são "comprimento variável". Não é um objetivo desta proposta poder interoperar com isso.

Reuniões do LDM