Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Observação
Este artigo é uma especificação de recurso. A especificação serve como o documento de design para o recurso. Ela 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 divergê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 especificações de recursos no padrão da linguagem C# no artigo sobre as especificações.
Problema do especialista: https://github.com/dotnet/csharplang/issues/8652
Resumo
As expressões de coleção apresentam uma nova sintaxe concisa, [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 na programação, em 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 apresentam desvantagens de desempenho. Veja a seguir 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 requerem sintaxe como
ImmutableArray.Create(...)
para inicializar os valores, o que pode causar alocações intermediárias e cópias 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 outros 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 no C# 11. Esse padrão permite a correspondência e a desconstrução de valores semelhantes a listas 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.
Ter 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 flexibilidade máxima da implementação do compilador para otimizar o literal e produzir um resultado pelo menos tão bom quanto o que um usuário poderia fornecer, mas com código simples. Muitas vezes, o compilador será capaz de ter um desempenho melhor, e a especificação visa permitir à implementação uma grande margem de manobra em termos de estratégia de implementação para que isso seja garantido.
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 deveria 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 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
comumente serão referidas 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 este 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 usados para representar os resultados da avaliação dename
, armazenados em um local para que seja avaliado 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. O objetivo desta regra é evitar a necessidade de repetir outras regras da linguagem que estão implícitas (por exemplo, sobre conversibilidade de expressões quando atribuídas a locais de armazenamento).
Não é necessária uma implementação para traduzir literais exatamente como especificado abaixo. Qualquer conversã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 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 ressaltar que isso significa que se qualquer etapa da conversão puder causar uma exceção no tempo de execução, o estado do programa ainda permanecerá 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, matrizes embutidas não podem ser inicializadas com uma expressão de coleção. Essa continua sendo uma proposta em aberto.Pressupõe-se que as coleções sejam bem comportadas. Por exemplo:
- Assume-se que o valor de
Count
de uma coleção produzirá o mesmo valor que o número de elementos quando enumerados. - Os tipos usados nesta especificação definidos no namespace
System.Collections.Generic
são considerados livres de efeitos colaterais. 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.
- Assume-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
T[]
unidimensional, caso em que o tipo de elemento éT
- Um tipo de intervalo:
System.Span<T>
System.ReadOnlySpan<T>
Nesse caso, 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 algum elemento, o tipo tem uma instância ou método de extensão
Add
onde:- O método pode ser invocado com um argumento de valor único.
- Se o método for genérico, os argumentos de tipo podem ser inferidos 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>
Nesse caso, o tipo de elemento éT
A conversão implícita existirá se o tipo tiver um tipo de elemento T
em que para cada elemento Eᵢ
na expressão de coleção:
- Se
Eᵢ
for um elemento de expressão, há uma conversão implícita deEᵢ
emT
. - Se
Eᵢ
for um elemento espalhado..Sᵢ
, haverá uma conversão implícita do tipo de iteração deSᵢ
paraT
.
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
.
Métodos de criação
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çãoCM
aplicáveis é determinado.
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 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, 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.
Métodos declarados em tipos base ou interfaces são ignorados e não fazem parte do conjunto 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 aplica.
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 fizer referência 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 tipoC<T0, T1, …>
tem um método do construtorB.M<U0, U1, …>()
associado, os argumentos de tipo genérico do tipo de destino são aplicados em ordem — e do tipo de conteúdo mais externo para o mais interno — ao método do construtor.
O parâmetro span para o método de criação pode ser marcado explicitamente como 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 quaisquer referências adicionais aos elementos referem-se aos resultados desta 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.
Presume-se que Length
, Count
e GetEnumerator
não tenham 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, a
Add
instância aplicável ou o método de extensão será invocado com a expressão como 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 espalhado, um dos seguintes 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 de 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, a
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, na ordem:
- Se o elemento for um elemento de expressão, o indexador de instância de inicialização será invocado para adicionar a expressão avaliada no índice atual.
- Se o elemento for um elemento espalhado, um dos seguintes 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 de 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 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 um intervalo for necessário, o compilador pode 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ᵢ
:
- Se
Eᵢ
for um elemento de expressão, uma inferência de tipo de entrada será feita deEᵢ
paraTₑ
.- Se
Eᵢ
for um elemento de propagação com um tipo de iteraçãoSᵢ
, então uma inferência de limite inferior é feita deSᵢ
paraTₑ
.- [regras existentes da primeira fase] ...
11.6.3.7 Inferências de tipo de saída
Uma inferência de tipo de saída é feita a partir 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 é feita usandoEᵢ
.- [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₁
emE₂
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_type eT₂
não é um span_type e existe uma conversão implícita deT₁
emT₂
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, é possível traduzir o elemento 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, 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.
Conversão de interface
Conversão de interface não mutável
Dado um tipo de alvo 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. 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 permitam introspecção dinâmica em cenários como vinculação de dados.
Uma implementação compatível é livre para:
- Use um tipo existente que implemente as interfaces necessárias.
- Sintetize um tipo que implemente as interfaces necessárias.
Em ambos os casos, o tipo usado permite implementar um conjunto maior de interfaces do que o estritamente necessário.
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 poderia usar nenhum tipo de armazenamento, optando por calcular 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, evitando que uma coleção não mutável seja mutada acidentalmente.
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 permite 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 pode construir esse intervalo diretamente como uma referência a parte do segmento de dados do programa.Os formulários acima (para matrizes e extensões) são as representações básicas da expressão de coleção e são usados para as seguintes regras de conversã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 desperdício de realocações à 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 evitar a realocação interna de armazenamento.
Conversão de comprimento desconhecida
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 propagação de qualquer tipo iterável, embora com a menor quantidade de otimização possível.
Se
T
for algumT1[]
, o literal tem a mesma semântica que:List<T1> __list = [...]; /* initialized using predefined rules */ T1[] __result = __list.ToArray();
O procedimento acima é ineficiente; ele cria a lista intermediária e depois cria uma cópia da matriz final a partir dela. As implementações são livres para otimização; por exemplo, produzindo códigos como este:
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 desnecessários.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. Estão incluídos:
- 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 uma 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
Existem duas ambiguidades sintáticas "verdadeiras" onde há múltiplas interpretações sintáticas legais do código que usa um
collection_literal_expression
.O
spread_element
é ambíguo em relação aorange_expression
. Tecnicamente, poderíamos 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. - Escolher 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 real, mas em que a sintaxe aumenta muito a complexidade da análise. Embora não seja um problema considerando o tempo de engenharia, isso ainda aumenta a sobrecarga cognitiva dos usuários ao analisar 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 nos outros casos, 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 indesejado. Esse tipo de código não parece ilógico de se escrever e provavelmente vai confundir as pessoas.
Desvantagens
- Isso apresenta outra forma para expressões de coleção além das inúmeras maneiras que já temos. Isso é uma complexidade extra para a linguagem. 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. - Usar
[
...]
em vez de{
...}
nos afasta da sintaxe que geralmente usamos para matrizes e inicializadores de coleção. Especificamente que usa[
...]
em vez de{
...}
. No entanto, isso já estava definido pela equipe de linguagem quando listamos os padrões. Tentamos fazer{
...}
funcionar com os padrões de lista e tivemos problemas insuperáveis. Por isso, nos mudamos para[
...]
que, embora seja algo novo para C#, parece natural em muitas linguagens de programação e nos permitiu começar do zero, sem ambiguidade. Usar[
...]
já que a forma literal correspondente é complementar às nossas últimas decisões e nos fornece um espaço limpo para trabalhar com tranquilidade.
Isso introduz verrugas na linguagem. Por exemplo, os seguintes elementos são legais e (felizmente) significam exatamente a mesma coisa:
int[] x = { 1, 2, 3 };
int[] x = [ 1, 2, 3 ];
Contudo, por causa da amplitude e da consistência trazidas pela nova sintaxe literal, devemos considerar recomendar que as pessoas migrassem para o novo formato. Sugestões e correções do IDE podem ajudar nesse sentido.
Alternativas
- Que outros projetos 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 gerenciamento de um buffer
requer mais esforço do que 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 depois todos os comprimentos? Ou deveríamos avaliar um elemento, depois seu comprimento, depois 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 implementar isso da maneira mais eficiente possível.
O texto a seguir existe para registrar 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 é indesejado devido à necessidade de forçar alocações de armazenamento temporário. Poderíamos ser mais eficientes se controlássemos a emissão.
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 registrar 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 adiante, da mesma forma comoparams T[]
.O compilador pode/deve emitir
Array.Empty<T>()
para[]
? Deveríamos exigir que isso seja feito 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 alocações/despachos excessivos em comparação à enumeração direta no código traduzido.Sim. Uma implementação pode utilizar outros métodos para inicializar um valor de coleção, supondo que esses métodos tenham 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 tirar vantagem nos 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? A linguagem não deveria ser especificada 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 formato 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 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 próprio literal. Sendo 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;
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.
Contudo, um tipo de iteração é sensível ao contexto em que foreach
é executado. 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, parece um tanto estranho que uma conversão seja sensível ao contexto dessa forma. A conversão é efetivamente "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.
Este é 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 vier 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 ser possível prever com segurança 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.
Proposal
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 correta (isso exclui quaisquer 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". Este é outro ponto de dependência de contexto baseado na acessibilidade. O propósito deste método é muito semelhante ao propósito de um método de conversão definido pelo usuário, e este deve ser público. Portanto, devemos considerar exigir que o método de criação 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 emT
.
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>
.
Aparentemente, a implementação assume que o tipo de iteração é object
, mas a especificação deixa esse fato sem especificar e simplesmente não exige que cada elemento seja convertido 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 as duas funcionalidades. 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.
Proposal
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[])'
}
}
Contudo, "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 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, houve cenários em que a conversão existiu, mas estava errada. 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 . Este é 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 já foi discutido anteriormente; veja 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. Veja uma citação:
Em particular, os manipuladores de cadeia de caracteres interpoladas foram originalmente especificados dessa maneira, 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. Veja 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 seja um forte indicador da intenção do autor de que o tipo seja um manipulador de cadeia de caracteres interpolada. É justo supor que a presença do atributo não seja uma coincidência.
Em contraste, o fato de um tipo ser "enumerável" não significa necessariamente que houve intenção do autor de que o tipo fosse construtível. A presença de um método de criação, no entanto, indicada com um atributo [CollectionBuilder(...)]
no tipo de coleção, parece indicar fortemente a intenção do autor de que o tipo seja construível.
Proposal
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 que é 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 grupos 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? A linguagem não deveria ser especificada 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. Ensine as pessoas a terem cuidado com a extensão. 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 extremo perigo. - Somente use stackalloc quando o literal tiver um número fixo de elementos (ou seja, sem elementos dispersos). 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 coisas como
[1, 2, ..s]
nunca seriam possíveis, mesmo que o usuário saiba que é completamente seguro em tempo de execução.
- Sempre use stackalloc. Ensine as pessoas a terem cuidado com a extensão. 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 alocações/despachos excessivos em comparação à 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 seja algo que o algoritmo de inferência possa estar ciente. 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/devemos fazer uma pausa aqui?
Ambiguidades sintáticas com
?[
.Pode valer a pena mudar as regras para
nullable index access
para afirmar 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