Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Pendahuluan
Tutorial ini mengajarkan Anda fitur dalam bahasa .NET dan C#. Anda akan mempelajari cara untuk:
- Hasilkan urutan dengan LINQ.
- Tulis metode yang dapat Anda gunakan dengan mudah dalam kueri LINQ.
- Bedakan antara evaluasi yang bersemangat dan malas.
Anda mempelajari teknik ini dengan membangun aplikasi yang menunjukkan salah satu keterampilan dasar pesulap mana pun: pengacakan faro. Pengacakan faro adalah teknik di mana Anda membagi tumpukan kartu tepat menjadi dua, kemudian mengocok setiap kartu dari masing-masing setengahnya secara bergantian untuk membentuk kembali tumpukan asli.
Pesulap menggunakan teknik ini karena setiap kartu berada di lokasi yang diketahui setelah setiap pengacakan, dan urutannya adalah pola berulang.
Tutorial ini menawarkan tampilan ringan dalam memanipulasi urutan data. Aplikasi ini membangun dek kartu, melakukan urutan pengacakan, dan menulis urutannya setiap kali. Ini juga membandingkan urutan yang diperbarui dengan urutan asli.
Tutorial ini memiliki beberapa langkah. Setelah setiap langkah, Anda dapat menjalankan aplikasi dan melihat kemajuannya. Anda juga dapat melihat sampel yang telah selesai di repositori GitHub dotnet/samples. Untuk petunjuk pengunduhan, lihat sampel dan Tutorial.
Prasyarat
Buat aplikasi
Buat aplikasi baru. Buka prompt perintah dan buat direktori baru untuk aplikasi Anda. Jadikan itu direktori saat ini. Ketik perintah dotnet new console -o LinqFaroShuffle pada prompt perintah. Perintah ini membuat file awal untuk aplikasi sederhana "Hello World".
Jika Anda belum pernah menggunakan C# sebelumnya, tutorial ini menjelaskan struktur program C#. Anda dapat membacanya lalu kembali ke sini untuk mempelajari lebih lanjut tentang LINQ.
Membuat himpunan data
Petunjuk / Saran
Untuk tutorial ini, Anda dapat mengatur kode Anda di namespace yang dipanggil LinqFaroShuffle untuk mencocokkan kode sampel, atau Anda dapat menggunakan namespace global default. Jika Anda memilih untuk menggunakan namespace, pastikan semua kelas dan metode Anda secara konsisten berada dalam namespace yang sama, atau tambahkan pernyataan yang sesuai using seperlunya.
Pertimbangkan apa yang dimaksud dengan setumpuk kartu. Dek kartu bermain memiliki empat jenis kartu, dan masing-masing setelan memiliki 13 nilai. Biasanya, Anda mungkin mempertimbangkan untuk segera membuat kelas Card dan mengisi kumpulan objek Card secara manual. Dengan LINQ, Anda bisa lebih ringkas daripada cara biasa untuk membuat dek kartu. Alih-alih membuat Card kelas, buat dua urutan untuk mewakili setelan dan peringkat. Buat sepasang metode iterator yang menghasilkan peringkat dan jenis sebagai IEnumerable<T> dari string:
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 metode ini di bawah pernyataan Console.WriteLine dalam file Anda Program.cs. Kedua metode ini sama-sama menggunakan yield return sintaks untuk menghasilkan urutan saat dijalankan. Kompilator membangun objek yang mengimplementasikan IEnumerable<T> dan menghasilkan urutan string saat diminta.
Sekarang, gunakan metode iterator ini untuk membuat dek kartu. Tempatkan kueri LINQ di bagian Program.cs atas file. Berikut tampilannya:
var startingDeck = from s in Suits()
from r in Ranks()
select (Suit: s, Rank: r);
// Display each card that's generated and placed in startingDeck
foreach (var card in startingDeck)
{
Console.WriteLine(card);
}
Beberapa from klausa menghasilkan SelectMany, yang membuat urutan tunggal dengan menggabungkan setiap elemen dalam urutan pertama dengan setiap elemen dalam urutan kedua. Urutannya penting untuk contoh ini. Unsur pertama dalam urutan sumber pertama (Suits) digabungkan dengan setiap unsur dalam urutan kedua (Ranks). Proses ini menghasilkan semua 13 kartu dari setelan pertama. Proses tersebut diulang dengan setiap elemen dalam urutan pertama (Sesuai). Hasil akhirnya adalah dek kartu yang diurutkan berdasarkan jenis kartu, diikuti dengan nilai.
Perlu diingat bahwa apakah Anda menulis LINQ Anda dalam sintaks kueri yang digunakan dalam sampel sebelumnya atau menggunakan sintaks metode sebagai gantinya, selalu dimungkinkan untuk pergi dari satu bentuk sintaks ke yang lain. Kueri sebelumnya yang ditulis dalam sintaks kueri dapat ditulis dalam sintaks metode sebagai:
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => (Suit: suit, Rank: rank )));
Pengkompilasi 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 yang paling sesuai untuk situasi Anda. Misalnya, jika Anda bekerja dalam tim di mana beberapa anggota mengalami kesulitan dengan sintaks metode, cobalah untuk lebih memilih menggunakan sintaks kueri.
Jalankan sampel yang Anda buat pada saat ini. Ini menampilkan semua 52 kartu di dek. Anda mungkin akan merasa terbantu menjalankan sampel ini dalam debugger untuk mengamati bagaimana metode Suits() dan Ranks() dieksekusi. Anda dapat dengan jelas melihat bahwa setiap string dalam setiap urutan dihasilkan hanya sesuai kebutuhan.
Memanipulasi pesanan
Selanjutnya, fokus pada bagaimana Anda mengocok kartu di tumpukan. Langkah pertama dalam acak yang baik adalah membagi tumpukan kartu menjadi dua.
Take dan Skip metode yang merupakan bagian dari API LINQ menyediakan fitur tersebut. Tempatkan mereka mengikuti perulangan foreach :
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
Namun, tidak ada metode pengacakan yang dapat digunakan di pustaka standar, jadi Anda perlu menulisnya sendiri. Metode pengacakan yang Anda buat menggambarkan beberapa teknik yang digunakan dalam program berbasis LINQ, dan setiap bagian dari proses ini dijelaskan dalam langkah-langkah.
Untuk menambahkan fungsionalitas ke cara Anda berinteraksi dengan IEnumerable<T> hasil kueri LINQ, Anda menulis beberapa jenis metode khusus yang disebut metode ekstensi. 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.
Beri metode ekstensi Anda rumah baru dengan menambahkan file kelas statis baru ke program Anda yang disebut Extensions.cs, lalu mulai membangun metode ekstensi pertama:
public static class CardExtensions
{
extension<T>(IEnumerable<T> sequence)
{
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
// Your implementation goes here
return default;
}
}
}
Nota
Jika Anda menggunakan editor selain Visual Studio (seperti Visual Studio Code), Anda mungkin perlu menambahkan using LinqFaroShuffle; ke bagian atas file Program.cs agar metode ekstensi dapat diakses. Visual Studio secara otomatis menambahkan pernyataan penggunaan ini, tetapi editor lain mungkin tidak.
Kontainer extension menentukan jenis yang diperluas. Simpul extension mendeklarasikan jenis dan nama parameter penerima untuk semua anggota di dalam extension kontainer. Dalam contoh ini, Anda memperluas IEnumerable<T>, dan parameter diberi nama sequence.
Deklarasi anggota ekstensi muncul seolah-olah mereka adalah anggota jenis penerima:
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
Anda memanggil metode seolah-olah itu adalah metode anggota dari jenis yang diperluas. Deklarasi metode ini juga mengikuti idiom standar di mana jenis input dan output adalah IEnumerable<T>. Praktik itu memungkinkan metode LINQ dirantai bersama untuk melakukan kueri yang lebih kompleks.
Karena Anda membagi dek menjadi dua bagian, Anda perlu menggabungkan bagian-bagian tersebut. Dalam kode, ini berarti Anda menghitung kedua urutan yang Anda peroleh melalui Take dan Skip sekaligus, menjalin elemen, dan membuat satu urutan: dek kartu Anda yang sekarang diacak. Menulis metode LINQ yang berfungsi dengan dua urutan mengharuskan Anda memahami cara IEnumerable<T> kerjanya.
Antarmuka IEnumerable<T> memiliki satu metode: GetEnumerator. Objek yang dikembalikan dengan GetEnumerator memiliki metode untuk berpindah ke elemen berikutnya dan properti yang mengambil elemen saat ini dalam urutan. Anda menggunakan kedua anggota tersebut untuk menghitung koleksi dan mengembalikan elemen. Metode Interleave ini adalah metode pengulangan, jadi alih-alih membangun koleksi dan mengembalikan koleksi, Anda menggunakan sintaks yield return seperti yang ditunjukkan dalam kode sebelumnya.
Berikut adalah implementasi metode tersebut:
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
var firstIter = sequence.GetEnumerator();
var secondIter = second.GetEnumerator();
while (firstIter.MoveNext() && secondIter.MoveNext())
{
yield return firstIter.Current;
yield return secondIter.Current;
}
}
Sekarang setelah Anda menulis metode ini, kembali ke metode Main dan mengacak kartu-kartu satu kali.
var shuffledDeck = top.InterleaveSequenceWith(bottom);
foreach (var c in shuffledDeck)
{
Console.WriteLine(c);
}
Perbandingan
Tentukan berapa banyak pengacakan yang diperlukan untuk mengatur dek kembali ke urutan aslinya. Untuk mengetahuinya, tulis metode yang menentukan apakah dua urutan sama. Setelah Anda memiliki metode itu, tempatkan kode yang mengocok dek ke dalam perulangan, dan periksa apakah dek sudah kembali berurutan.
Menulis metode untuk menentukan apakah kedua urutan sama harus mudah. Ini adalah struktur yang mirip dengan metode yang Anda tulis untuk mengacak tumpukan kartu. Namun, kali ini, alih-alih menggunakan yield return untuk setiap elemen, Anda membandingkan elemen yang cocok dari setiap urutan. Ketika seluruh urutan dijumlahkan, jika setiap elemen cocok, urutannya sama:
public bool SequenceEquals(IEnumerable<T> second)
{
var firstIter = sequence.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;
}
Metode ini menunjukkan idiom LINQ kedua: metode terminal. Mereka mengambil urutan sebagai input (atau dalam hal ini, dua urutan) dan mengembalikan nilai skalar tunggal. Saat Anda menggunakan metode terminal, metode tersebut selalu merupakan metode akhir dalam rantai metode untuk kueri LINQ.
Anda dapat melihat tindakan ini saat menggunakannya untuk menentukan kapan dek kembali dalam urutan aslinya. Letakkan kode pengacakan di dalam perulangan, dan hentikan ketika urutan kembali ke urutan aslinya dengan menerapkan metode SequenceEquals(). Anda dapat melihatnya akan selalu menjadi metode akhir dalam kueri apa pun karena mengembalikan satu nilai alih-alih urutan:
var startingDeck = from s in Suits()
from r in Ranks()
select (Suit: s, Rank: r);
// Display each card generated and placed in startingDeck in the console
foreach (var card in startingDeck)
{
Console.WriteLine(card);
}
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
var shuffledDeck = top.InterleaveSequenceWith(bottom);
var times = 0;
// Re-use the shuffle variable from earlier, or you can make a new one
shuffledDeck = startingDeck;
do
{
shuffledDeck = shuffledDeck.Take(26).InterleaveSequenceWith(shuffledDeck.Skip(26));
foreach (var card in shuffledDeck)
{
Console.WriteLine(card);
}
Console.WriteLine();
times++;
} while (!startingDeck.SequenceEquals(shuffledDeck));
Console.WriteLine(times);
Jalankan kode yang telah Anda buat sejauh ini dan perhatikan bagaimana tumpukan kartu disusun ulang pada setiap pengacakan. Setelah 8 pengacakan (iterasi dalam perulangan do-while), susunan kartu kembali ke konfigurasi asli saat Anda pertama kali membuatnya dari kueri LINQ awal.
Pengoptimalan
Sampel yang Anda buat sejauh ini menjalankan out shuffle, di mana kartu atas dan bawah tetap sama setiap kali dijalankan. Mari kita ubah: gunakan in shuffle sebagai gantinya, di mana semua 52 kartu berubah posisi. Untuk metode in shuffle, Anda menggabungkan dek sehingga kartu pertama di paruh bawah menjadi kartu pertama di dek. Itu berarti kartu terakhir di bagian atas menjadi kartu bawah. Perubahan ini memerlukan satu baris kode. Perbarui kueri acak saat ini dengan menukar posisi Take dan Skip. Perubahan ini mengalihkan urutan bagian atas dan bawah dek:
shuffledDeck = shuffledDeck.Skip(26).InterleaveSequenceWith(shuffledDeck.Take(26));
Jalankan program lagi, dan Anda melihat bahwa dibutuhkan 52 iterasi agar dek menyusun ulang dirinya sendiri. Anda juga melihat beberapa penurunan performa serius seiring dengan berjalannya program.
Ada beberapa alasan penurunan performa ini. Anda dapat mengatasi salah satu penyebab utama: penggunaan lazy evaluation yang tidak efisien.
Evaluasi tunda menyatakan bahwa evaluasi ekspresi tidak dilakukan sampai nilainya diperlukan. Kueri LINQ adalah pernyataan yang dievaluasi dengan malas. Urutan dibuat hanya saat elemen diminta. Biasanya, itu adalah manfaat utama dari LINQ. Namun, dalam program seperti ini, penundaan evaluasi menyebabkan pertumbuhan eksponensial terhadap waktu eksekusi.
Ingatlah bahwa Anda membuat dek asli menggunakan kueri LINQ. Setiap pengacakan dihasilkan dengan melakukan tiga kueri LINQ pada dek sebelumnya. Semua kueri ini dilakukan dengan malas. Itu juga berarti itu dijalankan lagi setiap kali urutan diminta. Pada saat Anda mencapai iterasi ke-52, Anda telah meregenerasi dek asal berkali-kali. Tulis log untuk menunjukkan perilaku ini. Setelah mengumpulkan data, Anda dapat meningkatkan performa.
Dalam file Anda Extensions.cs , ketik atau salin metode dalam sampel kode berikut. Metode ekstensi ini membuat file baru yang disebut debug.log dalam direktori proyek Anda dan merekam kueri apa yang saat ini sedang dijalankan ke file log. Tambahkan metode ekstensi ini ke kueri apa pun untuk menandai bahwa kueri dijalankan.
public IEnumerable<T> LogQuery(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;
}
Selanjutnya, tambahkan pesan log ke dalam definisi setiap kueri:
var startingDeck = (from s in Suits().LogQuery("Suit Generation")
from r in Ranks().LogQuery("Rank Generation")
select (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 Anda mengakses permintaan. Anda hanya mencatat tindakan saat membuat pertanyaan asli. Program ini masih membutuhkan waktu lama untuk dijalankan, tetapi sekarang Anda dapat melihat alasannya. Jika Anda kehilangan kesabaran saat melakukan pengacakan masuk dengan pencatatan diaktifkan, beralihlah kembali ke pengacakan keluar. Anda masih merasakan dampak dari evaluasi yang ditangguhkan. Dalam satu proses, ia menjalankan 2.592 kueri, termasuk nilai dan pembuatan setelan.
Anda dapat meningkatkan performa kode untuk mengurangi jumlah eksekusi yang Anda buat. Perbaikan sederhana adalah menyimpan hasil kueri LINQ asli yang membangun dek kartu. Saat ini, Anda menjalankan kueri lagi dan lagi setiap kali perulangan do-while melewati iterasi, menyusun ulang dek kartu dan mengacaukannya setiap saat. Untuk menyimpan dek kartu, terapkan metode ToArray LINQ dan ToList. Saat Anda menambahkannya ke kueri, mereka melakukan tindakan yang sama dengan yang Anda katakan kepada mereka, tetapi sekarang mereka menyimpan hasilnya dalam array atau daftar, tergantung pada metode mana yang Anda pilih untuk memanggil. Tambahkan metode ToArray LINQ ke kueri dan jalankan program lagi:
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, pengacakan keluar menjadi 30 permintaan. Jalankan lagi dengan dalam mode acak dan Anda melihat peningkatan serupa: sekarang menjalankan 162 kueri.
Contoh ini dirancang untuk menyoroti kasus penggunaan di mana evaluasi malas dapat menyebabkan kesulitan performa. Meskipun penting untuk melihat di mana evaluasi malas dapat memengaruhi performa kode, sama pentingnya untuk memahami bahwa tidak semua kueri harus berjalan dengan bersemangat. Kerugian kinerja yang Anda alami tanpa menggunakan ToArray adalah karena setiap pengaturan baru dek kartu dibangun dari pengaturan sebelumnya. Menggunakan evaluasi tertunda berarti setiap konfigurasi dek baru dibangun dari dek asli, bahkan dengan mengeksekusi kode yang membangunnya startingDeck. Itu menyebabkan sejumlah besar pekerjaan ekstra.
Dalam praktiknya, beberapa algoritma berjalan dengan baik menggunakan evaluasi bersemangat, dan yang lain berjalan dengan baik menggunakan evaluasi malas. Untuk penggunaan sehari-hari, evaluasi tunda biasanya merupakan pilihan yang lebih baik ketika sumber data adalah proses terpisah, seperti mesin basis data. Untuk database, evaluasi malas memungkinkan kueri yang lebih kompleks hanya membutuhkan satu kali interaksi dengan proses database dan kembali ke kode lainnya. LINQ fleksibel dalam memilih antara evaluasi tunda atau evaluasi cepat, jadi ukur proses Anda dan pilih evaluasi mana pun yang memberi Anda performa terbaik.
Kesimpulan
Dalam proyek ini, Anda membahas:
- Menggunakan kueri LINQ untuk menggabungkan data ke dalam urutan yang bermakna.
- Menulis metode ekstensi untuk menambahkan fungsionalitas kustom ke kueri LINQ.
- Menemukan area dalam kode di mana kueri LINQ mungkin mengalami masalah performa seperti kecepatan yang terdegradasi.
- Evaluasi malas (lazy) dan eager (eager) dalam kueri LINQ serta implikasi yang mungkin ada terhadap performa kueri.
Selain LINQ, Anda belajar tentang teknik yang digunakan pesulap untuk trik kartu. Pesulap menggunakan faro shuffle karena mereka dapat mengontrol di mana setiap kartu dipindahkan di dek. Sekarang setelah kau tahu, jangan merusaknya untuk orang lain!
Untuk informasi selengkapnya tentang LINQ, lihat: