LINQ uitbreiden
Alle op LINQ gebaseerde methoden volgen een van twee vergelijkbare patronen. Ze nemen een opsommingsvolgorde. Ze retourneren een andere reeks of één waarde. Met de consistentie van de shape kunt u LINQ uitbreiden door methoden te schrijven met een vergelijkbare vorm. In feite hebben de .NET-bibliotheken nieuwe methoden opgedaan in veel .NET-releases sinds LINQ voor het eerst werd geïntroduceerd. In dit artikel ziet u voorbeelden van het uitbreiden van LINQ door uw eigen methoden te schrijven die hetzelfde patroon volgen.
Aangepaste methoden toevoegen voor LINQ-query's
U kunt de set methoden die u gebruikt voor LINQ-query's uitbreiden door extensiemethoden toe te voegen aan de IEnumerable<T> interface. Naast de standaardgemiddelde of maximumbewerkingen maakt u bijvoorbeeld een aangepaste statistische methode om één waarde te berekenen op basis van een reeks waarden. U maakt ook een methode die werkt als een aangepast filter of een specifieke gegevenstransformatie voor een reeks waarden en retourneert een nieuwe reeks. Voorbeelden van dergelijke methoden zijn Distinct, Skipen Reverse.
Wanneer u de IEnumerable<T> interface uitbreidt, kunt u uw aangepaste methoden toepassen op elke enumerable verzameling. Zie Extensiemethoden voor meer informatie.
Met een statistische methode wordt één waarde berekend op basis van een set waarden. LINQ biedt verschillende statistische methoden, waaronder Average, Minen Max. U kunt uw eigen statistische methode maken door een extensiemethode toe te voegen aan de IEnumerable<T> interface.
In het volgende codevoorbeeld ziet u hoe u een extensiemethode maakt die wordt aangeroepen Median
om een mediaan te berekenen voor een reeks getallen van het type 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];
}
}
}
U roept deze extensiemethode aan voor elke inventariserbare verzameling op dezelfde manier als u andere statistische methoden aanroept vanuit de IEnumerable<T> interface.
In het volgende codevoorbeeld ziet u hoe u de Median
methode gebruikt voor een matrix van het type 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
U kunt uw statistische methode overbelasten, zodat deze reeksen van verschillende typen accepteert. De standaardmethode is het creëren van een overbelasting voor elk type. Een andere benadering is het maken van een overbelasting die een algemeen type gebruikt en converteert naar een specifiek type met behulp van een gemachtigde. U kunt beide benaderingen ook combineren.
U kunt een specifieke overbelasting maken voor elk type dat u wilt ondersteunen. In het volgende codevoorbeeld ziet u een overbelasting van de Median
methode voor het int
type.
// int overload
public static double Median(this IEnumerable<int> source) =>
(from number in source select (double)number).Median();
U kunt nu de Median
overbelastingen voor beide integer
typen double
aanroepen, zoals wordt weergegeven in de volgende code:
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
U kunt ook een overbelasting maken die een algemene reeks objecten accepteert. Deze overbelasting neemt een gemachtigde als parameter en gebruikt deze om een reeks objecten van een algemeen type te converteren naar een specifiek type.
De volgende code toont een overbelasting van de Median
methode die de Func<T,TResult> gemachtigde als parameter gebruikt. Deze gemachtigde gebruikt een object van het algemene type T en retourneert een object van het type double
.
// generic overload
public static double Median<T>(
this IEnumerable<T> numbers, Func<T, double> selector) =>
(from num in numbers select selector(num)).Median();
U kunt nu de Median
methode aanroepen voor een reeks objecten van elk type. Als het type geen eigen overbelasting van de methode heeft, moet u een gemachtigde parameter doorgeven. In C# kunt u hiervoor een lambda-expressie gebruiken. Als u in Visual Basic alleen de Aggregate
of Group By
component gebruikt in plaats van de methode-aanroep, kunt u elke waarde of expressie doorgeven die binnen het bereik van deze component valt.
In de volgende voorbeeldcode ziet u hoe u de Median
methode aanroept voor een matrix met gehele getallen en een matrix met tekenreeksen. Voor tekenreeksen wordt de mediaan voor de lengte van tekenreeksen in de matrix berekend. In het voorbeeld ziet u hoe u de Func<T,TResult> parameter voor gedelegeerden doorgeeft aan de Median
methode voor elke case.
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
U kunt de IEnumerable<T> interface uitbreiden met een aangepaste querymethode die een reeks waarden retourneert. In dit geval moet de methode een verzameling van het type IEnumerable<T>retourneren. Dergelijke methoden kunnen worden gebruikt om filters of gegevenstransformaties toe te passen op een reeks waarden.
In het volgende voorbeeld ziet u hoe u een extensiemethode maakt met de naam AlternateElements
die elk ander element in een verzameling retourneert, te beginnen met het eerste element.
// 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++;
}
}
U kunt deze extensiemethode aanroepen voor elke enumerable verzameling, net zoals u andere methoden aanroept vanuit de IEnumerable<T> interface, zoals wordt weergegeven in de volgende code:
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
Resultaten groeperen op aaneengesloten sleutels
In het volgende voorbeeld ziet u hoe u elementen groepeert in segmenten die subsequences van aaneengesloten sleutels vertegenwoordigen. Stel dat u de volgende reeks sleutel-waardeparen krijgt:
Sleutel | Weergegeven als |
---|---|
A | We |
A | Denk dat |
A | die |
B | Linq |
E | is |
A | Echt |
B | Cool |
B | ! |
De volgende groepen worden in deze volgorde gemaakt:
- Wij, denk, dat
- Linq
- is
- Echt
- Cool!
De oplossing wordt geïmplementeerd als een thread-safe-extensiemethode die de resultaten ervan op een streaming-manier retourneert. Het produceert de groepen terwijl deze door de bronvolgorde wordt verplaatst. In tegenstelling tot de group
operators orderby
kan het beginnen met het retourneren van groepen aan de beller voordat de hele reeks wordt gelezen. In het volgende voorbeeld ziet u zowel de extensiemethode als de clientcode die deze gebruikt:
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
Klasse
In de gepresenteerde code van de ChunkExtensions
klasse-implementatie doorloopt de while(true)
lus in de ChunkBy
methode de bronreeks en maakt een kopie van elk segment. Bij elke doorgang gaat de iterator naar het eerste element van het volgende segment, vertegenwoordigd door een Chunk
object, in de bronreeks. Deze lus komt overeen met de buitenste foreach-lus waarmee de query wordt uitgevoerd. In die lus voert de code de volgende acties uit:
- Haal de sleutel voor het huidige segment op en wijs deze toe aan
key
een variabele. De bron-iterator verbruikt de bronvolgorde totdat er een element wordt gevonden met een sleutel die niet overeenkomt. - Maak een nieuw segmentobject (groep) en sla het op in
current
een variabele. Het heeft één GroupItem, een kopie van het huidige bronelement. - Retourneer dat segment. Een segment is een
IGrouping<TKey,TSource>
, wat de retourwaarde van deChunkBy
methode is. Het segment heeft alleen het eerste element in de bronreeks. De resterende elementen worden alleen geretourneerd wanneer de clientcode foreach boven dit segment valt. ZieChunk.GetEnumerator
voor meer informatie. - Controleer of:
- Het segment heeft een kopie van alle bronelementen, of
- De iterator heeft het einde van de bronreeks bereikt.
- Wanneer de aanroeper alle segmentitems heeft geïnventariseerd, heeft de
Chunk.GetEnumerator
methode alle segmentitems gekopieerd. Als deChunk.GetEnumerator
lus niet alle elementen in het segment heeft opgesomd, moet u dit nu doen om te voorkomen dat de iterator wordt beschadigd voor clients die deze mogelijk op een afzonderlijke thread aanroepen.
Chunk
Klasse
De Chunk
klasse is een aaneengesloten groep van een of meer bronelementen met dezelfde sleutel. Een segment heeft een sleutel en een lijst met chunkItem-objecten, die kopieën zijn van de elementen in de bronvolgorde:
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();
}
Elke ChunkItem
(vertegenwoordigd door ChunkItem
klasse) heeft een verwijzing naar de volgende ChunkItem
in de lijst. De lijst bestaat uit head
de inhoud van het eerste bronelement dat deel uitmaakt van dit segment en tail
de bijbehorende inhoud. Dit is een einde aan de lijst. De staart wordt telkens opnieuw geplaatst wanneer er een nieuwe ChunkItem
wordt toegevoegd. De staart van de gekoppelde lijst is ingesteld null
op in de CopyNextChunkElement
methode als de sleutel van het volgende element niet overeenkomt met de sleutel van het huidige segment of als er geen elementen meer in de bron zijn.
Met CopyNextChunkElement
de methode van de Chunk
klasse wordt er een ChunkItem
toegevoegd aan de huidige groep items. Er wordt geprobeerd om de iterator op de bronvolgorde te bevorderen. Als de MoveNext()
methode de iteratie aan het einde retourneert false
en isLastSourceElement
is ingesteld op true
.
De CopyAllChunkElements
methode wordt aangeroepen nadat het einde van het laatste segment is bereikt. Er wordt gecontroleerd of er meer elementen in de bronreeks staan. Als dat zo is, wordt geretourneerd true
als de opsomming voor dit segment is uitgeput. In deze methode, wanneer het privéveld DoneCopyingChunk
wordt gecontroleerd op true
, als isLastSourceElement is false
, geeft het aan de buitenste iterator om door te gaan met herhalen.
De binnenste foreach-lus roept de GetEnumerator
methode van de Chunk
klasse aan. Deze methode blijft één element voor op de clientaanvragen. Het volgende element van het segment wordt pas toegevoegd nadat de client het vorige laatste element in de lijst heeft aangevraagd.