Collection Expression Arguments no C# 15
Os collection expression arguments são um recurso do C# 15 que permite passar argumentos ao construtor ou ao método de criação de uma coleção dentro da própria collection expression. Com isso, você define capacidade inicial, comparadores e outros parâmetros sem abrir mão da sintaxe concisa com colchetes.
O C# tem evoluído de forma consistente nas últimas versões, sempre com atenção à expressividade, à legibilidade e à eficiência do código. Neste artigo, você entende o problema que o recurso resolve, como a sintaxe funciona e quais restrições existem.
O que são collection expression arguments
As collection expressions chegaram no C# 12 e mudaram a forma de criar coleções. Porém, elas tinham uma limitação importante: não havia como fornecer argumentos ao construtor da coleção.
O C# 15 resolve essa lacuna com os collection expression arguments. Segundo a documentação oficial do C# 15, você usa um elemento with(...) como primeiro item da expressão para repassar argumentos ao construtor ou ao método de fábrica da coleção.
Em outras palavras, o recurso amplia a flexibilidade das collection expressions. Assim, ele elimina cenários em que o código ficava menos eficiente ou mais verboso do que deveria.
O recurso faz parte do conjunto de novidades do C# 15, que acompanha o .NET 11. Se quiser uma visão geral dessa versão, o artigo sobre o .NET 11 RC 1 e as novidades do C# 15 traz o panorama completo.
Relembrando as collection expressions do C# 12
As collection expressions permitem criar e combinar coleções com uma sintaxe compacta. Um exemplo comum envolve a concatenação de várias coleções:
List<int> numbers = [1, 2, 3];
List<int> squares = [1, 4, 9];
// O spread (..) expande os elementos de cada coleção
List<int> allNumbers = [.. numbers, .. squares];
Nesse cenário, o operador .., chamado de spread element, expande os elementos de uma coleção dentro de outra. Dessa forma, você combina listas sem chamar métodos como Concat ou ToList.
Repare que o exemplo declara o tipo de destino de forma explícita. Isso acontece porque uma collection expression não tem tipo natural, então var allNumbers = [.. numbers, .. squares]; não compila.
Além disso, a mesma expressão pode virar arrays, Span<T>, List<T> ou interfaces como IEnumerable<T> e IList<T>. Afinal, o compilador escolhe a forma mais eficiente de criar a coleção conforme o tipo de destino.
Embora a sintaxe seja elegante, ela apresentava limitações quando o desenvolvedor precisava controlar detalhes internos da coleção. Capacidade inicial e parâmetros específicos de inicialização ficavam de fora.
O problema da inicialização com parâmetros
Coleções como List<T> e HashSet<T> possuem construtores que aceitam parâmetros relevantes para desempenho e comportamento. Contudo, até o C# 14, as collection expressions sempre usavam o construtor padrão.
Capacidade inicial em List
No caso de List<T>, você pode definir a capacidade inicial da lista. Assim, evita realocações internas quando já conhece o número aproximado de elementos.
List<int> list = new(capacity: 10);
Ao usar collection expressions tradicionais, não havia como informar esse argumento. Então, o compilador criava a coleção com o construtor padrão.
Na prática, isso podia gerar realocações desnecessárias de memória quando a coleção crescia dinamicamente. Em listas grandes ou em código executado com frequência, esse custo se acumula.
Comparadores em HashSet
O problema fica ainda mais evidente em coleções como HashSet<T>. Nelas, o IEqualityComparer<T> influencia diretamente o comportamento lógico da coleção, como comparações que ignoram maiúsculas e minúsculas.
Considere o exemplo a seguir:
HashSet<string> names = ["Alice", "alice", "Bob"];
Nesse caso, a coleção trata "Alice" e "alice" como valores distintos, porque o comparador padrão diferencia maiúsculas de minúsculas. No C# tradicional, você alteraria esse comportamento assim:
var names = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"Alice",
"alice",
"Bob"
};
Com as collection expressions do C# 12, não existia uma forma equivalente de fornecer o comparador. Por isso, muita gente voltava à sintaxe antiga sempre que o cenário ficava um pouco mais sofisticado.
Como usar collection expression arguments
A solução do C# 15 consiste em um novo elemento chamado with(...). Ele precisa ser o primeiro item da collection expression e recebe os argumentos que seguem para o construtor.
A sintaxe with dos collection expression arguments
Veja o exemplo da lista com capacidade inicial reescrito com collection expression arguments:
List<int> numbers = [1, 2, 3];
List<int> squares = [1, 4, 9];
// A capacidade segue para o construtor de List<int>
List<int> allNumbers = [with(capacity: 10), .. numbers, .. squares];
Nesse exemplo, o argumento capacity vai direto para o construtor de List<int>. Assim, a lista nasce com a capacidade adequada antes da inserção dos elementos.
O mesmo conceito vale para coleções mais complexas, como HashSet<T>:
// O comparador segue para o construtor de HashSet<string>
HashSet<string> names = [with(StringComparer.OrdinalIgnoreCase), "Alice", "alice", "Bob"];
Console.WriteLine(names.Count); // 2
Agora o conjunto contém apenas dois elementos, porque "Alice" e "alice" passam a ser iguais. Com isso, o comportamento da coleção se torna configurável sem perder a expressividade dos colchetes.
Argumentos nomeados e expressões
Dentro do with(...), você pode usar qualquer expressão válida. Por exemplo, a documentação mostra a capacidade calculada a partir do tamanho de outra coleção:
string[] values = ["one", "two", "three"];
// A capacidade usa o dobro do tamanho do array de origem
List<string> names = [with(capacity: values.Length * 2), .. values];
Argumentos nomeados também funcionam, como no caso de capacity. Dessa forma, o código deixa explícito qual parâmetro do construtor você está preenchendo.
O compilador usa a resolução de sobrecarga normal para escolher o construtor. Ou seja, as mesmas regras que valem para um new comum valem para os collection expression arguments.
Collection expression arguments com interfaces
As collection expressions também convertem para interfaces, e o novo recurso acompanha esse comportamento. No entanto, o suporte varia conforme a interface de destino.
| Interface de destino | Elementos with aceitos |
|---|---|
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 indicado. Então, o exemplo abaixo gera uma lista com capacidade 100:
// 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 concreta nesses casos.
Collection expression arguments em tipos próprios
Tipos próprios podem aderir às collection expressions por meio do atributo CollectionBuilder. Esse atributo aponta para uma classe com um método Create que recebe um ReadOnlySpan<T> como último parâmetro.
A partir do C# 15, esse método Create pode ter parâmetros adicionais antes do ReadOnlySpan<T>. Assim, os collection expression arguments também chegam às suas coleções personalizadas.
[CollectionBuilder(typeof(TagSetBuilder), nameof(TagSetBuilder.Create))]
public class TagSet : IEnumerable<string>
{
private readonly HashSet<string> _items;
public TagSet(ReadOnlySpan<string> items, IEqualityComparer<string>? comparer = null)
{
_items = new HashSet<string>(comparer);
foreach (var item in items)
_items.Add(item);
}
public int Count => _items.Count;
public IEnumerator<string> GetEnumerator() => _items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
internal static class TagSetBuilder
{
// Sem argumentos extras
internal static TagSet Create(ReadOnlySpan<string> items) => new(items);
// O comparador vem antes do ReadOnlySpan<T>
internal static TagSet Create(IEqualityComparer<string> comparer, ReadOnlySpan<string> items) =>
new(items, comparer);
}
Com esse builder, o uso fica idêntico ao das coleções do .NET:
TagSet tags = [with(StringComparer.OrdinalIgnoreCase), "dotnet", "DotNet", "csharp"];
Console.WriteLine(tags.Count); // 2
O compilador seleciona a sobrecarga de Create pela resolução de sobrecarga, com base nos argumentos informados. Além disso, o ReadOnlySpan<T> com os elementos sempre ocupa a última posição.
Exemplos práticos de collection expression arguments
Os exemplos anteriores mostram a mecânica do recurso. Agora, vale observar como ele aparece em situações comuns de uma aplicação.
Remover duplicidades em dados de entrada
Imagine uma API que recebe tags digitadas pelo usuário. Muitas vezes, a mesma tag chega com variações de maiúsculas e minúsculas.
public static HashSet<string> NormalizeTags(IEnumerable<string> rawTags) =>
// O comparador ignora diferenças de caixa entre as tags
[with(StringComparer.OrdinalIgnoreCase), .. rawTags];
O método combina o spread com o comparador em uma única linha. Antes, você precisaria criar o HashSet<string> com new e depois chamar UnionWith ou um laço de Add.
Montar resultados com tamanho conhecido
Outro cenário frequente envolve juntar resultados de várias fontes cujo tamanho você já conhece. Nesse caso, a capacidade evita que a lista cresça em etapas.
public static List<Order> MergeOrders(Order[] pending, Order[] approved) =>
// A soma dos tamanhos define a capacidade final da lista
[with(capacity: pending.Length + approved.Length), .. pending, .. approved];
Vale lembrar que o compilador já otimiza muitos casos de spread. Mesmo assim, deixar a capacidade explícita documenta a intenção e garante o comportamento quando o tipo de origem não informa o tamanho.
Ordenar com um comparador próprio
Coleções ordenadas também se beneficiam do recurso. O SortedSet<T>, por exemplo, aceita um IComparer<T> no construtor.
// Ordena os nomes sem diferenciar maiúsculas e minúsculas
SortedSet<string> customers = [with(StringComparer.OrdinalIgnoreCase), "carla", "Bruno", "ana"];
foreach (var customer in customers)
Console.WriteLine(customer); // ana, Bruno, carla
Assim, o mesmo padrão serve para listas, conjuntos e coleções ordenadas. Ou seja, você aprende a sintaxe uma vez e a reaproveita em toda a biblioteca de coleções do .NET.
Comparação com os inicializadores tradicionais
Os inicializadores de coleção continuam válidos no C# 15. Portanto, vale entender quando os collection expression arguments trazem vantagem real.
Veja as duas formas lado a lado para o mesmo resultado:
// Inicializador tradicional
var legacy = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Alice", "alice", "Bob" };
// Collection expression arguments
HashSet<string> modern = [with(StringComparer.OrdinalIgnoreCase), "Alice", "alice", "Bob"];
Em um caso isolado, a diferença parece pequena. No entanto, o ganho cresce quando você combina várias fontes com spread, porque o inicializador tradicional não oferece um equivalente direto ao operador ...
Além disso, as collection expressions funcionam em qualquer lugar que espera uma coleção, como argumentos de métodos e propriedades com corpo de expressão. Então, com os collection expression arguments, essa flexibilidade passa a incluir também a configuração do construtor.
Por outro lado, o inicializador tradicional ainda faz sentido quando você usa var e prefere manter o tipo no lado direito. Nesse caso, a escolha fica mais ligada ao estilo do time do que ao desempenho.
Uma boa regra prática consiste em adotar collection expressions como padrão e recorrer ao new apenas quando algo não couber na nova sintaxe. Com o C# 15, essa exceção fica bem mais rara.
Restrições do elemento with
O recurso traz algumas regras que vale conhecer antes de usar em produção. Segundo a referência da linguagem, o elemento with(...) precisa respeitar as seguintes restrições:
- Ele deve ser o primeiro elemento da collection expression.
- Os argumentos não podem ter o tipo
dynamic. - Arrays,
Span<T>eReadOnlySpan<T>não aceitam o elemento. - Os argumentos não influenciam a existência da conversão para um tipo de destino.
A última regra merece atenção. Durante a resolução de sobrecarga e a inferência de tipos, o compilador ignora os argumentos e considera apenas a presença do with(...).
Por exemplo, um método com sobrecargas para List<int> e int[] não muda de escolha por causa do valor de capacity. Contudo, a simples presença do with já descarta o array como candidato.
Da proposta args à sintaxe with
Quando a funcionalidade apareceu como proposta, a sintaxe ainda estava em discussão. Versões iniciais, como a descrita na primeira edição deste artigo, usavam um prefixo args(...) para indicar os argumentos do construtor.
// Sintaxe da proposta inicial, substituída depois por with(...)
// List<int> allNumbers = [args(capacity: 10), .. numbers, .. squares];
Na época, o uso explícito de args levantava questionamentos quanto à clareza e à facilidade de descoberta do recurso. Por fim, a versão implementada nos previews do .NET 11 adotou a palavra with.
A escolha mantém a consistência com o modelo de inicialização existente na linguagem. Além disso, ela evita ambiguidades entre os elementos da coleção e os parâmetros de construção.
Como acontece com outros recursos recentes do C#, a sintaxe equilibra flexibilidade, legibilidade e previsibilidade. Esses aspectos importam bastante em uma linguagem tão presente em ambientes corporativos.
Benefícios dos collection expression arguments
A introdução dos collection expression arguments traz vários benefícios práticos para o dia a dia:
- Melhor desempenho, porque a coleção já nasce com a capacidade certa.
- Mais expressividade, sem código auxiliar nem inicializações intermediárias.
- Menos inconsistência entre coleções tradicionais e collection expressions.
- Uso das collection expressions em cenários reais e mais complexos.
Na prática, o maior ganho aparece na consistência do código. Antes, um mesmo projeto misturava colchetes em casos simples e new com inicializadores sempre que um comparador entrava em cena.
Agora, você pode padronizar a criação de coleções com uma única sintaxe. Isso reduz decisões de estilo em code review e facilita a leitura para quem chega ao projeto.
O recurso também conversa com outras novidades da linguagem. Os union types do C# 15, por exemplo, seguem a mesma linha de deixar o código mais declarativo.
Como testar collection expression arguments
O C# 15 acompanha o .NET 11, então você precisa do SDK do .NET 11 em versão preview para testar o recurso. A Microsoft também indica a versão insiders do Visual Studio 2026, que já inclui esse SDK.
Depois de instalar o SDK, crie um projeto que tenha o .NET 11 como alvo e escreva os exemplos deste artigo. Caso encontre algum problema, a equipe do C# pede feedback por meio de issues no repositório dotnet/roslyn.
# Cria um projeto de console e executa os exemplos
dotnet new console -n CollectionArgumentsDemo
cd CollectionArgumentsDemo
dotnet run
Como o recurso ainda faz parte de uma versão em desenvolvimento, detalhes podem mudar até o lançamento final. Por isso, acompanhe a página de novidades do C# 15 antes de adotar os collection expression arguments em código de produção.
Conclusão
Os collection expression arguments representam uma evolução natural das collection expressions do C# 12. Ao permitir a passagem de argumentos ao construtor, a linguagem elimina uma limitação significativa.
Com isso, o recurso se torna viável em contextos que exigem controle fino de desempenho e comportamento. Capacidade inicial, comparadores e parâmetros de builders personalizados cabem agora dentro dos colchetes.
A trajetória da sintaxe, de args(...) para with(...), também mostra o compromisso da equipe do C# em refinar recursos existentes. Assim, as funcionalidades ficam mais completas e alinhadas às necessidades reais de quem desenvolve.
Se o recurso chegar à versão final como está nos previews, ele deve se tornar uma ferramenta valiosa no desenvolvimento de aplicações modernas em .NET.
FAQ
Em qual versão do C# surgem os collection expression arguments?
O recurso faz parte do C# 15, que acompanha o .NET 11. Para testá-lo, você precisa do SDK de preview do .NET 11 ou do Visual Studio 2026 insiders.
O elemento with pode aparecer no meio da collection expression?
Não. O with(...) precisa ser o primeiro elemento da collection expression, antes de qualquer item ou spread.
Posso usar with com arrays ou Span?
Não. Arrays, Span<T> e ReadOnlySpan<T> não aceitam o elemento with(...), conforme as restrições da referência da linguagem.
Os collection expression arguments funcionam com coleções próprias?
Sim. Basta usar o atributo CollectionBuilder e criar um método Create com parâmetros extras antes do ReadOnlySpan<T>.
A sintaxe args ainda funciona?
Não. A sintaxe args(...) existiu apenas como proposta inicial, e a versão implementada adota with(...).