Megosztás a következőn keresztül:


Language-Integrated Lekérdezés (LINQ) használata

Bevezetés

Ez az oktatóanyag bemutatja a .NET és a C# nyelv funkcióit. Megtudhatja, hogyan:

  • 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. A faro shuffle egy olyan technika, ahol a kártyapaklit pontosan felére osztjuk, majd a keverés során minden félből felváltva tesszük a kártyákat, hogy újra összeállítsuk az eredeti paklit.

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.

Ez az oktatóanyag világos áttekintést nyújt az adatok sorozatainak manipulálására. Az alkalmazás létrehoz egy kártyapaklit, végrehajt egy sor összekeverést, és mindegyiket feljegyzi. Emellett összehasonlítja a frissített sorrendet az eredeti sorrenddel.

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 példákat és oktatóanyagokat.

Előfeltételek

Az alkalmazás létrehozása

Hozzon létre egy új alkalmazást. 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 -o LinqFaroShuffle a parancssorba. Ez a parancs létrehozza a kezdőfájlokat egy alapszintű "Hello World" 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

Jótanács

Ebben az oktatóanyagban rendszerezheti a kódot a mintakódnak megfelelő névtérben LinqFaroShuffle , vagy használhatja az alapértelmezett globális névteret. Ha névtér használata mellett dönt, győződjön meg arról, hogy az összes osztály és metódus következetesen ugyanabban a névtérben van, vagy szükség szerint adjon hozzá megfelelő using utasításokat.

Gondolja át, mi számít egy kártyapaklinak. A kártyapakli négy öltönyt tartalmaz, és mindegyiknek 13 értéke van. Általában azonnal érdemes létrehozni egy Card osztályt, és kézzel kitölteni egy Card objektumgyűjteményt. LINQ használatával tömörebben és egyszerűbben lehet kártyapaklit létrehozni a szokásos módszernél. Az osztály létrehozása Card helyett hozzon létre két sorozatot, amelyek öltönyöket és rangokat jelölnek. Hozzon létre egy iterátor-metóduspárt, amely a rangokat és a színeket sztringekként generálja:

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 metódusokat a fájlban lévő Console.WriteLineProgram.cs utasítás alá. Mindkét módszer a yield return szintaxist használja arra, hogy a futási idő alatti sorozatot előállítsa. A fordító létrehoz egy objektumot, amely a kérésnek megfelelően 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. Helyezze a LINQ-lekérdezést a Program.cs fájl tetejére. Így néz ki:

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

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 sorrend ebben a példában fontos. Az első forrássorozat első eleme (Öltönyök) a második sorozat (Rangok) minden elemével kombináljuk. Ez a folyamat mind a 13 első öltönykártyát létrehozza. Ez a folyamat az első sorozat egyes elemeivel (Öltönyök) ismétlődik. A végeredmény egy színek és értékek szerint rendezett kártyacsomag.

Ne feledje, hogy akár az előző mintában használt lekérdezési szintaxisba írja a LINQ-t, akár metódusszintaxisokat használ, mindig lehetséges a szintaxis egyik formájából a másikba lépni. A lekérdezés szintaxisában írt előző lekérdezés a következő metódusszintaxisban írható:

var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => (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 a helyzethez legjobban megfelelő szintaxist. Ha például olyan csapatban dolgozik, amelynek egyes tagjai nehezen használják a metódusszintaxist, próbálja meg inkább a lekérdezési szintaxist használni.

Futtassa most a létrehozott példakódot. Mind az 52 kártyát megjeleníti a pakliban. Hasznos lehet, ha ezt a mintát egy hibakereső alatt futtatja, hogy megfigyelje, hogyan hajtja végre a Suits() és Ranks() metódusokat. Világosan látható, hogy az egyes sorozatok minden karakterlánca csak szükség szerint kerül előállításra.

Egy konzolablak, amelyen az alkalmazás 52 kártyát ír ki.

A sorrend módosítása

Ezután koncentrálj arra, hogyan keveri 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 után:

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

A standard könyvtárban azonban nincs shuffle metódus, ezért sajátot kell írnia. A létrehozott shuffle metódus számos linq-alapú programmal használható technikát szemléltet, így a folyamat minden részét részletesen ismerteti.

Ha funkciókat szeretne hozzáadni a IEnumerable<T> LINQ-lekérdezések eredményeivel való interakcióhoz, írjon néhány speciális metódust, más néven bővítménymetelyeket. 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:

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

Megjegyzés:

Ha a Visual Studiótól (például a Visual Studio Code-tól) eltérő szerkesztőt használ, előfordulhat, hogy hozzá kell adnia using LinqFaroShuffle; a Program.cs fájl elejéhez, hogy a bővítmény módszerei elérhetők legyenek. A Visual Studio automatikusan hozzáadja ezt az utasítást, de előfordulhat, hogy más szerkesztők nem.

A extension tároló a kibővítendő típust adja meg. A extension csomópont deklarálja a fogadóparaméter típusát és nevét a extension tárolón belüli összes tag számára. Ebben a példában kiterjeszted IEnumerable<T>, és a paraméter neve sequence.

A bővítménytag-deklarációk úgy jelennek meg, mintha a fogadótípus tagjai lennének:

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

A metódust úgy hívja meg, mintha az a kiterjesztett típus 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.

Mivel ketté osztotta a kártyapaklit, össze kell illesztenie ezeket a feleket. A kódban ez azt jelenti, hogy felsorolnia kell mindkét sorozatot, amelyeket a Take és Skip segítségével egyszerre szerzett, az elemeket összekeverve, és így létrehoz egy sorozatot: a most elkeveredett kártyapaklit. 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. E két elem segítségével felsorolhatja a gyűjteményt, és vissza kell adnia az elemeket. Ez az Interleave metódus iterátormetódus, ezért gyűjtemény létrehozása és a gyűjtemény visszaadása helyett az yield return előző kódban látható szintaxist használja.

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

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

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

var shuffledDeck = top.InterleaveSequenceWith(bottom);

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

Összehasonlítások

Határozza meg, hogy hány keverés szükséges ahhoz, hogy a paklit visszaállítsa az eredeti sorrendbe. Ha tudni szeretné, írjon egy metódust, amely meghatározza, hogy két sorozat egyenlő-e. Miután megvan ez a módszer, helyezze a paklit keverő kódot egy ciklusba, és ellenőrizze, hogy a pakli mikor áll vissza az eredeti sorrendbe.

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 ön írt a kártyapakli megkeverésére. Ezúttal azonban az egyes elemek yield return használata helyett az egymásnak megfelelő elemeket hasonlítja össze a sorozatokban. Ha a teljes sorozat enumerálva van, ha minden elem megegyezik, a sorozatok megegyeznek:

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

Ez a metódus egy második LINQ-kifejezést mutat: terminálmetódusokat. Bemenetként egy sorozatot (vagy ebben az esetben két sorozatot) vesznek fel, és egyetlen skaláris értéket adnak vissza. Amikor terminálmetódusokat használ, azok mindig a LINQ-lekérdezések metódusláncának utolsó metódusai.

A működését akkor láthatja, amikor azt használja annak meghatározására, hogy a pakli mikor kerül vissza az eredeti sorrendbe. 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 az utolsó metódus, mert egy sorozat helyett egyetlen értéket ad vissza:

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

Futtassa le az eddig megírt kódot, és figyelje meg, hogyan rendeződik át a pakli minden keveréskor. 8 keverés (a do-while ciklus iterációi) után a pakli visszatér abba az eredeti konfigurációba, amiben volt, amikor először összeállította a kezdő LINQ-lekérdezés alapján.

Optimalizáció

Az eddig létrehozott minta egy kifelé osztást hajt végre, ahol a felső és az alsó kártyák minden futtatáskor ugyanazok maradnak. Tegyünk egy változtatást: használjunk inkább egy in shuffle-t, ahol mind az 52 kártya megváltoztatja pozícióját. Az "in shuffle" során a kártyapaklit úgy kell felváltva keverni, 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. Ehhez a módosításhoz egy kódsorra van szükség. Frissítse az aktuális keverési lekérdezést úgy, hogy cseréli Take és Skip pozícióit. Ez a módosítás a fedélzet felső és alsó felének sorrendjét váltja ki:

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

Futtassa újra a programot, és láthatja, hogy 52 iteráció szükséges ahhoz, hogy a fedélzet átrendezze magát. Ön is észrevesz néhány komoly teljesítménycsökkenést, amint a program továbbra is fut.

Ennek a teljesítménycsökkenésnek több oka is van. Ön foglalkozhat az egyik fő okkal: a lusta kiértékelés nem hatékony alkalmazása.

A lusta kiértékelés azt jelenti, hogy egy utasítás értékelése addig nem történik meg, amíg nincs szükség az értékére. A LINQ-lekérdezések lustán kiértékelt kifejezések. A sorozatok csak az elemek kérése alapján jönnek létre. Ez általában a LINQ egyik fő előnye. Egy ilyen programban azonban a lusta kiértékelés exponenciális növekedést okoz a végrehajtási időben.

Ne feledje, hogy egy LINQ-lekérdezéssel létrehozta az eredeti paklit. Az összes shuffle három LINQ-lekérdezés végrehajtásával jön létre az előző szinten. Ezeket a lekérdezéseket a rendszer lazán hajtja végre. 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, többször újra létrehozza az eredeti paklit. Írjon egy naplót ennek a viselkedésnek a bemutatásához. Az adatok összegyűjtése után javíthatja a teljesítményt.

A Extensions.cs fájljába írja be vagy másolja a metódust az alábbi kódrészletből. 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. Fűzze hozzá ezt a bővítménymetódust bármely lekérdezéshez a lekérdezés végrehajtásának megjelöléséhez.

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

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

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

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 a naplózással futó belső keverés használata közben, váltson vissza a külső keverésre. Továbbra is láthatók a lusta kiértékelési hatások. Egy futtatás során 2592 lekérdezést hajt végre, beleértve az érték és a szín generálást.

A kód teljesítményének javítása érdekében csökkentheti a végrehajtott végrehajtások számát. Az eredeti LINQ-lekérdezés eredményeinek gyorsítótárazása, amely a kártyapaklit hozza létre, egy egyszerű megoldás. Jelenleg folyamatosan újra végrehajtja a lekérdezéseket, amikor a do-while ciklus végighalad egy iteráción, rekonstruálja a kártyacsomagot, és minden alkalommal újrarendezi. A kártyák paklijának gyorsítótárazásához alkalmazza a LINQ metódusokat ToArray és ToLista . Amikor hozzáfűzi őket a lekérdezésekhez, ugyanazokat a műveleteket hajtják végre, amelyeket ön mondott nekik, de most tömbben vagy 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:

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 az out shuffle 30 lekérdezésre van csökkentve. Futtassa újra az in shuffle-t, és hasonló fejlesztéseket láthat: most 162 lekérdezést hajt végre.

Ez a példa arra szolgál , hogy kiemelje azokat a használati eseteket, amikor a lusta értékelés teljesítménybeli nehézségeket 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énycsökkenés, amelyet nem a ToArray használata okoz, azért van, mert a kártyacsomag minden új elrendezése az előző elrendezésből épül fel. A lusta kiértékelés alkalmazása azt jelenti, hogy minden új paklikonfiguráció az eredeti pakliból épül fel, még a kódot is lefuttatva, amely a startingDeck építéséért felelős. Ez nagy mennyiségű többletmunkát okoz.

A gyakorlatban egyes algoritmusok jól futnak éhes kiértékeléssel, míg mások lusta kiértékeléssel teljesítenek jól. 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 értékelés nyújtja a legjobb teljesítményt.

Következteté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 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.
  • A LINQ-lekérdezések lusta és korai kiértékelése, valamint azok hatása a lekérdezési teljesítményre.

A LINQ-n kívül megismerkedett egy technikával, amelyet a mágusok kártyatrükkökhöz használnak. A bűvészek a faro keverést használják, mert szabályozhatják, hogy minden kártya hol helyezkedik el a pakliban. Most, hogy tudod, ne rontsd el másoknak!

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