A maior e melhor Black Friday que já fizemos! Descontos a partir de 50% em até 12x sem juros - Garanta seu Desconto

Collection expression arguments: como usar no C# 15

Collection expression arguments: como usar no C# 15

Collection expression arguments são o recurso do C# 15 que permite passar argumentos ao construtor de uma coleção dentro dos colchetes. Para isso, basta usar um elemento with(...) como primeiro item da collection expression.

Até o C# 14, quem precisava definir capacidade, comparador ou outro parâmetro de construção tinha que abandonar a sintaxe de colchetes. Agora, a mesma expressão cria a coleção, configura a instância e adiciona os elementos.

Este artigo explica como o recurso funciona com construtores, com tipos que usam CollectionBuilder e com interfaces. Além disso, mostra as limitações do compilador e quando vale a pena adotar collection expression arguments no seu código.

O que são collection expression arguments

Uma collection expression é a sintaxe com colchetes que o C# 12 introduziu para criar coleções. Ela funciona com arrays, spans, List<T>, interfaces e qualquer tipo que siga as regras de coleção da linguagem.

Collection expression arguments estendem essa sintaxe com um novo tipo de elemento, o with(...). Ele recebe uma lista de argumentos, como uma chamada de método, e o compilador encaminha esses valores para quem cria a coleção.

string[] values = ["one", "two", "three"];

// Passa a capacidade para o construtor de List<T>
List<string> names = [with(capacity: values.Length * 2), .. values];

// Passa um comparador para o construtor de HashSet<T>
HashSet<string> set = [with(StringComparer.OrdinalIgnoreCase), "Hello", "HELLO", "hello"];

No primeiro caso, o compilador chama o construtor de List<string> que recebe capacity. No segundo, o HashSet<string> recebe StringComparer.OrdinalIgnoreCase e, por isso, guarda apenas um elemento.

Em outras palavras, o with(...) é a ponte entre a sintaxe enxuta dos colchetes e as opções de construção que antes exigiam new. A referência oficial de collection expressions no Microsoft Learn descreve o recurso e lista as regras de conversão para cada tipo de destino.

Como chegamos aqui: o C# 12 e as collection expressions

O C# 12 trouxe as collection expressions e o elemento de spread, representado por dois pontos seguidos. Com eles, inicializar e combinar coleções ficou bem mais legível.

int[] values = [1, 2, 3];
int[] all = [0, .. values, 4];

No entanto, essa primeira versão tinha uma lacuna clara. Não existia uma forma de informar capacidade, comparador ou opções de fábrica dentro dos colchetes.

Por isso, quem precisava de um HashSet que ignora maiúsculas voltava para a sintaxe antiga. O mesmo acontecia com listas pré-dimensionadas, que pediam um construtor e depois um AddRange.

// Antes do C# 15: duas etapas para configurar a coleção
var tags = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
tags.UnionWith(["csharp", "CSharp", "dotnet"]);

var ids = new List<int>(capacity: sourceIds.Count);
ids.AddRange(sourceIds);

Esse padrão funciona, mas quebra o fluxo de uma única expressão. Além disso, ele separa a configuração do conteúdo da coleção, o que dificulta a leitura.

O que o C# 13 e o C# 14 mudaram

Antes de chegar ao C# 15, vale separar bem o que cada versão fez. O C# 13 ampliou o modificador params, que passou a aceitar Span<T>, ReadOnlySpan<T> e interfaces de coleção, além de arrays.

// params com tipos de coleção além de arrays
public void LogValues(params ReadOnlySpan<int> numbers) { }

LogValues(1, 2, 3);

Essa mudança afeta a forma de declarar e chamar métodos, e não a forma de construir coleções. Já o C# 14 não trouxe mudanças nas collection expressions, segundo o comparativo de Laurent Kempé.

Versão O que mudou O que não mudou
C# 12 Collection expressions e spread (..) Nenhuma forma de passar argumentos de construção
C# 13 params com coleções além de arrays Sintaxe de construção das collection expressions
C# 14 Nenhuma mudança nas collection expressions
C# 15 Elemento with(...) no início da expressão Arrays e spans continuam sem argumentos

Na prática, o C# 13 melhora o formato dos parâmetros de método. O C# 15, por sua vez, melhora a construção da coleção com collection expression arguments.

Sintaxe do with nas collection expression arguments

A gramática da linguagem ganhou um novo tipo de elemento de coleção, ao lado dos elementos de expressão e de spread. Sempre que um elemento começa com a palavra with seguida de parênteses, o compilador o trata como argumento de construção.

Essa regra léxica evita ambiguidade com uma eventual chamada a um método chamado with. Caso esse método exista no seu código, a especificação indica o prefixo @, como em @with(...), para continuar chamando o método.

Dentro dos parênteses, você usa qualquer expressão válida. Também pode usar argumentos nomeados, como capacity: 100, e a resolução de sobrecarga funciona como em qualquer chamada.

A regra do primeiro elemento

O with(...) precisa ser o primeiro elemento da collection expression. Depois dele, literais, variáveis e spreads funcionam normalmente.

// Válido: with(...) no início
List<int> numbers = [with(capacity: 100), 1, 2, 3, .. more];

// Inválido: with(...) depois de outros elementos
List<int> invalid = [.. items, with(capacity: 100)];

Essa restrição deixa a leitura previsível. Quem bate o olho na expressão sabe imediatamente que a coleção recebe configuração antes do conteúdo.

A especificação justifica a escolha justamente pela leitura natural. A expressão cria uma coleção com os argumentos informados e, em seguida, adiciona os elementos.

Ordem de avaliação

O compilador avalia os argumentos do with(...) uma única vez, antes dos elementos da coleção. Depois disso, avalia os elementos da esquerda para a direita.

Existe um detalhe importante nos spreads. Em .. GetItems(), a chamada a GetItems() acontece na sua posição, mas a enumeração dos itens pode ocorrer depois das expressões seguintes.

Por isso, não dependa de efeitos colaterais da enumeração em collection expression arguments. Quando a ordem importa, materialize a sequência antes de montar a coleção.

Collection expression arguments com construtores

Quando o tipo de destino é uma classe ou struct que implementa IEnumerable, o compilador encaminha os argumentos para um construtor. Ele usa a resolução de sobrecarga normal para escolher o construtor mais adequado.

Diagrama de collection expression arguments mostrando o elemento with encaminhando argumentos para o construtor de uma classe, para o método Create de um CollectionBuilder e para uma List criada a partir de IList, com arrays e spans sem suporte
Para onde o compilador envia os argumentos do with(...) conforme o tipo de destino.

Em termos de código gerado, a especificação descreve uma tradução direta. A expressão [with(10), ...] para uma List<string> vira new List<string>(10), seguida da inicialização normal.

Capacidade inicial em List

O caso mais comum de collection expression arguments é a capacidade inicial. Quando você já sabe quantos itens a lista vai receber, informar esse número evita realocações do array interno.

List<int> ids = [with(capacity: sourceIds.Count), .. sourceIds];

// Reserva espaço extra para itens que entram depois
List<string> names = [with(capacity: values.Length * 2), .. values];

A inicialização fica em uma única expressão e a intenção aparece logo no começo. Além disso, o leitor entende que a capacidade faz parte do desenho da coleção, e não de um ajuste posterior.

Comparadores em HashSet

Outro uso frequente é o comparador. Um HashSet<string> com StringComparer.OrdinalIgnoreCase trata Hello, HELLO e hello como o mesmo valor.

HashSet<string> tags = [with(StringComparer.OrdinalIgnoreCase), "CSharp", "CSHARP", "csharp", "dotnet"];

Console.WriteLine(tags.Count); // 2

Sem collection expression arguments, esse comparador exigia o construtor explícito e uma etapa extra para os itens. Agora, a configuração e os dados ficam juntos no mesmo lugar.

Tipos sem construtor sem parâmetros

Para alguns tipos, o with(...) deixa de ser opcional. Quando a coleção não tem um construtor acessível sem parâmetros, você precisa informar os argumentos do construtor que existe.

public class NamedValues(string name) : List<int>
{
    public string Name { get; } = name;
}

NamedValues readings = [with("temperature"), 18, 21, 24];
Console.WriteLine($"{readings.Name}: {string.Join(", ", readings)}");
// temperature: 18, 21, 24

Antes do C# 15, um tipo como NamedValues não aceitava collection expressions. Com collection expression arguments, ele passa a usar a mesma sintaxe das coleções da biblioteca padrão.

Também vale citar o with() vazio, que chama o construtor sem argumentos. Ele raramente aparece em tipos comuns, mas ajuda a deixar explícita a escolha de uma sobrecarga.

Collection expression arguments em tipos com CollectionBuilder

Tipos personalizados podem aderir às collection expressions com o atributo CollectionBuilder. Esse atributo aponta para um método estático Create que recebe os elementos em um ReadOnlySpan<T>.

A partir do C# 15, o método Create pode ter parâmetros adicionais antes do ReadOnlySpan<T>. Os valores desses parâmetros chegam pelo with(...), enquanto o span continua sempre como último parâmetro.

[CollectionBuilder(typeof(Tags), nameof(Create))]
internal sealed class Tags(string[] values, StringComparer comparer)
    : HashSet<string>(values, comparer)
{
    // Recebe o comparador antes dos itens
    public static Tags Create(StringComparer comparer, ReadOnlySpan<string> items)
        => new([.. items], comparer);

    // Sobrecarga sem argumentos extras usa o comparador padrão
    public static Tags Create(ReadOnlySpan<string> items)
        => new([.. items], StringComparer.OrdinalIgnoreCase);
}

Com o tipo pronto, as três formas abaixo compilam e chegam ao mesmo resultado.

Tags tags = [with(StringComparer.OrdinalIgnoreCase), "CSharp", "CSHARP", "csharp", "dotnet"];
Tags newTags = [with(), "CSharp", "CSHARP", "csharp", "dotnet"];
Tags oldTags = ["CSharp", "CSHARP", "csharp", "dotnet"];

Nos três casos, a resolução de sobrecarga escolhe o Create adequado. Com o comparador, o compilador usa a primeira sobrecarga, enquanto o with() vazio e a ausência do with levam à segunda.

Esse modelo abre espaço para APIs mais ricas em bibliotecas internas. Um tipo de coleção pode receber opções de validação, ordenação ou comparação sem abrir mão da sintaxe de colchetes.

Por outro lado, mais sobrecargas aumentam o risco de ambiguidade. Por isso, revise com cuidado os métodos Create dos seus tipos quando adotar collection expression arguments.

Interfaces como destino das collection expression arguments

Collection expressions também aceitam interfaces como tipo de destino, e o recurso novo acompanha esse comportamento. Nesse caso, o compilador escolhe a implementação concreta e aplica um conjunto fixo de assinaturas.

Interface Elementos with suportados
IEnumerable<T>, IReadOnlyCollection<T>, IReadOnlyList<T> Apenas with() vazio
ICollection<T>, IList<T> with() e with(int capacity)

Para IList<T> e ICollection<T>, o compilador cria uma List<T> com o construtor correspondente. Assim, uma propriedade tipada como interface também pode receber capacidade inicial.

// O compilador cria uma List<int> com capacidade 100
IList<int> numbers = [with(capacity: 100), 1, 2, 3];

Já as interfaces somente leitura aceitam apenas o with() vazio. Isso faz sentido, porque o compilador tem liberdade para escolher a implementação e não expõe opções de construção.

Limitações das collection expression arguments

O recurso tem fronteiras bem definidas, e conhecê-las evita erros de compilação inesperados. A documentação oficial lista quatro restrições principais para collection expression arguments.

Arrays e spans ficam de fora

Collection expression arguments não funcionam com arrays nem com Span<T> e ReadOnlySpan<T>. Esses tipos não oferecem construtores ou métodos de criação que façam sentido receber argumentos.

Nem mesmo o with() vazio vale para eles. Segundo a especificação, não existe nenhuma assinatura que se encaixe nesses tipos.

// Erro: arrays não aceitam with(...)
int[] values = [with(capacity: 10), 1, 2, 3];

// Erro: spans também não aceitam
Span<int> buffer = [with(), 1, 2, 3];

Argumentos dynamic ficam proibidos

Os argumentos dentro do with(...) não podem ter o tipo dynamic. Se o valor vier de uma variável dynamic, faça uma conversão explícita para o tipo esperado antes de usá-lo.

O with não participa da inferência de tipos

Esta é a limitação mais sutil das collection expression arguments. Os argumentos do with(...) não influenciam qual conversão o compilador escolhe para a expressão.

Considere dois métodos Print, um que recebe List<string> e outro que recebe HashSet<string>. Apenas o HashSet<string> tem um construtor que aceita o comparador, mas mesmo assim a chamada abaixo continua ambígua.

static void Print(List<string> values) { }
static void Print(HashSet<string> values) { }

// Erro CS0121: chamada ambígua entre as duas sobrecargas
Print([with(StringComparer.OrdinalIgnoreCase), "one", "ONE"]);

O compilador ignora os argumentos do with(...) ao escolher a sobrecarga de Print. Para resolver, atribua a expressão a uma variável HashSet<string> antes de chamar o método.

A inferência de tipos genéricos segue a mesma regra. Em Use([with(10)]), o compilador não descobre o tipo T a partir da capacidade e gera o erro CS0411.

static void Use<T>(List<T> values) { }

// Erro CS0411: não há como inferir T
Use([with(10)]);

// Correto: informe o tipo explicitamente
Use<int>([with(10)]);

Esse comportamento segue o mesmo precedente do new(...) com tipo alvo. Depois que o tipo de destino fica definido, aí sim os argumentos participam da escolha do construtor ou do método Create.

Regras normais de sobrecarga continuam valendo

A resolução de sobrecarga de construtores e métodos Create funciona como em qualquer chamada. Se mais de um candidato aceitar os argumentos, o compilador acusa ambiguidade.

Exemplo prático com collection expression arguments

Para ver o recurso em um cenário real, considere um serviço que monta índices de produtos em memória. Ele recebe uma coleção de produtos e precisa de um conjunto de categorias sem duplicidade e de uma lista de nomes.

public sealed record Product(string Name, string Category);

public sealed class ProductIndex
{
    public HashSet<string> Categories { get; }
    public List<string> Names { get; }

    public ProductIndex(IReadOnlyCollection<Product> products)
    {
        // Categorias sem diferenciar maiúsculas e minúsculas
        Categories = [with(StringComparer.OrdinalIgnoreCase), .. products.Select(p => p.Category)];

        // Lista já dimensionada para o total de produtos
        Names = [with(capacity: products.Count), .. products.Select(p => p.Name)];
    }
}

As duas propriedades nascem configuradas em uma única linha cada. Assim, quem lê o construtor entende a regra de comparação e a capacidade sem procurar chamadas espalhadas.

Em versões anteriores, o mesmo trecho pediria um new explícito para cada coleção. Depois, viria uma chamada a UnionWith ou AddRange para preencher os itens.

// Mesmo resultado sem o with(...)
Categories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Categories.UnionWith(products.Select(p => p.Category));

Names = new List<string>(products.Count);
Names.AddRange(products.Select(p => p.Name));

A diferença parece pequena em um exemplo isolado. Contudo, em classes com várias coleções, a versão com with(...) reduz o ruído e destaca a intenção de cada inicialização.

Vale observar também que a capacidade usa products.Count, que o compilador avalia antes dos elementos. Como os argumentos do with(...) rodam primeiro, a lista já reserva o espaço antes de o spread começar a adicionar os nomes.

Esse mesmo raciocínio vale para propriedades tipadas como interface. Se Names fosse um IList<string>, o compilador criaria uma List<string> com a capacidade informada.

Quando adotar collection expression arguments

O recurso brilha quando a intenção de configuração é clara. Capacidade inicial, comparadores e opções de tipos personalizados aparecem como os casos mais óbvios.

Primeiro, prefira collection expression arguments quando a coleção nasce já configurada. Isso reduz instruções separadas e deixa a criação em um único ponto.

Em seguida, mantenha o modelo mental de versões bem separado. O C# 13 mudou a forma de chamar métodos com params, enquanto o C# 15 mudou a forma de construir coleções.

Também revise os tipos com CollectionBuilder antes de adicionar novas sobrecargas de Create. Assim, você evita ambiguidades que só aparecem quando alguém usa o with(...).

Por fim, não force o uso em arrays e spans, porque o compilador recusa o código. Nesses casos, a sintaxe tradicional continua sendo a escolha certa.

Para quem acompanha a evolução da linguagem, o recurso conversa com outras mudanças recentes. O balta.io já mostrou, por exemplo, o novo modelo de extension members do C# 14 e as novidades do .NET 10 Preview 1.

Em projetos com o .NET 11, o C# 15 é a versão padrão da linguagem. Dessa forma, collection expression arguments já ficam disponíveis sem configuração extra no arquivo de projeto.

Conclusão

Collection expression arguments resolvem uma lacuna que existia desde o C# 12. A sintaxe de colchetes agora cobre também a configuração da coleção, e não apenas o conteúdo.

Na prática, o with(...) encaminha argumentos para construtores, para métodos Create de tipos com CollectionBuilder e para interfaces como IList<T>. Com isso, listas pré-dimensionadas e conjuntos com comparador cabem em uma única expressão.

Mesmo assim, o recurso respeita limites claros. O with(...) precisa vir primeiro, não aceita dynamic, não funciona com arrays e spans e não influencia a inferência de tipos.

Seguindo essas regras, collection expression arguments deixam o código mais curto e a intenção mais visível. Vale revisar as inicializações em duas etapas do seu projeto e migrar as que ganham clareza com o novo formato.

FAQ

O que são collection expression arguments no C# 15?

São argumentos passados por um elemento with(...) no início de uma collection expression. O compilador envia esses valores ao construtor, ao método Create ou à implementação de uma interface.

O with precisa ser o primeiro elemento?

Sim, o with(...) deve abrir a collection expression. Colocá-lo depois de outros elementos gera erro de compilação.

Posso usar with com arrays ou Span?

Não, arrays, Span<T> e ReadOnlySpan<T> não aceitam o with(...), nem mesmo vazio. Para esses tipos, use a collection expression sem argumentos.

O comparador no with ajuda a escolher a sobrecarga de um método?

Não, o compilador ignora os argumentos do with(...) ao escolher a sobrecarga ou inferir tipos genéricos. Atribua a expressão a uma variável tipada para resolver a ambiguidade.

Como faço um tipo próprio aceitar argumentos no with?

Aplique o atributo CollectionBuilder e crie um método Create com os parâmetros extras antes do ReadOnlySpan<T>. Outra opção é expor um construtor público em um tipo que implementa IEnumerable.