Aracılığıyla paylaş


Language-Integrated Sorgusu (LINQ) ile çalışma

Giriş

Bu öğretici size .NET Core ve C# dilindeki özellikleri öğretir. Nasıl yapılacağını öğreneceksiniz:

  • LINQ ile diziler oluşturun.
  • LINQ sorgularında kolayca kullanılabilecek yazma yöntemleri.
  • İstekli ve gecikmeli değerlendirmeyi ayırt edin.

Herhangi bir sihirbazın temel becerilerinden birini gösteren bir uygulama oluşturarak bu teknikleri öğreneceksiniz: faro shuffle. Kısaca, faro karıştırma, bir kart destesini tam olarak ikiye böldüğünüz, ardından her iki yarıdan birer kart alarak özgün desteyi yeniden oluşturduğunuz bir tekniktir.

Sihirbazlar bu tekniği kullanır çünkü her kart her karıştırmadan sonra bilinen bir konumdadır ve sıra yinelenen bir düzendir.

Amaçlarınız doğrultusunda, veri dizilerini manipüle etmeye yönelik eğlenceli bir bakıştır. Derlediğiniz uygulama bir kart destesi oluşturur ve ardından her seferinde sırayı yazarak bir dizi karıştırma işlemi gerçekleştirir. Ayrıca güncelleştirilmiş siparişi özgün siparişle karşılaştıracaksınız.

Bu öğreticide birden fazla adım bulunuyor. Her adımdan sonra uygulamayı çalıştırabilir ve ilerleme durumunu görebilirsiniz. Tamamlanmış örneği dotnet/samples GitHub deposunda da görebilirsiniz. İndirme yönergeleri için bkz. Örnekler ve Öğreticiler.

Önkoşullar

Uygulamayı Oluşturma

İlk adım yeni bir uygulama oluşturmaktır. Bir komut istemi açın ve uygulamanız için yeni bir dizin oluşturun. Geçerli dizin olarak seçin. Komut istemine komut dotnet new console yazın. Bu, temel bir "Merhaba Dünya" uygulaması için başlangıç dosyalarını oluşturur.

Daha önce hiç C# kullanmadıysanız , bu öğreticide bir C# programının yapısı açıklanmaktadır. Bunu okuyup linq hakkında daha fazla bilgi edinmek için buraya dönebilirsiniz.

Veri Kümesi Oluşturma

Başlamadan önce, aşağıdaki satırların, dotnet new console tarafından oluşturulan dosyanın en üstünde olduğundan emin olun Program.cs.

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

Bu üç satır (using yönergeler) dosyanın en üstünde değilse, programınız derlenmeyebilir.

Artık ihtiyacınız olacak tüm referanslara sahip olduğunuz için, kart destesini neyin oluşturduğunu göz önünde bulundurun. Genellikle, bir oyun kartı destesinin dört kıyafeti vardır ve her takım elbisenin on üç değeri vardır. Normalde, hemen başında bir Card sınıf oluşturmayı ve bir Card nesne koleksiyonunu el ile doldurmayı düşünebilirsiniz. LINQ ile, kart destesi oluşturmanın alışılmış yönteminden daha öz olabilirsiniz. Sınıf oluşturmak Card yerine, sırasıyla renkler ve dereceleri temsil eden iki dizi oluşturabilirsiniz. Çok basit bir yineleyici yöntemler çifti oluşturacaksınız; bu yöntemler dizeler olarak IEnumerable<T>, dereceleri ve uygunları üretecek.

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

Bunları dosyanızdaki Program.cs yönteminin Main altına yerleştirin. Bu iki yöntem de yield return söz dizimini kullanarak çalıştırıldıklarında bir dizi oluşturur. Derleyici, IEnumerable<T> arayüzünü uygulayan ve istenen dize dizisini oluşturan bir nesne oluşturur.

Şimdi bu yineleyici yöntemleri kullanarak kart destesini oluşturun. LINQ sorgusunu Main yöntemimize yerleştireceksiniz. Buna şöyle bir göz atalım:

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

Birden çok from yan tümcesi, ilk sıradaki her öğeyi ikinci sıradaki her öğeyle birleştirerek tek bir dizi oluşturan bir SelectManyoluşturur. Sipariş, amaçlarımız açısından önemlidir. İlk kaynak dizisindeki ilk öğe (Suit) ikinci sıradaki (Dereceler) her öğeyle birleştirilir. Bu, ilk takım elbisenin on üç kartının tamamını üretir. Bu işlem, ilk dizideki her bir öğeyle (Suits) yinelenir. Sonuç, renklere göre ve ardından değerlere göre sıralanmış bir kart destesidir.

LINQ'inizi yukarıda kullanılan sorgu söz dizimine yazmayı veya bunun yerine yöntem söz dizimini kullanmayı tercih ettiğinizi unutmayın; bir söz diziminden diğerine geçmek her zaman mümkündür. Sorgu söz diziminde yazılan yukarıdaki sorgu yöntem söz diziminde şöyle yazılabilir:

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

Derleyici, sorgu söz dizimi ile yazılmış LINQ deyimlerini eşdeğer yöntem çağrısı söz dizimine çevirir. Bu nedenle, söz dizimi seçiminizden bağımsız olarak, sorgunun iki sürümü aynı sonucu üretir. Durumunuz için en uygun söz dizimini seçin: Örneğin, bazı üyelerin yöntem söz dizimi konusunda zorluk yaşadığı bir ekipte çalışıyorsanız, sorgu söz dizimini kullanmayı tercih etmeyi deneyin.

Devam edin ve bu noktada oluşturduğunuz örneği çalıştırın. Destedeki 52 kartın tümünü görüntüler. Bu örneği bir hata ayıklayıcısı altında çalıştırarak Suits() ve Ranks() yöntemlerinin nasıl çalıştığını gözlemlemenin çok yararlı olabileceğini görebilirsiniz. Her dizideki her dizenin yalnızca gerektiği gibi oluşturulduğunu açıkça görebilirsiniz.

Uygulamanın 52 kartı yazdığını gösteren bir konsol penceresi.

Sırayı Değiştirme

Ardından, destedeki kartları nasıl karıştıracağınız üzerinde odaklanın. İyi bir karıştırmada ilk adım, desteyi ikiye bölmektir. LINQ API'lerinin Take parçası olan ve Skip yöntemleri bu özelliği sizin için sağlar. Bunları döngünün foreach altına yerleştirin:

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

Ancak standart kütüphanede yararlanabileceğiniz bir karıştırma yöntemi yoktur, bu nedenle kendi yönteminiz yazmanız gerekir. Oluşturacağınız karıştırma yöntemi LINQ tabanlı programlarla kullanacağınız çeşitli teknikleri gösterir, bu nedenle bu işlemin her bölümü adımlarda açıklanacaktır.

LINQ sorgularından geri alacağınız IEnumerable<T> ile nasıl etkileşim kuracağınıza ilişkin bazı işlevsellik eklemek için uzantı yöntemleri olarak adlandırılan bazı özel yöntemler yazmanız gerekir. Kısaca, uzantı yöntemi, işlevsellik eklemek istediğiniz özgün türü değiştirmek zorunda kalmadan zaten var olan bir türe yeni işlevler ekleyen özel amaçlı bir statik yöntemdir .

Programınıza Extensions.cs adlı yeni bir statik sınıf dosyası ekleyerek uzantı yöntemlerinize yeni bir yuva bulun ve ardından ilk uzantı yöntemini geliştirmeye başlayın.

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

Bir an için yöntem imzasını, özellikle de parametrelerine bakın:

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

Yöntemin ilk bağımsız değişkenine this değiştiricisinin eklenmesini görebilirsiniz. Bu, yöntemi, ilk argümanın türünün bir üye yöntemiymiş gibi çağırdığınız anlamına gelir. Bu yöntem bildirimi, giriş ve çıkış türlerinin IEnumerable<T>olduğu standart bir deyimi de izler. Bu uygulama, LINQ yöntemlerinin daha karmaşık sorgular gerçekleştirmek için birbirine zincirle bağlanmasını sağlar.

Doğal olarak, desteyi yarıya böldüğünüz için bu yarıları birleştirmeniz gerekir. Kodda bu, hem aracılığıyla hem de aynı anda edindiğiniz TakeSkip dizileri, interleaving öğeleri numaralandırıp tek bir sıra oluşturabileceğiniz anlamına gelir: artık karıştırılan kart desteniz. İki diziyle çalışan bir LINQ yöntemi yazmak için nasıl IEnumerable<T> çalıştığını anlamanız gerekir.

Arabirimin IEnumerable<T> tek bir yöntemi vardır: GetEnumerator. GetEnumerator tarafından döndürülen nesnenin, sonraki öğeye geçmek için bir yöntemi ve sıradaki mevcut öğeyi çağıran bir özelliği vardır. Koleksiyonu numaralandırmak ve öğeleri döndürmek için bu iki üyeyi kullanacaksınız. Bu Interleave yöntemi yineleyici bir yöntem olacaktır, bu nedenle bir koleksiyon oluşturup koleksiyonu döndürmek yerine yukarıda gösterilen söz dizimini yield return kullanacaksınız.

Bu yöntemin uygulanması aşağıdadır:

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

Şimdi bu yöntemi yazdığınıza göre Main yöntemine dönün ve desteyi bir kez karıştırın.

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

Karşılaştırmalar

Desteyi orijinal düzenine geri döndürmek için kaç karıştırma gerekir? Bunu öğrenmek için iki dizinin eşit olup olmadığını belirleyen bir yöntem yazmanız gerekir. Bu yönteme sahip olduktan sonra, desteyi karıştıran kodu bir döngüye yerleştirmeniz ve destenin tekrar başlangıç sıralamasına dönüp dönmediğini kontrol etmeniz gerekir.

İki dizinin eşit olup olmadığını belirlemek için bir yöntem yazmak basit olmalıdır. Desteyi karıştırmak için yazdığınız yönteme benzer bir yapı. Bu sefer, her öğeyi yield return yerine, her dizinin eşleşen öğelerini karşılaştıracaksınız. Tüm sıra numaralandırıldığında, her öğe eşleşirse, diziler aynıdır:

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

Bu, ikinci bir LINQ deyimini gösterir: terminal yöntemleri. Bir diziyi giriş (veya bu örnekte iki dizi) olarak alır ve tek bir skaler değer döndürür. Terminal yöntemlerini kullanırken, bunlar her zaman LINQ sorgusunun yöntem zincirindeki son yöntemdir ve bu nedenle "terminal" adıdır.

Destenin özgün sırasına ne zaman geri döndüğünü belirlemek için kullandığınızda, bunun nasıl çalıştığını görebilirsiniz. Karıştırma kodunu bir döngüye yerleştirin ve SequenceEquals() yöntemini uygulayarak sıranın özgün sırasına geri döndüğü anda durdurun. Herhangi bir sorguda her zaman son yöntem olduğunu görebilirsiniz, çünkü sıra yerine tek bir değer döndürür:

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

Şimdiye kadar sahip olduğunuz kodu çalıştırın ve destenin her karışıklığı nasıl yeniden düzenleyişini not edin. 8 karıştırmadan (do-while döngüsünün yinelemeleri) sonra deste, başlangıçtaki LINQ sorgusundan ilk oluşturduğunuzda içinde olduğu özgün yapılandırmaya döner.

İyileştirmeler

Şimdiye kadar oluşturduğunuz örnek, üst ve alt kartların her çalıştırmada aynı kaldığı bir out karışıklığı yürütür. Şimdi bir değişiklik yapalım: Bunun yerine, 52 kartın da konumunu değiştirdiği karışık bir karıştırma kullanacağız. Karıştırma için desteyi birbirine bağlarsınız, böylece alt yarıdaki ilk kart destedeki ilk kart olur. Bu, üst yarıdaki son kartın alt kart olduğu anlamına gelir. Bu, tekil bir kod satırında yapılan basit bir değişikliktir. Mevcut karıştırma sorgusunu Take ve Skip konumlarını değiştirerek güncelleyin. Bu, destenin üst ve alt yarısının sırasını değiştirir:

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

Programı yeniden çalıştırdığınızda destenin kendisini yeniden sıralamasının 52 yinelemeye sahip olduğunu göreceksiniz. Program çalışmaya devam ettikçe bazı ciddi performans düşüşleri de fark edeceksiniz.

Bunun bir dizi nedeni vardır. Bu performans düşüşünün başlıca nedenlerinden biriyle başa çıkabilirsiniz: gecikmeli değerlendirmenin verimsiz kullanımı.

Kısaca, tembel değerlendirme, bir deyimin değerlendirmesinin, değeri gerekinceye kadar gerçekleştirilmediğini belirtir. LINQ sorguları, gevşek olarak değerlendirilen deyimlerdir. Diziler yalnızca öğeler istendikçe oluşturulur. Bu genellikle LINQ'in önemli bir avantajıdır. Ancak, bu program gibi bir kullanımda, bu yürütme süresinde üstel büyümeye neden olur.

Özgün desteyi LINQ sorgusu kullanarak oluşturduğumuz unutmayın. Her karıştırma, önceki destede üç LINQ sorgusu gerçekleştirilerek oluşturulur. Tüm bunlar tembelce gerçekleştirilir. Bu aynı zamanda sıra istendiği her durumda yeniden gerçekleştirildikleri anlamına gelir. 52. yinelemeye vardığınızda özgün desteyi birçok kez yeniden oluşturursunuz. Şimdi bu davranışı göstermek için bir günlük yazalım. O zaman düzeltirsin.

Extensions.cs dosyanızda aşağıdaki metodu yazın veya kopyalayın. Bu uzantı yöntemi proje dizininizde adlı debug.log yeni bir dosya oluşturur ve şu anda günlük dosyasına yürütülmekte olan sorguyu kaydeder. Bu uzantı yöntemi, sorgunun yürütüldüğünü işaretlemek için herhangi bir sorguya eklenebilir.

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

File altında kırmızı dalgalı bir çizgi görürsünüz, yani o mevcut değildir. Derleyici ne File olduğunu bilmediğinden derlenmez. Bu sorunu çözmek için içindeki ilk satırın altına aşağıdaki kod satırını Extensions.cseklediğinizden emin olun:

using System.IO;

Bu, sorunu çözmelidir ve kırmızı hata kaybolur.

Ardından, her sorgunun tanımına bir günlük iletisi ekleyin.

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

Bir sorguya her erişişiniz için günlüğe kaydetmediğinize dikkat edin. Yalnızca özgün sorguyu oluşturduğunuzda günlüğe kaydedersiniz. Programın çalıştırılması hala uzun sürüyor, ancak şimdi nedenini görebilirsiniz. Günlük kaydı açık durumda olan "in shuffle" işlemini çalıştırırken sabrınız tükenirse, "out shuffle" işlemine geri dönün. Hâlâ tembel değerlendirme etkilerini görmeye devam edersiniz. Tek bir çalıştırmada, tüm değer ve takım elbise oluşturma dahil olmak üzere 2592 sorgu yürütür.

Yaptığınız yürütme sayısını azaltmak için buradaki kodun performansını geliştirebilirsiniz. Yapabileceğiniz basit bir düzeltme, kart destesini oluşturan özgün LINQ sorgusunun sonuçlarını önbelleğe almaktır . Şu anda do-while döngüsü bir yinelemeden geçtiğinde sorguları tekrar tekrar yürütüyor, kart destesini yeniden oluşturup her seferinde yeniden dağıtıyorsunuz. Kart destesini önbelleğe almak için LINQ yöntemlerinden ToArray yararlanabilirsiniz ve ToListbunları sorgulara eklediğinizde, onlara belirttiğiniz eylemleri gerçekleştirirler, ancak şimdi sonuçları çağırmayı seçtiğiniz yönteme bağlı olarak bir dizide veya listede depolarlar. LINQ yöntemini ToArray her iki sorguya da ekleyin ve programı yeniden çalıştırın:

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

Artık "out shuffle" 30 sorguya indi. "in shuffle" yöntemi ile tekrar çalıştırdığınızda benzer iyileştirmeler göreceksiniz: artık 162 sorgu yürütüyor.

Bu örneğin, gecikmeli değerlendirmenin performans sorunlarına neden olabileceği kullanım örneklerini vurgulamak için tasarlandığını lütfen unutmayın. Gecikmeli değerlendirmenin kod performansını nerede etkileyeceğini görmek önemli olsa da, tüm sorguların hevesle çalıştırılmaması gerektiğini anlamak da aynı derecede önemlidir. ToArray'yi kullanmadan ortaya çıkan performans kaybı, kart destesinin her yeni düzenlemesinin önceki düzenlemeden oluşturulmuş olmasından kaynaklanır. Gecikmeli değerlendirmenin kullanılması, her yeni deste yapılandırmasının özgün desteden oluşturulduğu, hatta oluşturan kodun startingDeckyürütüldüğünü gösterir. Bu, büyük miktarda fazladan çalışmaya neden olur.

Pratikte, bazı algoritmalar hevesli değerlendirme kullanarak iyi, diğerleri ise gecikmeli değerlendirme kullanarak iyi çalışır. Günlük kullanım için, veri kaynağı veritabanı motoru gibi başka bir süreç olduğunda tembel değerlendirme genellikle daha iyi bir seçimdir. Veritabanları için gecikmeli değerlendirme, daha karmaşık sorguların veritabanı işlemiyle ve kodunuzun geri kalanıyla tek bir gidiş dönüşle çalışmasına olanak tanır. LINQ, ister gecikmeli ister istekli değerlendirmeyi tercih edin esnektir, bu nedenle süreçlerinizi ölçün ve size en iyi performansı veren değerlendirme türünü seçin.

Sonuç

Bu projede şunları ele aldık:

  • verileri anlamlı bir sırada toplamak için LINQ sorgularını kullanma
  • LINQ sorgularına kendi özel işlevlerimizi eklemek için Uzantı yöntemleri yazma
  • KODUmuzda LINQ sorgularımızın düşük hız gibi performans sorunlarıyla karşılaşabileceği alanları bulma
  • LINQ sorguları bağlamında tembel ve istekli değerlendirme ve bunların sorgu performansı üzerindeki etkileri

LINQ dışında sihirbazların kart numaraları için kullandıkları teknik hakkında biraz bilgi edinmişsinizdir. Sihirbazlar Faro karışıklığı kullanır çünkü her kartın destedeki yerini kontrol edebilir. Artık bildiğine göre, herkese sürprizi bozma!

LINQ hakkında daha fazla bilgi için bkz: