Aracılığıyla paylaş


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

Giriş

Bu öğreticide size .NET ve C# dilindeki özellikler öğretildi. Şunları yapmayı öğreneceksiniz:

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

Bu teknikleri, herhangi bir sihirbazın temel becerilerinden birini gösteren bir uygulama oluşturarak öğrenirsiniz: faro karıştırma. Faro karıştırma, bir kart destesini tam olarak ikiye böldüğünüz ve ardından iki yarıdan her bir kartı birbirine geçirerek 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.

Bu öğretici, veri dizilerini işlemeye yönelik eğlenceli bir yaklaşım sunar. Uygulama bir kart destesi oluşturur, bir dizi karıştırma gerçekleştirir ve her seferinde diziyi yazar. Ayrıca, güncelleştirilmiş siparişi özgün siparişle karşılaştırır.

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

Yeni bir uygulama oluşturun. 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 -o LinqFaroShuffle yazın. Bu komut, temel bir "Hello World" 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ümesini oluşturma

Tavsiye

Bu öğreticide, kodunuzu adlı LinqFaroShuffle bir ad alanında örnek kodla eşleşecek şekilde düzenleyebilir veya varsayılan genel ad alanını kullanabilirsiniz. Ad alanı kullanmayı seçerseniz, tüm sınıflarınızın ve yöntemlerinizin tutarlı bir şekilde aynı ad alanı içinde olduğundan emin olun veya gerektiğinde uygun using deyimleri ekleyin.

Kart destesinin ne olduğunu düşünün. Bir oyun kartı destesinde dört takım vardır ve her takım elbisenin 13 değeri vardır. Normalde, hemen bir Card sınıf oluşturmayı ve bir nesne koleksiyonunu Card el ile doldurmayı düşünebilirsiniz. LINQ ile, kart destesi oluşturmanın geleneksel yöntemlerinden daha kısa ve öz bir şekilde çalışabilirsiniz. Card sınıfı oluşturmak yerine, renk ve değeri temsil eden iki dizi oluşturun. Dereceleri ve renkleri dizeler olarak oluşturan bir çift yineleyici yöntemi oluşturun:

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

Bu yöntemleri dosyanızdaki Console.WriteLine deyiminin Program.cs altına yerleştirin. Bu iki yöntem de yield return sözdizimini kullanarak çalışırken bir dizi oluşturur. Derleyici, IEnumerable<T> uygulayan ve istenildiğinde dize dizisini üreten bir nesne oluşturur.

Şimdi bu yineleyici yöntemleri kullanarak kart destesini oluşturun. LINQ sorgusunu Program.cs dosyasının en üstüne yerleştirin. Şöyle görünür:

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

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. Bu örnek için sıra önemlidir. İlk kaynak dizisindeki ilk öğe (Suit) ikinci sıradaki (Dereceler) her öğeyle birleştirilir. Bu işlem ilk takım elbisenin 13 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 değerinizi önceki örnekte kullanılan sorgu söz diziminde yazmanız veya bunun yerine yöntem söz dizimini kullanmanız fark etmeksizin, bir söz diziminden diğerine geçmenin her zaman mümkün olduğunu unutmayın. Sorgu söz diziminde yazılmış önceki sorgu yöntem söz diziminde şöyle yazılabilir:

var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => (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 çektiği bir ekipte çalışıyorsanız, sorgu söz dizimlerini kullanmayı tercih etmeyi deneyin.

Bu noktada oluşturduğunuz örneği çalıştırın. Destedeki 52 kartın tümünü görüntüler. Suits() ve Ranks() yöntemlerinin nasıl çalıştığını gözlemlemek için bu örneği bir hata ayıklayıcı altında çalıştırmayı yararlı bulabilirsiniz. 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ırdığınıza 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 sağlar. foreach döngüsünden sonra yerleştirin.

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

Ancak standart kitaplıkta yararlanabileceğiniz bir karıştırma yöntemi olmadığından, kendi yönteminizi yazmanız gerekir. Oluşturduğunuz karıştırma yöntemi LINQ tabanlı programlarla kullandığınız çeşitli teknikleri gösterir, bu nedenle bu işlemin her bölümü adımlarla açıklanmıştır.

LINQ sorgularının sonuçlarıyla IEnumerable<T> nasıl etkileşim kurabileceğinize işlevsellik eklemek için uzantı yöntemleri olarak adlandırılan bazı özel yöntem türleri yazarsınız. 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 adlı yeni bir Extensions.cs sınıf dosyası ekleyerek uzantı yöntemlerinize yeni bir yuva bulun ve ardından ilk uzantı yöntemini geliştirmeye başlayın.

public static class CardExtensions
{
    extension<T>(IEnumerable<T> sequence)
    {
        public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
        {
            // Your implementation goes here
            return default;
        }
    }
}

Uyarı

Visual Studio dışında bir düzenleyici (Visual Studio Code gibi) kullanıyorsanız uzantı yöntemlerinin erişilebilir olması için using LinqFaroShuffle; dosyanızın en üstüne eklemeniz gerekebilir. Visual Studio bu using ifadesini otomatik olarak ekler, ancak diğer düzenleyiciler bunu eklemeyebilir.

Kapsayıcı extension , genişletilmekte olan türü belirtir. extension düğümü, alıcı parametresinin türünü ve adını extension kapsayıcı içindeki tüm üyeler için bildirir. Bu örnekte, IEnumerable<T> öğesini genişletiyorsunuz ve parametreye sequence adı veriliyor.

Uzantı üyesi bildirimleri, alıcı türünün üyesiymiş gibi görünür:

public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)

yöntemini genişletilmiş türün üye yöntemiymiş gibi çağırırsınız. 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.

Desteyi yarıya böldüğünüz için bu yarımları birleştirmeniz gerekir. Kodda bu, Take ve Skip aracılığıyla edindiğiniz dizilerin her ikisini de aynı anda numaralandırıp, öğeleri iç içe geçirerek ve tek bir sıra oluşturarak karıştırılmış kart destesini oluşturmanız anlamına gelir. İ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, bir sonraki öğeye geçmek için bir yöntemi ve dizideki geçerli öğeyi döndüren bir özelliği vardır. Koleksiyonu numaralandırmak ve öğeleri döndürmek için bu iki üyeyi kullanırsınız. Bu Interleave yöntemi yineleyici bir yöntemdir, bu nedenle bir koleksiyon oluşturup koleksiyonu döndürmek yerine önceki kodda gösterilen söz dizimini kullanırsınız yield return .

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

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

Bu yöntemi yazdığınıza göre Main yöntemine geri dönün ve desteyi bir kez karıştırarak tamamlayın.

var shuffledDeck = top.InterleaveSequenceWith(bottom);

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

Karşılaştırmalar

Desteyi özgün sırasına geri döndürmek için kaç karıştırma gerektiğini belirleyin. Öğrenmek için, iki dizinin eşit olup olmadığını belirleyen bir yöntem yazın. Bu yönteme sahip olduktan sonra, desteyi karıştıran kodu bir döngüye yerleştirin ve destenin eski düzenine döndüğünü kontrol edin.

İ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ı. Ancak bu kez, her bir öğeye yield return kullanmak yerine, her dizinin eşleşen öğelerini karşılaştırırsınız. Tüm sıra numaralandırıldığında, her öğe eşleşirse, sıralar aynıdır:

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

Bu yöntem 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 kullandığınızda, bunlar her zaman LINQ sorgusunun yöntem zincirindeki son yöntemdir.

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 olacağını görebilirsiniz çünkü sıra yerine tek bir değer döndürür:

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

Şimdiye kadar oluşturduğunuz kodu çalıştırın ve her karıştırmada destenin nasıl yeniden düzenlendiğini fark 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 dış karıştırma yürütür. Şimdi tek bir değişiklik yapalım: bunun yerine, 52 kartın da konumunu değiştirdiği bir iç karıştırma yapın. 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 değişiklik için bir kod satırı gerekir. Mevcut karıştırma sorgusunu Take ve Skip konumlarını değiştirerek güncelleyin. Bu değişiklik destenin üst ve alt yarısının sırasını değiştirir:

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

Programı yeniden çalıştırdığınızda, destenin kendisini yeniden sıralaması için 52 yineleme gerektiğini görürsünüz. Program çalışmaya devam ettikçe ciddi performans düşüşü de fark edeceksiniz.

Bu performans düşüşü için çeşitli nedenler vardır. Önemli nedenlerden biriyle başa çıkabilirsiniz: tembel değerlendirmenin verimsiz kullanımı.

Gecikmeli değerlendirme, bir ifadenin değerlendirilmesinin, o ifadenin değeri gerekene kadar yapılmadığı anlamına gelir. 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, bunun gibi bir programda, tembel değerlendirme yürütme süresinin üstel büyümesine neden olur.

Özgün desteyi LINQ sorgusu kullanarak oluşturduğunuzu unutmayın. Her karıştırma, önceki destede üç LINQ sorgusu gerçekleştirilerek oluşturulur. Tüm bu sorgular tembel bir şekilde gerçekleştirilir. Bu, sıra istendiği her durumda yeniden gerçekleştirildikleri anlamına da gelir. 52. yinelemeye vardığınızda özgün desteyi birçok kez yeniden oluşturursunuz. Bu davranışı göstermek için bir günlük yazın. Veri topladıktan sonra performansı geliştirebilirsiniz.

Dosyanızdaki Extensions.cs içerisine, aşağıdaki kod örneğindeki 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. Sorgunun yürütüldüğünü işaretlemek için bu uzantı yöntemini herhangi bir sorguya ekleyin.

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

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

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

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. Hala tembel değerlendirme etkilerini görüyorsunuz. Tek bir çalıştırmada değer ve takım elbise oluşturma dahil olmak üzere 2.592 sorgu yürütür.

Yaptığınız yürütme sayısını azaltmak için kodun performansını geliştirebilirsiniz. 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ünün her yinelemesinde sorguları tekrar tekrar yürütüyor, kart destesini tekrar oluşturuyor ve her seferinde yeniden karıştırıyorsunuz. Kart destesini önbelleğe almak için LINQ yöntemlerini ToArray ve ToListuygulayın. Bunları 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:

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. "Shuffle tekniği ile tekrar çalıştırın ve benzer geliştirmeler göreceksiniz: artık 162 sorgu yürütüyor."

Bu örnek, gecikmeli değerlendirmenin performans sorunlarına neden olabileceği kullanım örneklerini vurgulamak için tasarlanmıştır . 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, her iki durumda da esnektir. Bu nedenle, süreçlerinizi ölçün ve size en iyi performansı sağlayan değerlendirmeyi seçin.

Sonuç

Bu projede şunları ele aldık:

  • Verileri anlamlı bir sırayla toplamak için LINQ sorgularını kullanma.
  • LINQ sorgularına özel işlevsellik eklemek için uzantı yöntemleri yazma.
  • Kodda LINQ sorgularının düşük hız gibi performans sorunlarıyla karşılaşabileceği alanları bulma.
  • LINQ sorgularında gecikmeli ve istekli değerlendirme ve bunların sorgu performansı üzerindeki etkileri.

LINQ dışında sihirbazların kart numaraları için kullandıkları bir tekniği öğrendin. Sihirbazlar faro karıştırmayı 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: