Dela via


Så här utökar du LINQ

Alla LINQ-baserade metoder följer ett av två liknande mönster. De tar en uppräkningsbar sekvens. De returnerar antingen en annan sekvens eller ett enda värde. Med formens konsekvens kan du utöka LINQ genom att skriva metoder med en liknande form. I själva verket har .NET-biblioteken fått nya metoder i många .NET-versioner sedan LINQ först introducerades. I den här artikeln visas exempel på hur du utökar LINQ genom att skriva egna metoder som följer samma mönster.

Lägga till anpassade metoder för LINQ-frågor

Du utökar den uppsättning metoder som du använder för LINQ-frågor genom att lägga till tilläggsmetoder i IEnumerable<T> gränssnittet. Utöver standardåtgärderna för genomsnitt eller maximalt skapar du till exempel en anpassad aggregeringsmetod för att beräkna ett enda värde från en sekvens med värden. Du skapar också en metod som fungerar som ett anpassat filter eller en specifik datatransformering för en sekvens med värden och returnerar en ny sekvens. Exempel på sådana metoder är Distinct, Skipoch Reverse.

När du utökar IEnumerable<T> gränssnittet kan du använda dina anpassade metoder för alla uppräkningsbara samlingar. Mer information finns i Tilläggsmetoder.

En aggregeringsmetod beräknar ett enda värde från en uppsättning värden. LINQ innehåller flera aggregeringsmetoder, inklusive Average, Minoch Max. Du kan skapa en egen aggregeringsmetod genom att lägga till en tilläggsmetod i IEnumerable<T> gränssnittet.

I följande kodexempel visas hur du skapar en tilläggsmetod med namnet Median för att beräkna en median för en sekvens med tal av typen 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];
        }
    }
}

Du anropar den här tilläggsmetoden för en uppräkningsbar samling på samma sätt som du anropar andra aggregeringsmetoder från IEnumerable<T> gränssnittet.

Följande kodexempel visar hur du använder Median metoden för en matris av typen 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

Du kan överbelasta din aggregeringsmetod så att den accepterar sekvenser av olika typer. Standardmetoden är att skapa en överlagring för varje typ. En annan metod är att skapa en överlagring som tar en allmän typ och konvertera den till en viss typ med hjälp av ett ombud. Du kan också kombinera båda metoderna.

Du kan skapa en specifik överlagring för varje typ som du vill stödja. I följande kodexempel visas en överlagring av Median metoden för int typen.

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

Nu kan du anropa överlagringarna Median för båda integer typerna, double som du ser i följande kod:

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

Du kan också skapa en överlagring som accepterar en allmän sekvens med objekt. Den här överlagringen tar ett ombud som en parameter och använder den för att konvertera en sekvens av objekt av allmän typ till en viss typ.

Följande kod visar en överlagring av metoden Median som tar ombudet Func<T,TResult> som en parameter. Det här ombudet tar ett objekt av allmän typ T och returnerar ett objekt av typen double.

// generic overload
public static double Median<T>(
    this IEnumerable<T> numbers, Func<T, double> selector) =>
    (from num in numbers select selector(num)).Median();

Nu kan du anropa Median metoden för en sekvens av objekt av vilken typ som helst. Om typen inte har en egen metodöverlagring måste du skicka en delegatparameter. I C# kan du använda ett lambda-uttryck för det här ändamålet. Om du använder Aggregate satsen eller Group By i stället för metodanropet i Visual Basic kan du också skicka valfritt värde eller uttryck som finns i omfånget för den här satsen.

Följande exempelkod visar hur du anropar Median metoden för en matris med heltal och en matris med strängar. För strängar beräknas medianen för längden på strängarna i matrisen. Exemplet visar hur du skickar Func<T,TResult> delegatparametern till Median metoden för varje ärende.

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

Du kan utöka IEnumerable<T> gränssnittet med en anpassad frågemetod som returnerar en sekvens med värden. I det här fallet måste metoden returnera en samling av typen IEnumerable<T>. Sådana metoder kan användas för att tillämpa filter eller datatransformering på en sekvens med värden.

I följande exempel visas hur du skapar en tilläggsmetod med namnet AlternateElements som returnerar alla andra element i en samling, med början från det första elementet.

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

Du kan anropa den här tilläggsmetoden för valfri uppräkningsbar samling precis som du anropar andra metoder från IEnumerable<T> gränssnittet, som du ser i följande kod:

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

Gruppera resultat efter sammanhängande nycklar

I följande exempel visas hur du grupperar element i segment som representerar underfrågor för sammanhängande nycklar. Anta till exempel att du får följande sekvens med nyckel/värde-par:

Tangent Värde
A We
A Tror
A Att
F Linq
C är
A Verkligen
F Cool
F !

Följande grupper skapas i den här ordningen:

  1. Vi tror att
  2. Linq
  3. är
  4. Verkligen
  5. Cool!

Lösningen implementeras som en trådsäker tilläggsmetod som returnerar resultatet på ett strömmande sätt. Den skapar sina grupper när den rör sig genom källsekvensen. Till skillnad från operatorerna group eller orderby kan den börja returnera grupper till anroparen innan hela sekvensen läss. I följande exempel visas både tilläggsmetoden och klientkoden som använder den:

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 Klass

I den presenterade koden för ChunkExtensions klassimplementeringen itererar loopen while(true)ChunkBy i metoden genom källsekvensen och skapar en kopia av varje segment. Vid varje pass går iteratorn vidare till det första elementet i nästa segment, som representeras av ett Chunk objekt, i källsekvensen. Den här loopen motsvarar den yttre foreach-loopen som kör frågan. I den loopen utför koden följande åtgärder:

  1. Hämta nyckeln för det aktuella segmentet och tilldela den till key variabeln. Käll iteratorn använder källsekvensen tills den hittar ett element med en nyckel som inte matchar.
  2. Skapa ett nytt segmentobjekt (grupp) och lagra det i current variabeln. Den har en GroupItem, en kopia av det aktuella källelementet.
  3. Returnera det segmentet. Ett segment är ett IGrouping<TKey,TSource>, vilket är metodens ChunkBy returvärde. Segment har bara det första elementet i sin källsekvens. De återstående elementen returneras endast när klientkoden förgrenas över det här segmentet. Mer Chunk.GetEnumerator information finns i.
  4. Kontrollera om:
    • Segmentet har en kopia av alla dess källelement, eller
    • Iteratorn nådde slutet av källsekvensen.
  5. När anroparen har räknat upp alla segmentobjekt Chunk.GetEnumerator har metoden kopierat alla segmentobjekt. Om loopen Chunk.GetEnumerator inte räkna upp alla element i segmentet gör du det nu för att undvika att skada iteratorn för klienter som kanske anropar den i en separat tråd.

Chunk Klass

Klassen Chunk är en sammanhängande grupp med ett eller flera källelement som har samma nyckel. Ett segment har en nyckel och en lista över ChunkItem-objekt, som är kopior av elementen i källsekvensen:

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

Varje ChunkItem (representeras av ChunkItem klass) har en referens till nästa ChunkItem i listan. Listan består av dess head - som lagrar innehållet i det första källelementet som tillhör det här segmentet och dess tail - vilket är slutet på listan. Svansen flyttas varje gång en ny ChunkItem läggs till. Den länkade listans svans anges till null i CopyNextChunkElement -metoden om nyckeln för nästa element inte matchar det aktuella segmentets nyckel, eller om det inte finns några fler element i källan.

Metoden CopyNextChunkElement för Chunk klassen lägger till en ChunkItem i den aktuella gruppen med objekt. Den försöker föra iteratorn framåt i källsekvensen. MoveNext() Om metoden returnerar false iterationen är i slutet och isLastSourceElement är inställd truepå .

Metoden CopyAllChunkElements anropas efter att slutet av det sista segmentet nåddes. Den kontrollerar om det finns fler element i källsekvensen. I så fall returnerar true den om uppräknaren för det här segmentet var uttömd. I den här metoden, när det privata DoneCopyingChunk fältet är markerat för true, om isLastSourceElement är false, signalerar det till den yttre iteratorn att fortsätta iterera.

Den inre foreach-loopen GetEnumerator anropar -metoden för Chunk klassen. Den här metoden ligger ett element före klientbegäranden. Nästa element i segmentet läggs till först efter att klienten har begärt det föregående sista elementet i listan.