Bagikan melalui


Cara memperluas LINQ

Semua metode berbasis LINQ mengikuti salah satu dari dua pola serupa. Mereka mengambil urutan yang dapat dijumlahkan. Mereka mengembalikan urutan yang berbeda, atau satu nilai. Konsistensi bentuk memungkinkan Anda memperluas LINQ dengan menulis metode dengan bentuk yang sama. Bahkan, pustaka .NET telah mendapatkan metode baru dalam banyak rilis .NET sejak LINQ pertama kali diperkenalkan. Dalam artikel ini, Anda melihat contoh perluasan LINQ dengan menulis metode Anda sendiri yang mengikuti pola yang sama.

Menambahkan metode kustom untuk kueri LINQ

Anda memperluas set metode yang Anda gunakan untuk kueri LINQ dengan menambahkan metode ekstensi ke antarmuka IEnumerable<T>. Misalnya, selain operasi rata-rata standar atau maksimum, Anda membuat metode agregat kustom untuk mengomputasi satu nilai dari urutan nilai. Anda juga membuat metode yang berfungsi sebagai filter kustom atau transformasi data tertentu untuk urutan nilai dan menampilkan urutan baru. Contoh metode tersebut adalah Distinct, Skip, dan Reverse.

Ketika memperluas antarmuka IEnumerable<T>, Anda dapat menerapkan metode kustom ke koleksi apa pun yang dapat dijumlahkan. Untuk informasi selengkapnya, lihat Metode Ekstensi.

Metode agregat menghitung satu nilai dari sekumpulan nilai. LINQ menyediakan beberapa metode agregat, termasuk Average, Min, dan Max. Anda dapat membuat metode agregat sendiri dengan menambahkan metode ekstensi ke antarmuka IEnumerable<T>.

Contoh kode berikut menunjukkan cara membuat metode ekstensi yang disebut Median untuk mengomputasi median untuk urutan jumlah jenis 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];
        }
    }
}

Anda memanggil metode ekstensi ini untuk koleksi apa pun yang dapat dijumlahkan dengan cara yang sama seperti Anda memanggil metode agregat lain dari antarmuka IEnumerable<T>.

Contoh kode berikut menunjukkan cara menggunakan metode Median untuk array jenis 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

Anda dapat membebani metode agregat Anda sehingga menerima urutan berbagai jenis. Pendekatan standarnya adalah untuk membuat kelebihan beban bagi setiap jenis. Pendekatan lain adalah membuat kelebihan beban yang mengambil jenis generik dan mengonversinya ke jenis tertentu dengan menggunakan delegasi. Anda juga dapat menggabungkan kedua pendekatan tersebut.

Anda dapat membuat kelebihan beban tertentu untuk setiap jenis yang ingin Anda dukung. Contoh kode berikut menunjukkan kelebihan beban metode Median untuk jenis int.

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

Sekarang Anda dapat memanggil kelebihan beban Median untuk jenis integer dan double, seperti yang ditunjukkan dalam kode berikut:

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

Anda juga dapat membuat kelebihan beban yang menerima urutan objek generik. Kelebihan beban ini mengambil delegasi sebagai parameter dan menggunakannya untuk mengonversi urutan objek dari jenis generik menjadi jenis tertentu.

Kode berikut menunjukkan beban lebih metode Median yang mengambil delegasi Func<T,TResult> sebagai parameter. Delegasi ini mengambil objek jenis generik T dan mengembalikan objek jenis double.

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

Anda sekarang dapat memanggil metode Median untuk urutan objek dari jenis apa pun. Jika jenis tidak memiliki beban lebih metodenya sendiri, Anda harus meneruskan parameter delegasi. Di C#, Anda dapat menggunakan ekspresi lambda untuk tujuan ini. Selain itu, khusus di Visual Basic, jika Anda menggunakan klausul Aggregate atau Group By, alih-alih panggilan metode, Anda dapat meneruskan nilai atau ekspresi yang ada dalam cakupan klausul ini.

Contoh kode berikut menunjukkan cara memanggil metode Median untuk array integer dan array string. Untuk string, median panjang string dalam array akan dihitung. Contoh ini menunjukkan cara meneruskan parameter delegasi Func<T,TResult> ke metode Median untuk setiap kasus.

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

Anda dapat memperluas IEnumerable<T> antarmuka dengan metode kueri kustom yang mengembalikan urutan nilai. Dalam hal ini, metode harus menampilkan koleksi jenis IEnumerable<T>. Metode tersebut dapat digunakan untuk menerapkan filter atau transformasi data ke urutan nilai.

Contoh berikut menunjukkan cara membuat metode ekstensi bernama AlternateElements yang menampilkan setiap elemen lain dalam koleksi, mulai dari elemen pertama.

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

Anda dapat memanggil metode ekstensi ini untuk koleksi apa pun yang dapat dijumlahkan sama seperti saat Anda memanggil metode lain dari antarmuka IEnumerable<T>, sebagaimana yang ditunjukkan dalam kode berikut:

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

Mengelompokkan hasil berdasarkan kunci yang berdampingan

Contoh berikut menunjukkan cara mengelompokkan elemen ke dalam potongan yang mewakili suburutan kunci yang berdampingan. Misalnya, asumsikan bahwa Anda diberi urutan pasangan kunci-nilai berikut:

Tombol Nilai
A Kami
A berpikir
A bahwa
B Linq
C is
A sangat
B keren
B !

Grup berikut dibuat dalam urutan ini:

  1. Kami, berpikir, bahwa
  2. Linq
  3. is
  4. sangat
  5. keren, !

Solusi ini diimplementasikan sebagai metode ekstensi aman utas yang mengembalikan hasilnya dengan cara streaming. Ini menghasilkan grupnya saat bergerak melalui urutan sumber. group Tidak seperti operator atau orderby , operator dapat mulai mengembalikan grup ke pemanggil sebelum membaca seluruh urutan. Contoh berikut menunjukkan metode ekstensi sekaligus kode klien yang menggunakannya:

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 kelas

Dalam kode ChunkExtensions implementasi kelas yang disajikan, while(true) perulangan dalam ChunkBy metode berulang melalui urutan sumber dan membuat salinan setiap Gugus. Pada setiap pass, iterator maju ke elemen pertama dari "Chunk" berikutnya, yang diwakili oleh Chunk objek, dalam urutan sumber. Perulangan ini sesuai dengan perulangan foreach luar yang menjalankan kueri. Dalam perulangan itu, kode melakukan tindakan berikut:

  1. Dapatkan kunci untuk Chunk saat ini dan tetapkan ke key variabel. Iterator sumber mengonsumsi urutan sumber sampai menemukan elemen dengan kunci yang tidak cocok.
  2. Buat objek Chunk (grup) baru, dan simpan dalam current variabel. Ini memiliki satu GroupItem, salinan elemen sumber saat ini.
  3. Kembalikan Chunk itu. Potongan adalah IGrouping<TKey,TSource>, yang merupakan nilai pengembalian dari ChunkBy metode . Chunk hanya memiliki elemen pertama dalam urutan sumbernya. Elemen yang tersisa hanya dikembalikan ketika kode klien foreach di atas gugus ini. Lihat Chunk.GetEnumerator untuk informasi selengkapnya.
  4. Periksa untuk melihat apakah:
    • Gugus memiliki salinan semua elemen sumbernya, atau
    • Iterator mencapai akhir urutan sumber.
  5. Ketika penelepon telah menghitung semua item gugus, Chunk.GetEnumerator metode telah menyalin semua item gugus. Jika perulangan Chunk.GetEnumerator tidak menghitung semua elemen dalam gugus, lakukan sekarang untuk menghindari kerusakan iterator untuk klien yang mungkin memanggilnya pada utas terpisah.

Chunk kelas

Kelas Chunk adalah grup yang berdekatan dari satu atau beberapa elemen sumber yang memiliki kunci yang sama. Chunk memiliki kunci dan daftar objek ChunkItem, yang merupakan salinan elemen dalam urutan sumber:

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

Masing-masing ChunkItem (diwakili oleh ChunkItem kelas) memiliki referensi ke berikutnya ChunkItem dalam daftar. Daftar ini terdiri dari head - yang menyimpan konten elemen sumber pertama yang termasuk dalam gugus ini, dan tail - yang merupakan akhir dari daftar. Ekor diposisikan ulang setiap kali baru ChunkItem ditambahkan. Ekor daftar tertaut diatur ke null dalam CopyNextChunkElement metode jika kunci elemen berikutnya tidak cocok dengan kunci gugus saat ini, atau tidak ada lagi elemen dalam sumbernya.

Metode CopyNextChunkElementChunk kelas menambahkan satu ChunkItem ke grup item saat ini. Ini mencoba untuk memajukan iterator pada urutan sumber. MoveNext() Jika metode mengembalikan false iterasi ada di akhir, dan isLastSourceElement diatur ke true.

Metode CopyAllChunkElements ini dipanggil setelah akhir gugus terakhir tercapai. Ini memeriksa apakah ada lebih banyak elemen dalam urutan sumber. Jika ada, itu mengembalikan true jika enumerator untuk gugus ini habis. Dalam metode ini, ketika bidang privat DoneCopyingChunk diperiksa untuk true, jika isLastSourceElement adalah false, ia memberi sinyal ke iterator luar untuk terus melakukan iterasi.

Perulangan foreach dalam memanggil GetEnumerator metode Chunk kelas. Metode ini tetap satu elemen di depan permintaan klien. Ini menambahkan elemen berikutnya dari gugus hanya setelah klien meminta elemen terakhir sebelumnya dalam daftar.