Sdílet prostřednictvím


Rozšíření LINQ

Všechny metody založené na LINQ se řídí jedním ze dvou podobných vzorů. Převezmou výčet sekvenci. Vrátí buď jinou sekvenci, nebo jednu hodnotu. Konzistence obrazce umožňuje rozšířit LINQ zápisem metod s podobným obrazcem. Knihovny .NET ve skutečnosti získaly nové metody v mnoha verzích .NET od prvního zavedení LINQ. V tomto článku vidíte příklady rozšíření LINQ napsáním vlastních metod, které se řídí stejným vzorem.

Přidání vlastních metod pro dotazy LINQ

Sadu metod, které používáte pro dotazy LINQ, rozšíříte přidáním rozšiřujících metod do IEnumerable<T> rozhraní. Kromě standardních průměrných nebo maximálních operací například vytvoříte vlastní agregační metodu pro výpočet jedné hodnoty z posloupnosti hodnot. Vytvoříte také metodu, která funguje jako vlastní filtr nebo konkrétní transformace dat pro sekvenci hodnot a vrací novou sekvenci. Příklady takových metod jsou Distinct, Skipa Reverse.

Když rozhraní rozšíříte IEnumerable<T> , můžete vlastní metody použít na libovolnou výčtové kolekce. Další informace naleznete v tématu Metody rozšíření.

Agregační metoda vypočítá jednu hodnotu ze sady hodnot. LINQ poskytuje několik agregačních metod, včetně Average, Mina Max. Vlastní agregační metodu můžete vytvořit přidáním rozšiřující metody do IEnumerable<T> rozhraní.

Následující příklad kódu ukazuje, jak vytvořit rozšiřující metodu volanou Median pro výpočet mediánu pro sekvenci čísel 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];
        }
    }
}

Tuto metodu rozšíření voláte pro všechny výčtové kolekce stejným způsobem, jakým voláte jiné agregační metody z IEnumerable<T> rozhraní.

Následující příklad kódu ukazuje, jak použít metodu Median pro pole 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

Agregační metodu můžete přetížit tak, aby přijímala sekvence různých typů. Standardním přístupem je vytvoření přetížení pro každý typ. Dalším přístupem je vytvoření přetížení, které přebírá obecný typ a převede ho na konkrétní typ pomocí delegáta. Můžete také kombinovat oba přístupy.

Pro každý typ, který chcete podporovat, můžete vytvořit konkrétní přetížení. Následující příklad kódu ukazuje přetížení Median metody pro int typ.

// int overload
public static double Median(this IEnumerable<int> source) =>
    (from number in source select (double)number).Median();

Nyní můžete volat Median přetížení pro oba integer typy double , jak je znázorněno v následujícím kódu:

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

Můžete také vytvořit přetížení, které přijímá obecnou posloupnost objektů. Toto přetížení přebírá delegáta jako parametr a používá ho k převodu posloupnosti objektů obecného typu na určitý typ.

Následující kód ukazuje přetížení Median metody, která přebírá delegáta Func<T,TResult> jako parametr. Tento delegát přebírá objekt obecného typu T a vrací objekt 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();

Nyní můžete volat metodu Median pro posloupnost objektů libovolného typu. Pokud typ nemá vlastní přetížení metody, musíte předat parametr delegáta. V jazyce C# můžete pro tento účel použít výraz lambda. V jazyce Visual Basic můžete také pouze v případě, že místo volání metody použijete Aggregate klauzuli nebo Group By klauzuli, můžete předat libovolnou hodnotu nebo výraz, který je v oboru této klauzule.

Následující příklad kódu ukazuje, jak volat metodu Median pro pole celých čísel a pole řetězců. U řetězců se vypočítá medián délky řetězců v poli. Příklad ukazuje, jak předat Func<T,TResult> parametr delegáta Median metodě pro každý případ.

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

Rozhraní můžete rozšířit IEnumerable<T> vlastní metodou dotazu, která vrací posloupnost hodnot. V tomto případě musí metoda vrátit kolekci typu IEnumerable<T>. Tyto metody lze použít k použití filtrů nebo transformací dat na sekvenci hodnot.

Následující příklad ukazuje, jak vytvořit rozšiřující metodu s názvem AlternateElements , která vrací všechny ostatní prvky v kolekci počínaje prvním prvkem.

// 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++;
    }
}

Tuto metodu rozšíření můžete volat pro libovolnou výčet kolekcí stejně, jako byste volali jiné metody z IEnumerable<T> rozhraní, jak je znázorněno v následujícím kódu:

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

Seskupení výsledků podle sousedních klíčů

Následující příklad ukazuje, jak seskupit prvky do bloků, které představují dílčí posloupnosti souvislých klíčů. Předpokládejme například, že máte následující posloupnost párů klíč-hodnota:

Key Hodnota
A Pří
A Myslet
A Že
T Linq
C is
A Vážně
T Cool
T !

V tomto pořadí se vytvoří následující skupiny:

  1. Myslíme si, že
  2. Linq
  3. is
  4. Vážně
  5. Cool!

Řešení se implementuje jako metoda rozšíření bezpečné pro přístup z více vláken, která vrací výsledky způsobem streamování. Vytvoří své skupiny, protože prochází zdrojovou sekvencí. group Na rozdíl od operátorů nebo orderby operátorů může volajícímu začít vracet skupiny před čtením celé sekvence. Následující příklad ukazuje metodu rozšíření i klientský kód, který ho používá:

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}");
            }
        }
    }
}

Třída ChunkExtensions

V zobrazeném ChunkExtensions kódu implementace třídy smyčka while(true) v ChunkBy metodě iteruje prostřednictvím zdrojové sekvence a vytvoří kopii každého bloku dat. Při každém průchodu iterátor přejde na první prvek dalšího bloku dat reprezentovaný objektem Chunk ve zdrojové sekvenci. Tato smyčka odpovídá vnější smyčce foreach, která spouští dotaz. V této smyčce kód provede následující akce:

  1. Získejte klíč pro aktuální blok dat a přiřaďte ho proměnné key . Zdrojový iterátor využívá zdrojovou sekvenci, dokud nenajde prvek s klíčem, který neodpovídá.
  2. Vytvořte nový objekt bloku (group) a uložte ho do current proměnné. Má jednu GroupItem, kopii aktuálního zdrojového prvku.
  3. Vraťte tento blok dat. Blok dat je , IGrouping<TKey,TSource>což je návratová hodnota ChunkBy metody. Blok dat obsahuje pouze první prvek ve zdrojové sekvenci. Zbývající prvky jsou vráceny pouze v případě, že kód klienta foreach je přes tento blok dat. Další informace najdete Chunk.GetEnumerator .
  4. Zkontrolujte, jestli:
    • Blok dat obsahuje kopii všech jeho zdrojových prvků, nebo
    • Iterátor dosáhl konce zdrojové sekvence.
  5. Když volající vyčíslil všechny položky bloku, Chunk.GetEnumerator metoda zkopírovala všechny položky bloku. Pokud smyčka Chunk.GetEnumerator nevypíše všechny prvky v bloku dat, vyhnete se poškození iterátoru pro klienty, kteří ho můžou volat v samostatném vlákně.

Třída Chunk

Třída Chunk je souvislá skupina jednoho nebo více zdrojových prvků, které mají stejný klíč. Blok dat má klíč a seznam objektů ChunkItem, které jsou kopiemi prvků ve zdrojové sekvenci:

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ždý ChunkItem (reprezentovaný ChunkItem třídou) má odkaz na další ChunkItem v seznamu. Seznam se skládá z jeho head - který ukládá obsah prvního zdrojového prvku, který patří s tímto blokem dat, a jeho tail - což je konec seznamu. Ocas se přeloží pokaždé, když se přidá nový ChunkItem . Konec propojeného seznamu je nastaven v nullCopyNextChunkElement metodě, pokud klíč dalšího prvku neodpovídá klíč aktuálního bloku dat nebo neexistují žádné další prvky ve zdroji.

Metoda CopyNextChunkElementChunk třídy přidá jednu ChunkItem do aktuální skupiny položek. Pokusí se o přechod iterátoru ve zdrojové sekvenci. Pokud metoda MoveNext() vrátí false iteraci je na konci a isLastSourceElement je nastavena na true.

Metoda CopyAllChunkElements se volá po dosažení konce posledního bloku dat. Kontroluje, jestli ve zdrojové sekvenci existují další prvky. Pokud ano, vrátí true se, pokud byl enumerátor pro tento blok vyčerpán. V této metodě, když je privátní DoneCopyingChunk pole zaškrtnuté true, pokud isLastSourceElement je false, signalizuje vnější iterátor pokračovat iterace.

Vnitřní smyčka foreach vyvolá GetEnumerator metodu Chunk třídy. Tato metoda zůstává jedním prvkem před požadavky klienta. Přidá další prvek bloku dat až poté, co klient požádá o předchozí poslední prvek v seznamu.