DilLe Tümleşik Sorgu (LINQ) ile çalışma

Giriş

Bu öğreticide .NET Core ve C# dilindeki özellikler öğretildi. Şunları öğrenirsiniz:

  • 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 karıştırmanın özgün desteyi yeniden oluşturmak için her iki yarıdan her bir kartı birleştirdiği 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.

Sizin amaçlarınız doğrultusunda, veri dizilerini işlemeye yönelik hafif 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 çok adım vardır. 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

Makinenizi .NET core çalıştıracak şekilde ayarlamanız gerekir. Yükleme yönergelerini .NET Core İndirme sayfasında bulabilirsiniz. Bu uygulamayı Windows, Ubuntu Linux veya OS X üzerinde ya da bir Docker kapsayıcısında çalıştırabilirsiniz. Sık kullandığınız kod düzenleyicisini yüklemeniz gerekir. Aşağıdaki açıklamalarda açık kaynak, platformlar arası düzenleyici olan Visual Studio Code kullanılmaktadır. Ancak, rahatça kullanabileceğiniz araçları kullanabilirsiniz.

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. Bunu geçerli dizin yapın. Komut istemine komutu 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 tarafından dotnet new consoleoluşturulan dosyanın en üstünde Program.cs olduğundan emin olun:

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

Bu üç satır (using deyimler) dosyanın en üstünde değilse, programımız derlenmez.

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, doğrudan bat'ın hemen dışında bir Card sınıf oluşturmayı ve bir nesne koleksiyonunu Card el ile doldurmayı düşünebilirsiniz. LINQ ile, kart destesi oluşturmanın alışılmış yönteminden daha kısa olabilirsiniz. Sınıf oluşturmak Card yerine, sırasıyla uygun ve dereceleri temsil eden iki dizi oluşturabilirsiniz. Dizeler olarak IEnumerable<T>dereceleri ve uygunları oluşturacak çok basit bir yineleyici yöntemi çifti oluşturacaksınız:

// 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 söz dizimini yield return kullanarak çalıştırılırken bir dizi oluşturur. Derleyici, istenen dize dizisini uygulayan IEnumerable<T> ve oluşturan bir nesnesi oluşturur.

Şimdi bu yineleyici yöntemleri kullanarak kart destesini oluşturun. LINQ sorgusunu yöntemimize Main 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 sıradaki her öğeyle (Suit) yinelenir. Sonuç, takım elbiselerine göre sıralanmış ve ardından değerler gelen 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. ve Ranks() yöntemlerinin nasıl çalıştığını gözlemlemek için bu örneği bir hata ayıklayıcısı altında çalıştırmayı Suits() çok yararlı bulabilirsiniz. Her dizideki her dizenin yalnızca gerektiği gibi oluşturulduğunu açıkça görebilirsiniz.

A console window showing the app writing out 52 cards.

Sırayı Değiştirme

Ardından, destedeki kartları nasıl karıştıracağınız üzerinde odaklanın. İyi bir karıştırmanın 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 kitaplıkta yararlanabileceğiniz karıştırma yöntemi yoktur, bu nedenle kendi kitaplığınızı 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 nasıl etkileşim IEnumerable<T> kurabileceğinize ilişkin bazı işlevler 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 adlı Extensions.csyeni bir statik sınıf dosyası ekleyerek uzantı yöntemlerinize yeni bir giriş yapın ve ardından ilk uzantı yöntemini oluşturmaya 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öntemine ilk bağımsız değişkende değiştiricinin eklenmesini this görebilirsiniz. Bu, yöntemini ilk bağımsız değişkenin türünün ü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. tarafından GetEnumerator döndürülen nesnesinin sonraki öğeye gitmek için bir yöntemi ve dizideki geçerli öğeyi alan 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;
    }
}

Bu yöntemi yazdığınıza göre yöntemine Main 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 ne zaman sıralandığını 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ı. Yalnızca bu kez, yield returnher öğeyi ing 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 bunu çalışırken görebilirsiniz. Karıştırma kodunu bir döngüye yerleştirin ve yöntemini uygulayarak SequenceEquals() sıra özgün sırasına geri döndüğünde 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. ve Skipkonumlarını değiştirerek geçerli karıştırma sorgusunu güncelleştirinTake. 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, gecikmeli değerlendirme, bir deyiminin 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 lazily 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.

Dosyanızda Extensions.cs , aşağıdaki yöntemi 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;
}

altında Filekırmızı dalgalı bir dalgalı çizgi görürsünüz, yani yoktur. 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ını bir günlük iletisiyle izleme:

// 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. Oturum açma açık durumdayken karıştırmayı çalıştırırken sabrınız tükenirse, çıkış karıştırmasına geri dönün. Gecikmeli 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 karışıklığı 30 sorguya indi. karışık olarak komutunu yeniden çalıştırdığınızda benzer geliştirmeler görürsünüz: şimdi 162 sorgu yürütür.

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. Kullanmadan ToArray ortaya çıkan performans isabeti, kart destesinin her yeni düzenlemesinin önceki düzenlemeden oluşturulmuş olmasıdı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ı altyapısı gibi ayrı bir işlem olduğunda gecikmeli değerlendirme genellikle daha iyi bir seçimdir. Veritabanları için gecikmeli değerlendirme, daha karmaşık sorguların veritabanı işlemine tek bir gidiş dönüş ve kodunuzun geri kalanına geri dönmesine 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ı ve bunların sorgu performansı üzerindeki etkileri açısından gecikmeli ve istekli değerlendirme

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, bunu herkes için bozma!

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