Jak rozszerzyć LINQ
Wszystkie metody oparte na LINQ są zgodne z jednym z dwóch podobnych wzorców. Przyjmują sekwencję wyliczalną. Zwracają inną sekwencję lub pojedynczą wartość. Spójność kształtu umożliwia rozszerzenie LINQ przez pisanie metod o podobnym kształcie. W rzeczywistości biblioteki .NET zyskały nowe metody w wielu wersjach platformy .NET od czasu wprowadzenia LINQ. W tym artykule przedstawiono przykłady rozszerzania LINQ przez napisanie własnych metod, które są zgodne z tym samym wzorcem.
Dodawanie niestandardowych metod dla zapytań LINQ
Rozszerzysz zestaw metod używanych dla zapytań LINQ, dodając metody rozszerzenia do interfejsu IEnumerable<T> . Na przykład oprócz standardowej średniej lub maksymalnej operacji można utworzyć niestandardową metodę agregacji, aby obliczyć pojedynczą wartość z sekwencji wartości. Utworzysz również metodę, która działa jako filtr niestandardowy lub konkretna transformacja danych dla sekwencji wartości i zwraca nową sekwencję. Przykłady takich metod to Distinct, Skipi Reverse.
Po rozszerzeniu interfejsu IEnumerable<T> można zastosować metody niestandardowe do dowolnej kolekcji z możliwością wyliczania. Aby uzyskać więcej informacji, zobacz Metody rozszerzeń.
Metoda agregacji oblicza pojedynczą wartość z zestawu wartości. LINQ udostępnia kilka metod agregacji, w tym Average, Mini Max. Możesz utworzyć własną metodę agregacji, dodając metodę rozszerzenia do interfejsu IEnumerable<T> .
W poniższym przykładzie kodu pokazano, jak utworzyć metodę rozszerzenia o nazwie Median
w celu obliczenia mediany dla sekwencji liczb typu 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];
}
}
}
Ta metoda rozszerzenia jest wywoływana dla dowolnej kolekcji wyliczalnej w taki sam sposób, jak w przypadku wywoływania innych metod agregacji z interfejsu IEnumerable<T> .
Poniższy przykład kodu pokazuje, jak używać Median
metody dla tablicy typu 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
Możesz przeciążyć metodę agregacji, aby akceptowała sekwencje różnych typów. Standardowe podejście polega na utworzeniu przeciążenia dla każdego typu. Innym podejściem jest utworzenie przeciążenia, które przyjmuje typ ogólny i konwertowanie go na określony typ przy użyciu delegata. Można również połączyć oba podejścia.
Można utworzyć określone przeciążenie dla każdego typu, który chcesz obsługiwać. Poniższy przykład kodu przedstawia przeciążenie Median
metody dla int
typu.
// int overload
public static double Median(this IEnumerable<int> source) =>
(from number in source select (double)number).Median();
Teraz można wywołać Median
przeciążenia dla typów integer
i double
, jak pokazano w poniższym kodzie:
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
Można również utworzyć przeciążenie, które akceptuje ogólną sekwencję obiektów. To przeciążenie przyjmuje delegata jako parametr i używa go do konwertowania sekwencji obiektów typu ogólnego na określony typ.
Poniższy kod przedstawia przeciążenie Median
metody, która przyjmuje delegata Func<T,TResult> jako parametr. Ten delegat przyjmuje obiekt typu ogólnego T i zwraca obiekt typu double
.
// generic overload
public static double Median<T>(
this IEnumerable<T> numbers, Func<T, double> selector) =>
(from num in numbers select selector(num)).Median();
Teraz można wywołać metodę Median
dla sekwencji obiektów dowolnego typu. Jeśli typ nie ma własnego przeciążenia metody, musisz przekazać parametr delegata. W języku C#do tego celu można użyć wyrażenia lambda. Ponadto tylko w języku Visual Basic, jeśli używasz klauzuli Aggregate
lub Group By
zamiast wywołania metody, możesz przekazać dowolną wartość lub wyrażenie, które znajduje się w zakresie tej klauzuli.
Poniższy przykładowy kod pokazuje, jak wywołać metodę Median
dla tablicy liczb całkowitych i tablicy ciągów. W przypadku ciągów obliczana jest mediana długości ciągów w tablicy. W przykładzie pokazano, jak przekazać parametr delegata Func<T,TResult> do Median
metody dla każdego przypadku.
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
Interfejs można rozszerzyć IEnumerable<T> za pomocą niestandardowej metody zapytania, która zwraca sekwencję wartości. W takim przypadku metoda musi zwrócić kolekcję typu IEnumerable<T>. Takie metody mogą służyć do stosowania filtrów lub przekształceń danych do sekwencji wartości.
W poniższym przykładzie pokazano, jak utworzyć metodę rozszerzenia o nazwie AlternateElements
, która zwraca każdy inny element w kolekcji, zaczynając od pierwszego elementu.
// 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++;
}
}
Tę metodę rozszerzenia można wywołać dla dowolnej kolekcji wyliczalnej, tak jak wywołasz inne metody z interfejsu IEnumerable<T> , jak pokazano w poniższym kodzie:
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
Grupowanie wyników według ciągłych kluczy
W poniższym przykładzie pokazano, jak grupować elementy w fragmenty reprezentujące podsekwencje kluczy ciągłych. Załóżmy na przykład, że otrzymujesz następującą sekwencję par klucz-wartość:
Key | Wartość |
---|---|
A | Dobr |
A | Myśleć |
A | Że |
B | Linq |
C | is |
A | Naprawdę |
B | Cool |
B | ! |
W tej kolejności są tworzone następujące grupy:
- Uważamy, że
- Linq
- is
- Naprawdę
- Cool!
Rozwiązanie jest implementowane jako metoda rozszerzenia bezpiecznego wątkowo, która zwraca wyniki w sposób przesyłania strumieniowego. Tworzy swoje grupy podczas przechodzenia przez sekwencję źródłową. group
W przeciwieństwie do operatorów lub orderby
może rozpocząć zwracanie grup do elementu wywołującego przed odczytaniem całej sekwencji. W poniższym przykładzie przedstawiono zarówno metodę rozszerzenia, jak i kod klienta, który go używa:
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}");
}
}
}
}
Klasa ChunkExtensions
W przedstawionym kodzie ChunkExtensions
implementacji klasy pętla while(true)
w ChunkBy
metodzie iteruje przez sekwencję źródłową i tworzy kopię każdego fragmentu. W każdym przebiegu iterator przechodzi do pierwszego elementu następnego "fragmentu", reprezentowanego przez Chunk
obiekt w sekwencji źródłowej. Ta pętla odpowiada zewnętrznej pętli foreach, która wykonuje zapytanie. W tej pętli kod wykonuje następujące akcje:
- Pobierz klucz bieżącego fragmentu i przypisz go do
key
zmiennej. Iterator źródłowy zużywa sekwencję źródłową, dopóki nie znajdzie elementu z kluczem, który nie jest zgodny. - Utwórz nowy obiekt Fragment (grupa) i zapisz go w
current
zmiennej. Ma jeden element GroupItem, kopię bieżącego elementu źródłowego. - Zwróć ten fragment. Fragment jest fragmentem
IGrouping<TKey,TSource>
, który jest zwracaną wartościąChunkBy
metody . Fragment zawiera tylko pierwszy element w sekwencji źródłowej. Pozostałe elementy są zwracane tylko wtedy, gdy kod klienta jest zastępowany przez ten fragment. ZobaczChunk.GetEnumerator
, aby uzyskać więcej informacji. - Sprawdź, czy:
- Fragment zawiera kopię wszystkich elementów źródłowych lub
- Iterator osiągnął koniec sekwencji źródłowej.
- Gdy obiekt wywołujący wyliczył wszystkie elementy fragmentu,
Chunk.GetEnumerator
metoda skopiowała wszystkie elementy fragmentu. Jeśli pętlaChunk.GetEnumerator
nie wyliczyła wszystkich elementów we fragmentu, zrób to teraz, aby uniknąć uszkodzenia iteratora dla klientów, którzy mogą wywoływać go w osobnym wątku.
Klasa Chunk
Klasa Chunk
jest ciągłą grupą jednego lub większej liczby elementów źródłowych, które mają ten sam klucz. Fragment zawiera klucz i listę obiektów fragmentitem, które są kopiami elementów w sekwencji źródłowej:
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();
}
Każda ChunkItem
(reprezentowana przez ChunkItem
klasę) ma odwołanie do następnego ChunkItem
na liście. Lista składa się z jej head
— która przechowuje zawartość pierwszego elementu źródłowego, który należy do tego fragmentu, i jego tail
— który jest końcem listy. Ogon jest zmieniany za każdym razem, gdy zostanie dodany nowy ChunkItem
. Część listy połączonej jest ustawiona na null
wartość w CopyNextChunkElement
metodzie , jeśli klucz następnego elementu nie jest zgodny z kluczem bieżącego fragmentu lub nie ma więcej elementów w źródle.
Metoda CopyNextChunkElement
Chunk
klasy dodaje jeden ChunkItem
do bieżącej grupy elementów. Próbuje przejść iterator w sekwencji źródłowej. MoveNext()
Jeśli metoda zwraca false
iterację jest na końcu i isLastSourceElement
jest ustawiona na true
wartość .
Metoda CopyAllChunkElements
jest wywoływana po osiągnięciu końca ostatniego fragmentu. Sprawdza, czy w sekwencji źródłowej znajduje się więcej elementów. Jeśli istnieją, zwraca true
wartość , jeśli moduł wyliczający dla tego fragmentu został wyczerpany. W tej metodzie, gdy pole prywatne DoneCopyingChunk
jest sprawdzane pod kątem true
wartości , jeśli isLastSourceElement ma false
wartość , sygnalizuje to iteratorowi zewnętrznemu, aby kontynuować iterację.
Wewnętrzna pętla foreach wywołuje metodę GetEnumerator
Chunk
klasy . Ta metoda pozostaje jednym elementem przed żądaniami klienta. Dodaje następny element fragmentu dopiero po żądaniu przez klienta poprzedniego ostatniego elementu na liście.