Delen via


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:

  1. Wij, denk, dat
  2. Linq
  3. is
  4. Echt
  5. 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:

  1. 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.
  2. Maak een nieuw segmentobject (groep) en sla het op in current een variabele. Het heeft één GroupItem, een kopie van het huidige bronelement.
  3. Retourneer dat segment. Een segment is een IGrouping<TKey,TSource>, wat de retourwaarde van de ChunkBy 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. Zie Chunk.GetEnumerator voor meer informatie.
  4. Controleer of:
    • Het segment heeft een kopie van alle bronelementen, of
    • De iterator heeft het einde van de bronreeks bereikt.
  5. Wanneer de aanroeper alle segmentitems heeft geïnventariseerd, heeft de Chunk.GetEnumerator methode alle segmentitems gekopieerd. Als de Chunk.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.