Skip to main content

Collection is null or empty | Validação de lista vazia | .NET 8 | CSharp

  • Collection is null or empty | Validação de lista vazia | .NET 8 | CSharp

var isNullOrEmpty = collection is { Length: 0 } or is { };
var isNullOrEmpty = collection is null or [];



Esta série analisa em profundidade as expressões de coleção, que foram introduzidas com o C#12. Esta série analisa principalmente o código que é gerado quando você usa expressões de coleção em seu aplicativo, para entender como as expressões de coleção funcionam nos bastidores.

Neste primeiro post, eu forneço uma introdução às expressões de coleção do C#12. Já existem muitas boas introduções às expressões de coleção pela internet, incluindo um post no blog .NET , mas mais um não faz mal!

Inicializadores de coleção clássicos

Temos "inicializadores de coleção" em C# desde C# 3.0. Eles usam o {}padrão para inicializar qualquer IEnumerableimplementação que tenha um Add()método. Por exemplo, isso cria um novo List<int>e o inicializa com 5 valores:

var values = new List<string> { "1", "2", "3", "4", "5" };

Nos bastidores, o compilador emite um código que se parece um pouco com este:

List<string> values = new List<string>();
values.Add("1");
values.Add("2");
values.Add("3");
values.Add("4");
values.Add("5");

A inicialização de arrays em C# é especial, pois você pode inicializá-los de ainda mais maneiras do que outras coleções, embora eles pareçam semelhantes aos inicializadores de coleção padrão:

var values1 = new[] { "1", "2", "3", "4" };
var values2 = new string[4] { "1", "2", "3", "4" };
string[] values3 = { "1", "2", "3", "4" };

Eles funcionam de forma diferente dos inicializadores de coleção; não há nenhum Add()método sendo chamado aqui. Em vez disso, o compilador gera o mesmo código de inicialização que você geraria se tivesse que fazer tudo manualmente:

string[] array = new string[4];
array[0] = "1";
array[1] = "2";
array[2] = "3";
array[3] = "4";

Intencionalmente não usei um tipo primitivo como intno exemplo acima. Se você criar e int[]usar inicializadores de array, o compilador carrega os dados do array diretamente de uma série constante de bytes, o que é mais eficiente. Veremos com mais detalhes esse mecanismo em postagens subsequentes.

Então, vimos inicializadores de coleção para coleções gerais (como List<T>) e matrizes, mas também há stackallocexpressões , que se tornaram muito mais úteis no mundo de Span<T>, pois você não precisa usar unsafecódigo:

Span<int> array = stackalloc []{ 1, 2, 3, 4 };

Uma stackallocexpressão aloca memória na pilha, em contraste com um padrão new int[]que aloca memória na pilha. Um bloco de memória alocado na pilha criado em um método é descartado automaticamente quando o método retorna, portanto, não adiciona nenhuma pressão ao coletor de lixo. No entanto, use com cuidado: se você stackallocexagerar, causará um erro StackOverflowExceptione travará seu aplicativo!

Nos bastidores, o compilador transforma esse stackallocinicializador em algum unsafecódigo como o seguinte:

unsafe
{
byte* num = stackalloc byte[16];
*(int*)num = 1;
*(int*)(num + 4) = 2;
*(int*)(num + (nint)2 * (nint)4) = 3;
*(int*)(num + (nint)3 * (nint)4) = 4;
Span<int> array = new Span<int>(num, 4);
}

O código acima cria uma matriz na pilha e, em seguida, percorre cada elemento (usando alguma aritmética de ponteiros), definindo cada valor.

Unificando a sintaxe com expressões de coleção

Vimos que há pelo menos três cenários distintos em que inicializamos coleções:

  • Matrizes

  • Coleções comoList<T>

  • ReadOnlySpan<T>stackalloc

    com

Cada um deles requer uma sintaxe ligeiramente diferente, por exemplo:

int[] array = new[] { 1, 2, 3, 4 };// One of several options!
List<int> list = new() { 1, 2, 3, 4 };
HashSet<int> hashset = new() { 1, 2, 3, 4 };
ReadOnlySpan<int> span = stackalloc [] { 1, 2, 3, 4 };

Isso tudo é um pouco confuso e irritante. As expressões de coleção , introduzidas no C#12, fornecem uma sintaxe simplificada e unificada em todos esses diferentes tipos de coleção. Por exemplo:

int[] array = [1, 2, 3, 4]
List<int> list = [1, 2, 3, 4];
HashSet<int> hashset = [1, 2, 3, 4];
ReadOnlySpan<int> span = [ 1, 2, 3, 4 ];

A consistência das expressões de coleção em todos os tipos de coleção é uma vantagem real, mas não é a única vantagem. As expressões de coleção podem oferecer benefícios de desempenho (que veremos em postagens posteriores), bem como recursos adicionais em comparação aos inicializadores de coleção.

Para reiterar, os inicializadores de coleção e matriz usam a sintaxe "antiga" new [] / new () , enquanto as expressões de coleção usam a sintaxe "nova" [ ].

Começaremos analisando uma área onde expressões de coleção podem ser usadas onde inicializadores de coleção simplesmente não podem.

Inferindo tipos de interface com expressões de coleção

Imagine que você quer criar uma coleção, mas tudo o que você quer é que ela implemente IEnumerable<int>. Você tem que decidir por si mesmo qual tipo de suporte usar:

IEnumerable<int> list1 = new List<int> { 1, 2, 3, 4 };
IEnumerable<int> list2 = new HashSet<int> { 1, 2, 3, 4 };
IEnumerable<int> list3 = new int[] { 1, 2, 3, 4 };

Então qual você deve usar? Isso importa? Se tudo o que você precisa fazer é enumerar a lista, então provavelmente não importa qual tipo você escolher, certo? Então qual é a opção correta?

Também é um pouco irritantemente prolixo, pois você tem que escrever o tipo de coleção e o tipo de variável que deseja. IEnumerable<int>

Com expressões de coleção, você pode adiar a escolha para o compilador. Em vez de especificar explicitamente o tipo de suporte, você pode deixar isso para o compilador. E um bônus adicional é a concisão extra das expressões de coleção:

IEnumerable<int> ienumerable = [ 1, 2, 3, 4 ];
IList<int> ilist = [ 1, 2, 3, 4 ];
IReadOnlyCollection<int> icollection = [ 1, 2, 3, 4 ];

Nos bastidores, o compilador cria uma coleção que implementa a interface necessária de forma totalmente transparente, para que você não precise se preocupar com isso.

Claro que você pode se perguntar qual é a coleção que é criada nos bastidores, como eu fiz. Fique ligado para o resto da série, porque a resposta para isso é "depende"!

Vale ressaltar que, embora o compilador escolha automaticamente um tipo concreto para uma coleção de interfaces, você precisa especificar algum tipo. Você não pode, por exemplo, usar var:

var values = [ 1, 2, 3, 4 ];// ❌ Does not compile, CS9176: There is no target type for the collection expression
Sum(values);
Sum([ 1, 2, 3, 4 ]);// ✅ This is fine
int Sum(IEnumerable<int> v) => v.Sum();

O problema é que, da maneira como o compilador C# funciona, ele não pode inferir que o tipo de valuesdeveria ser IEnumerable<int>, então gera um erro. É possível que isso mude em uma versão futura do C#, mas provavelmente seria resolvido, por exemplo, sempre escolhendo int[]nesta situação, o que não é necessariamente ideal, então eu não prenderia a respiração.

Stackalloc automático eficiente para ReadOnlySpan

É uma história semelhante para instâncias ReadOnlySpan<T>Span<T>também se você estiver usando apenas inicializadores de coleção. Se você só precisa de alguns dados em a Span<T>ou ReadOnlySpan<T>, então com inicializadores de coleção você precisa decidir onde colocar esses dados e então pegar o Span<T>deles:

Span<int> spans2 = stackalloc[] { 1, 2, 3, 4 };// stackalloc an array
Span<int> spans3 = new[] { 1, 2, 3, 4 };// allocate on the heap
Span<string> spans4 = new[] { "1", "2", "3", "4" };// can't use stackalloc in this case

Não é uma grande decisão neste caso, pois provavelmente existem apenas duas opções sensatas, mas ainda é algo extra para se pensar. Além disso, você não pode fazer stackallocisso string[]sem pular vários InlineArrayaros.

Com expressões de coleção, você pode, novamente, delegar a decisão ao compilador, e ele fará a Coisa Certa™.

ReadOnlySpan<int> readonlyspans = [ 1, 2, 3, 4 ];
Span<string> spans = [ "1", "2", "3", "4" ];

Mais adiante na série, você verá que esses casos de expressões de coleção em particular são altamente otimizados!

Expressões de coleção tornam a refatoração mais simples

Todos os exemplos que mostrei até agora atribuíram expressões de coleção a variáveis, mas você também pode usar expressões de coleção diretamente como argumentos de método, para que você possa fazer coisas como esta:

using System.Linq;
using System.Collections.Generic;// create a method that takes an IEnumerable

int Sum(IEnumerable<int> values) => values.Sum();// Call the method using collection expressions
Sum([1, 2, 3, 4]);

Um bom benefício desse padrão em particular é que, se eu alterar a assinatura de Sum(), não preciso alterar o site da chamada. Compare isso por um momento com se você estivesse usando inicializadores de coleção:

// if the method takes an array...
int Sum1(int[] values) => values.Sum();

Sum1(new [] { 1, 2, 3, 4 });// ...you have to use array syntax (one of several syntaxes!)// if the method takes an IEnumerable<T>...

int Sum2(IEnumerable<int> values) => values.Sum();
Sum2(new List<int> { 1, 2, 3, 4 });// ...you have to use an explicit type e.g. List<T> or similar// if the method takes a ReadOnlySpan<T>...

int Sum3(ReadOnlySpan<int> values)
{

// You can use foreach with IReadOnlySpan<T>// but it doesn't implement IEnumerable<T>, so can't// use the Linq convenience methods here!var total = 0;
foreach (var value in values)
{
total += value;
}
return total;
}
Sum3(new []{ 1, 2, 3, 4 });// ...you have to choose between a standard array,
Sum3(stackalloc int[] { 1, 2, 3, 4 });// ... or use a stackalloc'd array (for example)

Se usarmos expressões de coleção, podemos usar exatamente a mesma sintaxe para chamar todas as três Sum()implementações:

Sum1([ 1, 2, 3, 4 ]);
Sum2([ 1, 2, 3, 4 ]);
Sum3([ 1, 2, 3, 4 ]);

E o compilador usará a implementação mais eficiente possível para criar uma coleção do tipo necessário.

Isso pode parecer uma coisa pequena, e até certo ponto é , mas são todos esses pequenos aspectos de conveniência que tornam as expressões de coleção um recurso tão interessante no geral!

Coleções vazias

Outra característica das expressões de coleção é que o compilador reconhece explicitamente a sintaxe de coleção vazia , portanto, em vez de escrever algo assim:

var empty = new int[]{};// You should generally never do this...
var empty = Array.Empty<int>();// ...instead, prefer this!

agora você pode usar []para gerar uma versão vazia apropriada da coleção:

int[] empty = [];

As expressões de coleção, novamente, têm dois benefícios principais em relação aos inicializadores explícitos:

  • O compilador pode escolher a forma mais eficiente de criar a coleção vazia, escolhendo Array.Empty<int>()

    por exemplo (ou equivalente).

  • Você pode usar uma sintaxe consistente para todos os tipos de coleção.

O seguinte mostra um monte de tipos de coleção, e como você pode usar []para criar uma versão vazia de todos eles. O comentário para cada linha mostra o código que o compilador gera para o tipo específico:

int[] array = [];// Array.Empty<int>()
HashSet<int> hashset = [];// new HashSet<int>()
List<int> list = [];// new List<int>()
IEnumerable<int> ienumerable = [];// Array.Empty<int>()
ICollection<int> icollection = [];// new List<int>()
IList<int> ilist = [];// new List<int>()
IReadOnlyCollection<int> readonlycollection = [];// Array.Empty<int>()
IReadOnlyList<int> readonlyList = [];// Array.Empty<int>()
Span<int> span = [];// default(Span<int>)
ReadOnlySpan<int> readonlyspan = [];// default(ReadOnlySpan<int>)
ImmutableArray<int> immutablearray = [];// ImmutableCollectionsMarshal.AsImmutableArray(Array.Empty<int>())
ImmutableList<int> immutablelist = [];// ImmutableList.Create(default(ReadOnlySpan<int>));

Como você pode ver, o compilador é tão eficiente quanto possível; se o tipo for mutável, como a HashSet<T>ou List<T>então ele não terá outra opção a não ser criar uma nova instância do tipo, mas se conseguir usar uma versão sem alocação, como Array.Empty<int>(), então ele o fará!

Construindo coleções de outras pessoas com o elemento spread

Até agora vimos dois benefícios das expressões de coleção:

  • Sintaxe consistente
  • Implementações eficientes geradas pelo compilador

O outro grande recurso nas expressões de coleção é elemento spread ,.. . Isso lhe dá a capacidade de criar coleções com mais facilidade a partir de outras instâncias de coleção.

A funcionalidade de propagação (às vezes também chamada de "splat") já existe em outras linguagens como Python, JavaScript e Ruby há muito tempo, então é bom vê-la finalmente chegar em C#.

Como exemplo concreto, digamos que você tenha duas IEnumerable<T>coleções e queira concatená-las como um array. Isso é muito fácil de fazer com o LINQ, pois existem métodos de extensão para fazer exatamente isso:

int[] ConcatAsArray(IEnumerable<int> first, IEnumerable<int> second)
{
return first.Concat(second).ToArray();
}

Ótimo, mas e se agora você precisa want para trabalhar com ReadOnlySpan<T>em vez de IEnumerable<T>? Infelizmente, como discutimos antes, ReadOnlySpan<T>não implementa IEnumerable<T>, então podemos fazer algo assim em vez disso:

int[] ConcatAsArray(ReadOnlySpan<int> first, ReadOnlySpan<int> second)
{
var list = new List<int>(first.Length + second.Length);
list.AddRange(first);
list.AddRange(second);
return list.ToArray();
}

O que não é terrível , mas ainda é chato ter que pensar em cada tipo de coleção diferente. Com expressões de coleção obtemos um belo atalho, que pode ser usado com todos os tipos de coleção suportados usando o operador spread. Ambas as sobrecargas acima poderiam ser implementadas da mesma maneira:

int[] ConcatAsArray(IEnumerable<int> first, IEnumerable<int> second)
=> [..first, ..second];int[] ConcatAsArray(ReadOnlySpan<int> first, ReadOnlySpan<int> second)
=> [..first, ..second];

E novamente, a consistência das expressões de coleção significa que se você alterar os parâmetros ou o tipo de retorno de ConcatAsArray(), não será necessário alterar a expressão de coleção, ela simplesmente funciona!

..elemento significa "escrever todos os valores da coleção", para dar outro exemplo:

int[] array = [1, 2, 3, 4]
IEnumerable<int> oddValues = array.Where(int.IsOddInteger);// 1, 3
int[] evenValues = [..array.Where(int.IsEvenInteger)];// 2, 4 in array (using spread)
int[] allValues = [..oddValues, ..evenValues];// 1, 3, 2, 4

O código acima usa o elemento spread várias vezes, mas em cada caso significa "escrever todos os elementos da coleção". Então, na etapa final, allValuescontém todos os elementos de oddValuesseguidos por todos os valores de evenValues.

Você também pode misturar valores únicos e coleções espalhadas na sua expressão de coleção, por exemplo:

int[] arr = [1, 2, 3, 4];
int[] myValues = [ 0, ..arr, 5 , 6];// 0, 1, 2, 3, 4, 5, 6

e o resultado final é a combinação como se você tivesse iterado arre adicionado cada valor.

Note que o elemento spread ..é diferente do operador range (por exemplo, in or ) que é usado para indexar em um array. No entanto, você pode combiná-los, usando um range para selecionar um subconjunto de elementos e, então, distribuindo -os em uma expressão de coleção:..1..32..^

int[] primes = [1, 2, 3, 5, 7, 9, 11];
int[] some = [0, ..primes[1..^1]];// 0, 2, 3, 5, 7, 9

Este código pega o 1º ao N-1º elemento da primesmatriz (ou seja, 2, 3, 4, 5, 7, 9) usando o 1..^1operador de intervalo e, em seguida, usa spread ..na expressão de coleção.

As expressões de coleção adicionam uma boa simetria à criação de coleções (o que pode ser particularmente útil ao refatorar de uma coleção para outra) e tornam a combinação de coleções muito mais simples com o elemento spread. Mas as expressões de coleção não tratam apenas de sintaxe. Uma parte importante é que as expressões de coleção dão ao compilador a opção de otimizar o código que ele gera. No próximo post veremos como isso funciona.