Como estender o LINQ
Todos os métodos baseados em LINQ seguem um de dois padrões semelhantes. Eles tomam uma sequência enumerável. Eles retornam uma sequência diferente ou um único valor. A consistência da forma permite estender o LINQ escrevendo métodos com uma forma semelhante. Na verdade, as bibliotecas .NET ganharam novos métodos em muitas versões do .NET desde que o LINQ foi introduzido pela primeira vez. Neste artigo, você verá exemplos de extensão do LINQ escrevendo seus próprios métodos que seguem o mesmo padrão.
Adicionar métodos personalizados para consultas LINQ
Você estende o conjunto de métodos que você usa para consultas LINQ adicionando métodos de extensão à IEnumerable<T> interface. Por exemplo, além das operações médias ou máximas padrão, você cria um método de agregação personalizado para calcular um único valor a partir de uma sequência de valores. Você também cria um método que funciona como um filtro personalizado ou uma transformação de dados específica para uma sequência de valores e retorna uma nova sequência. Exemplos de tais métodos são Distinct, Skip, e Reverse.
Ao estender a IEnumerable<T> interface, você pode aplicar seus métodos personalizados a qualquer coleção enumerável. Para obter mais informações, consulte Métodos de extensão.
Um método agregado calcula um único valor a partir de um conjunto de valores. O LINQ fornece vários métodos agregados, incluindo Average, Mine Max. Você pode criar seu próprio método de agregação adicionando um método de extensão à IEnumerable<T> interface.
O exemplo de código a seguir mostra como criar um método de extensão chamado Median
para calcular uma mediana para uma sequência de números do tipo double
.
public static class EnumerableExtension
{
public static double Median(this IEnumerable<double>? source)
{
if (source is null || !source.Any())
{
throw new InvalidOperationException("Cannot compute median for a null or empty set.");
}
var sortedList =
source.OrderBy(number => number).ToList();
int itemIndex = sortedList.Count / 2;
if (sortedList.Count % 2 == 0)
{
// Even number of items.
return (sortedList[itemIndex] + sortedList[itemIndex - 1]) / 2;
}
else
{
// Odd number of items.
return sortedList[itemIndex];
}
}
}
Você chama esse método de extensão para qualquer coleção enumerável da mesma forma que chama outros métodos agregados da IEnumerable<T> interface.
O exemplo de código a seguir mostra como usar o Median
método para uma matriz do tipo double
.
double[] numbers = [1.9, 2, 8, 4, 5.7, 6, 7.2, 0];
var query = numbers.Median();
Console.WriteLine($"double: Median = {query}");
// This code produces the following output:
// double: Median = 4.85
Você pode sobrecarregar seu método agregado para que ele aceite sequências de vários tipos. A abordagem padrão é criar uma sobrecarga para cada tipo. Outra abordagem é criar uma sobrecarga que usa um tipo genérico e convertê-lo em um tipo específico usando um delegado. Você também pode combinar ambas as abordagens.
Você pode criar uma sobrecarga específica para cada tipo que deseja suportar. O exemplo de código a seguir mostra uma sobrecarga do Median
método para o int
tipo.
// int overload
public static double Median(this IEnumerable<int> source) =>
(from number in source select (double)number).Median();
Agora você pode chamar as Median
sobrecargas para ambos e double
integer
tipos, conforme mostrado no código a seguir:
double[] numbers1 = [1.9, 2, 8, 4, 5.7, 6, 7.2, 0];
var query1 = numbers1.Median();
Console.WriteLine($"double: Median = {query1}");
int[] numbers2 = [1, 2, 3, 4, 5];
var query2 = numbers2.Median();
Console.WriteLine($"int: Median = {query2}");
// This code produces the following output:
// double: Median = 4.85
// int: Median = 3
Você também pode criar uma sobrecarga que aceita uma sequência genérica de objetos. Essa sobrecarga usa um delegado como parâmetro e o usa para converter uma sequência de objetos de um tipo genérico em um tipo específico.
O código a seguir mostra uma sobrecarga do método que toma o Median
Func<T,TResult> delegado como um parâmetro. Este delegado pega um objeto do tipo genérico T e retorna um objeto do tipo double
.
// generic overload
public static double Median<T>(
this IEnumerable<T> numbers, Func<T, double> selector) =>
(from num in numbers select selector(num)).Median();
Agora você pode chamar o Median
método para uma sequência de objetos de qualquer tipo. Se o tipo não tiver sua própria sobrecarga de método, você terá que passar um parâmetro delegado. Em C#, você pode usar uma expressão lambda para essa finalidade. Além disso, somente no Visual Basic, se você usar a Aggregate
cláusula ou Group By
em vez da chamada de método, poderá passar qualquer valor ou expressão que esteja no escopo desta cláusula.
O código de exemplo a seguir mostra como chamar o Median
método para uma matriz de inteiros e uma matriz de cadeias de caracteres. Para strings, a mediana para os comprimentos de strings na matriz é calculada. O exemplo mostra como passar o Func<T,TResult> parâmetro delegate para o Median
método para cada caso.
int[] numbers3 = [1, 2, 3, 4, 5];
/*
You can use the num => num lambda expression as a parameter for the Median method
so that the compiler will implicitly convert its value to double.
If there is no implicit conversion, the compiler will display an error message.
*/
var query3 = numbers3.Median(num => num);
Console.WriteLine($"int: Median = {query3}");
string[] numbers4 = ["one", "two", "three", "four", "five"];
// With the generic overload, you can also use numeric properties of objects.
var query4 = numbers4.Median(str => str.Length);
Console.WriteLine($"string: Median = {query4}");
// This code produces the following output:
// int: Median = 3
// string: Median = 4
Você pode estender a IEnumerable<T> interface com um método de consulta personalizado que retorna uma sequência de valores. Nesse caso, o método deve retornar uma coleção do tipo IEnumerable<T>. Tais métodos podem ser usados para aplicar filtros ou transformações de dados a uma sequência de valores.
O exemplo a seguir mostra como criar um método de extensão chamado AlternateElements
que retorna todos os outros elementos em uma coleção, começando a partir do primeiro elemento.
// Extension method for the IEnumerable<T> interface.
// The method returns every other element of a sequence.
public static IEnumerable<T> AlternateElements<T>(this IEnumerable<T> source)
{
int index = 0;
foreach (T element in source)
{
if (index % 2 == 0)
{
yield return element;
}
index++;
}
}
Você pode chamar esse método de extensão para qualquer coleção enumerável da mesma forma que chamaria outros métodos da IEnumerable<T> interface, conforme mostrado no código a seguir:
string[] strings = ["a", "b", "c", "d", "e"];
var query5 = strings.AlternateElements();
foreach (var element in query5)
{
Console.WriteLine(element);
}
// This code produces the following output:
// a
// c
// e
Resultados do grupo por chaves contíguas
O exemplo a seguir mostra como agrupar elementos em partes que representam subsequências de chaves contíguas. Por exemplo, suponha que você receba a seguinte sequência de pares chave-valor:
Key | valor |
---|---|
A | Nós |
A | Pense |
A | Isso |
N | Linq |
C | é |
A | realmente |
N | Fresco |
N | ! |
Os seguintes grupos são criados nesta ordem:
- Nós, pensamos, que
- Linq
- é
- realmente
- Fresco!
A solução é implementada como um método de extensão thread-safe que retorna seus resultados de forma de streaming. Ele produz seus grupos à medida que se move através da sequência de origem. Ao contrário dos operadores ororderby
, ele pode começar a group
retornar grupos para o chamador antes de ler toda a sequência. O exemplo a seguir mostra o método de extensão e o código do cliente que o usa:
public static class ChunkExtensions
{
public static IEnumerable<IGrouping<TKey, TSource>> ChunkBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector) =>
source.ChunkBy(keySelector, EqualityComparer<TKey>.Default);
public static IEnumerable<IGrouping<TKey, TSource>> ChunkBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
IEqualityComparer<TKey> comparer)
{
// Flag to signal end of source sequence.
const bool noMoreSourceElements = true;
// Auto-generated iterator for the source array.
IEnumerator<TSource>? enumerator = source.GetEnumerator();
// Move to the first element in the source sequence.
if (!enumerator.MoveNext())
{
yield break; // source collection is empty
}
while (true)
{
var key = keySelector(enumerator.Current);
Chunk<TKey, TSource> current = new(key, enumerator, value => comparer.Equals(key, keySelector(value)));
yield return current;
if (current.CopyAllChunkElements() == noMoreSourceElements)
{
yield break;
}
}
}
}
public static class GroupByContiguousKeys
{
// The source sequence.
static readonly KeyValuePair<string, string>[] list = [
new("A", "We"),
new("A", "think"),
new("A", "that"),
new("B", "LINQ"),
new("C", "is"),
new("A", "really"),
new("B", "cool"),
new("B", "!")
];
// Query variable declared as class member to be available
// on different threads.
static readonly IEnumerable<IGrouping<string, KeyValuePair<string, string>>> query =
list.ChunkBy(p => p.Key);
public static void GroupByContiguousKeys1()
{
// ChunkBy returns IGrouping objects, therefore a nested
// foreach loop is required to access the elements in each "chunk".
foreach (var item in query)
{
Console.WriteLine($"Group key = {item.Key}");
foreach (var inner in item)
{
Console.WriteLine($"\t{inner.Value}");
}
}
}
}
ChunkExtensions
Classe
No código apresentado da implementação da classe, o while(true)
loop no método itera ChunkBy
através da ChunkExtensions
sequência de origem e cria uma cópia de cada Chunk. Em cada passagem, o iterador avança para o primeiro elemento do próximo "Chunk", representado por um Chunk
objeto, na sequência de origem. Esse loop corresponde ao loop foreach externo que executa a consulta. Nesse loop, o código executa as seguintes ações:
- Obtenha a chave para o Chunk atual e atribua-a à
key
variável. O iterador de origem consome a sequência de origem até encontrar um elemento com uma chave que não corresponde. - Crie um novo objeto Chunk (grupo) e armazene-o em
current
variável. Ele tem um GroupItem, uma cópia do elemento de origem atual. - Devolva esse Chunk. Um Chunk é um
IGrouping<TKey,TSource>
, que é o valor de retorno doChunkBy
método. O Chunk tem apenas o primeiro elemento em sua sequência de origem. Os elementos restantes são retornados somente quando o código do cliente é foreach sobre este bloco. ConsulteChunk.GetEnumerator
para mais informações. - Verifique se:
- O bloco tem uma cópia de todos os seus elementos de origem, ou
- O iterador chegou ao final da sequência de origem.
- Quando o chamador enumerou todos os itens de bloco, o
Chunk.GetEnumerator
método copiou todos os itens de bloco. Se oChunk.GetEnumerator
loop não enumerou todos os elementos no bloco, faça-o agora para evitar corromper o iterador para clientes que podem estar chamando-o em um thread separado.
Chunk
Classe
A Chunk
classe é um grupo contíguo de um ou mais elementos de origem que têm a mesma chave. Um Chunk tem uma chave e uma lista de objetos ChunkItem, que são cópias dos elementos na sequência de origem:
class Chunk<TKey, TSource> : IGrouping<TKey, TSource>
{
// INVARIANT: DoneCopyingChunk == true ||
// (predicate != null && predicate(enumerator.Current) && current.Value == enumerator.Current)
// A Chunk has a linked list of ChunkItems, which represent the elements in the current chunk. Each ChunkItem
// has a reference to the next ChunkItem in the list.
class ChunkItem
{
public ChunkItem(TSource value) => Value = value;
public readonly TSource Value;
public ChunkItem? Next;
}
public TKey Key { get; }
// Stores a reference to the enumerator for the source sequence
private IEnumerator<TSource> enumerator;
// A reference to the predicate that is used to compare keys.
private Func<TSource, bool> predicate;
// Stores the contents of the first source element that
// belongs with this chunk.
private readonly ChunkItem head;
// End of the list. It is repositioned each time a new
// ChunkItem is added.
private ChunkItem? tail;
// Flag to indicate the source iterator has reached the end of the source sequence.
internal bool isLastSourceElement;
// Private object for thread synchronization
private readonly object m_Lock;
// REQUIRES: enumerator != null && predicate != null
public Chunk(TKey key, [DisallowNull] IEnumerator<TSource> enumerator, [DisallowNull] Func<TSource, bool> predicate)
{
Key = key;
this.enumerator = enumerator;
this.predicate = predicate;
// A Chunk always contains at least one element.
head = new ChunkItem(enumerator.Current);
// The end and beginning are the same until the list contains > 1 elements.
tail = head;
m_Lock = new object();
}
// Indicates that all chunk elements have been copied to the list of ChunkItems.
private bool DoneCopyingChunk => tail == null;
// Adds one ChunkItem to the current group
// REQUIRES: !DoneCopyingChunk && lock(this)
private void CopyNextChunkElement()
{
// Try to advance the iterator on the source sequence.
isLastSourceElement = !enumerator.MoveNext();
// If we are (a) at the end of the source, or (b) at the end of the current chunk
// then null out the enumerator and predicate for reuse with the next chunk.
if (isLastSourceElement || !predicate(enumerator.Current))
{
enumerator = default!;
predicate = default!;
}
else
{
tail!.Next = new ChunkItem(enumerator.Current);
}
// tail will be null if we are at the end of the chunk elements
// This check is made in DoneCopyingChunk.
tail = tail!.Next;
}
// Called after the end of the last chunk was reached.
internal bool CopyAllChunkElements()
{
while (true)
{
lock (m_Lock)
{
if (DoneCopyingChunk)
{
return isLastSourceElement;
}
else
{
CopyNextChunkElement();
}
}
}
}
// Stays just one step ahead of the client requests.
public IEnumerator<TSource> GetEnumerator()
{
// Specify the initial element to enumerate.
ChunkItem? current = head;
// There should always be at least one ChunkItem in a Chunk.
while (current != null)
{
// Yield the current item in the list.
yield return current.Value;
// Copy the next item from the source sequence,
// if we are at the end of our local list.
lock (m_Lock)
{
if (current == tail)
{
CopyNextChunkElement();
}
}
// Move to the next ChunkItem in the list.
current = current.Next;
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
}
Cada ChunkItem
um (representado por ChunkItem
classe) tem uma referência para o próximo ChunkItem
na lista. A lista consiste em seu head
- que armazena o conteúdo do primeiro elemento de origem que pertence a este bloco, e seu tail
- que é um fim da lista. A cauda é reposicionada cada vez que um novo ChunkItem
é adicionado. A cauda da lista vinculada é definida como null
no CopyNextChunkElement
método se a chave do próximo elemento não corresponder à chave do bloco atual ou se não houver mais elementos na fonte.
O CopyNextChunkElement
método da Chunk
classe adiciona um ChunkItem
ao grupo atual de itens. Ele tenta avançar o iterador na sequência de origem. Se o MoveNext()
método retornar false
a iteração está no final e isLastSourceElement
é definido como true
.
O CopyAllChunkElements
método é chamado depois que o final do último bloco foi alcançado. Ele verifica se há mais elementos na sequência de origem. Se houver, ele retornará true
se o enumerador para este bloco foi esgotado. Neste método, quando o campo privado DoneCopyingChunk
é verificado para true
, se isLastSourceElement é false
, ele sinaliza para o iterador externo para continuar a iteração.
O loop foreach interno invoca o GetEnumerator
Chunk
método da classe. Esse método permanece um elemento à frente das solicitações do cliente. Ele adiciona o próximo elemento do bloco somente depois que o cliente solicita o último elemento anterior na lista.