Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Úvod
V tomto kurzu se naučíte funkce v .NET a jazyce C#. Naučíte se:
- Generování sekvencí pomocí LINQ
- Napište metody, které můžete snadno použít v dotazech LINQ.
- Rozlišovat mezi časným a opožděným vyhodnocením
Tyto techniky se naučíte vytvořením aplikace, která demonstruje jednu ze základních dovedností každého kouzelníka: faro shuffle. Technika faro shuffle je metoda, při které rozdělíte balíček karet přesně na polovinu a poté proloží jednotlivé karty z každé poloviny, aby znovu sestavila původní balíček.
Kouzelníci používají tuto techniku, protože každá karta je ve známém místě po každém prohazování a pořadí je opakující se vzorec.
Tento kurz nabízí světlý pohled na manipulaci s posloupnostmi dat. Aplikace vytvoří balíček karet, provede sekvenci prohazování a pokaždé zapíše tuto sekvenci. Porovná také aktualizované pořadí s původní objednávkou.
Tento kurz obsahuje několik kroků. Po každém kroku můžete aplikaci spustit a sledovat průběh. Můžete si také prohlédnout dokončenou ukázku v úložišti dotnet/samples na GitHubu. Pokyny ke stažení najdete ve vzorech a návodech .
Požadavky
- Nejnovější sada .NET SDK
- editor Visual Studio Code editoru
- C# DevKit
Vytvoření aplikace
Vytvořte novou aplikaci. Otevřete příkazový řádek a vytvořte nový adresář pro vaši aplikaci. Nastavte to jako aktuální adresář. Na příkazovém řádku zadejte příkaz dotnet new console -o LinqFaroShuffle. Tento příkaz vytvoří počáteční soubory pro základní aplikaci Hello World.
Pokud jste jazyk C# ještě nikdy nepoužívali, tento kurz vysvětluje strukturu programu jazyka C#. Můžete si to přečíst a vrátit se sem, abyste se dozvěděli více o LINQ.
Vytvoření datové sady
Návod
Pro účely tohoto kurzu můžete kód uspořádat do volaného LinqFaroShuffle oboru názvů tak, aby odpovídal vzorovém kódu, nebo můžete použít výchozí globální obor názvů. Pokud se rozhodnete použít obor názvů, ujistěte se, že všechny třídy a metody jsou konzistentně ve stejném oboru názvů, nebo podle potřeby přidejte odpovídající using příkazy.
Zvažte, co představuje balíček karet. Balíček hracích karet má čtyři obleky a každý oblek má 13 hodnot. Za normálních okolností můžete zvážit vytvoření Card třídy hned a naplnění kolekce Card objektů ručně. S LINQ můžete být stručnější než obvyklý způsob vytváření balíčku karet. Místo vytvoření Card třídy vytvořte dvě sekvence, které představují obleky a pořadí. Vytvořte dvojici iteračních metod, které generují hodnoty a druhy jako IEnumerable<T> řetězců:
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";
}
Tyto metody umístěte pod Console.WriteLine příkaz ve vašem Program.cs souboru. Obě tyto dvě metody používají yield return syntaxi k vytvoření sekvence při jejich spuštění. Kompilátor sestaví objekt, který implementuje IEnumerable<T> a generuje sekvenci řetězců podle jejich požadavků.
Teď pomocí těchto metod iterátoru vytvořte balíček karet. Umístěte dotaz LINQ na začátek Program.cs souboru. Vypadá takto:
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);
}
Více klauzulí from vytvoří SelectMany, což vytvoří jednu sekvenci kombinací každého prvku v první sekvenci s každým prvkem ve druhé sekvenci. Pořadí je pro tento příklad důležité. První prvek v první zdrojové sekvenci (Suits) je kombinován s každým prvkem ve druhé sekvenci (Ranks). Tento proces vytvoří všechny 13 karet prvního obleku. Tento proces se opakuje s každým prvkem v první sekvenci (v „Suits“). Konečným výsledkem je balíček karet seřazených podle obleků následovaných hodnotami.
Mějte na paměti, že bez ohledu na to, jestli napíšete LINQ v syntaxi dotazu použité v předchozí ukázce nebo místo toho použijete syntaxi metody, je vždy možné přejít z jedné formy syntaxe na druhou. Předchozí dotaz napsaný v syntaxi dotazu lze zapsat v syntaxi metody takto:
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => (Suit: suit, Rank: rank )));
Kompilátor přeloží příkazy LINQ napsané pomocí syntaxe dotazu do ekvivalentní syntaxe volání metody. Proto bez ohledu na vaši volbu syntaxe vytvoří dvě verze dotazu stejný výsledek. Zvolte syntaxi, která nejlépe vyhovuje vaší situaci. Pokud například pracujete v týmu, kde někteří členové mají potíže se syntaxí metody, zkuste raději použít syntaxi dotazu.
Spusťte ukázku, kterou jste vytvořili v tomto okamžiku. Zobrazuje všech 52 karet v balíčku. Může být užitečné spustit tuto ukázku v ladicím programu, abyste mohli pozorovat, jak se metody Suits() a Ranks() provádějí. Jasně vidíte, že každý řetězec v každé sekvenci je generován pouze podle potřeby.
Manipulovat s pořadím
Dále se zaměřte na to, jak v balíčku mícháte karty. Prvním krokem v každém dobrém míchání je rozdělit balíček na dvě části. Metody Take a Skip, které jsou součástí rozhraní API LINQ, poskytují tuto funkci. Umístěte je podle smyčky foreach :
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
Neexistuje však žádná metoda pro náhodné prohazování ve standardní knihovně, takže potřebujete napsat vlastní. Metoda míchání, kterou vytvoříte, ilustruje několik technik, které používáte s programy založenými na LINQ, takže každá část tohoto procesu je vysvětlena krok za krokem.
Pokud chcete přidat funkce pro interakci s IEnumerable<T> výsledky dotazů LINQ, napíšete některé speciální druhy metod označovaných jako rozšiřující metody. Rozšiřující metoda je speciální statická metoda , která přidává nové funkce do již existujícího typu, aniž byste museli upravovat původní typ, do kterého chcete přidat funkce.
Dejte metodám rozšíření nový domov přidáním nového statického souboru třídy do programu s názvem Extensions.csa pak začněte vytvářet první metodu rozšíření:
public static class CardExtensions
{
extension<T>(IEnumerable<T> sequence)
{
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
// Your implementation goes here
return default;
}
}
}
Poznámka:
Pokud používáte jiný editor než Visual Studio (například Visual Studio Code), možná budete muset přidat using LinqFaroShuffle; na začátek souboru Program.cs , aby byly metody rozšíření přístupné. Visual Studio tento příkaz using automaticky přidá, ale jiné editory nemusí.
Kontejner extension určuje typ, který se rozšiřuje. Uzel extension deklaruje typ a název parametru příjemce pro všechny členy uvnitř kontejneru extension . V tomto příkladu rozšiřujete IEnumerable<T>a parametr má název sequence.
Deklarace členů rozšíření se zobrazují, jako by byly členy typu příjemce:
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
Voláte metodu, jako by byla metodou člena rozšířeného typu. Tato deklarace metody také následuje standardní vzorec, kde vstupní a výstupní typy jsou IEnumerable<T>. Tento postup umožňuje zřetězeným metodám LINQ provádět složitější dotazy.
Protože rozdělíte balíček na poloviny, musíte tyto poloviny spojit dohromady. V kódu to znamená, že obě sekvence, které jste získali přes Take a Skip, vyjmenujete současně, prokládáte prvky a spojíte je do jedné sekvence: vaše nyní zamíchaná sada karet. Vytvoření metody LINQ, která funguje se dvěma sekvencemi, vyžaduje, abyste pochopili, jak IEnumerable<T> funguje.
Rozhraní IEnumerable<T> má jednu metodu: GetEnumerator. Objekt vrácený GetEnumerator má metodu pro přesunutí na další prvek a vlastnost, která získá aktuální prvek v posloupnosti. Tyto dva členy použijete k vytvoření výčtu kolekce a vrácení prvků. Tato metoda prokládání je metoda iterátoru, takže místo sestavení kolekce a vrácení kolekce použijete yield return syntaxi zobrazenou v předchozím kódu.
Tady je implementace této metody:
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;
}
}
Teď, když jste napsali tuto metodu, vraťte se k metodě Main a zamíchejte balíček jednou.
var shuffledDeck = top.InterleaveSequenceWith(bottom);
foreach (var c in shuffledDeck)
{
Console.WriteLine(c);
}
Porovnání
Určete, kolik zamíchání je potřeba, aby byl balíček vrácen do původního pořadí. Pokud chcete zjistit, napište metodu, která určuje, zda jsou dvě sekvence rovny. Až budete mít tuto metodu, umístěte do smyčky kód, který zamíchá balíček, a zkontrolujte, kdy je balíček znovu seřazen v původním pořadí.
Napsání metody, která určí, jestli jsou dvě sekvence stejné, by měly být jednoduché. Je to podobná struktura jako metoda, kterou jste napsali na zamíchání balíčku. Tentokrát ale místo použití yield return pro každý prvek porovnáte odpovídající prvky jednotlivých sekvencí. Pokud se zobrazí výčet celé sekvence, pokud se všechny prvky shodují, jsou sekvence stejné:
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;
}
Tato metoda ukazuje druhý idiom LINQ: terminálové metody. Jako vstup přebírají sekvenci (nebo v tomto případě dvě sekvence) a vrátí jednu skalární hodnotu. Když používáte terminálové metody, jsou vždy konečnou metodou v řetězu metod pro dotaz LINQ.
Můžete to vidět v praxi, když ji použijete k určení, kdy se balíček vrátí do původního pořadí. Umístěte kód míchání do smyčky a přestaňte, jakmile se sekvence vrátí do původního pořadí použitím metody SequenceEquals(). V libovolném dotazu by to byla vždy konečná metoda, protože místo sekvence vrací jednu hodnotu:
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);
Spusťte kód, který jste zatím vytvořili, a všimněte si, jak se balíček přeuspořádá při každém zamíchání. Po osmi přeházeních (iteracích smyčky do-while) se balíček vrátí do původní konfigurace, ve které byl, když jste ho poprvé vytvořili z výchozího dotazu LINQ.
Optimalizace
Ukázka, kterou jste zatím vytvořili, provádí vnější prohazování, při kterém horní a dolní karty zůstávají při každém spuštění na svém místě. Pojďme udělat jednu změnu: místo toho použijte funkci náhodného náhodného prohazu , kde se změní všech 52 karet. V případě vnitřního míchání prokládáte balíček tak, aby první karta v dolní polovině byla první kartou v balíčku. To znamená, že poslední karta v horní polovině se stane dolní kartou. Tato změna vyžaduje jeden řádek kódu. Aktualizujte aktuální náhodný dotaz přepnutím pozic Take a Skip. Tato změna přepne pořadí horních a dolních polovin paluby:
shuffledDeck = shuffledDeck.Skip(26).InterleaveSequenceWith(shuffledDeck.Take(26));
Spusťte program znovu a uvidíte, že k přeuspořádání balíčku je zapotřebí 52 iterací. Všimněte si také vážného snížení výkonu, protože program pokračuje v provozu.
Tento pokles výkonu má několik důvodů. Můžete se zaměřit na jednu z hlavních příčin: neefektivní využívání opožděného hodnocení.
Opožděné vyhodnocení uvádí, že vyhodnocení příkazu se neprovádí, dokud nebude potřeba jeho hodnota. Dotazy LINQ jsou příkazy, které se vyhodnocují líně. Sekvence se generují pouze tehdy, když jsou prvky požadovány. Obvykle je to hlavní výhoda LINQ. Avšak v programu, jako je tento, opožděné vyhodnocení způsobuje exponenciální růst v čase vykonání.
Mějte na paměti, že jste vygenerovali původní balíček pomocí dotazu LINQ. Každé zamíchání se generuje provedením tří dotazů LINQ na předchozím balíčku. Všechny tyto dotazy se provádějí líně. To také znamená, že při každém vyžádání sekvence se znovu provádějí. V době, kdy se dostanete k 52. iteraci, mnohokrát znovu vygenerujete původní balíček. Napište protokol, který toto chování demonstruje. Jakmile shromáždíte data, můžete zvýšit výkon.
Do souboru Extensions.cs zadejte nebo zkopírujte metodu z následující ukázky kódu. Tato metoda rozšíření vytvoří nový soubor s názvem debug.log v adresáři projektu a zaznamená, jaký dotaz se právě spouští do souboru protokolu. Připojte tuto metodu rozšíření k libovolnému dotazu pro označení, že se dotaz provedl.
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;
}
Dále přidejte k definici každého dotazu logovací zprávu:
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);
Všimněte si, že se nepřihlašujete pokaždé, když přistupujete k dotazu. Provádíte záznam pouze při vytváření původního dotazu. Program stále trvá dlouhou dobu, ale teď můžete zjistit, proč. Pokud při míchání v režimu 'in' se zapnutým protokolováním vyčerpáte trpělivost, přepněte zpátky na míchání v režimu 'out'. Stále vidíte účinky líného vyhodnocování. V jednom spuštění provede 2 592 dotazů, včetně hodnoty a generování obleku.
Můžete zlepšit výkon kódu, abyste snížili počet provádění, které provedete. Jednoduchá oprava spočívá v ukládání výsledků původního dotazu LINQ do mezipaměti, který sestavuje balíček karet. V současné době spouštíte dotazy znovu a znovu pokaždé, když smyčka do-while prochází iterací, rekonstruujete balíček karet a pokaždé ho znovu zamícháte. K uložení balíčku karet do mezipaměti použijte metody ToArray LINQ a ToList. Když je připojíte k dotazům, provádějí stejné akce, ke kterým jste jim řekli, ale teď ukládají výsledky do pole nebo seznamu v závislosti na tom, jakou metodu se rozhodnete volat. Připojte metodu LINQ ToArray k dotazům a spusťte program znovu:
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);
Teď je vnější míchání omezeno na 30 dotazů. Spusťte znovu příkazem náhodného prohazu a uvidíte podobná vylepšení: teď spouští 162 dotazů.
Tento příklad je navržený tak , aby zvýrazňoval případy použití, kdy opožděné vyhodnocení může způsobit potíže s výkonem. I když je důležité zjistit, kde opožděné vyhodnocení může mít vliv na výkon kódu, je stejně důležité pochopit, že ne všechny dotazy by měly běžet okamžitě. Bez použití ToArray dochází k dopadu na výkon, protože každé nové uspořádání balíčku karet se vytváří z předchozího uspořádání. Použití opožděného vyhodnocení znamená, že každá nová konfigurace balíčku karet je vytvořena z původního balíčku, dokonce i spuštěním kódu, který vytvořil startingDeck. To způsobuje velké množství nadbytečné práce.
V praxi některé algoritmy běží dobře při využití striktního vyhodnocování, a jiné při využití opožděného vyhodnocování. V případě denního využití je opožděné vyhodnocení obvykle lepší volbou, pokud je zdrojem dat samostatný proces, jako je databázový stroj. U databází umožňuje líné vyhodnocování provádět složitější dotazy pouze s jednou cestou tam a zpět do databázového procesu a zpět k vašemu kódu. LINQ je flexibilní, ať už se rozhodnete pro líné nebo horlivé vyhodnocení, proto změřte své procesy a vyberte to hodnocení, které vám poskytne nejlepší výkon.
Závěr
V tomto projektu jste se zabývali:
- Použití dotazů LINQ k agregaci dat do smysluplné sekvence
- Psaní metod rozšíření pro přidání vlastních funkcí do dotazů LINQ
- Vyhledání oblastí v kódu, ve kterých mohou dotazy LINQ narazit na problémy s výkonem, jako je snížení rychlosti.
- Opožděné a bezodkladné vyhodnocení v dotazech LINQ a jejich důsledky na výkon dotazů.
Kromě LINQ jste se dozvěděli o technikách, které kouzelníci používají pro triky karet. Kouzelníci používají faro míchecí techniku, protože mohou ovládat, kam se každá karta v balíčku posune. Teď, když to víte, nekazte to ostatním!
Další informace o LINQ najdete tady: