Expressões de coleção
Nota
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ele inclui alterações de especificação propostas, juntamente com as informações necessárias durante o design e o desenvolvimento do recurso. Esses artigos são publicados até que as alterações de especificação propostas sejam finalizadas e incorporadas na especificação ECMA atual.
Pode haver algumas discrepâncias entre a especificação do recurso e a implementação concluída. Essas diferenças são capturadas nas notas pertinentes da reunião de design de idioma (LDM).
Você pode saber mais sobre o processo de adoção de speclets de recursos no padrão de linguagem C# no artigo sobre as especificações de .
Resumo
As expressões de coleção introduzem uma nova sintaxe terse, [e1, e2, e3, etc]
, para criar valores comuns de coleção. É possível incluir outras coleções nesses valores usando um elemento de propagação ..e
desta forma: [e1, ..c2, e2, ..c2]
.
Vários tipos semelhantes à coleção podem ser criados sem a necessidade de suporte a BCL externo. Esses tipos são:
- Tipos de matriz, como
int[]
. Span<T>
eReadOnlySpan<T>
.- Tipos que dão suporte a inicializadores de coleção, como
List<T>
.
Há suporte adicional para tipos semelhantes a coleções não cobertos anteriormente, por meio de um novo atributo e modelo de API que podem ser adotados diretamente no próprio tipo.
Motivação
Valores semelhantes a coleções estão muito presentes em 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# precisam usar várias abordagens diferentes e, infelizmente, verbosas para criar instâncias desses valores. Algumas abordagens também têm desvantagens de desempenho. Aqui estão alguns exemplos comuns:
- Matrizes, que exigem
new Type[]
ounew[]
antes dos valores de{ ... }
. - Intervalos, que podem usar
stackalloc
e outras estruturas complexas. - Inicializadores de coleção, que exigem sintaxe como
new List<T>
(sem inferência de umaT
possivelmente detalhada) antes de seus valores e que podem causar várias realocações de memória porque usam N.Add
invocações 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 (comoImmutableArray.CreateBuilder
) são inconvenientes e ainda produzem lixo inevitável.
- Matrizes, que exigem
Olhando para o ecossistema ao redor, também encontramos exemplos em todos os lugares de como a criação de listas pode ser mais conveniente e agradável de usar. TypeScript, Dart, Swift, Elm, Python e mais optam por uma sintaxe sucinta para essa finalidade, com uso generalizado e com grande efeito. Investigações superficiais não revelaram problemas sérios surgindo nesses ecossistemas com a incorporação embutida desses literais.
O C# também adicionou padrões de lista de no C# 11. Esse padrão permite a correspondência e a desconstrução de valores semelhantes à lista usando uma sintaxe limpa e intuitiva. No entanto, ao contrário de quase todos os outros constructos de padrão, essa sintaxe para correspondência/desconstrução não tem uma sintaxe de construção correspondente.
Obter o melhor desempenho para construir cada tipo de coleção pode ser complicado. Soluções simples geralmente desperdiçam CPU e memória. Ter um formulário 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.
Uma solução inclusiva é necessária para C#. Ele deve atender à grande maioria dos casos para os clientes em termos de tipos e valores semelhantes aos de uma coleção que eles já possuem. Deve também parecer natural na linguagem 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 padrão de [p1, p2, p3, p-etc]
e [p1, ..p2, p3]
.
Design detalhado
As seguintes produções gramaticais são adicionadas:
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
;
Literais de coleção são tipadas como alvo.
Esclarecimentos de especificação
Para fins de brevidade,
collection_expression
será chamado de "literal" nas seções a seguir.Instâncias
expression_element
comumente serão referidas comoe1
,e_n
, etc.Instâncias
spread_element
geralmente serão conhecidas como..s1
,..s_n
etc.tipo de faixa significa
Span<T>
ouReadOnlySpan<T>
.Os literais geralmente serão mostrados como
[e1, ..s1, e2, ..s2, etc]
para transmitir qualquer número de elementos em qualquer ordem. É importante ressaltar que esse formulário será usado para representar todos os casos, como:- Literais vazios
[]
- Literais sem
expression_element
neles. - Literais sem
spread_element
neles. - Literais com ordenação arbitrária de qualquer tipo de elemento.
- Literais vazios
O tipo de iteração de
..s_n
é o tipo da variável de iteração determinado como ses_n
fosse usado como a expressão que está sendo iterada em umforeach_statement
.Variáveis que começam com
__name
são usadas para representar os resultados da avaliação dename
, armazenadas em um local para que ela seja avaliada apenas uma vez. Por exemplo,__e1
é a avaliação dee1
.List<T>
,IEnumerable<T>
, etc. se referem aos respectivos tipos no namespaceSystem.Collections.Generic
.A especificação define uma tradução do literal para os constructos C# existentes. Semelhante à tradução da expressão de consulta, o literal só é válido se a tradução resultar em código válido. A finalidade dessa regra é evitar a necessidade de repetir outras regras do idioma que estão implícitas (por exemplo, sobre a conversibilidade 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 será 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 poderia traduzir literais como
[1, 2, 3]
diretamente para uma expressãonew int[] { 1, 2, 3 }
que integra os dados brutos no assembly, eliminando a necessidade de__index
ou de uma sequência de instruções para atribuir cada valor. Importante, isso significa que, se qualquer etapa da tradução puder causar uma exceção em runtime, o estado do programa ainda será deixado no estado indicado pela tradução.
- Por exemplo, uma implementação poderia traduzir literais como
As referências à "alocação de pilha" referem-se a qualquer estratégia para alocar na pilha e não no heap. É importante ressaltar que ela não implica ou exige que essa estratégia utilize o mecanismo real de
stackalloc
. Por exemplo, o uso de matrizes embutidas também é uma abordagem permitida e desejável para realizar a alocação de pilha quando disponível. Observe que, no C# 12, as matrizes embutidas não podem ser inicializadas com uma expressão de coleção. Isso continua sendo uma proposta aberta.Pressupõe-se que as coleções sejam 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. - Os tipos usados nessa especificação definidos no namespace
System.Collections.Generic
são presumidamente livres de efeito colateral. Dessa forma, o compilador pode otimizar cenários em que esses tipos podem ser usados como valores intermediários, mas de outra forma não serão expostos. - Supõe-se que uma chamada a algum membro
.AddRange(x)
aplicável em uma coleção resultará no mesmo valor final que iterar sobrex
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 se comportam bem é indefinido.
- Supõe-se que o valor de
Conversões
Uma conversão de expressão de coleção permite que uma expressão de coleção seja convertida em um tipo.
Existe uma conversão de expressão de coleção implícita de uma expressão de coleção para os seguintes tipos:
- Um tipo de matriz unidimensional
T[]
, nesse caso, o tipo de elemento éT
- Um tipo de intervalo:
System.Span<T>
System.ReadOnlySpan<T>
em quais casos o tipo de elemento éT
- Um tipo de com um método de criação apropriado; nesse caso, o tipo de elemento é o tipo de iteração determinado a partir de um método de instância
GetEnumerator
ou interface enumerável, e não de um método de extensão - Um struct ou tipo de classe que implementa
System.Collections.IEnumerable
em que:O tipo tem um construtor aplicável que pode ser invocado sem argumentos e o construtor está acessível no local da expressão de coleção.
Se a expressão de coleção tiver elementos, o tipo terá uma instância ou método de extensão
Add
em que:- O método pode ser invocado com um argumento de valor único.
- Se o método for genérico, os argumentos de tipo poderão ser inferidos da coleção e do argumento.
- O método é acessível no local da expressão da 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 existirá se o tipo tiver um tipo de elemento U
em que para cada elemento Eᵢ
na expressão de coleção:
- Se
Eᵢ
for um elemento de expressão , haverá uma conversão implícita deEᵢ
paraU
. - Se
Eᵢ
for um elemento espalhado..Sᵢ
, haverá uma conversão implícita do tipo de iteração deSᵢ
paraU
.
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 para os quais há uma conversão de expressão de coleçã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.
Existem as seguintes conversões implícitas adicionais a partir de uma expressão de coleção :
Para um tipo de valor anulável
T?
, onde há uma conversão de expressão de coleção da expressão de coleção para um tipo de valorT
. A conversão é uma conversão de expressão de coleção paraT
seguida por uma conversão implícita anulável deT
paraT?
.Para um tipo de referência
T
em que há um método de criação associado aT
que retorna um tipoU
e uma conversão de referência implícita deU
paraT
. A conversão é uma conversão de expressão de coleção paraU
seguida por uma conversão de referência implícita deU
paraT
.Para um tipo de interface
I
em que há um método de criação associado aI
que retorna um tipoV
e uma conversão boxing implícita deV
paraI
. A conversão é uma conversão de expressão de coleção paraV
seguida por uma conversão boxing implícita deV
paraI
.
Criar métodos
Um método de criação é 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 struct
ou interface
.
O atributo não é herdado, embora o atributo possa ser aplicado a um class
base ou a um abstract class
.
O tipo de construtor deve ser um class
não genérico ou um struct
não genérico.
Primeiro, o conjunto de métodos de criação aplicávelCM
é determinado.
Ele consiste em métodos que atendem aos seguintes requisitos:
- O método deve ter o nome especificado no atributo
[CollectionBuilder(...)]
. - O método deve ser definido diretamente no tipo de construtor.
- O método deve ser
static
. - O método deve ser acessível onde a expressão da 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, uma conversão de referência implícita , ou uma conversão boxing do tipo de retorno do método para o tipo de coleção.
Os métodos declarados em tipos base ou interfaces são ignorados e não fazem parte do conjunto de CM
.
Se o conjunto de 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 aplicam.
Se apenas um método entre aqueles no conjunto de CM
tiver uma conversão de identidade de E
para o tipo de elemento dentro do tipo de coleção , esse método será o método de criação para o tipo de coleção . Caso contrário, o tipo de coleção não tem um método de criação .
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, …>
em que a declaração de tipo C<T0, T1, …>
tem um método de construtor associadoB.M<U0, U1, …>()
, os argumentos de tipo genérico do tipo de destino são aplicados na ordem – e do tipo de contenção mais externo ao mais interno – ao método construtor .
O parâmetro de intervalo para o método criar pode ser marcado explicitamente scoped
ou [UnscopedRef]
. Se o parâmetro for implícita ou explicitamente scoped
, o compilador poderá alocar o armazenamento para a faixa na pilha ao invés do heap.
Por exemplo, um possível método de criação 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 criar 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 as referências adicionais aos elementos referem-se aos resultados dessa avaliação inicial.
Um elemento de propagação pode ser iterado antes ou depois de os elementos subsequentes na expressão de coleção serem avaliados.
Uma exceção sem tratamento lançada de qualquer um dos métodos usados durante a construção não será capturada e impedirá etapas subsequentes na construção.
Length
, Count
e GetEnumerator
não têm efeitos colaterais.
Se o tipo de destino for um struct ou um tipo de classe que implementa System.Collections.IEnumerable
e o tipo de destino não tiver um método de criação , a construção da instância da coleção será a seguinte:
Os elementos são avaliados em 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 contáveis ou propriedades equivalentes de interfaces ou tipos conhecidos, em cada expressão de elemento de propagação.
O construtor que pode ser utilizado sem argumentos é invocado.
Para cada elemento, na ordem:
- Se o elemento for um elemento de expressão , o método de instância ou extensão
Add
aplicável será invocado com o elemento expressão como o argumento. (Ao contrário do comportamento do inicializador de coleção clássico, a avaliação de elementos e as chamadas deAdd
não são necessariamente intercaladas.) - Se o elemento for um elemento de distribuição um destes elementos será usado:
- Um método de instância ou extensão
GetEnumerator
aplicável é invocado na expressão de elemento de propagação , e para cada item do enumerador, o método de instância ou extensãoAdd
aplicável é invocado na instância de coleção , com o item como argumento. Se o enumerador implementarIDisposable
,Dispose
será chamado após a enumeração, independentemente das exceções. - Um método de instância ou extensão aplicável
AddRange
é invocado na instância da coleção com a expressão do elemento de propagação como argumento. - Um método de instância ou extensão
CopyTo
aplicável é invocado na expressão do elemento de propagação com a instância da coleção e o índiceint
como argumentos.
- Um método de instância ou extensão
- Se o elemento for um elemento de expressão , o método de instância ou extensão
Durante as etapas de construção acima, uma instância
EnsureCapacity
ou método de extensão aplicável pode ser invocado uma ou mais vezes na instância de coleção com um argumento de capacidadeint
.
Se o tipo de destino for uma matriz, um intervalo, um tipo com um método de criação, ou uma interface, a construção da instância da coleção será a seguinte:
Os elementos são avaliados em 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 contáveis ou propriedades equivalentes de interfaces ou tipos conhecidos, em cada expressão de elemento de propagação.
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 intervalo ou um tipo com um método de criação, e a coleção tiver um comprimento conhecido, será criado um intervalo com o comprimento esperado referindo-se a um armazenamento contíguo.
- Caso contrário, será alocado o armazenamento intermediário.
Para cada elemento em 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 ao índice atual.
- Se o elemento for um elemento de distribuição um destes elementos será usado:
- Um membro de uma interface ou tipo bem conhecido é invocado para copiar itens da expressão do elemento de propagação para a instância de inicialização.
- Um método de instância ou método de extensão
GetEnumerator
aplicável é invocado na expressão de elemento de propagação e, para cada item do enumerador, a instância de inicialização do indexador é invocada para adicionar o item ao índice atual. Se o enumerador implementarIDisposable
,Dispose
será chamado após a enumeração, independentemente das exceções. - Um método de instância ou extensão
CopyTo
aplicável é invocado na expressão do elemento de propagação com a instância da inicialização e o índiceint
como argumentos.
Se o armazenamento intermediário tiver sido alocado para a coleção, uma instância de coleção será alocada com o comprimento real da coleção e os valores da instância de inicialização serão copiados para a instância de coleção ou, se for necessário, o compilador poderá usar um intervalo do comprimento real da coleção do armazenamento intermediário. Caso contrário, a instância de inicialização é a instância de coleção.
Se o tipo de destino tiver um método de criação , o método de criação será invocado com a instância de intervalo.
Observação: O compilador pode atrasar a adição de elementos à coleção — ou atrasar a iteração por meio de elementos de propagação — até que os elementos subsequentes sejam avaliados. (Quando os elementos de propagação subsequentes têm propriedades contáveis que permitiriam calcular o comprimento esperado da coleção antes de alocar a coleção.) Por outro lado, o compilador pode de forma imediata adicionar elementos à coleção — e de forma imediata iterar por meio de elementos de propagação — quando não há nenhuma vantagem em atrasar.
Considere a seguinte expressão de coleção:
int[] x = [a, ..b, ..c, d];
Se os elementos de propagação
b
ec
forem contáveis, o compilador poderá atrasar a adição dos itens dea
eb
até quec
seja avaliado, permitindo assim a alocação da matriz resultante no comprimento esperado. Depois disso, o compilador pode adicionar prontamente itens dec
, antes de avaliard
.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 convertido implicitamente 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á nenhuma outra conversão envolvida:
var v = []; // illegal
A propagação de um literal vazio pode ser suprimida. Por exemplo:
bool b = ... List<int> l = [x, y, .. b ? [1, 2, 3] : []];
Aqui, se
b
for falso, não será necessário que qualquer valor seja realmente construído para a expressão de coleção vazia, pois ele será imediatamente espalhado em valores zero no literal final.A expressão de coleção vazia é permitida para 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 = [];
Segurança de ref
Consulte em restrição de contexto seguro as definições dos valores de contexto seguro: bloco de declaração, membro de função e contexto de chamador.
O 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 tipo de span
System.ReadOnlySpan<T>
eT
for um dos tipos primitivosbool
,sbyte
,byte
,short
,ushort
,char
,int
,uint
,long
,ulong
,float
oudouble
, e a expressão de coleção contém apenas valores constantes , o contexto seguro da expressão de coleção é o contexto de chamador .Se o tipo de destino for um tipo
System.Span<T>
ouSystem.ReadOnlySpan<T>
de intervalo, o contexto seguro da expressão de coleção será o bloco de declaração .Se o tipo de destino for um tipo de struct ref com um método de criação , o contexto seguro da expressão de coleção será o contexto seguro de uma invocação do método de criação em que a expressão de coleção é o argumento de intervalo 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 no bloco de declaração com um contexto seguro de não pode ultrapassar o escopo delimitador, e o compilador pode armazenar a coleção na pilha em vez do heap.
Para permitir que uma expressão de coleção de um tipo de struct ref 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 maneira.
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 a de inferência de tipo de entrada e de inferência de tipo de saída para expressões de coleção.
11.6.3.2 A primeira fase
Para cada um dos argumentos do método
Eᵢ
:
- Um de inferência de tipo de entrada é feito de
Eᵢ
para o tipo de parâmetroTᵢ
correspondente.Um de inferência de tipo de entrada é feito de uma expressão
E
para um tipoT
da seguinte maneira:
- Se
E
for uma expressão de coleção com elementosEᵢ
, eT
for um tipo com um tipo de elementoTₑ
ouT
for um tipo de valor anulávelT0?
, eT0
tiver um tipo de elementoTₑ
, então, para cadaEᵢ
:- [regras existentes da primeira fase]...
11.6.3.7 Inferências de tipo de saída
Um de inferência de tipo de saída é feito de uma expressão
E
para um tipoT
da seguinte maneira:
- Se
E
for uma expressão de coleção com elementosEᵢ
, eT
for um tipo com um tipo de elementoTₑ
ouT
for um tipo de valor anulávelT0?
, eT0
tiver um tipo de elementoTₑ
, então, para cadaEᵢ
:
- Se
Eᵢ
for um elemento de expressão, uma inferência de tipo de saída será feita deEᵢ
paraTₑ
.- Se
Eᵢ
for um elemento de distribuição , nenhuma inferência será feita deEᵢ
.- [regras existentes de inferências de tipo de saída]...
Métodos de extensão
Nenhuma alteração nas regras de invocação do método de extensão .
12.8.10.3 Invocações de método de extensão
Um método de extensão
Cᵢ.Mₑ
será qualificado 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 de expressão é atualizada para favorecer certos tipos de destino em conversões de expressão de coleção.
Nas regras atualizadas:
- Um span_type é um dos seguintes:
System.Span<T>
-
System.ReadOnlySpan<T>
.
- Uma array_or_array_interface é uma das seguintes opções:
- um tipo de matriz
- um dos seguintes tipos de interface implementados 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>
Considerando uma conversão implícita
C₁
que transforma uma expressãoE
em um tipoT₁
e uma conversão implícitaC₂
que transforma uma expressãoE
em um tipoT₂
,C₁
é uma conversão melhor do queC₂
caso uma das seguintes condições seja atendida:
E
é uma expressão de coleção e uma das seguintes retenções:
T₁
éSystem.ReadOnlySpan<E₁>
eT₂
éSystem.Span<E₂>
e existe uma conversão implícita deE₁
paraE₂
T₁
éSystem.ReadOnlySpan<E₁>
ouSystem.Span<E₁>
eT₂
é um array_or_array_interface com tipo de elementoE₂
e existe uma conversão implícita deE₁
paraE₂
T₁
não é um span_typeeT₂
não é um span_typee existe uma conversão implícita deT₁
paraT₂
E
não é uma expressão de coleção e uma das seguintes retenções: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 intervalo
Os tipos de intervalo 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 um desses intervalos resultará em uma matriz T[] criada na pilha se a matriz de parâmetros estiver dentro dos limites (se houver) definidos pelo compilador. Caso contrário, a matriz será alocada no heap.
Se o compilador optar por alocar na pilha, não será necessário traduzir um literal diretamente para um stackalloc
naquele ponto específico. Por exemplo, considerando que:
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 a segurança de faixa 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 também pode usar matrizes embutidas, se disponíveis, ao optar por alocar na pilha. Observe que, no C# 12, as matrizes embutidas não podem ser inicializadas com uma expressão de coleção. Esse recurso é uma proposta aberta.
Se o compilador decidir alocar no heap, a conversão para Span<T>
será simplesmente:
T[] __array = [...]; // using existing rules
Span<T> __result = __array;
Conversão literal da coleção
Uma expressão de coleção tem um comprimento conhecido se o tipo no tempo de compilação de cada elemento de propagação na expressão de coleção for contável.
Tradução de interface
Tradução de interface não mutável
Dado um tipo de destino que não contém membros em mutação, ou seja, IEnumerable<T>
, IReadOnlyCollection<T>
e IReadOnlyList<T>
, uma implementação em conformidade é necessária para produzir um valor que implemente essa interface. Recomenda-se que o tipo sintetizado implemente todas essas interfaces, bem como ICollection<T>
e IList<T>
, independentemente de qual tipo de interface foi alvo. Isso garante a compatibilidade máxima com bibliotecas existentes, incluindo aquelas que analisam as interfaces implementadas por um valor para destacar as otimizações de desempenho.
Além disso, o valor deve implementar as interfaces ICollection
e IList
não genéricas. Isso permite que expressões de coleção ofereçam suporte à introspecção dinâmica em cenários como associação de dados.
Uma implementação em conformidade é gratuita para:
- Use um tipo existente que implemente as interfaces necessárias.
- Sintetize um tipo que implementa as interfaces necessárias.
Em ambos os casos, o tipo usado tem permissão para implementar um conjunto maior de interfaces do que aqueles estritamente necessários.
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 nele mesmo, evitando a necessidade de alocações internas adicionais de coleções. Um tipo sintetizado também não pôde usar nenhum armazenamento, optando por computar os valores diretamente. Por exemplo, retornando index + 1
para [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.
- O valor deve retornar
true
quando consultado paraICollection<T>.IsReadOnly
(se implementado) eIList.IsReadOnly
eIList.IsFixedSize
não genéricas. Isso garante que os consumidores possam perceber adequadamente que a coleção não é mutável, apesar de implementar visões mutáveis. - O valor deve ser lançado em qualquer chamada para um método de mutação (como
IList<T>.Add
). Isso garante a segurança, impedindo que uma coleção não mutável seja acidentalmente alterada.
Tradução de interface mutável
Dado o tipo de destino que contém membros mutáveis, a saber, ICollection<T>
ou IList<T>
:
- O valor deve ser uma instância de
List<T>
.
Conversão de comprimento conhecida
Ter um comprimento conhecido permite a construção eficiente de um resultado com potencial para não haver cópia de dados e sem espaço extra desnecessário no 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 devido à produção e processamento dos dados, seguidos pela migração para o destino final.
Para um literal de comprimento conhecido
[e1, ..s1, etc]
, a conversão começa da seguinte forma:int __len = count_of_expression_elements + __s1.Count; ... __s_n.Count;
Dado o tipo de destino
T
para este literal:Se
T
for algumT1[]
, então o literal será convertido 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 algumSpan<T1>
, o literal será convertido da mesma forma que acima, exceto que a inicialização__result
será convertida como:Span<T1> __result = new T1[__len]; // same assignments as the array translation
A conversão pode usar
stackalloc T1[]
ou uma matriz embutida em vez denew T1[]
se de segurança de intervalo for mantida.Se
T
for algumReadOnlySpan<T1>
, o literal será convertido da mesma forma que no caso deSpan<T1>
, exceto que o resultado final será queSpan<T1>
seja convertido implicitamente de para umReadOnlySpan<T1>
.Uma
ReadOnlySpan<T1>
em queT1
é algum tipo primitivo e todos os elementos da coleção são constantes não precisa que seus dados estejam no heap ou na pilha. Por exemplo, uma implementação poderia construir esse intervalo diretamente como uma referência a parte do segmento de dados do programa.Os formulários acima (para matrizes e intervalos) 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
for algumC<S0, S1, …>
que tenha um método de criação correspondenteB.M<U0, U1, …>()
, então o literal é convertido 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 criar deve ter um tipo de argumento de alguns
ReadOnlySpan<T>
instanciados, a regra de conversão para intervalos se aplica ao passar a expressão de coleção para o método de criação.Se
T
oferece suporte para inicializadores de coleção em, então:se o tipo
T
contiver um construtor acessível com um único parâmetroint capacity
, o literal será 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
Observação: o nome do parâmetro deve ser
capacity
.Esse formulário permite o uso de um literal para especificar o tipo recém-criado quanto à contagem de elementos, o que possibilita uma alocação eficiente do armazenamento interno. Isso evita realocações desperdiçadas à medida que os elementos são adicionados.
caso contrário, o literal será convertido 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 impedir a realocação interna do armazenamento.
Tradução de comprimento desconhecido
Dado um tipo de destino
T
para um literal de comprimento desconhecido:Se
T
suporta inicializadores de coleção , o literal é convertido 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
for algumT1[]
, o literal terá a mesma semântica que:List<T1> __list = [...]; /* initialized using predefined rules */ T1[] __result = __list.ToArray();
No entanto, o acima é ineficiente; ele cria a lista intermediária e, em seguida, cria uma cópia da matriz final dela. As implementações são gratuitas 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 o mínimo de desperdício e duplicação, sem a sobrecarga adicional que as coleções de bibliotecas possam incorrer.
As contagens passadas para
CreateArray
são usadas para fornecer uma dica de tamanho inicial para evitar redimensionamentos desperdiçados.Se
T
for algum tipo de intervalo, uma implementação poderá seguir a estratégia deT[]
acima 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 de lista,CollectionsMarshal.AsSpan(__list)
poderia ser usado para obter um valor de intervalo diretamente.
Cenários sem suporte
Embora literais de coleção possam ser usados para muitos cenários, há alguns que eles não conseguem substituir. Estes incluem:
- Matrizes multidimensionais (por exemplo,
new int[5, 10] { ... }
). Não há nenhuma ferramenta para incluir as dimensões, e todos os literais de coleção são apenas estruturas lineares ou de mapa. - Coleções que passam valores especiais a seus construtores. Não há nenhum recurso para acessar o construtor que está sendo utilizado.
- Inicializadores de coleção aninhados, por exemplo,
new Widget { Children = { w1, w2, w3 } }
. Este formulário precisa permanecer, pois tem semântica muito diferente deChildren = [w1, w2, w3]
. O primeiro chama.Add
repetidamente em.Children
, enquanto o segundo atribuiria uma nova coleção sobre.Children
. Poderíamos considerar fazer com que a última forma volte a adicionar a uma coleção existente se.Children
não puder ser atribuído, mas isso parece ser extremamente confuso.
Ambiguidades de sintaxe
Há duas ambiguidades sintacticas "verdadeiras" em que há várias interpretações sintacticas legais de código que usam um
collection_literal_expression
.O
spread_element
é ambíguo em relação aorange_expression
. Tecnicamente, pode-se ter:Range[] ranges = [range1, ..e, range2];
Para resolver isso, podemos:
- Exigir que os usuários coloquem entre parênteses
(..e)
ou incluam um índice de início0..e
se desejarem um intervalo. - Escolha uma sintaxe diferente (como
...
) para a propagação. Isso seria lamentável pela falta de consistência nos padrões de corte.
- Exigir que os usuários coloquem entre parênteses
Há dois casos em que não há uma ambiguidade verdadeira, 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 examinar o código.
Ambiguidade entre
collection_literal_expression
eattributes
em instruções 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 análise antecipada complexa, seria impossível determinar sem consumir toda a expressão literal.
As opções para resolver isso incluem:
- Permita isso, fazendo o trabalho de análise para determinar qual desses casos é esse.
- Não permita isso e exija que o usuário encapsule o literal em parênteses como
([X(), Y, Z()]).ForEach(...)
. - Ambiguidade entre um
collection_literal_expression
em umconditional_expression
e umnull_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 uma análise antecipada complexa, seria impossível determinar sem consumir toda a expressão literal.
Observação: esse é um problema mesmo sem um tipo natural porque a tipagem de destino se aplica por meio de
conditional_expressions
.Assim como com os outros, poderíamos exigir parênteses para desambiguar. Em outras palavras, presuma a interpretação
null_conditional_operation
, a menos que seja escrito assim:x ? ([1, 2, 3]) :
. No entanto, isso parece bastante infeliz. Esse tipo de código não parece ilógico de se escrever e provavelmente vai confundir as pessoas.
Inconvenientes
- Isso apresenta outra forma para expressões de coleção além das inúmeras maneiras que já temos. Isso é complexidade extra para o idioma. Dito isto, isso também torna possível unificar em um anel de sintaxe
para governar todos eles, o que significa que as bases de código existentes podem ser simplificadas e movidas para um visual uniforme em todos os aspectos. - Usando
[
...]
em vez de{
...}
se afasta da sintaxe que geralmente já 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 fizemos padrões de lista. Tentamos fazer{
...}
funcionar com os padrões de lista e tivemos problemas insuperá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 às nossas decisões mais recentes, e nos dá um lugar limpo para trabalhar sem problemas.
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 se mudem para a nova forma. Sugestões e correções do IDE podem ajudar nesse aspecto.
Alternativas
- Quais outros designs foram considerados? Qual é o impacto de não fazer isso?
Perguntas resolvidas
O compilador deve usar
stackalloc
para alocação de pilha quando matrizes embutidas não estiverem disponíveis e o tipo de iteração for um tipo primitivo?Resolução: Não. O gerenciamento de um buffer de
stackalloc
requer esforço adicional em uma matriz embutida para garantir que o buffer não seja alocado repetidamente quando a expressão de coleção estiver dentro de um loop. O aumento da complexidade no compilador e no código gerado supera o benefício da alocação de pilha em plataformas mais antigas.Em que ordem devemos comparar a avaliação dos elementos literais com a da propriedade Length/Count? Devemos avaliar todos os elementos primeiro e, em seguida, todos os comprimentos? Ou devemos avaliar um elemento, então seu comprimento, o próximo elemento e assim por diante?
Resolução: avaliamos todos os elementos primeiro, e depois disso, todo o resto segue.
Um literal de comprimento desconhecido pode criar um tipo de coleção que precise de uma de comprimento conhecido, como uma matriz, intervalo ou coleção construto (matriz/intervalo)? Isso seria mais difícil de fazer com eficiência, mas pode ser possível por meio do uso inteligente de matrizes em pool e/ou construtores.
Resolução: sim, permitimos a criação de uma coleção de comprimento fixo a partir de uma literal de comprimento desconhecido. O compilador tem permissão para implementá-lo da maneira mais eficiente possível.
O texto a seguir existe para gravar a discussão original deste tópico.
Os usuários sempre podem transformar um literal de comprimento desconhecido em um de comprimento conhecido com código como este:
ImmutableArray<int> x = [a, ..unknownLength.ToArray(), b];
No entanto, isso é lamentável devido à necessidade de forçar alocações de armazenamento temporário. Poderíamos ser mais eficientes se controlássemos como isso foi emitido.
Um
collection_expression
pode ser tipado como destino para umaIEnumerable<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 tipado como destino para qualquer tipo de interface
I<T>
queList<T>
implementa. Por exemplo,IEnumerable<long>
. Isso equivale a determinar o tipo de destino paraList<long>
e, em seguida, atribuir esse resultado ao tipo de interface especificado. O texto a seguir existe para gravar a discussão original deste tópico.A pergunta aberta aqui é determinar qual tipo subjacente realmente criar. Uma opção é examinar a proposta de
params IEnumerable<T>
. Lá, geraríamos uma matriz para passar os valores, semelhante ao que acontece comparams T[]
.O compilador pode/deve emitir
Array.Empty<T>()
para[]
? Devemos exigir que ele faça isso, para 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, direcionandoT[]
,IEnumerable<T>
,IReadOnlyCollection<T>
ouIReadOnlyList<T>
. Ele não deve usarArray.Empty<T>
quando o destino for mutável (ICollection<T>
ouIList<T>
).Devemos expandir os inicializadores de coleção para procurar o método
AddRange
muito comum? Ele pode ser usado pelo tipo subjacente construído para realizar a adição de elementos distribuídos de maneira 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/expedições em comparação com a enumeração direta 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 trazer consequências negativas, como, por exemplo, o processo de "boxing" em uma coleção de estruturas (structs).
Uma implementação deve aproveitar os casos em que não há desvantagens. Por exemplo, com um método
.AddRange(ReadOnlySpan<T>)
.
Perguntas não resolvidas
- Devemos permitir inferir o tipo de elemento , quando o tipo de iteração for considerado "ambíguo" (segundo 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;
}
Deve ser legal criar e acessar imediatamente um literal de coleção? Observação: isso requer uma resposta para a questão ainda não resolvida abaixo de se os literais de coleção têm um tipo natural .
Alocações de pilha para grandes coleções podem explodir a pilha. O compilador deve ter uma heurística para colocar esses dados no heap? O idioma não deve ser especificado para permitir essa flexibilidade? Devemos seguir a especificação padrão para
params Span<T>
.Precisamos definir tipo-alvo para
spread_element
? Considere, por exemplo:Span<int> span = [a, ..b ? [c] : [d, e], f];
Observação: isso geralmente pode aparecer no seguinte formulário para permitir a inclusão condicional de alguns conjuntos de elementos ou nada se a condição for falsa:
Span<int> span = [a, ..b ? [c, d, e] : [], f];
Para avaliar esse literal inteiro, precisamos avaliar as expressões de elemento dentro dele. Isso significa ser capaz de avaliar
b ? [c] : [d, e]
. No entanto, sem um tipo de destino para avaliar essa expressão no contexto e sem qualquer tipo de tipo natural, isso não seria possível 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 destino implícito equivalente ao tipo de destino do literal em si. Portanto, no 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;
A especificação de um tipo de coleção , construível utilizando um método de criação , é sensível ao contexto em que a conversão é classificada.
Uma existência da conversão nesse caso depende da noção de um tipo de iteração do tipo de coleção . Se houver um método de criação que usa uma ReadOnlySpan<T>
em que T
é o tipo de iteração , a conversão existe. Caso contrário, não acontece.
No entanto, um tipo de iteração é sensível ao contexto no qual foreach
é executada. Para o mesmo tipo de coleção pode ser diferente dependendo de quais métodos de extensão estão no escopo e também pode ser indefinido.
Isso parece bom para o propósito de foreach
quando o tipo não é projetado para permitir foreach em si mesmo. Se assim for o caso, os métodos de extensão não poderão alterar a forma como o tipo é iterado usando 'foreach', independentemente do contexto.
No entanto, isso parece um pouco estranho para uma conversão ser sensível ao contexto como essa. Efetivamente, a conversão é "instável". Um tipo de coleção explicitamente criado para ser construível, poderá omitir a definição de um detalhe muito importante - seu tipo de iteração . Deixando o tipo "inconversível" em si mesmo.
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.
}
}
}
Considerando o design atual, se o tipo não definir por si só 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 de criação. Se o tipo de iteração vem do contexto, não há garantia de que o tipo sempre será usado em um contexto semelhante.
O recurso Params Collections também é afetado por isso. É estranho não conseguir prever de forma confiável o tipo de elemento de um parâmetro params
no ponto de declaração. A proposta atual também requer garantir que o método de criação seja pelo menos tão acessível quanto o tipo de coleção params
. É impossível realizar essa verificação de forma confiável, a menos que o tipo de coleção defina por si mesmo seu tipo de iteração .
Observe que também temos https://github.com/dotnet/roslyn/issues/69676 aberto para o compilador, que basicamente observa o mesmo problema, mas o aborda do ponto de vista da otimização.
Proposta
Exigir um tipo que utilize o atributo CollectionBuilder
para definir por si mesmo seu tipo de iteração .
Em outras palavras, isso significa que o tipo deve implementar IEnumarable
/IEnumerable<T>
ou deve ter um método GetEnumerator
público com a assinatura certa (isso exclui todos os métodos de extensão).
Além disso, agora o método de criação é necessário para "estar acessível onde a expressão da coleção é usada". Esse é outro ponto de dependência de contexto com base na acessibilidade. A finalidade desse método é muito semelhante à finalidade de um método de conversão definido pelo usuário, e essa deve ser pública. 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 consistentemente em conversões
- Para um struct ou tipo de classe que implementa
System.Collections.Generic.IEnumerable<T>
em que:
- Para cada elemento
Ei
há uma conversão implícita paraT
.
Parece que é feita uma suposição de que T
é necessário o tipo de iteração do struct ou tipo de classe nesse caso.
No entanto, essa suposição está incorreta. 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 pressupõe 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 o tipo object
. O que pode ser observado no exemplo a seguir:
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 do tipo de iteração é fundamental para o recurso Coleções de parâmetros. E esse problema leva a uma estranha discrepância entre os dois recursos. 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 de outra.
Proposta
Especifique a conversibilidade de struct ou tipo de classe que implementa System.Collections.Generic.IEnumerable<T>
ou System.Collections.IEnumerable
em termos do tipo de iteração e exija uma conversão implícita para cada elemento Ei
para o tipo de iteração .
Conclusão
Aprovado LDM-2024-01-08
A conversão de expressão de coleção deve exigir a disponibilidade de um conjunto mínimo de APIs para construção?
Um tipo de coleção construível de acordo com conversões pode na verdade não ser construível, o que provavelmente resultará em 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 pode 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 sequer 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 o grupo de métodos para conversões delegadas. Ou seja, havia cenários em que a conversão existia, mas era errônea. Decidimos melhorar isso garantindo que, se a conversão for errônea, ela não exista.
Observe que com o recurso "Coleções de Parâmetros", teremos um problema semelhante. Pode ser bom não permitir o uso do modificador params
para coleções não construtíveis. No entanto, na proposta atual, a verificação se baseia na seção de conversões . 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 o assunto foi um pouco discutido anteriormente, consulte https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-10-02.md#collection-expressions. Naquela época, argumentou-se que as regras, como estão especificadas agora, são consistentes com a forma como os manipuladores de cadeias de caracteres interpoladas 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, também há 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:
O tipo
T
é dito ser um applicable_interpolated_string_handler_type se for atribuído aSystem.Runtime.CompilerServices.InterpolatedStringHandlerAttribute
. Existe uma conversão implícita interpolated_string_handler paraT
a partir de uma expressão de string interpolada , ou uma expressão aditiva composta inteiramente por expressões de string interpoladas 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 assumir que a presença do atributo não é uma coincidência.
Por outro lado, o fato de um tipo ser "enumerável", não significa que houve a intenção do autor para que o tipo fosse construível. Uma presença de um método de criação, no entanto, que é indicada com um atributo [CollectionBuilder(...)]
no tipo de coleção , parece um forte indicador da intenção do autor para que o tipo seja construível.
Proposta
Para um tipo de classe struct ou que implementa System.Collections.IEnumerable
e que não tem um método de criação a seção conversões deve exigir a presença pelo menos das seguintes APIs:
- Um construtor acessível aplicável sem argumentos.
- Um método de instância ou extensão acessível de
Add
que pode ser invocado com um valor do tipo de iteração como argumento.
Para o recurso Coleções de parâmetros, tais tipos são válidos como tipos de params
quando essas APIs são declaradas públicas e são métodos de instância (em vez 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 de 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 itens de agenda
Alocações de pilha para grandes coleções podem explodir a pilha. O compilador deve ter uma heurística para colocar esses dados no heap? O idioma não deve ser especificado para permitir essa flexibilidade? Devemos seguir o que a especificação/implementação faz para
params Span<T>
. As opções são:- Sempre use stackalloc. Ensinar as pessoas a ter cuidado com Span. Isso permite que coisas como
Span<T> span = [1, 2, ..s]
funcionem corretamente, desde ques
seja pequeno. Se isso pudesse explodir a pilha, os usuários sempre poderiam criar uma matriz e, em seguida, obter um intervalo em torno disso. Isso parece ser o mais alinhado com o que as pessoas podem querer, mas com perigo extremo. - Somente stackalloc quando o literal tiver um número de elementos fixo (ou seja, nenhum elemento de propagação). Isso provavelmente torna as coisas sempre seguras, com uso fixo da pilha, e o compilador, espero eu, capaz de 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 runtime.
- Sempre use stackalloc. Ensinar as pessoas a ter cuidado com Span. Isso permite que coisas como
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ção para procurar o método
AddRange
muito comum? Ele pode ser usado pelo tipo subjacente construído para realizar a adição de elementos distribuídos de maneira 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/expedições em comparação com a enumeração direta no código traduzido.A inferência de tipo genérico deve ser atualizada para transferir informações de tipo para/de literais de coleção. Por exemplo:
void M<T>(T[] values); M([1, 2, 3]);
Parece natural que isso deva ser algo que o algoritmo de inferência possa ser informado. Quando isso tiver suporte para os casos de tipo de coleção construível 'base' (
T[]
,I<T>
,Span<T>
new T()
), ele também deverá sair do casoCollect(constructible_type)
. Por exemplo:void M<T>(ImmutableArray<T> values); M([1, 2, 3]);
Aqui,
Immutable<T>
é construível por meio de um métodoinit void Construct(T[] values)
. Portanto, o tipoT[] values
seria usado em inferência contra[1, 2, 3]
, levando a uma inferência deint
paraT
.Ambiguidade de conversão/índice.
Hoje, a seguinte é uma expressão que é indexada em
var v = (Expr)[1, 2, 3];
Mas seria bom poder fazer coisas como:
var v = (ImmutableArray<int>)[1, 2, 3];
Podemos fazer uma pausa aqui?
Ambiguidades sintacticas com
?[
.Talvez valha a pena alterar as regras de
nullable index access
para indicar que nenhum espaço pode ocorrer entre?
e[
. Isso acarretaria uma mudança significativa (mas provavelmente menor, já que o Visual Studio já junta automaticamente se você digitá-los com um espaço). Se fizermos isso, então podemos quex?[y]
seja analisado diferentemente dex ? [y]
.Uma coisa semelhante ocorre se quisermos optar por https://github.com/dotnet/csharplang/issues/2926. Nesse mundo,
x?.y
é ambíguo comx ? .y
. Se exigirmos que o?.
esteja adjacente, podemos distinguir os dois casos de forma trivial.
C# feature specifications