Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
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
- A legújabb .NET SDK
- Visual Studio Code szerkesztő
- A C# fejlesztőkészlet
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.
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: