Language-Integrated Query (LINQ) használata

Introduction

Ez az oktatóanyag bemutatja a .NET Core és a C# nyelv funkcióit. A következő témákkal fog megismerkedni:

  • Sorozatok létrehozása a LINQ használatával.
  • A LINQ-lekérdezésekben könnyen használható írási módszerek.
  • Különbséget kell tenni a lelkes és a lusta értékelés között.

Ezeket a technikákat egy olyan alkalmazás létrehozásával sajátíthatja el, amely bemutatja a bűvészek egyik alapkészségét: a faro shuffle-t. Röviden, a faro shuffle egy technika, ahol felosztott egy kártya pakli pontosan felére, majd a shuffle interleaves minden egyes kártyát minden fél, hogy újraépítse az eredeti pakli.

A mágusok azért használják ezt a technikát, mert minden kártya egy ismert helyen van az egyes shuffle után, és a sorrend ismétlődő minta.

Az Ön céljaira ez egy világos szívű pillantás az adatsorozatok manipulálására. A létrehozandó alkalmazás felépít egy kártyacsomagot, majd végrehajt egy sorrendet, és minden alkalommal kiírja a sorozatot. A frissített rendelést az eredeti sorrenddel is összehasonlítja.

Ez az oktatóanyag több lépésből áll. Minden lépés után futtathatja az alkalmazást, és megtekintheti az előrehaladást. A kész mintát a dotnet/samples GitHub-adattárban is láthatja. A letöltési utasításokért tekintse meg a példákat és az oktatóanyagokat.

Előfeltételek

A .NET Core futtatásához be kell állítania a gépet. A telepítési utasításokat a .NET Core letöltési oldalán találja. Ezt az alkalmazást Windows, Ubuntu Linux vagy OS X rendszeren vagy Docker-tárolóban is futtathatja. Telepítenie kell a kedvenc kódszerkesztőt. Az alábbi leírások a Visual Studio Code-ot használják, amely egy nyílt forráskód, platformfüggetlen szerkesztő. Azonban bármilyen eszközt használhat, amelyet kényelmesen használhat.

Az alkalmazás létrehozása

Az első lépés egy új alkalmazás létrehozása. Nyisson meg egy parancssort, és hozzon létre egy új könyvtárat az alkalmazás számára. Állítsa be az aktuális könyvtárat. Írja be a parancsot dotnet new console a parancssorba. Ez létrehozza a kezdőfájlokat egy alapszintű ""Helló világ!" alkalmazás" alkalmazáshoz.

Ha még soha nem használta a C# programot, ez az oktatóanyag bemutatja a C# program felépítését. Ezt elolvashatja, majd visszatérhet ide, hogy többet tudjon meg a LINQ-ról.

Az adatkészlet létrehozása

Mielőtt hozzákezdene, győződjön meg arról, hogy a következő sorok vannak a fájl tetején, amelyet dotnet new consolea Program.cs következő hozott létre:

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

Ha ez a három sor (using utasítás) nem szerepel a fájl tetején, a program nem fordítja le.

Most, hogy minden szükséges referenciával rendelkezik, gondolja át, mi számít kártyacsomagnak. Általában egy pakli kártya négy öltönyt tartalmaz, és mindegyiknek tizenhárom értéke van. Általában érdemes lehet közvetlenül az ütőről létrehozni egy Card osztályt, és kézzel feltölteni Card egy objektumgyűjteményt. A LINQ használatával tömörebb lehet, mint a szokásos módszer a kártyacsomagok létrehozására. Az osztály létrehozása Card helyett két sorozatot hozhat létre, amelyek az öltönyöket és a rangokat jelölik. Létrehoz egy nagyon egyszerű iterátor metóduspárt, amely sztringként IEnumerable<T>hozza létre a rangokat és a megfelelőket:

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

Helyezze ezeket a Main metódus alá a Program.cs fájlban. Ez a két módszer egyaránt a szintaxist használja a yield return sorozat futás közbeni előállításához. A fordító létrehoz egy objektumot, amely igény szerint implementálja IEnumerable<T> és létrehozza a sztringek sorozatát.

Most használja ezeket az iterátor módszereket a kártyák paklijának létrehozásához. A LINQ-lekérdezést a metódusban Main fogja elhelyezni. Íme egy pillantás:

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

A több from záradék létrehoz egy SelectMany, amely egyetlen sorozatot hoz létre az első sorozat egyes elemeinek és a második sorozat egyes elemeinek kombinálásával. A megrendelés fontos a mi céljainkhoz. Az első forrásütemezés első eleme (Öltönyök) a második sorozat (Ranks) minden elemével kombinálva van. Ez mind a tizenhárom kártyát az első öltöny. Ez a folyamat az első sorozat egyes elemeivel (Öltönyök) ismétlődik. A végeredmény egy öltönyök által rendezett kártyacsomag, amelyet értékek követnek.

Fontos szem előtt tartani, hogy függetlenül attól, hogy a LINQ-t a fent használt lekérdezési szintaxisba írja, vagy inkább metódusszintaxist használ, mindig lehetséges az egyik szintaxisból a másikba lépni. A lekérdezési szintaxisban írt fenti lekérdezés a következő metódusszintaxisban írható:

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

A fordító lefordítja a lekérdezési szintaxissal írt LINQ-utasításokat az egyenértékű metódushívási szintaxisra. Ezért a szintaxis választásától függetlenül a lekérdezés két verziója ugyanazt az eredményt eredményezi. Válassza ki, hogy melyik szintaxis működik a legjobban a helyzethez: ha például olyan csapatban dolgozik, amelyben a tagok némelyike nehezen használja a metódusszintaxist, próbálja meg inkább a lekérdezési szintaxist használni.

Futtassa az ezen a ponton létrehozott mintát. Mind az 52 kártyát megjeleníti a pakliban. Hasznos lehet, ha ezt a mintát egy hibakereső alatt futtatja, hogy megfigyelje a metódusok és Ranks() a Suits() végrehajtás módját. Jól látható, hogy az egyes sorozatok minden sztringje csak a szükséges módon jön létre.

A console window showing the app writing out 52 cards.

A rendelés kezelése

Most koncentráljon arra, hogyan fogja elkeverni a kártyákat a pakliban. Minden jó osztás első lépése a pakli kettéosztása. A Take LINQ API-k részét képező és Skip metódusok biztosítják ezt a funkciót. Helyezze őket a foreach hurok alá:

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

A standard kódtárban azonban nincs shuffle metódus, ezért sajátot kell írnia. A létrehozandó shuffle metódus számos linq-alapú programmal használható technikát mutat be, így a folyamat minden részét részletesen ismertetjük.

Ahhoz, hogy bizonyos funkciókkal bővíthesse a IEnumerable<T> LINQ-lekérdezésekből visszaérkezendő műveleteket, meg kell írnia néhány speciális metódust, az úgynevezett bővítménymetelyeket. Röviden: a bővítménymetódus egy speciális célú statikus módszer , amely új funkciókat ad hozzá egy már meglévő típushoz anélkül, hogy módosítania kellene azt az eredeti típust, amelyhez funkciókat szeretne hozzáadni.

Adjon új otthont a bővítménymetódusoknak, ha hozzáad egy új statikus osztályfájlt a programhoz, Extensions.csmajd kezdje el kiépíteni az első bővítménymetódust:

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

Tekintsd meg egy pillanatra a metódus aláírását, különösen a paramétereket:

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

A módosító hozzáadását az this első argumentumban láthatja a metódushoz. Ez azt jelenti, hogy úgy hívja meg a metódust, mintha az az első argumentum típusának tagmetódusa lenne. Ez a metódusdeklaráció egy szabványos kifejezésmódot is követ, ahol a bemeneti és kimeneti típusok a következők IEnumerable<T>. Ez a gyakorlat lehetővé teszi, hogy a LINQ-metódusok össze legyenek kötve az összetettebb lekérdezések végrehajtásához.

Természetesen, mivel felére osztotta a fedélzetet, össze kell illesztenie ezeket a feleket. A kódban ez azt jelenti, hogy számba fogja venni azokat a sorozatokat, amelyeken keresztül Take és Skip egyszerre szerezte be az elemeket, interleaving és létrehoz egy sorozatot: a most elkeveredett kártyacsomagot. A két sorozattal működő LINQ-metódus írásához ismernie kell a működést IEnumerable<T> .

A IEnumerable<T> felületnek egy metódusa van: GetEnumerator. A visszaadott GetEnumerator objektumnak van egy metódusa, amellyel a következő elemre léphet, és egy tulajdonság, amely lekéri a sorozat aktuális elemét. Ezzel a két taggal számba veszi a gyűjteményt, és visszaadja az elemeket. Ez az Interleave metódus iterátor metódus lesz, ezért gyűjtemény létrehozása és a gyűjtemény visszaadása helyett a yield return fent látható szintaxist fogja használni.

Ennek a módszernek a megvalósítása a következő:

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

Most, hogy megírta ezt a módszert, térjen vissza a Main metódushoz, és keverje el egyszer a paklit:

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

Összehasonlítások

Hány shuffles kell ahhoz, hogy visszaállítsa a fedélzetet az eredeti sorrendbe? Ha tudni szeretné, meg kell írnia egy metódust, amely meghatározza, hogy két sorozat egyenlő-e. Miután elvégezte ezt a módszert, el kell helyeznie a kódot, amely egy hurokba keveri a fedélzetet, és ellenőriznie kell, hogy mikor van újra rendben a pakli.

A két sorozat egyenlőségének megállapítására használható módszer írásának egyszerűnek kell lennie. Ez egy hasonló szerkezet, mint a módszer, amit írt, hogy shuffle a fedélzetet. Csak ezúttal, az egyes elemek helyett yield returnaz egyes sorozatok egyező elemeit fogja összehasonlítani. A teljes sorozat számbavétele után, ha minden elem megegyezik, a sorozatok megegyeznek:

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

Ez egy második LINQ-kifejezést mutat: terminálmetszeteket. Bemenetként egy sorozatot (vagy ebben az esetben két sorozatot) vesznek fel, és egyetlen skaláris értéket adnak vissza. Terminálmetódusok használatakor mindig ezek a linq lekérdezések metódusláncának végső metódusai, ezért a "terminál" név.

Ez akkor jelenik meg működés közben, ha azt használja annak meghatározására, hogy a pakli mikor áll vissza az eredeti sorrendben. Helyezze a shuffle kódot egy hurokba, és állítsa le, amikor a sorozat az eredeti sorrendbe kerül a SequenceEquals() metódus alkalmazásával. Láthatja, hogy minden lekérdezésben mindig ez lenne a végső metódus, mert egy sorozat helyett egyetlen értéket ad vissza:

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

Futtassa az eddig kapott kódot, és jegyezze fel, hogyan rendezi át a pakli az egyes shuffle-eket. 8 shuffles (a do-while ciklus iterációi) után a pakli visszatér az eredeti konfigurációhoz, amely akkor volt, amikor először létrehozta a kezdő LINQ-lekérdezésből.

Optimalizálás

Az eddig létrehozott minta egy kifelé osztást hajt végre, ahol a felső és az alsó kártyák minden futtatáskor azonosak maradnak. Tegyünk egy változtatást: helyette egy váltógombot használunk, ahol mind az 52 kártya pozíciót vált. Az elsüllyedés esetén a fedélzetet úgy kell összefűzni, hogy az alsó felében lévő első kártya legyen a pakli első kártyája. Ez azt jelenti, hogy a felső felében lévő utolsó kártya lesz az alsó kártya. Ez egy egyszerű módosítás egy egyedi kódsorra. Frissítse az aktuális shuffle lekérdezést az és a TakeSkip. Ez megváltoztatja a fedélzet felső és alsó felének sorrendjét:

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

Futtassa újra a programot, és látni fogja, hogy 52 iteráció szükséges ahhoz, hogy a pakli átrendezze magát. Emellett komoly teljesítménycsökkenést is tapasztalhat, mivel a program továbbra is fut.

Ennek több oka is van. A teljesítménycsökkenés egyik fő oka a lusta értékelés nem hatékony használata.

Röviden a lusta értékelés azt állítja, hogy az utasítás kiértékelése csak akkor történik meg, ha az értékére szükség van. A LINQ-lekérdezések lazán kiértékelt utasítások. A sorozatok csak az elemek kérése alapján jönnek létre. Ez általában a LINQ egyik fő előnye. A programhoz hasonló használat esetén azonban ez exponenciális növekedést okoz a végrehajtási időben.

Ne feledje, hogy egy LINQ-lekérdezéssel hoztuk létre az eredeti fedélzetet. Az összes shuffle három LINQ-lekérdezés végrehajtásával jön létre az előző szinten. Ezek mindegyike lazán történik. Ez azt is jelenti, hogy a rendszer minden alkalommal újra végrehajtja őket, amikor a sorozatot kérik. Mire eléri az 52. iterációt, az eredeti pakli újragenerálása sok-sok alkalommal. Írjunk egy naplót ennek a viselkedésnek a bemutatásához. Akkor meg fogja javítani.

Extensions.cs A fájlba írja be vagy másolja ki az alábbi metódust. Ez a bővítménymetódus létrehoz egy új fájlt a projektkönyvtárban, debug.log és rögzíti, hogy jelenleg milyen lekérdezést hajtanak végre a naplófájlban. Ez a bővítménymetódus bármely lekérdezéshez hozzáfűzhető a lekérdezés végrehajtásának megjelöléséhez.

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

A piros hullámos vonal alatt Filepiros hullám jelenik meg, ami azt jelenti, hogy nem létezik. Nem fog lefordítani, mivel a fordító nem tudja, mi File az. A probléma megoldásához vegye fel a következő kódsort a következő sorba a következő sorba Extensions.cs:

using System.IO;

Ennek meg kell oldania a problémát, és a piros hiba eltűnik.

Ezután egy naplóüzenettel adja meg az egyes lekérdezések definícióját:

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

Figyelje meg, hogy nem naplóz minden alkalommal, amikor hozzáfér egy lekérdezéshez. Csak az eredeti lekérdezés létrehozásakor jelentkezik be. A program futtatása még sok időt vesz igénybe, de most már láthatja, miért. Ha elfogy a türelme, és be van kapcsolva a naplózás, váltson vissza a kifelé váltógombra. Továbbra is látni fogja a lusta kiértékelési hatásokat. Egy futtatás során 2592 lekérdezést hajt végre, beleértve az összes értéket és a megfelelő generációt.

Itt javíthatja a kód teljesítményét a végrehajtott végrehajtások számának csökkentése érdekében. Egy egyszerű javítást tehet, ha gyorsítótárazza az eredeti LINQ-lekérdezés eredményeit, amely a kártyacsomagot hozza létre. Jelenleg minden alkalommal újra és újra végrehajtja a lekérdezéseket, amikor a do-while hurok végighalad egy iteráción, újraépli a kártyacsomagot, és minden alkalommal újrakonvertálja. A kártyák paklijának gyorsítótárazásához használhatja a LINQ metódusokat ToArray , és ToListha hozzáfűzi őket a lekérdezésekhez, ugyanazokat a műveleteket hajtják végre, amelyeket ön mondott nekik, de most egy tömbben vagy egy listában tárolják az eredményeket attól függően, hogy melyik metódust választja. Fűzze hozzá a LINQ metódust ToArray mindkét lekérdezéshez, és futtassa újra a programot:

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

Most a kifelé elosztás 30 lekérdezésre van leveve. Futtassa újra az in shuffle-t, és hasonló fejlesztéseket fog látni: most 162 lekérdezést hajt végre.

Vegye figyelembe, hogy ez a példa arra szolgál, hogy kiemelje azokat a használati eseteket, amikor a lusta értékelés teljesítményproblémát okozhat. Bár fontos látni, hogy a lusta kiértékelés hol befolyásolhatja a kód teljesítményét, ugyanilyen fontos tisztában lenni azzal, hogy nem minden lekérdezésnek kell lelkesen futnia. A teljesítménybeli találat, amelyet használat ToArray nélkül ér el, az az, hogy a kártyacsomag minden új elrendezése az előző elrendezésből épül fel. A lusta kiértékelés azt jelenti, hogy minden új csomagkonfiguráció az eredeti pakliból épül fel, még a kódot is végrehajtva, amely a startingDeck. Ez nagy mennyiségű többletmunkát okoz.

A gyakorlatban egyes algoritmusok jól futnak a lelkes kiértékelés, mások pedig lusta kiértékeléssel. A napi használathoz a lusta kiértékelés általában jobb választás, ha az adatforrás egy külön folyamat, például egy adatbázismotor. Az adatbázisok esetében a lusta kiértékelés lehetővé teszi, hogy az összetettebb lekérdezések csak egy körúton hajtják végre az adatbázis-folyamatot, és térjenek vissza a kód többi részéhez. A LINQ rugalmas, függetlenül attól, hogy lusta vagy lelkes kiértékelést választ, ezért mérje fel a folyamatokat, és válassza ki, hogy melyik kiértékelési típus nyújtja a legjobb teljesítményt.

Összefoglalás

Ebben a projektben a következő témaköröket tárgyalta:

  • LINQ-lekérdezések használata az adatok értelmes sorozatba való összesítéséhez
  • bővítménymetelyek írása saját egyéni funkciók linq-lekérdezésekhez való hozzáadásához
  • a kód azon területeinek megkeresése, ahol a LINQ-lekérdezések teljesítményproblémákba, például csökkentett sebességbe ütközhetnek
  • lusta és türelmetlen értékelés a LINQ-lekérdezések tekintetében, valamint a lekérdezési teljesítményre gyakorolt hatásukról

A LINQ-n kívül megismerkedett egy olyan technikával, amelyet a mágusok kártyatrükkökhöz használnak. A bűvészek a Faro shuffle-t használják, mert szabályozhatják, hogy hol mozog minden kártya a fedélzeten. Most, hogy tudod, ne rontsd el mindenki másnak!

További információ a LINQ-ról: