Bagikan melalui


Bekerja dengan Kueri Terintegrasi Bahasa (LINQ)

Pendahuluan

Tutorial ini mengajarkan Anda tentang fitur dalam .NET Core dan bahasa C#. Anda akan mempelajari cara:

  • Membuat urutan dengan LINQ.
  • Menulis metode yang dapat dengan mudah digunakan dalam kueri LINQ.
  • Membedakan antara evaluasi yang cepat dan lambat.

Anda akan mempelajari teknik ini dengan membuat aplikasi yang menunjukkan salah satu keterampilan dasar pesulap: faro shuffle. Secara singkat, faro shuffle adalah teknik ketika Anda membagi dek kartu tepat menjadi dua, kemudian menyisipkan masing-masing satu kartu pada pengocokan dek kartu yang dibagi menjadi dua untuk membangun kembali dek asli.

Pesulap menggunakan teknik ini karena setiap kartu berada di lokasi yang diketahui setelah setiap pengocokan, dan urutannya adalah pola yang berulang.

Untuk tujuan Anda, teknik ini adalah tampilan yang ringan dalam memanipulasi urutan data. Aplikasi yang akan Anda buat akan membuat dek kartu dan kemudian melakukan urutan pengocokan, menuliskan urutannya setiap kali dikocok. Anda juga akan membandingkan urutan baru dengan urutan awal.

Tutorial ini terdiri dari beberapa langkah. Setelah setiap langkah, Anda dapat menjalankan aplikasi dan melihat progresnya. Anda juga dapat melihat sampel lengkap di repositori dotnet/samples GitHub. Untuk petunjuk pengunduhan, lihat Sampel dan Tutorial.

Prasyarat

Anda harus menyiapkan komputer Anda untuk menjalankan core .NET. Anda dapat menemukan petunjuk penginstalan di halaman Unduhan .NET Core. Anda dapat menjalankan aplikasi ini di Windows, Ubuntu Linux, atau OS X, atau dalam kontainer Docker. Anda harus menginstal editor kode favorit Anda. Deskripsi di bawah ini menggunakan Visual Studio Code yang merupakan editor lintas platform sumber terbuka. Namun, Anda dapat menggunakan alat apa pun yang Anda sukai.

Membuat Aplikasi

Langkah pertama adalah membuat aplikasi baru. Buka perintah dan buat direktori baru untuk aplikasi Anda. Jadikan direktori baru tersebut direktori saat ini. Ketik perintah dotnet new console pada perintah. Perintah ini membuat file starter untuk aplikasi dasar "Halo Dunia".

Jika Anda belum pernah menggunakan C# sebelumnya, tutorial ini menjelaskan struktur program C#. Anda dapat membacanya lalu kembali ke sini untuk mempelajari selengkapnya tentang LINQ.

Membuat Himpunan Data

Sebelum memulai, pastikan baris berikut berada di bagian atas file Program.cs yang dihasilkan oleh dotnet new console:

// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;

Jika tiga baris ini (pernyataan using) tidak berada di bagian atas file, program kami tidak akan dikompilasi.

Setelah memiliki semua referensi yang Anda perlukan, pertimbangkan apa yang dimaksud dengan dek kartu. Umumnya, dek kartu remi terdiri dari empat setelan, dan setiap setelan memiliki tiga belas nilai. Biasanya, Anda mungkin mempertimbangkan untuk membuat kelas Card langsung dan mengisi koleksi objek Card secara manual. Dengan LINQ, Anda bisa menggunakan cara yang lebih sederhana dalam membuat dek kartu. Daripada membuat kelas Card, Anda dapat membuat dua urutan untuk masing-masing mewakili setelan dan peringkat. Anda akan membuat pasangan metode iterator yang sangat sederhana yang akan membuat peringkat dan setelan sebagai IEnumerable<T>string:

// Program.cs
// The Main() method

static IEnumerable<string> Suits()
{
    yield return "clubs";
    yield return "diamonds";
    yield return "hearts";
    yield return "spades";
}

static IEnumerable<string> Ranks()
{
    yield return "two";
    yield return "three";
    yield return "four";
    yield return "five";
    yield return "six";
    yield return "seven";
    yield return "eight";
    yield return "nine";
    yield return "ten";
    yield return "jack";
    yield return "queen";
    yield return "king";
    yield return "ace";
}

Tempatkan ini di bawah metode Main di file Program.cs Anda. Kedua metode ini menggunakan sintaks yield return untuk membuat urutan saat dijalankan. Pengompilasi membuat objek yang mengimplementasikan IEnumerable<T> dan membuat urutan string seperti yang diminta.

Sekarang, gunakan metode iterator ini untuk membuat dek kartu. Anda akan menempatkan kueri LINQ dalam metode Main kami. Berikut tampilannya:

// Program.cs
static void Main(string[] args)
{
    var startingDeck = from s in Suits()
                       from r in Ranks()
                       select new { Suit = s, Rank = r };

    // Display each card that we've generated and placed in startingDeck in the console
    foreach (var card in startingDeck)
    {
        Console.WriteLine(card);
    }
}

Beberapa klausa from menghasilkan SelectMany, yang membuat satu urutan dari menggabungkan setiap elemen di urutan pertama dengan setiap elemen di urutan kedua. Urutan tersebut penting untuk tujuan kita. Elemen pertama dalam urutan sumber pertama (Setelan) digabungkan dengan setiap elemen dalam urutan kedua (Peringkat). Hal ini menghasilkan semua tiga belas kartu dari setelan pertama. Proses tersebut diulangi dengan setiap elemen di urutan pertama (Setelan). Hasil akhirnya adalah dek kartu yang diurutkan berdasarkan setelan, diikuti dengan nilai.

Penting untuk diingat bahwa apakah Anda memilih untuk menulis LINQ dalam sintaks kueri yang digunakan di atas atau menggunakan sintaks metode sebagai gantinya, Anda dapat beralih dari satu bentuk sintaks ke bentuk lainnya kapan saja. Kueri di atas yang ditulis dalam sintaks kueri dapat ditulis dalam sintaks metode sebagai:

var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => new { Suit = suit, Rank = rank }));

Pengompilasi menerjemahkan pernyataan LINQ yang ditulis dengan sintaks kueri ke dalam sintaks panggilan metode yang setara. Oleh karena itu, terlepas dari pilihan sintaks Anda, dua versi kueri menghasilkan hasil yang sama. Pilih sintaks mana yang paling cocok untuk situasi Anda: misalnya, jika Anda bekerja dalam tim di mana beberapa anggota mengalami kesulitan dengan sintaks metode, cobalah untuk memilih menggunakan sintaks kueri.

Lanjutkan dan jalankan sampel yang Anda buat saat ini. Sintaks ini akan menampilkan semua 52 kartu di dek. Anda mungkin merasa sangat terbantu untuk menjalankan sampel ini di bawah debugger guna mengamati bagaimana metode Suits() dan Ranks() dijalankan. Anda dapat dengan jelas melihat bahwa setiap string di setiap urutan dibuat hanya jika diperlukan.

A console window showing the app writing out 52 cards.

Memanipulasi Urutan

Selanjutnya, fokuslah pada bagaimana Anda akan mengocok kartu di dek. Langkah pertama dalam mengocok kartu yang baik adalah membagi dek menjadi dua. Metode Take dan Skip yang merupakan bagian dari API LINQ menyediakan fitur tersebut untuk Anda. Tempatkan mereka di bawah perulangan foreach:

// Program.cs
public static void Main(string[] args)
{
    var startingDeck = from s in Suits()
                       from r in Ranks()
                       select new { Suit = s, Rank = r };

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    // 52 cards in a deck, so 52 / 2 = 26
    var top = startingDeck.Take(26);
    var bottom = startingDeck.Skip(26);
}

Namun, tidak ada metode pengocokan yang dapat dimanfaatkan di pustaka standar, jadi Anda harus menulis sendiri. Metode pengocokan yang akan Anda buat menggambarkan beberapa teknik yang akan digunakan dengan program berbasis LINQ, jadi setiap bagian dari proses ini akan dijelaskan dalam beberapa langkah.

Untuk menambahkan beberapa fungsi ke cara Anda berinteraksi dengan IEnumerable<T> yang akan Anda dapatkan kembali dari kueri LINQ, Anda harus menulis beberapa jenis metode khusus yang disebut metode ekstensi. Secara singkat, metode ekstensi adalah metode statis tujuan khusus yang menambahkan fungsionalitas baru ke jenis yang sudah ada tanpa harus mengubah jenis asli yang ingin Anda tambahkan fungsionalitasnya.

Berikan metode ekstensi Anda rumah baru dengan menambahkan file kelas statis baru ke program Anda yang bernama Extensions.cs, lalu mulai buat metode ekstensi pertama:

// Extensions.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqFaroShuffle
{
    public static class Extensions
    {
        public static IEnumerable<T> InterleaveSequenceWith<T>(this IEnumerable<T> first, IEnumerable<T> second)
        {
            // Your implementation will go here soon enough
        }
    }
}

Lihatlah tanda tangan metode sejenak, khususnya parameternya:

public static IEnumerable<T> InterleaveSequenceWith<T> (this IEnumerable<T> first, IEnumerable<T> second)

Anda dapat melihat penambahan pengubah this pada argumen pertama ke metode. Itu berarti Anda memanggil metode seolah-olah itu adalah metode anggota dari jenis argumen pertama. Deklarasi metode ini juga mengikuti idiom standar di mana jenis input dan outputnya adalah IEnumerable<T>. Praktik tersebut memungkinkan metode LINQ dirantai bersama untuk menjalankan kueri yang lebih kompleks.

Secara alami, karena Anda membagi dek menjadi dua, Anda harus menggabungkan bagian-bagian tersebut bersama-sama. Dalam kode, ini berarti Anda akan menghitung kedua urutan yang diperoleh melalui Take dan Skip sekaligus, interleaving elemen, dan membuat satu urutan: dek kartu Anda yang sekarang diacak. Menulis metode LINQ yang bekerja dengan dua urutan mengharuskan Anda memahami cara kerja IEnumerable<T>.

Antarmuka IEnumerable<T> memiliki satu metode: GetEnumerator. Objek yang dikembalikan oleh GetEnumerator memiliki metode untuk berpindah ke elemen berikutnya, dan properti yang mengambil elemen saat ini dalam urutan. Anda akan menggunakan dua anggota tersebut untuk menghitung koleksi dan mengembalikan elemen. Metode Penyisipan ini akan menjadi metode iterator, jadi sebagai ganti membangun koleksi dan mengembalikan koleksi, Anda akan menggunakan sintaks yield return yang ditunjukkan di atas.

Berikut implementasi dari metode tersebut:

public static IEnumerable<T> InterleaveSequenceWith<T>
    (this IEnumerable<T> first, IEnumerable<T> second)
{
    var firstIter = first.GetEnumerator();
    var secondIter = second.GetEnumerator();

    while (firstIter.MoveNext() && secondIter.MoveNext())
    {
        yield return firstIter.Current;
        yield return secondIter.Current;
    }
}

Setelah Anda menulis metode ini, kembali ke metode Main dan kocok dek sekali:

// Program.cs
public static void Main(string[] args)
{
    var startingDeck = from s in Suits()
                       from r in Ranks()
                       select new { Suit = s, Rank = r };

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    var top = startingDeck.Take(26);
    var bottom = startingDeck.Skip(26);
    var shuffle = top.InterleaveSequenceWith(bottom);

    foreach (var c in shuffle)
    {
        Console.WriteLine(c);
    }
}

Perbandingan

Berapa banyak pengocokan yang diperlukan untuk mengembalikan dek ke urutan semula? Untuk mengetahuinya, Anda harus menulis metode yang menentukan apakah kedua urutan sama. Setelah menyelesaikan metode tersebut, Anda harus menempatkan kode yang mengocok dek dalam satu putaran, dan memeriksa untuk melihat kapan dek kembali berurutan.

Menulis sebuah metode untuk menentukan apakah kedua urutan tersebut sama harus dilakukan secara langsung. Ini adalah struktur yang mirip dengan metode yang Anda tulis untuk mengocok dek. Hanya kali ini, sebagai ganti yield returning setiap elemen, Anda akan membandingkan elemen yang cocok dari setiap urutan. Ketika seluruh urutan telah disebutkan, jika setiap elemen cocok, urutannya akan sama:

public static bool SequenceEquals<T>
    (this IEnumerable<T> first, IEnumerable<T> second)
{
    var firstIter = first.GetEnumerator();
    var secondIter = second.GetEnumerator();

    while ((firstIter?.MoveNext() == true) && secondIter.MoveNext())
    {
        if ((firstIter.Current is not null) && !firstIter.Current.Equals(secondIter.Current))
        {
            return false;
        }
    }

    return true;
}

Hal ini menunjukkan idiom LINQ kedua: metode terminal. Metode ini mengambil urutan sebagai input (atau dalam hal ini, dua urutan), dan mengembalikan nilai skalar tunggal. Saat menggunakan metode terminal, metode tersebut selalu merupakan metode terakhir dalam rantai metode untuk kueri LINQ, oleh karena itu dinamakan "terminal".

Anda dapat melihat bagaimana metode ini berjalan saat menggunakannya untuk menentukan kapan dek kembali ke urutan awalnya. Letakkan kode acak di dalam satu lingkaran, dan hentikan saat urutan kembali ke urutan semula dengan menerapkan metode SequenceEquals(). Anda dapat melihatnya akan selalu menjadi metode terakhir dalam kueri apa pun, karena ini mengembalikan satu nilai bukan urutan:

// Program.cs
static void Main(string[] args)
{
    // Query for building the deck

    // Shuffling using InterleaveSequenceWith<T>();

    var times = 0;
    // We can re-use the shuffle variable from earlier, or you can make a new one
    shuffle = startingDeck;
    do
    {
        shuffle = shuffle.Take(26).InterleaveSequenceWith(shuffle.Skip(26));

        foreach (var card in shuffle)
        {
            Console.WriteLine(card);
        }
        Console.WriteLine();
        times++;

    } while (!startingDeck.SequenceEquals(shuffle));

    Console.WriteLine(times);
}

Jalankan kode yang Anda dapatkan sejauh ini dan perhatikan bagaimana dek diatur ulang pada setiap pengocokan. Setelah 8 pengocokan (perulangan dari perulangan do-while), dek kembali ke konfigurasi awal saat Anda pertama kali membuatnya dari kueri LINQ awal.

Optimasi

Sampel yang Anda buat sejauh ini menjalankan out shuffle, di mana kartu atas dan bawah tetap sama pada setiap putaran. Mari kita buat satu perubahan: kita akan menggunakan in shuffle sebagai gantinya, di mana semua 52 kartu berubah posisi. Untuk in shuffle, Anda menyisipkan dek sehingga kartu pertama di bagian bawah menjadi kartu pertama di dek. Itu berarti kartu terakhir di paruh atas menjadi kartu bawah. Ini adalah perubahan sederhana pada baris kode tunggal. Perbarui kueri acak saat ini dengan mengganti posisi Take dan Skip. Ini akan mengubah urutan bagian atas dan bawah dek:

shuffle = shuffle.Skip(26).InterleaveSequenceWith(shuffle.Take(26));

Jalankan program lagi, dan Anda akan melihat bahwa dibutuhkan 52 perulangan agar dek menyusun ulang dirinya sendiri. Anda juga akan mulai melihat beberapa penurunan kinerja yang serius saat program terus berjalan.

Ada beberapa alasan untuk hal ini. Anda dapat mengatasi salah satu penyebab utama penurunan perulangan ini: penggunaan evaluasi lambat yang tidak efisien.

Secara singkat, evaluasi lambat menyatakan bahwa evaluasi suatu pernyataan tidak dilakukan sampai nilainya dibutuhkan. Kueri LINQ adalah pernyataan yang dievaluasi dengan lambat. Urutan dibuat hanya sebagai elemen yang diminta. Biasanya, itulah manfaat utama dari LINQ. Namun, dalam penggunaan seperti program ini, hal ini menyebabkan pertumbuhan eksponensial dalam waktu eksekusi.

Ingatlah bahwa kami membuat dek asli menggunakan kueri LINQ. Setiap pengacakan dibuat dengan melakukan tiga kueri LINQ di dek sebelumnya. Semua ini dilakukan dengan lambat. Itu juga berarti pengacakan dilakukan lagi setiap kali urutan diminta. Pada saat Anda mencapai perulangan ke-52, Anda meregenerasi dek awal berkali-kali. Mari kita menulis log untuk mendemonstrasikan perilaku ini. Kemudian, Anda akan memperbaikinya.

Dalam file Extensions.cs Anda, ketik atau salin metode di bawah ini. Metode ekstensi ini membuat file baru bernama debug.log dalam direktori proyek Anda dan mencatat kueri apa yang sedang dijalankan ke file log. Metode ekstensi ini dapat ditambahkan ke kueri apa pun untuk menandai bahwa kueri tersebut dieksekusi.

public static IEnumerable<T> LogQuery<T>
    (this IEnumerable<T> sequence, string tag)
{
    // File.AppendText creates a new file if the file doesn't exist.
    using (var writer = File.AppendText("debug.log"))
    {
        writer.WriteLine($"Executing Query {tag}");
    }

    return sequence;
}

Anda akan melihat garis berlekuk-lekuk berwarna merah di bawah File, yang menandakan tidak ada. Hal ini tidak akan dikompilasi, karena pengompilasi tidak tahu apa itu File. Untuk mengatasi masalah ini, pastikan untuk menambahkan baris kode berikut di bawah baris pertama di Extensions.cs:

using System.IO;

Ini akan menyelesaikan masalah dan kesalahan merah hilang.

Selanjutnya, lengkapi definisi setiap kueri dengan pesan log:

// Program.cs
public static void Main(string[] args)
{
    var startingDeck = (from s in Suits().LogQuery("Suit Generation")
                        from r in Ranks().LogQuery("Rank Generation")
                        select new { Suit = s, Rank = r }).LogQuery("Starting Deck");

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    Console.WriteLine();
    var times = 0;
    var shuffle = startingDeck;

    do
    {
        // Out shuffle
        /*
        shuffle = shuffle.Take(26)
            .LogQuery("Top Half")
            .InterleaveSequenceWith(shuffle.Skip(26)
            .LogQuery("Bottom Half"))
            .LogQuery("Shuffle");
        */

        // In shuffle
        shuffle = shuffle.Skip(26).LogQuery("Bottom Half")
                .InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
                .LogQuery("Shuffle");

        foreach (var c in shuffle)
        {
            Console.WriteLine(c);
        }

        times++;
        Console.WriteLine(times);
    } while (!startingDeck.SequenceEquals(shuffle));

    Console.WriteLine(times);
}

Perhatikan bahwa Anda tidak mencatat setiap kali mengakses kueri. Anda hanya mencatat saat membuat kueri asli. Program ini masih membutuhkan waktu lama untuk dijalankan, tetapi sekarang Anda dapat melihat alasannya. Jika Anda sudah tidak sabar menjalankan pengacakan dengan pengelogan aktif, beralihlah kembali ke out shuffle. Anda masih akan melihat efek evaluasi lambat. Dalam sekali proses, ia mengeksekusi 2592 kueri, termasuk semua nilai dan pembuatan setelan.

Anda dapat meningkatkan perulangan kode di sini untuk mengurangi jumlah eksekusi yang dibuat. Perbaikan sederhana yang dapat Anda lakukan adalah men-cache hasil kueri LINQ asli yang menyusun dek kartu. Saat ini, Anda menjalankan kueri lagi dan lagi setiap kali perulangan do-while melalui perulangan, membangun kembali dek kartu dan mengubahnya setiap saat. Untuk menyimpan cache dek kartu, Anda dapat memanfaatkan metode LINQ ToArray dan ToList; ketika menambahkannya ke kueri, metode tersebut akan melakukan tindakan yang sama dengan yang Anda katakan kepada mereka, tetapi sekarang metode akan menyimpan hasilnya dalam array atau daftar, tergantung pada metode mana yang Anda pilih untuk dipanggil. Tambahkan metode LINQ ToArray ke kedua kueri dan jalankan program lagi:

public static void Main(string[] args)
{
    IEnumerable<Suit>? suits = Suits();
    IEnumerable<Rank>? ranks = Ranks();

    if ((suits is null) || (ranks is null))
        return;

    var startingDeck = (from s in suits.LogQuery("Suit Generation")
                        from r in ranks.LogQuery("Value Generation")
                        select new { Suit = s, Rank = r })
                        .LogQuery("Starting Deck")
                        .ToArray();

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    Console.WriteLine();

    var times = 0;
    var shuffle = startingDeck;

    do
    {
        /*
        shuffle = shuffle.Take(26)
            .LogQuery("Top Half")
            .InterleaveSequenceWith(shuffle.Skip(26).LogQuery("Bottom Half"))
            .LogQuery("Shuffle")
            .ToArray();
        */

        shuffle = shuffle.Skip(26)
            .LogQuery("Bottom Half")
            .InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
            .LogQuery("Shuffle")
            .ToArray();

        foreach (var c in shuffle)
        {
            Console.WriteLine(c);
        }

        times++;
        Console.WriteLine(times);
    } while (!startingDeck.SequenceEquals(shuffle));

    Console.WriteLine(times);
}

Sekarang out shuffle turun menjadi 30 kueri. Jalankan lagi dengan in shuffle dan Anda akan melihat peningkatan serupa: sekarang menjalankan 162 kueri.

Harap dicatat bahwa contoh ini dirancang untuk menyoroti kasus penggunaan di mana evaluasi lambat dapat menyebabkan kesulitan performa. Meskipun penting untuk melihat di mana evaluasi lambat dapat memengaruhi performa kode, sama pentingnya untuk memahami bahwa tidak semua kueri harus berjalan dengan cepat. Performa yang Anda timbulkan tanpa penggunaan ToArray adalah karena setiap pengaturan baru dek kartu dibangun dari pengaturan sebelumnya. Menggunakan evaluasi lambat berarti setiap konfigurasi dek baru dibangun dari dek awal, bahkan mengeksekusi kode yang membangun startingDeck. Inilah menyebabkan sejumlah besar pekerjaan tambahan.

Dalam praktiknya, beberapa algoritma berjalan dengan baik menggunakan evaluasi cepat, dan yang lain berjalan dengan baik menggunakan evaluasi lambat. Untuk penggunaan sehari-hari, evaluasi lambat biasanya merupakan pilihan yang lebih baik ketika sumber data adalah proses yang terpisah, seperti mesin database. Untuk database, evaluasi lambat memungkinkan kueri yang lebih kompleks mengeksekusi hanya satu perjalanan bolak-balik ke proses database dan kembali ke sisa kode Anda. LINQ bersifat fleksibel apakah Anda memilih untuk menggunakan evaluasi lambat atau cepat, jadi ukur proses dan pilih jenis evaluasi mana yang memberi Anda perulangan terbaik.

Kesimpulan

Dalam proyek ini, Anda akan mempelajari:

  • menggunakan kueri LINQ untuk menggabungkan data menjadi urutan yang bermakna
  • menulis Metode ekstensi untuk menambahkan fungsionalitas kustom kami sendiri ke kueri LINQ
  • menemukan area dalam kode kami di mana kueri LINQ kami mungkin mengalami masalah perulangan seperti kecepatan yang menurun
  • evaluasi lambat dan cepat sehubungan dengan kueri LINQ dan implikasinya terhadap perulangan kueri

Selain LINQ, Anda belajar sedikit tentang teknik yang digunakan pesulap untuk trik kartu. Pesulap menggunakan Faro shuffle karena mereka dapat mengontrol di mana setiap kartu bergerak di dek. Setelah mengetahui trik ini, jangan beri tahu orang lain!

Untuk informasi selengkapnya tentang LINQ, lihat: