Compartilhar via


Expressões de coleção

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

As expressões de coleção introduzem uma nova sintaxe concisa, [e1, e2, e3, etc], para criar valores de coleção comuns. É possível inserir outras coleções nesses valores utilizando um elemento spread ..e, como neste exemplo: [e1, ..c2, e2, ..c2].

Vários tipos semelhantes a coleções podem ser criados sem a necessidade de suporte BCL externo. Estes tipos são:

Há suporte adicional para tipos semelhantes a coleções não abrangidos pelo mencionado acima, através de um novo atributo e padrão de API que podem ser diretamente adotados no próprio tipo.

Motivação

  • Valores semelhantes a coleções estão extremamente presentes na programação, algoritmos e, especialmente, no ecossistema C#/.NET. Quase todos os programas utilizarão esses valores para armazenar dados e enviar ou receber dados de outros componentes. Atualmente, quase todos os programas C# devem usar muitas abordagens diferentes e, infelizmente, detalhadas para criar instâncias de tais valores. Algumas abordagens também têm desvantagens de desempenho. Aqui estão alguns exemplos comuns:

    • Matrizes, que requerem a presença de new Type[] ou new[] antes dos valores { ... }.
    • Spans, que pode usar stackalloc e outras construções complicadas.
    • Inicializadores de coleção, que exigem sintaxe como new List<T> (sem inferência de um Tpossivelmente verboso) antes dos valores, e que podem causar várias realocações de memória porque usam N invocações .Add sem fornecer uma capacidade inicial.
    • Coleções imutáveis, que exigem sintaxe como ImmutableArray.Create(...) para inicializar os valores e que podem causar alocações intermediárias e cópia de dados. Formas de construção mais eficientes (como ImmutableArray.CreateBuilder) são pesadas e ainda produzem lixo inevitável.
  • Olhando para o ecossistema circundante, também encontramos exemplos em todos os lugares onde a criação de listas é mais conveniente e agradável de usar. TypeScript, Dart, Swift, Elm, Python e muito mais optam por uma sintaxe sucinta para este fim, com uso generalizado e com grande efeito. Investigações rápidas não revelaram problemas substantivos surgindo nesses ecossistemas com a incorporação desses literais.

  • O C# também adicionou padrões de lista no C# 11. Este padrão permite a correspondência e desconstrução de valores semelhantes a listas usando uma sintaxe limpa e intuitiva. No entanto, ao contrário de quase todas as outras construções de padrão, esta sintaxe de correspondência/desconstrução não tem a sintaxe de construção correspondente.

  • Obter o melhor desempenho para construir cada tipo de coleção pode ser complicado. Soluções simples muitas vezes desperdiçam CPU e memória. Ter uma forma literal permite a máxima flexibilidade da implementação do compilador para otimizar o literal para produzir pelo menos um resultado tão bom quanto um usuário poderia fornecer, mas com código simples. Muitas vezes, o compilador será capaz de fazer melhor, e a especificação visa permitir a implementação de grandes quantidades de margem de manobra em termos de estratégia de implementação para garantir isso.

É necessária uma solução inclusiva para C#. Deve atender à grande maioria dos casos para clientes em termos dos tipos e valores semelhantes a coleções que eles já possuem. Deve também parecer natural na língua e refletir o trabalho realizado na correspondência de padrões.

Isso leva a uma conclusão natural de que a sintaxe deve ser como [e1, e2, e3, e-etc] ou [e1, ..c2, e2], que correspondem aos equivalentes de padrão de [p1, p2, p3, p-etc] e [p1, ..p2, p3].

Projeto de detalhe

São adicionadas as seguintes produções gramaticais :

primary_no_array_creation_expression
  ...
+ | collection_expression
  ;

+ collection_expression
  : '[' ']'
  | '[' collection_element ( ',' collection_element )* ']'
  ;

+ collection_element
  : expression_element
  | spread_element
  ;

+ expression_element
  : expression
  ;

+ spread_element
  : '..' expression
  ;

Os literais de coleção de tipo de destino são .

Esclarecimentos de especificações

  • Por uma questão de brevidade, collection_expression será designado como "literal" nas seguintes secções.

  • expression_element instâncias serão comumente referidas como e1, e_n, etc.

  • spread_element instâncias serão normalmente referidas como ..s1, ..s_n, etc.

  • tipo span significa Span<T> ou ReadOnlySpan<T>.

  • Literais geralmente serão mostrados como [e1, ..s1, e2, ..s2, etc] para transmitir qualquer número de elementos em qualquer ordem. É importante salientar que este formulário será utilizado para representar todos os casos, tais como:

    • Literais vazios []
    • Literais que não contêm expression_element.
    • Literais que não contêm spread_element.
    • Literais com ordenação arbitrária de qualquer tipo de elemento.
  • O tipo de iteração de ..s_n é o tipo da variável de iteração determinada como se s_n fosse usada como a expressão que está sendo iterada em um foreach_statement.

  • As variáveis que começam com __name são usadas para representar os resultados da avaliação de name, armazenadas em um local para que seja avaliado apenas uma vez. Por exemplo, __e1 é a avaliação de e1.

  • List<T>, IEnumerable<T>, etc. referem-se aos respetivos tipos no namespace System.Collections.Generic.

  • A especificação define uma tradução do literal para as construções C# existentes. À semelhança dode tradução da expressão de consulta , o literal só é legal se resultar num código jurídico. O objetivo desta regra é evitar ter que repetir outras regras da linguagem que estão implícitas (por exemplo, sobre a convertibilidade de expressões quando atribuídas a locais de armazenamento).

  • Uma implementação não é necessária para traduzir literais exatamente como especificado abaixo. Qualquer tradução é legal se o mesmo resultado for produzido e não houver diferenças observáveis na produção do resultado.

    • Por exemplo, uma implementação pode traduzir literais como [1, 2, 3] diretamente para uma expressão new int[] { 1, 2, 3 } que incorpora os dados brutos no assembly, eliminando assim a necessidade de __index ou de uma sequência de instruções para atribuir cada valor. É importante ressaltar que isso significa que, se qualquer etapa da tradução pode causar uma exceção no tempo de execução, o estado do programa ainda é deixado no estado indicado pela tradução.
  • As referências à 'alocação de pilha' referem-se a qualquer estratégia destinada a alocar na pilha em vez do heap. É importante salientar que não implica nem exige que essa estratégia seja através do próprio mecanismo de stackalloc. Por exemplo, o uso de matrizes em linha também é uma abordagem permitida e desejável para realizar a alocação de pilha, quando disponível. Observe que no C# 12, matrizes embutidas não podem ser inicializadas com uma expressão de coleção. Esta continua a ser uma proposta em aberto.

  • Presume-se que as coleções são bem comportadas. Por exemplo:

    • Supõe-se que o valor de Count em uma coleção produzirá o mesmo valor que o número de elementos quando enumerado.
    • Presume-se que os tipos usados nessa especificação definidos no namespace System.Collections.Generic estejam livres de efeitos colaterais. Como tal, o compilador pode otimizar cenários onde tais tipos podem ser usados como valores intermediários, mas de outra forma não serão expostos.
    • Supõe-se que uma chamada para algum membro .AddRange(x) aplicável em uma coleção resultará no mesmo valor final que iterar sobre x e adicionar todos os seus valores enumerados individualmente à coleção com .Add.
    • O comportamento de literais de coleção com coleções que não são bem comportadas é indefinido.

Conversões

Uma conversão de expressão de coleção permite que uma expressão de coleção seja convertida para um tipo específico.

Existe uma conversão implícita de expressão de coleção para nos seguintes tipos:

  • Um tipo de matriz unidimensionalT[], caso em que o tipo de elemento é T
  • Um tipo de extensão :
    • System.Span<T>
    • System.ReadOnlySpan<T>
      em que casos o tipo de elemento é T
  • Um tipo com um método create apropriado, caso em que o tipo de elemento é o tipo de iteração determinado a partir de um método de instância GetEnumerator ou interface enumerável, não de um método de extensão
  • Um struct ou tipo de classe que implementa System.Collections.IEnumerable onde:
    • O type tem um construtor aplicável que pode ser invocado sem argumentos, e o construtor é acessível no local da expressão de coleção.

    • Se a expressão de coleção tiver quaisquer elementos, o tipo terá um método de instância ou extensão Add onde:

      • O método pode ser invocado com um único argumento de valor.
      • Se o método for genérico, os argumentos de tipo podem ser inferidos a partir da coleção e do argumento.
      • O método é acessível no local da expressão de coleção.

      Nesse caso, o tipo de elemento é o tipo de iteração do tipo .

  • Um tipo de interface :
    • System.Collections.Generic.IEnumerable<T>
    • System.Collections.Generic.IReadOnlyCollection<T>
    • System.Collections.Generic.IReadOnlyList<T>
    • System.Collections.Generic.ICollection<T>
    • System.Collections.Generic.IList<T>
      em quais casos o tipo de elemento é T

A conversão implícita existe se o tipo tiver um tipo de elemento U onde para cada elemento Eᵢ na expressão da coleção:

  • Se Eᵢ é um elemento de expressão , há uma conversão implícita de Eᵢ para U.
  • Se Eᵢ for um elemento de dispersão ..Sᵢ, existe uma conversão implícita do tipo de iteração de Sᵢ para o U.

Não há conversão de expressão de coleção de uma expressão de coleção para um tipo de matriz multidimensional.

Os tipos que podem receber uma conversão implícita de uma expressão de coleção são os tipos de destino válidos para essa expressão de coleção.

As seguintes conversões implícitas adicionais existem a partir de uma expressão de coleção :

  • Para um tipo de valor anulávelT? onde há uma expressão de coleção conversão da expressão de coleção para um tipo de valor T. A conversão é uma conversão de uma expressão de coleção para T seguida por uma conversão implícita anulável de T para T?.

  • Para um tipo de referência T onde há um método associado a T que retorna um tipo U e uma conversão de referência implícita de U para T. A conversão é uma conversão de expressão de coleção para U seguida por uma conversão de referência implícita de U para T.

  • Para um tipo de interface I onde há um método criar associado a I que retorna um tipo V e uma conversão implícita de boxing de V para I. A conversão é uma conversão de expressão de coleção para V seguida por uma conversão implícita de encaixotamento de V para I.

Criar métodos

Um método create é indicado com um atributo [CollectionBuilder(...)] no tipo de coleção . O atributo especifica o tipo de construtor e o nome do método de um método a ser invocado para construir uma instância do tipo de coleção.

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(
        AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface,
        Inherited = false,
        AllowMultiple = false)]
    public sealed class CollectionBuilderAttribute : System.Attribute
    {
        public CollectionBuilderAttribute(Type builderType, string methodName);
        public Type BuilderType { get; }
        public string MethodName { get; }
    }
}

O atributo pode ser aplicado a um class, struct, ref structou interface. O atributo não é herdado, embora o atributo possa ser aplicado a um class base ou a um abstract class.

O tipo construtor deve ser um não genérico class ou struct.

Primeiro, o conjunto de métodos de criação de aplicáveisCM é determinado.
Consiste em métodos que cumprem os seguintes requisitos:

  • O método deve ter o nome especificado no atributo [CollectionBuilder(...)].
  • Deve-se definir o método diretamente no tipo de builder .
  • O método deve ser static.
  • O método deve estar acessível onde a expressão de coleção é usada.
  • A aridade do método deve corresponder à aridade do tipo de coleção.
  • O método deve ter um único parâmetro do tipo System.ReadOnlySpan<E>, passado por valor.
  • Há uma conversão de identidade , conversão de referência implícitaou conversão de encaixotamento do tipo de retorno do método para o tipo de coleção .

Os métodos declarados em tipos de base ou interfaces são ignorados e não fazem parte do conjunto de CM.

Se o conjunto CM estiver vazio, o tipo de coleção não tem um tipo de elemento e não tem um método de criação . Nenhuma das etapas a seguir se aplica.

Se apenas um método entre aqueles no conjunto de tiver um de conversão de identidade de para o tipo de elemento do tipo de coleção , esse é o método create para o tipo de coleção . Caso contrário, o tipo de coleção não terá um método create.

Um erro será relatado se o atributo [CollectionBuilder] não se referir a um método invocável com a assinatura esperada.

Para uma expressão de coleção com um tipo de destino C<S0, S1, …> onde a declaração de tipo associada C<T0, T1, …> possui um método construtor associado B.M<U0, U1, …>(), os argumentos genéricos de tipo do tipo de destino são aplicados em ordem, do tipo mais externo para o mais interno, ao método construtor .

O parâmetro span para o método create pode ser explicitamente marcado como scoped ou [UnscopedRef]. Se o parâmetro for implícita ou explicitamente scoped, o compilador pode alocar o armazenamento para o span na pilha em vez do heap.

Por exemplo, um possível método criar para ImmutableArray<T>:

[CollectionBuilder(typeof(ImmutableArray), "Create")]
public struct ImmutableArray<T> { ... }

public static class ImmutableArray
{
    public static ImmutableArray<T> Create<T>(ReadOnlySpan<T> items) { ... }
}

Com o método create acima, ImmutableArray<int> ia = [1, 2, 3]; pode ser emitido como:

[InlineArray(3)] struct __InlineArray3<T> { private T _element0; }

Span<int> __tmp = new __InlineArray3<int>();
__tmp[0] = 1;
__tmp[1] = 2;
__tmp[2] = 3;
ImmutableArray<int> ia =
    ImmutableArray.Create((ReadOnlySpan<int>)__tmp);

Construção

Os elementos de uma expressão de coleção são avaliados em ordem, da esquerda para a direita. Cada elemento é avaliado exatamente uma vez, e quaisquer outras referências aos elementos referem-se aos resultados desta avaliação inicial.

Um elemento spread pode ser iterado antes ou depois que os elementos subsequentes na expressão de coleção são avaliados.

Uma exceção não tratada de qualquer um dos métodos usados durante a construção não será detetada e impedirá novas etapas na construção.

Length, Counte GetEnumerator presume-se que não tenham efeitos colaterais.


Se o tipo de destino for um struct ou tipo de classe que implementa System.Collections.IEnumerable, e o tipo de destino não tiver um método create, a construção da instância de coleta será a seguinte:

  • Os elementos são avaliados por ordem. Alguns ou todos os elementos podem ser avaliados durante as etapas abaixo em vez de antes.

  • O compilador pode determinar o comprimento conhecido da expressão de coleção ao invocar propriedadesenumeráveis — ou propriedades equivalentes de interfaces ou tipos bem conhecidos — em cada elemento distribuído .

  • O construtor que é aplicável sem argumentos é invocado.

  • Para cada elemento, por ordem:

    • Se o elemento for um elemento de expressão , a instância de Add aplicável ou o método de extensão será invocado com o elemento expressão como o argumento. (Ao contrário do comportamento clássico do inicializador de coleção , a avaliação de elementos e as chamadas de Add não são necessariamente intercaladas.)
    • Se o elemento for um elemento spread então uma das seguintes opções será usada:
      • Uma instância de GetEnumerator aplicável ou método de extensão é invocado na expressão de elemento espalhado e, para cada item do enumerador, a instância de Add aplicável ou o método de extensão é invocado na instância da coleção com o item como argumento. Se o enumerador implementar IDisposable, Dispose será chamado após a enumeração, independentemente das exceções.
      • Uma instância aplicável de ou um método de extensão é invocado na instância de coleção de , com a expressão de elemento spread como argumento.
      • Uma instância aplicável de CopyTo ou um método de extensão é invocado sobre a expressão do elemento de propagação , utilizando como argumentos a instância de coleção e o índice int.
  • Durante as etapas de construção acima, uma instância de EnsureCapacity aplicável ou um método de extensão pode ser invocado uma ou mais vezes na instância de coleta com um argumento de capacidade int.


Se o tipo de destino for uma matriz , um span , um tipo com um método create, ou uma interface , a construção da instância de coleção é a seguinte:

  • Os elementos são avaliados por ordem. Alguns ou todos os elementos podem ser avaliados durante as etapas abaixo em vez de antes.

  • O compilador pode determinar o comprimento conhecido da expressão de coleção invocando propriedades de contáveis — ou propriedades equivalentes de interfaces ou tipos conhecidos — em cada expressão de elemento spread.

  • Uma instância de inicialização é criada da seguinte maneira:

    • Se o tipo de destino for uma matriz e a expressão de coleção tiver um comprimento conhecido, uma matriz será alocada com o comprimento esperado.
    • Se o tipo de destino for um span de ou um tipo com um método de criação, e a coleção tiver um comprimento conhecido, será criado um span com o comprimento esperado referente a um armazenamento contíguo.
    • Caso contrário, o armazenamento de memória intermediário é alocado.
  • Para cada elemento, por ordem:

    • Se o elemento for um elemento de expressão , a instância de inicialização indexador será invocada para adicionar a expressão avaliada no índice atual.
    • Se o elemento for um elemento spread então uma das seguintes opções será usada:
      • Um membro de uma interface ou tipo bem conhecido é invocado para copiar itens da expressão de elemento expandido para a instância de inicialização.
      • Uma instância aplicável GetEnumerator ou método de extensão é invocado na expressão de elemento de dispersão e, para cada item do enumerador, a instância de inicialização indexador é invocada para adicionar o item no índice atual. Se o enumerador implementar IDisposable, Dispose será chamado após a enumeração, independentemente das exceções.
      • Uma instância aplicável de CopyTo ou um método de extensão é invocado na expressão do elemento spread com a instância de inicialização e o índice int como argumentos.
  • Se o armazenamento intermédio foi alocado para a coleção, uma instância de coleção é alocada com o comprimento real da coleção e os valores da instância de inicialização são copiados para a instância de coleção, ou se uma 'span' for necessária, o compilador pode utilizar uma 'span' do comprimento real da coleção a partir do armazenamento intermédio. Caso contrário, a instância de inicialização será a instância de coleção.

  • Se o tipo de destino tiver um método create, o método create será invocado com a instância span.


Nota: O compilador pode atrasar adição de elementos à coleção — ou atrasar iteração através de elementos spread — até depois de avaliar os elementos subsequentes. (Quando os elementos de spread subsequentes tiverem propriedades de contáveis que permitam calcular o comprimento esperado da coleção antes de alocar a coleção.) Por outro lado, o compilador pode ansiosamente adicionar elementos à coleção — e ansiosamente iterar através de elementos spread — quando não há vantagem em atrasar.

Considere a seguinte expressão de coleção:

int[] x = [a, ..b, ..c, d];

Se os elementos spread b e c forem contáveis, o compilador pode atrasar a adição de itens de a e b até que c seja avaliado, para permitir a alocação da matriz resultante no comprimento esperado. Depois disso, o compilador poderia ansiosamente adicionar itens de c, antes de avaliar d.

var __tmp1 = a;
var __tmp2 = b;
var __tmp3 = c;
var __result = new int[2 + __tmp2.Length + __tmp3.Length];
int __index = 0;
__result[__index++] = __tmp1;
foreach (var __i in __tmp2) __result[__index++] = __i;
foreach (var __i in __tmp3) __result[__index++] = __i;
__result[__index++] = d;
x = __result;

Literal de coleção vazia

  • O literal vazio [] não tem tipo. No entanto, semelhante ao literal nulo , esse literal pode ser implicitamente convertido em qualquer tipo de coleção construível.

    Por exemplo, o seguinte não é legal, pois não há tipo de destino e não há outras conversões envolvidas:

    var v = []; // illegal
    
  • É permitido omitir um literal vazio. Por exemplo:

    bool b = ...
    List<int> l = [x, y, .. b ? [1, 2, 3] : []];
    

    Aqui, se b é falso, não é necessário que qualquer valor seja realmente construído para a expressão de coleção vazia, uma vez que seria imediatamente distribuído como valores zero no literal resultante.

  • A expressão de coleção vazia pode ser um singleton se usada para construir um valor de coleção final que é conhecido por não ser mutável. Por exemplo:

    // Can be a singleton, like Array.Empty<int>()
    int[] x = []; 
    
    // Can be a singleton. Allowed to use Array.Empty<int>(), Enumerable.Empty<int>(),
    // or any other implementation that can not be mutated.
    IEnumerable<int> y = [];
    
    // Must not be a singleton.  Value must be allowed to mutate, and should not mutate
    // other references elsewhere.
    List<int> z = [];
    

Ref segurança

Consulte a restrição de contexto segura para definições dos valores de contexto seguro de : bloco de declaração, membro da função, e contexto do chamador.

A de contexto seguro de uma expressão de coleção é:

  • O contexto seguro de uma expressão de coleção vazia [] é o contexto do chamador .

  • Se o tipo de destino for um span de tipo System.ReadOnlySpan<T>, e T for um dos tipos primitivos bool, sbyte, byte, short, ushort, char, int, uint, long, ulong, floatou double, e a expressão de coleção contiver apenas valores constantes , o contexto seguro da expressão de coleção é o contexto de chamador .

  • Se o tipo de destino for um tipo de segmento System.Span<T> ou System.ReadOnlySpan<T>, o contexto seguro da expressão de coleção será o bloco de declaração .

  • Se o tipo de destino for um tipo ref struct com um método de criação , o contexto seguro da expressão de coleção é o contexto seguro de uma invocação do método de criação, onde a expressão de coleção é o argumento span para o método.

  • Caso contrário, o contexto seguro da expressão de coleção é o contexto do chamador.

Uma expressão de coleção com um contexto seguro de bloco de declaração não pode sair do escopo envolvente, e o compilador pode guardar a coleção na pilha em vez de no heap.

Para permitir que uma expressão de coleção de um tipo ref struct escape do bloco de declaração , pode ser necessário converter a expressão para outro tipo.

static ReadOnlySpan<int> AsSpanConstants()
{
    return [1, 2, 3]; // ok: span refers to assembly data section
}

static ReadOnlySpan<T> AsSpan2<T>(T x, T y)
{
    return [x, y];    // error: span may refer to stack data
}

static ReadOnlySpan<T> AsSpan3<T>(T x, T y, T z)
{
    return (T[])[x, y, z]; // ok: span refers to T[] on heap
}

Inferência de tipo

var a = AsArray([1, 2, 3]);          // AsArray<int>(int[])
var b = AsListOfArray([[4, 5], []]); // AsListOfArray<int>(List<int[]>)

static T[] AsArray<T>(T[] arg) => arg;
static List<T[]> AsListOfArray<T>(List<T[]> arg) => arg;

As regras de inferência de tipo são atualizadas da seguinte forma.

As regras existentes para a primeira fase são extraídas para uma nova seção de inferência de tipo de entrada, e uma regra é adicionada às inferências de tipo de entrada e de tipo de saída para expressões de coleção.

11.6.3.2 Primeira fase

Para cada um dos argumentos do método Eᵢ

  • Uma inferência de tipo de entrada é feita deEᵢpara o tipo de parâmetro correspondenteTᵢ.

Uma de inferência de tipo de entrada é feita de uma expressão para um tipo da seguinte maneira:

  • Se E for uma expressão de coleção com elementos Eᵢ, e T for um tipo com um tipo de elemento Tₑ ou T for um tipo de valor anulável T0? e T0 tiver um tipo de elemento Tₑ, então para cada Eᵢ:
    • Se Eᵢ for um elemento de expressão , então uma inferência de tipo de entrada será feita deEᵢparaTₑ.
    • Se for um elemento spread com um tipo de iteração , então uma de inferência de limite inferior será feita depara.
  • [regras existentes desde a primeira fase] ...

11.6.3.7 Inferências do tipo de saída

Uma de inferência de tipo de saída é feita de uma expressão para um tipo da seguinte maneira:

  • Se E é uma expressão de coleção com elementos Eᵢ, e T é um tipo com um tipo de elemento Tₑ ou T é um tipo de valor anulável T0? e T0 tem um tipo de elemento Tₑ, então para cada Eᵢ:
    • Se Eᵢ for um elemento de expressão , então uma inferência de tipo de saída será feita deEᵢparaTₑ.
    • Se Eᵢ é um elemento spread, nenhuma inferência é feita a partir de Eᵢ.
  • [regras existentes a partir de inferências de tipo de saída] ...

Métodos de extensão

Não há alterações nas regras de invocação do método de extensão .

12.8.10.3 Invocações do método de extensão

Um método de extensão Cᵢ.Mₑé elegível se:

  • ...
  • Existe uma conversão implícita de identidade, referência ou boxing de expr para o tipo do primeiro parâmetro de Mₑ.

Uma expressão de coleção não tem um tipo natural, portanto, as conversões existentes de tipo não são aplicáveis. Como resultado, uma expressão de coleção não pode ser usada diretamente como o primeiro parâmetro para uma invocação de método de extensão.

static class Extensions
{
    public static ImmutableArray<T> AsImmutableArray<T>(this ImmutableArray<T> arg) => arg;
}

var x = [1].AsImmutableArray();           // error: collection expression has no target type
var y = [2].AsImmutableArray<int>();      // error: ...
var z = Extensions.AsImmutableArray([3]); // ok

Resolução de sobrecarga

Melhor conversão da expressão são atualizadas para preferir certos tipos de destino nas conversões de expressões de coleção.

Nas regras atualizadas:

  • Um span_type é um dos seguintes:
    • System.Span<T>
    • System.ReadOnlySpan<T>.
  • Um array_or_array_interface é um dos seguintes:
    • Um tipo de matriz
    • Um dos seguintes tipos de interface implementado por um tipo de matriz :
      • System.Collections.Generic.IEnumerable<T>
      • System.Collections.Generic.IReadOnlyCollection<T>
      • System.Collections.Generic.IReadOnlyList<T>
      • System.Collections.Generic.ICollection<T>
      • System.Collections.Generic.IList<T>

Dado um conversão implícita C₁ que converte uma expressão E para um tipo T₁, e uma conversão implícita C₂ que converte uma expressão E para um tipo T₂, C₁ é uma melhor conversão do que C₂ se uma das seguintes condições for verdadeira:

  • E é uma expressão de coleção e uma das seguintes retenções:
    • T₁ é System.ReadOnlySpan<E₁>, e T₂ é System.Span<E₂>, e existe uma conversão implícita de E₁ para E₂
    • T₁ é System.ReadOnlySpan<E₁> ou System.Span<E₁>, e T₂ é um array_or_array_interface com tipo de elementoE₂, e existe uma conversão implícita de E₁ para E₂
    • T₁ não é um span_type, e T₂ não é um span_type, e existe uma conversão implícita de T₁ para T₂
  • E não é uma expressão de coleção e uma das seguintes condições:
    • E corresponde exatamente T₁ e E não corresponde exatamente a T₂
    • E corresponde exatamente a ambos ou a nenhum dos T₁ e T₂, e T₁ é um alvo de conversão melhor do que T₂
  • E é um grupo de métodos, ...

Exemplos de diferenças na resolução de sobrecarga entre inicializadores de matriz e expressões de coleção:

static void Generic<T>(Span<T> value) { }
static void Generic<T>(T[] value) { }

static void SpanDerived(Span<string> value) { }
static void SpanDerived(object[] value) { }

static void ArrayDerived(Span<object> value) { }
static void ArrayDerived(string[] value) { }

// Array initializers
Generic(new[] { "" });      // string[]
SpanDerived(new[] { "" });  // ambiguous
ArrayDerived(new[] { "" }); // string[]

// Collection expressions
Generic([""]);              // Span<string>
SpanDerived([""]);          // Span<string>
ArrayDerived([""]);         // ambiguous

Tipos de span

Os tipos de span ReadOnlySpan<T> e Span<T> são tipos de coleção construíveis. O suporte para eles segue o design para params Span<T>. Especificamente, a construção de qualquer uma dessas extensões resultará em uma matriz T[] criada na pilha se a matriz params estiver dentro dos limites (se houver) definidos pelo compilador. Caso contrário, a matriz será alocada no montão.

Se o compilador optar por alocação na pilha de memória, não é necessário converter um literal diretamente para o valor stackalloc naquele ponto específico. Por exemplo, dado:

foreach (var x in y)
{
    Span<int> span = [a, b, c];
    // do things with span
}

O compilador tem permissão para traduzir isso usando stackalloc desde que o significado de Span permaneça o mesmo e que a segurança de extensão de seja mantida. Por exemplo, ele pode traduzir o acima para:

Span<int> __buffer = stackalloc int[3];
foreach (var x in y)
{
    __buffer[0] = a
    __buffer[1] = b
    __buffer[2] = c;
    Span<int> span = __buffer;
    // do things with span
}

O compilador pode também utilizar matrizes embutidas, se disponíveis, ao decidir alocar na pilha. Observe que no C# 12, matrizes embutidas não podem ser inicializadas com uma expressão de coleção. Trata-se de uma proposta em aberto.

Se o compilador decidir alocar no heap, a tradução para Span<T> é simplesmente:

T[] __array = [...]; // using existing rules
Span<T> __result = __array;

Tradução literal da coleção

Uma expressão de coleção tem um comprimento conhecido se o tipo de tempo de compilação de cada elemento spread na expressão de coleção for contável.

Tradução de interfaces

Tradução de interface não mutável

Dado um tipo de destino que não contém membros mutantes, ou seja, IEnumerable<T>, IReadOnlyCollection<T>e IReadOnlyList<T>, uma implementação compatível é necessária para produzir um valor que implemente essa interface. Se um tipo é sintetizado, recomenda-se que o tipo sintetizado implemente todas essas interfaces, bem como ICollection<T> e IList<T>, independentemente de qual tipo de interface foi visado. Isso garante a máxima compatibilidade com bibliotecas existentes, incluindo aquelas que introspeccionam as interfaces implementadas por um valor, a fim de habilitar otimizações de desempenho.

Além disso, o valor deve implementar as interfaces não genéricas ICollection e IList. Isso permite que as expressões de coleção ofereçam suporte à introspeção dinâmica em cenários como a vinculação de dados.

Uma implementação compatível é livre para:

  1. Use um tipo existente que implemente as interfaces necessárias.
  2. Sintetize um tipo que implemente as interfaces necessárias.

Em ambos os casos, o tipo usado pode implementar um conjunto maior de interfaces do que as estritamente necessárias.

Os tipos sintetizados são livres para empregar qualquer estratégia para implementar corretamente as interfaces necessárias. Por exemplo, um tipo sintetizado pode embutir os elementos diretamente dentro de si mesmo, evitando a necessidade de alocações adicionais de coleta interna. Um tipo sintetizado também não poderia usar qualquer tipo de armazenamento, optando por calcular os valores diretamente. Por exemplo, retornar index + 1 para [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].

  1. O valor deve retornar true ao consultar ICollection<T>.IsReadOnly (se implementado) e os não genéricos IList.IsReadOnly e IList.IsFixedSize. Isso garante que os consumidores possam dizer adequadamente que a coleção não é mutável, apesar de implementar as visualizações mutáveis.
  2. O valor deve lançar em qualquer chamada para um método de mutação (como IList<T>.Add). Isso garante a segurança, evitando que uma coleção não mutável seja acidentalmente mutada.

Tradução de interface mutável

Dado o tipo de destino que contém membros mutantes, ou seja, ICollection<T> ou IList<T>:

  1. O valor deve ser uma instância de List<T>.

Tradução de comprimento conhecido

Ter um comprimento conhecido permite a construção eficiente de um resultado com o potencial de nenhuma cópia de dados e nenhum espaço ocioso desnecessário em um resultado.

Não ter um comprimento conhecido não impede que qualquer resultado seja criado. No entanto, isso pode resultar em custos extras de CPU e memória na produção dos dados e, em seguida, movê-los para o destino final.

  • Para um comprimento literal conhecido [e1, ..s1, etc], a tradução começa assim:

    int __len = count_of_expression_elements +
                __s1.Count;
                ...
                __s_n.Count;
    
  • Dado um tipo de destino T para esse literal:

    • Se T é algum T1[], então o literal é traduzido como:

      T1[] __result = new T1[__len];
      int __index = 0;
      
      __result[__index++] = __e1;
      foreach (T1 __t in __s1)
          __result[__index++] = __t;
      
      // further assignments of the remaining elements
      

      A implementação tem permissão para utilizar outros meios para preencher a matriz. Por exemplo, utilizando métodos eficientes de cópia em massa como .CopyTo().

    • Se T for algum Span<T1>, então o literal é traduzido da mesma forma que acima, exceto que a inicialização __result é traduzida como:

      Span<T1> __result = new T1[__len];
      
      // same assignments as the array translation
      

      A tradução pode usar stackalloc T1[] ou um matriz em linha em vez de new T1[] se segurança de âmbito for mantida.

    • Se T é algum ReadOnlySpan<T1>, então o literal é traduzido da mesma forma que para o caso de Span<T1>, exceto que o resultado final será que Span<T1>implicitamente convertido em um(a) ReadOnlySpan<T1>.

      Um ReadOnlySpan<T1>, em que T1 é um tipo primitivo e todos os elementos da coleção são constantes, não precisa que os seus dados estejam no heap ou na pilha. Por exemplo, uma implementação pode construir esse intervalo diretamente como uma referência a uma parte do segmento de dados do programa.

      Os formulários acima (para matrizes e vãos) são as representações base da expressão de coleção e são usados para as seguintes regras de tradução:

      • Se T é algum C<S0, S1, …> que tem um método de criação B.M<U0, U1, …>()correspondente, então o literal é traduzido como:

        // Collection literal is passed as is as the single B.M<...>(...) argument
        C<S0, S1, …> __result = B.M<S0, S1, …>([...])
        

        Como o método create deve ter um tipo de argumento de algum ReadOnlySpan<T>instanciado, a regra de conversão para extensões se aplica ao passar a expressão de coleção para o método create.

      • Se T oferecer suporte a inicializadores de coleção , então:

        • Se o tipo T contém um construtor acessível com um único parâmetro int capacity, então o literal é traduzido como:

          T __result = new T(capacity: __len);
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          Nota: o nome do parâmetro deve ser capacity.

          Este formulário permite que um literal informe o tipo recém-construído sobre o número de elementos, permitindo assim uma alocação eficiente do armazenamento interno. Isso evita relocações desnecessárias à medida que os elementos são adicionados.

        • caso contrário, a expressão literal é traduzida como:

          T __result = new T();
          
          __result.Add(__e1);
          foreach (var __t in __s1)
              __result.Add(__t);
          
          // further additions of the remaining elements
          

          Isso permite criar o tipo de destino, embora sem otimização de capacidade para evitar a realocação interna do armazenamento.

Tradução de comprimento desconhecido

  • Dado um tipo de destino T para um comprimento desconhecido literal:

    • Se T oferecer suporte a inicializadores de coleção , o literal será traduzido como:

      T __result = new T();
      
      __result.Add(__e1);
      foreach (var __t in __s1)
          __result.Add(__t);
      
      // further additions of the remaining elements
      

      Isso permite a disseminação de qualquer tipo iterável, embora com a menor quantidade de otimização possível.

    • Se T é algum T1[], então o literal tem a mesma semântica que:

      List<T1> __list = [...]; /* initialized using predefined rules */
      T1[] __result = __list.ToArray();
      

      No entanto, o acima exposto é ineficiente; ele cria a lista de intermediários e, em seguida, cria uma cópia da matriz final a partir dela. As implementações são livres para otimizar isso, por exemplo, produzindo código assim:

      T1[] __result = <private_details>.CreateArray<T1>(
          count_of_expression_elements);
      int __index = 0;
      
      <private_details>.Add(ref __result, __index++, __e1);
      foreach (var __t in __s1)
          <private_details>.Add(ref __result, __index++, __t);
      
      // further additions of the remaining elements
      
      <private_details>.Resize(ref __result, __index);
      

      Isso permite um mínimo de desperdício e duplicação, sem sobrecarga adicional que as coleções da biblioteca possam acarretar.

      As contagens passadas para CreateArray são utilizadas para fornecer uma sugestão de tamanho inicial, evitando redimensionamentos desnecessários.

    • Se T for tipo de span, uma implementação pode seguir a estratégia acima T[], ou qualquer outra estratégia com a mesma semântica, mas melhor desempenho. Por exemplo, em vez de alocar a matriz como uma cópia dos elementos da lista, CollectionsMarshal.AsSpan(__list) poderia ser usado para obter diretamente um valor de intervalo.

Cenários sem suporte

Embora os literais de coleção possam ser usados para muitos cenários, há alguns que eles não são capazes de substituir. Estes incluem:

  • Matrizes multidimensionais (por exemplo, new int[5, 10] { ... }). Não há facilidade para incluir as dimensões, e todos os literais de coleção são lineares ou apenas estruturas de mapa.
  • Coleções que passam valores especiais aos seus construtores. Não há nenhum mecanismo para aceder ao construtor que está a ser usado.
  • Inicializadores de coleção aninhados, por exemplo, new Widget { Children = { w1, w2, w3 } }. Esta forma precisa permanecer, pois tem semânticas muito diferentes de Children = [w1, w2, w3]. O primeiro chama .Add repetidamente em .Children enquanto o segundo atribuiria uma nova coleção para .Children. Poderíamos considerar que o último formulário voltasse a ser adicionado a uma coleção existente se .Children não puder ser atribuído, mas isso parece ser extremamente confuso.

Ambiguidades sintáticas

  • Existem duas ambiguidades sintáticas "verdadeiras" onde há múltiplas interpretações sintáticas legais de código que usa uma collection_literal_expression.

    • O spread_element é ambíguo em relação a um range_expression. Tecnicamente, poderíamos ter:

      Range[] ranges = [range1, ..e, range2];
      

      Para resolver este problema, podemos:

      • Exija que os utilizadores parentizem (..e) ou incluam um índice inicial 0..e se quiserem um intervalo.
      • Escolha uma sintaxe diferente (como ...) para distribuição. Isso seria lamentável pela falta de consistência com os padrões de fatias.
  • Há dois casos em que não há uma verdadeira ambiguidade, mas em que a sintaxe aumenta muito a complexidade da análise. Embora não seja um problema dado o tempo de engenharia, isso ainda aumenta a sobrecarga cognitiva para os usuários ao olhar para o código.

    • Ambiguidade entre collection_literal_expression e attributes em enunciados ou funções locais. Considere:

      [X(), Y, Z()]
      

      Este pode ser um dos seguintes:

      // A list literal inside some expression statement
      [X(), Y, Z()].ForEach(() => ...);
      
      // The attributes for a statement or local function
      [X(), Y, Z()] void LocalFunc() { }
      

      Sem uma antevisão complexa, seria impossível dizer sem consumir a totalidade do texto literal.

      As opções para resolver este problema incluem:

      • Permita isso, fazendo o trabalho de análise para determinar qual desses casos é.
      • Não permita isso e exija que o usuário envolva o literal entre parênteses como ([X(), Y, Z()]).ForEach(...).
      • Ambiguidade entre um collection_literal_expression num conditional_expression e um null_conditional_operations. Considere:
      M(x ? [a, b, c]
      

      Este pode ser um dos seguintes:

      // A ternary conditional picking between two collections
      M(x ? [a, b, c] : [d, e, f]);
      
      // A null conditional safely indexing into 'x':
      M(x ? [a, b, c]);
      

      Sem um olhar complexo, seria impossível dizer sem consumir a totalidade do literal.

      Nota: este é um problema mesmo sem um tipo natural porque a digitação de destino se aplica através de conditional_expressions.

      Tal como acontece com os outros, poderíamos exigir parênteses para desambiguar. Em outras palavras, assuma a interpretação null_conditional_operation, a menos que seja escrita desta forma: x ? ([1, 2, 3]) :. No entanto, isso parece bastante lamentável. Este tipo de código não parece irracional de escrever e provavelmente vai confundir as pessoas.

Desvantagens

  • Isso introduz mais uma forma para expressões de coleção além das inúmeras maneiras que já temos. Trata-se de uma complexidade extra para a língua. Dito isto, isto também torna possível unificar numa anel sintaxe única para reger todos eles, o que significa que as bases de código existentes podem ser simplificadas e adotar uma aparência uniforme em todo o lado.
  • Usando [...] em vez de {...} se afasta da sintaxe que geralmente usamos para matrizes e inicializadores de coleção. Especificamente que ele usa [...] em vez de {...}. No entanto, isso já foi resolvido pela equipe de idiomas quando listamos padrões. Tentámos fazer com que {} funcionasse com padrões de lista e esbarrámos em problemas intransponíveis. Por causa disso, mudamos para [] que, embora novo para C#, parece natural em muitas linguagens de programação e nos permitiu começar de novo sem ambiguidade. Usar [...] como a forma literal correspondente é complementar com as nossas últimas decisões e oferece-nos um espaço livre de problemas para trabalhar.

Isso introduz verrugas na linguagem. Por exemplo, os seguintes são legais e (felizmente) significam exatamente a mesma coisa:

int[] x = { 1, 2, 3 };
int[] x = [ 1, 2, 3 ];

No entanto, dada a amplitude e consistência trazidas pela nova sintaxe literal, devemos considerar recomendar que as pessoas mudem para a nova forma. Sugestões e correções do IDE podem ajudar nesse sentido.

Alternativas

  • Que outros desenhos foram considerados? Qual é o impacto de não fazer isso?

Questões resolvidas

  • O compilador deve usar stackalloc para alocação de pilha quando matrizes embutidas não estão disponíveis e o tipo de iteração é um tipo primitivo?

    Resolução: Não. O gerenciamento de um buffer de requer esforço adicional em um de matriz embutida para garantir que o buffer não seja alocado repetidamente quando a expressão de coleção estiver dentro de um loop. A complexidade adicional tanto no compilador quanto no código gerado supera o benefício da alocação de pilha de memória em plataformas mais antigas.

  • Em que ordem devemos avaliar os elementos literais em comparação com a avaliação da propriedade Length/Count? Devemos avaliar todos os elementos primeiro, depois todos os comprimentos? Ou devemos avaliar um elemento, depois o seu comprimento, depois o próximo elemento, e assim por diante?

    Resolução: Avaliamos todos os elementos primeiro, e depois tudo o resto segue.

  • Um literal de comprimento desconhecido pode criar um tipo de coleção que exija um comprimento conhecido, como um array, um span ou uma coleção Construct(array/span)? Isso seria mais difícil de fazer de forma eficiente, mas pode ser possível através do uso inteligente de matrizes agrupadas e/ou construtores.

    Resolução: Sim, permitimos a criação de uma coleção de comprimentos fixos a partir de um comprimento desconhecido literal. O compilador tem permissão para implementar isso da maneira mais eficiente possível.

    O texto a seguir existe para registrar a discussão original deste tópico.

    Os utilizadores podem sempre converter um comprimento desconhecido literal num comprimento conhecido com um código como este:

    ImmutableArray<int> x = [a, ..unknownLength.ToArray(), b];
    

    No entanto, tal é lamentável devido à necessidade de forçar a atribuição de licenças de armazenamento temporário. Poderíamos ser potencialmente mais eficientes se controlássemos a forma como isso era emitido.

  • Uma collection_expression pode ser direcionada para um IEnumerable<T> ou outras interfaces de coleção?

    Por exemplo:

    void DoWork(IEnumerable<long> values) { ... }
    // Needs to produce `longs` not `ints` for this to work.
    DoWork([1, 2, 3]);
    

    Resolução: Sim, um literal pode ser destinado a qualquer tipo de interface I<T> implementada por List<T>. Por exemplo, IEnumerable<long>. Isso é o mesmo que digitar o destino para List<long> e, em seguida, atribuir esse resultado ao tipo de interface especificado. O texto a seguir existe para registrar a discussão original deste tópico.

    A questão em aberto aqui é determinar qual tipo subjacente realmente criar. Uma opção consiste em analisar a proposta de params IEnumerable<T>. Lá, geraríamos uma matriz para passar os valores, semelhante ao que acontece com params T[].

  • Pode/deve o compilador emitir Array.Empty<T>() para []? Devemos exigir que isso seja feito, a fim de evitar alocações sempre que possível?

    Sim. O compilador deve emitir Array.Empty<T>() para qualquer caso em que isso seja legal e o resultado final não seja mutável. Por exemplo, segmentar T[], IEnumerable<T>, IReadOnlyCollection<T> ou IReadOnlyList<T>. Não deve utilizar Array.Empty<T> quando o alvo é mutável (ICollection<T> ou IList<T>).

  • Devemos expandir os inicializadores de coleção para procurar o muito comum método AddRange? Poderia ser utilizado pelo tipo construído subjacente para realizar a adição de elementos de propagação de forma potencialmente mais eficiente. Podemos querer procurar coisas como .CopyTo também. Pode haver desvantagens aqui, pois esses métodos podem acabar causando excesso de alocações/despachos em vez de enumerar diretamente no código traduzido.

    Sim. Uma implementação pode utilizar outros métodos para inicializar um valor de coleção, sob a presunção de que esses métodos têm semântica bem definida e que os tipos de coleção devem ser "bem comportados". Na prática, porém, uma implementação deve ser cautelosa, pois os benefícios de uma forma (cópia em massa) também podem vir com consequências indesejáveis (por exemplo, o boxeamento de uma coleção de structs).

    Uma implementação deve tirar partido nos casos em que não existem desvantagens. Por exemplo, com um método .AddRange(ReadOnlySpan<T>).

Questões por resolver

  • Devemos permitir inferir o tipo de elemento quando o tipo de iteração é "ambíguo" (por alguma definição)? Por exemplo:
Collection x = [1L, 2L];

// error CS1640: foreach statement cannot operate on variables of type 'Collection' because it implements multiple instantiations of 'IEnumerable<T>'; try casting to a specific interface instantiation
foreach (var x in new Collection) { }

static class Builder
{
    public Collection Create(ReadOnlySpan<long> items) => throw null;
}

[CollectionBuilder(...)]
class Collection : IEnumerable<int>, IEnumerable<string>
{
    IEnumerator<int> IEnumerable<int>.GetEnumerator() => throw null;
    IEnumerator<string> IEnumerable<string>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}
  • Deveria ser legal criar e indexar imediatamente numa coleção literal? Nota: isso requer uma resposta para a questão não resolvida abaixo sobre se os literais de coleção têm um tipo natural.

  • Alocações de pilha para coleções enormes podem explodir a pilha. O compilador deve ter uma heurística para colocar esses dados no heap? A linguagem não deve ser especificada para permitir essa flexibilidade? Devemos seguir as especificações para params Span<T>.

  • Precisamos de ter como alvo o tipo spread_element? Considere, por exemplo:

    Span<int> span = [a, ..b ? [c] : [d, e], f];
    

    Nota: isso geralmente pode surgir da seguinte forma para permitir a inclusão condicional de algum conjunto de elementos, ou nada se a condição for falsa:

    Span<int> span = [a, ..b ? [c, d, e] : [], f];
    

    Para avaliar este literal completo, precisamos avaliar as expressões dos elementos contidos. Isso significa ser capaz de avaliar b ? [c] : [d, e]. No entanto, na ausência de um tipo de alvo para avaliar esta expressão no seu contexto, e na ausência de qualquer tipo natural , não seríamos capazes de determinar o que fazer com [c] ou [d, e] aqui.

    Para resolver isso, poderíamos dizer que, ao avaliar a expressão spread_element de um literal, havia um tipo de alvo implícito equivalente ao tipo de destino do próprio literal. Assim, no texto acima, isso seria reescrito como:

    int __e1 = a;
    Span<int> __s1 = b ? [c] : [d, e];
    int __e2 = f;
    
    Span<int> __result = stackalloc int[2 + __s1.Length];
    int __index = 0;
    
    __result[__index++] = a;
    foreach (int __t in __s1)
      __result[index++] = __t;
    __result[__index++] = f;
    
    Span<int> span = __result;
    

Especificação de um tipo de coleção construível utilizando um método criar é sensível ao contexto no qual a conversão é classificada.

A existência da conversão, neste caso, depende da noção de um tipo de iteração do tipo de coleção . Se houver um de método create que leve um em que é o tipo de iteração , a conversão existe. Caso contrário, não ocorre.

No entanto, um tipo de iteração é sensível ao contexto no qual foreach é executada. Para o mesmo tipo de coleção ele pode ser diferente com base em quais métodos de extensão estão no escopo, e também pode ser indefinido.

Isso é adequado para o propósito de foreach quando o tipo não é projetado para ser iterável por si mesmo. Se for, os métodos de extensão não podem alterar a forma como o tipo é iterado com foreach, independentemente do contexto.

No entanto, isso parece um pouco estranho para uma conversão ser sensível ao contexto assim. Efetivamente, a conversão é "instável". Um tipo de coleção explicitamente projetado para ser construível pode deixar de fora uma definição de um detalhe muito importante - seu tipo de iteração . Deixando o tipo "inconversível" por conta própria.

Aqui está um exemplo:

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

[CollectionBuilder(typeof(MyCollectionBuilder), nameof(MyCollectionBuilder.Create))]
class MyCollection
{
}
class MyCollectionBuilder
{
    public static MyCollection Create(ReadOnlySpan<long> items) => throw null;
    public static MyCollection Create(ReadOnlySpan<string> items) => throw null;
}

namespace Ns1
{
    static class Ext
    {
        public static IEnumerator<long> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                long s = l;
            }
        
            MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                               2];
        }
    }
}

namespace Ns2
{
    static class Ext
    {
        public static IEnumerator<string> GetEnumerator(this MyCollection x) => throw null;
    }
    
    class Program
    {
        static void Main()
        {
            foreach (var l in new MyCollection())
            {
                string s = l;
            }
        
            MyCollection x1 = ["a",
                               2]; // error CS0029: Cannot implicitly convert type 'int' to 'string'
        }
    }
}

namespace Ns3
{
    class Program
    {
        static void Main()
        {
            // error CS1579: foreach statement cannot operate on variables of type 'MyCollection' because 'MyCollection' does not contain a public instance or extension definition for 'GetEnumerator'
            foreach (var l in new MyCollection())
            {
            }
        
            MyCollection x1 = ["a", 2]; // error CS9188: 'MyCollection' has a CollectionBuilderAttribute but no element type.
        }
    }
}

Dado o design atual, se o tipo não definir por si mesmo o tipo de iteração , o compilador não poderá validar de forma confiável a aplicação de um atributo CollectionBuilder. Se não soubermos o tipo de iteração , não sabemos qual deve ser a assinatura do método create. Se o tipo de iteração vem do contexto, não há garantia de que o tipo sempre será usado em um contexto semelhante.

A funcionalidade Params Collections também é afetada por isto. Parece estranho ser incapaz de prever de forma confiável o tipo de elemento de um parâmetro params no ponto de declaração. A presente proposta exige igualmente que o método criar seja, pelo menos, tão acessível como o tipo de coleção params. É impossível realizar esta verificação de forma confiável, a menos que o tipo de coleção defina o seu tipo de iteração por si mesmo.

Note, que também temos https://github.com/dotnet/roslyn/issues/69676 aberto para compilador, que basicamente observa o mesmo problema, mas fala sobre isso a partir da perspetiva da otimização.

Proposta

Requer um tipo que utiliza o atributo CollectionBuilder para definir o seu tipo de iteração em si mesmo. Em outras palavras, isso significa que o tipo deve implementar IEnumarable/IEnumerable<T>, ou deve ter um método público GetEnumerator com a assinatura correta (isso exclui quaisquer métodos de extensão).

Além disso, agora é necessário criar o método para "estar acessível onde a expressão de coleção é usada". Este é outro ponto de dependência de contexto baseado na acessibilidade. O objetivo deste método é muito semelhante ao propósito de um método de conversão definido pelo usuário, e que um deve ser público. Portanto, devemos considerar exigir que o método criar também seja público.

Conclusão

Aprovado com modificações LDM-2024-01-08

A noção de tipo de iteração não é aplicada de forma consistente nas conversões.

  • Para um struct ou tipo de classe que implementa System.Collections.Generic.IEnumerable<T> onde:
    • Para cada elemento Ei há uma conversão implícita para T.

Parece que há uma suposição de que, neste caso, T é necessário como o tipo de iteração da estrutura ou do tipo de classe . No entanto, esta presunção é errada. O que pode levar a um comportamento muito estranho. Por exemplo:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public void Add(string l) => throw null;
    
    public IEnumerator<string> GetEnumerator() => throw null; 
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
        
        MyCollection x1 = ["a", // error CS0029: Cannot implicitly convert type 'string' to 'long'
                           2];
        MyCollection x2 = new MyCollection() { "b" };
    }
}
  • Para um struct ou tipo de classe que implementa System.Collections.IEnumerable e não implementaSystem.Collections.Generic.IEnumerable<T>.

Parece que a implementação assume que o tipo de iteração é object, mas a especificação deixa esse fato não especificado e simplesmente não exige que cada elemento converta em nada. Em geral, no entanto, o tipo de iteração não é necessariamente do tipo object. O que pode ser observado no seguinte exemplo:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    public IEnumerator<string> GetEnumerator() => throw null; 
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

class Program
{
    static void Main()
    {
        foreach (var l in new MyCollection())
        {
            string s = l; // Iteration type is string
        }
    }
}

A noção de tipo de iteração é fundamental para a funcionalidade Params Collections. E esta questão leva a uma estranha discrepância entre as duas características. Por exemplo:

using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable<long>
{
    IEnumerator<long> IEnumerable<long>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 

    public void Add(long l) => throw null; 
    public void Add(string l) => throw null; 
}

class Program
{
    static void Main()
    {
        Test("2"); // error CS0029: Cannot implicitly convert type 'string' to 'long'
        Test(["2"]); // error CS1503: Argument 1: cannot convert from 'collection expressions' to 'string'
        Test(3); // error CS1503: Argument 1: cannot convert from 'int' to 'string'
        Test([3]); // Ok

        MyCollection x1 = ["2"]; // error CS0029: Cannot implicitly convert type 'string' to 'long'
        MyCollection x2 = [3];
    }

    static void Test(params MyCollection a)
    {
    }
}
using System.Collections;
using System.Collections.Generic;

class MyCollection : IEnumerable
{
    IEnumerator IEnumerable.GetEnumerator() => throw null;

    public IEnumerator<string> GetEnumerator() => throw null; 
    public void Add(object l) => throw null;
}

class Program
{
    static void Main()
    {
        Test("2", 3); // error CS1503: Argument 2: cannot convert from 'int' to 'string'
        Test(["2", 3]); // Ok
    }

    static void Test(params MyCollection a)
    {
    }
}

Provavelmente será bom alinhar de uma maneira ou outra.

Proposta

Especifique a convertibilidade de struct ou da classe do tipo que implementa System.Collections.Generic.IEnumerable<T> ou System.Collections.IEnumerable em termos do tipo de iteração , e requeira uma conversão implícita para cada elemento Ei ao tipo de iteração .

Conclusão

Aprovado LDM-2024-01-08

A conversão da expressão de coleção deve exigir a disponibilidade de um conjunto mínimo de APIs necessárias para construção?

Um tipo de coleção construível de acordo com as conversões pode realmente não ser construível. Isto provavelmente levará a um comportamento inesperado de resolução de sobrecarga. Por exemplo:

class C1
{
    public static void M1(string x)
    {
    }
    public static void M1(char[] x)
    {
    }
    
    void Test()
    {
        M1(['a', 'b']); // error CS0121: The call is ambiguous between the following methods or properties: 'C1.M1(string)' and 'C1.M1(char[])'
    }
}

No entanto, o «C1. M1(string)' não é um candidato que possa ser usado porque:

error CS1729: 'string' does not contain a constructor that takes 0 arguments
error CS1061: 'string' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)

Aqui está outro exemplo com um tipo definido pelo usuário e um erro mais forte que nem menciona um candidato válido:

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(C1 x)
    {
    }
    public static void M1(char[] x)
    {
    }

    void Test()
    {
        M1(['a', 'b']); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
    }

    public static implicit operator char[](C1 x) => throw null;
    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

Parece que a situação é muito semelhante ao que costumávamos ter com grupos de métodos para delegar conversões. Ou seja, havia cenários em que a conversão existia, mas estava errada. Decidimos melhorar isso, garantindo que, se a conversão estiver errada, ela não existe.

Note, que com o recurso "Params Collections" estaremos enfrentando um problema semelhante. Pode ser bom não permitir o uso do modificador params para coleções que não podem ser construídas. No entanto, na proposta atual, essa verificação baseia-se em conversões seção. Aqui está um exemplo:

using System.Collections;
using System.Collections.Generic;

class C1 : IEnumerable<char>
{
    public static void M1(params C1 x) // It is probably better to report an error about an invalid `params` modifier
    {
    }
    public static void M1(params ushort[] x)
    {
    }

    void Test()
    {
        M1('a', 'b'); // error CS1061: 'C1' does not contain a definition for 'Add' and no accessible extension method 'Add' accepting a first argument of type 'C1' could be found (are you missing a using directive or an assembly reference?)
        M2('a', 'b'); // Ok
    }

    public static void M2(params ushort[] x)
    {
    }

    IEnumerator<char> IEnumerable<char>.GetEnumerator() => throw null;
    IEnumerator IEnumerable.GetEnumerator() => throw null;
}

Parece que a questão foi um pouco discutida anteriormente, ver https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions. Naquela época, foi apresentado um argumento de que as regras, conforme especificadas atualmente, são consistentes com a forma como os manipuladores de strings interpolados são especificados. Aqui está uma citação:

Em particular, os manipuladores de cadeia de caracteres interpolados foram originalmente especificados dessa forma, mas revisamos a especificação depois de considerar esse problema.

Embora haja alguma semelhança, há também uma distinção importante que vale a pena considerar. Aqui está uma citação de https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md#interpolated-string-handler-conversion:

Diz-se que o tipo T é um applicable_interpolated_string_handler_type se for atribuído a System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Existe um interpolated_string_handler_conversion implícito de T a partir de um interpolated_string_expression, ou um additive_expression composto inteiramente de _interpolated_string_expression_s e usando apenas + operadores.

O tipo de destino deve ter um atributo especial que é um forte indicador da intenção do autor para que o tipo seja um manipulador de cadeia de caracteres interpolado. É justo supor que a presença do atributo não é uma coincidência. Em contraste, o fato de um tipo ser "enumerável", não significa necessariamente que houve intenção do autor para que o tipo fosse construível. A presença de um método create, que é indicado com um atributo [CollectionBuilder(...)] no tipo de coleção , no entanto, parece um forte indicador da intenção do autor de que o tipo seja construível.

Proposta

Para uma estrutura ou um tipo de classe que implementa System.Collections.IEnumerable e que não tem um método de criação ,as conversões, a seção deve exigir a presença de pelo menos as seguintes APIs:

  • Um construtor acessível que é aplicável sem argumentos.
  • Uma instância Add acessível ou método de extensão que pode ser invocado com um valor do tipo de iteração como argumento.

Para os fins do recurso Params Collectons, esses tipos são válidos params quando essas APIs são declaradas públicas e quando se trata de métodos de instância (em contraste com métodos de extensão).

Conclusão

Aprovado com modificações LDM-2024-01-10

Reuniões de design

https://github.com/dotnet/csharplang/blob/main/meetings/2021/LDM-2021-11-01.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-03-09.md#ambiguity-of--in-collection-expressions https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-09-28.md#collection-literals https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-08.md https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-01-10.md

Reuniões do grupo de trabalho

https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-06.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-14.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2022-10-21.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-05.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-04-28.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-05-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-12.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-06-26.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-03.md https://github.com/dotnet/csharplang/blob/main/meetings/working-groups/collection-literals/CL-2023-08-10.md

Próximos pontos da ordem do dia

  • Alocações de pilha para coleções enormes podem explodir a pilha. O compilador deve ter uma heurística para colocar esses dados na heap? A linguagem não deve ser especificada para permitir essa flexibilidade? Devemos seguir o que a especificação / impl faz para params Span<T>. As opções são:

    • Sempre stackalloc. Ensine as pessoas a terem cuidado com o Span. Isso permite que as coisas como Span<T> span = [1, 2, ..s] funcionem e fiquem bem, desde que s seja pequena. Se isso pudesse sobrecarregar a pilha, os usuários sempre poderiam criar uma matriz em vez disso e, em seguida, obter um intervalo ao seu redor. Isso parece o mais de acordo com o que as pessoas podem querer, mas com extremo perigo.
    • Apenas utilize stackalloc quando o literal tiver um número fixo de elementos (ou seja, sem elementos spread). Isso provavelmente torna as operações sempre seguras, com um uso fixo da pilha, e o compilador, com sorte, consegue reutilizar esse buffer fixo. No entanto, isso significa que coisas como [1, 2, ..s] nunca seriam possíveis, mesmo que o usuário saiba que é completamente seguro em tempo de execução.
  • Como funciona a resolução de sobrecarga? Se uma API tiver:

    public void M(T[] values);
    public void M(List<T> values);
    

    O que acontece com M([1, 2, 3])? Provavelmente precisamos definir "melhoria" para essas conversões.

  • Devemos expandir os inicializadores de coleções para procurar pelo método AddRange, muito comum? Poderia ser utilizado pelo tipo construído subjacente para realizar a adição de elementos de propagação de forma potencialmente mais eficiente. Também podemos querer procurar coisas como .CopyTo. Pode haver desvantagens aqui, pois esses métodos podem acabar causando excesso de alocações/despachos em vez de enumerar diretamente no código traduzido.

  • A inferência de tipo genérica deve ser atualizada para propagar informações de tipo entre literais de coleção. Por exemplo:

    void M<T>(T[] values);
    M([1, 2, 3]);
    

    Parece natural que isso seja algo que o algoritmo de inferência possa estar ciente. Uma vez que isso é suportado para os casos do tipo de coleção construível 'base' (T[], I<T>, Span<T>new T()), então ele também deve cair fora do caso Collect(constructible_type). Por exemplo:

    void M<T>(ImmutableArray<T> values);
    M([1, 2, 3]);
    

    Aqui, Immutable<T> é construível através de um método init void Construct(T[] values). Assim, o tipo T[] values seria usado para inferir contra [1, 2, 3], resultando numa inferência de int para T.

  • Ambiguidade do elenco/índice.

    Hoje, a seguinte é uma expressão que está indexada em

    var v = (Expr)[1, 2, 3];
    

    Mas seria bom poder fazer coisas como:

    var v = (ImmutableArray<int>)[1, 2, 3];
    

    Podemos / devemos fazer uma pausa aqui?

  • Ambiguidades sintáticas com ?[.

    Pode valer a pena mudar as regras de nullable index access para indicar que nenhum espaço pode ocorrer entre ? e [. Isso seria uma mudança disruptiva (mas provavelmente menor, pois o Visual Studio já força esses elementos juntos se os utilizadores os digitarem com um espaço). Se o fizermos, poderemos ter x?[y] a ser analisado de maneira diferente da x ? [y].

    Algo semelhante ocorre se quisermos optar por https://github.com/dotnet/csharplang/issues/2926. Nesse mundox?.y é ambíguo com x ? .y. Se exigirmos que o ?. se apareça, podemos distinguir sintaticamente os dois casos trivialmente.