Not
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Inledning
I den här handledningen lär du dig funktioner i .NET och C#. Du lär dig att:
- Generera sekvenser med LINQ.
- Skrivmetoder som du enkelt kan använda i LINQ-frågor.
- Skilja mellan ivrig och lat utvärdering.
Du lär dig dessa tekniker genom att skapa ett program som visar en av de grundläggande färdigheterna hos alla trollkarlar: faro shuffle. En faro shuffle är en teknik där du delar en kortlek exakt i hälften, och sedan vävs varje kort från varje halva in för att återskapa den ursprungliga kortleken.
Trollkarlar använder den här tekniken eftersom varje kort finns på en känd plats efter varje blandning, och ordningen är ett upprepande mönster.
I den här självstudien får du en lättsam titt på hur du manipulerar datasekvenser. Programmet konstruerar en kortlek, utför en sekvens med blandningar och skriver ut sekvensen varje gång. Den jämför även den uppdaterade ordningen med den ursprungliga ordningen.
Den här handledningen har flera steg. Efter varje steg kan du köra programmet och se förloppet. Du kan också se slutförda exempel i GitHub-lagringsplatsen dotnet/samples. Instruktioner för nedladdning finns i Exempelfiler och Handledningar.
Förutsättningar
- Den senaste versionen av .NET SDK
- Visual Studio Code-redigerare
- C# DevKit
Skapa programmet
Skapa ett nytt program. Öppna en kommandotolk och skapa en ny katalog för ditt program. Gör detta till den aktuella katalogen. Skriv kommandot dotnet new console -o LinqFaroShuffle i kommandotolken. Det här kommandot skapar startfilerna för ett grundläggande "Hello World"-program.
Om du aldrig har använt C# tidigare den här självstudien förklarar strukturen för ett C#-program. Du kan läsa det och sedan gå tillbaka hit för att lära dig mer om LINQ.
Skapa datauppsättningen
Tips/Råd
I den här självstudien kan du ordna koden i ett namnområde som heter LinqFaroShuffle för att matcha exempelkoden, eller så kan du använda det globala standardnamnområdet. Om du väljer att använda ett namnområde kontrollerar du att alla klasser och metoder är konsekvent inom samma namnområde eller lägger till lämpliga using instruktioner efter behov.
Tänk på vad som utgör en kortlek. En kortlek med spelkort har fyra kostymer och varje kostym har 13 värden. Normalt kan du överväga att skapa en Card klass direkt och fylla i en samling Card objekt för hand. Med LINQ kan du vara mer koncis än det vanliga sättet att skapa en kortlek. I stället för att skapa en Card klass skapar du två sekvenser för att representera kostymer och rangordningar. Skapa ett par iteratormetoder som genererar valörer och färger i form av IEnumerable<T>strängar:
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";
}
Placera dessa metoder under -instruktionen Console.WriteLine i filen Program.cs . Båda dessa två metoder använder syntaxen yield return för att skapa en sekvens när de körs. Kompilatorn skapar ett objekt som implementerar IEnumerable<T> och genererar sekvensen med strängar när de begärs.
Använd nu dessa iteratormetoder för att skapa kortleken. Placera LINQ-frågan överst i Program.cs filen. Så här ser det ut:
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);
}
De många from-satserna skapar en SelectMany, som resulterar i en enda sekvens genom att kombinera varje element i den första sekvensen med varje element i den andra sekvensen. Ordningen är viktig för det här exemplet. Det första elementet i den första källsekvensen (Suits) kombineras med varje element i den andra sekvensen (Rangordning). Den här processen producerar alla 13 kort i den första färgen. Den processen upprepas med varje element i den första sekvensen (Suits). Slutresultatet är en kortlek som ordnas efter färg, följt av värden.
Tänk på att oavsett om du skriver din LINQ i frågesyntaxen som användes i föregående exempel eller använder metodsyntax i stället, är det alltid möjligt att gå från en form av syntax till en annan. Den föregående frågan som skrivits i frågesyntaxen kan skrivas i metodsyntaxen som:
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => (Suit: suit, Rank: rank )));
Kompilatorn översätter LINQ-uttryck skrivna med frågesyntax till motsvarande metodanropssyntax. Oavsett val av syntax ger därför de två versionerna av frågan samma resultat. Välj den syntax som passar bäst för din situation. Om du till exempel arbetar i ett team där vissa medlemmar har problem med metodsyntax kan du försöka att föredra att använda frågesyntax.
Kör exemplet som du skapade just nu. Den visar alla 52 kort i kortleken. Det kan vara bra att köra det här exemplet under ett felsökningsprogram för att se hur Suits() metoderna och Ranks() körs. Du kan tydligt se att varje sträng i varje sekvens endast genereras efter behov.
Ändra ordningen
Fokusera sedan på hur du blandar korten i leken. Det första steget i en bra kortblandning är att dela kortleken i två. Metoderna Take och Skip som ingår i LINQ-API:erna tillhandahåller den funktionen. Placera dem efter loopen foreach :
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
Det finns dock ingen shuffle-metod att dra nytta av i standardbiblioteket, så du måste skriva egna. Metoden shuffle som du skapar illustrerar flera tekniker som du använder med LINQ-baserade program, så varje del av den här processen förklaras i steg.
Om du vill lägga till funktioner i hur du interagerar med IEnumerable<T> resultatet av LINQ-frågor skriver du några särskilda typer av metoder som kallas tilläggsmetoder. En tilläggsmetod är en statisk metod för specialändamål som lägger till nya funktioner i en redan befintlig typ utan att du behöver ändra den ursprungliga typen som du vill lägga till funktioner i.
Ge tilläggsmetoderna ett nytt hem genom att lägga till en ny statisk-klassfil i ditt program med namnet Extensions.csoch börja sedan skapa den första tilläggsmetoden:
public static class CardExtensions
{
extension<T>(IEnumerable<T> sequence)
{
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
// Your implementation goes here
return default;
}
}
}
Anmärkning
Om du använder en annan redigerare än Visual Studio (till exempel Visual Studio Code) kan du behöva lägga using LinqFaroShuffle; till längst upp i filen Program.cs för att tilläggsmetoderna ska vara tillgängliga. Visual Studio lägger automatiskt till detta med hjälp av instruktionen, men andra redigerare kanske inte gör det.
Containern extension anger vilken typ som utökas. Noden extension deklarerar typ och namn på mottagarparametern för alla medlemmar i containern extension . I det här exemplet utökar du IEnumerable<T>, och parametern heter sequence.
Deklarationer av utökade medlemmar visas som om de är medlemmar av mottagartypen.
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
Du anropar metoden som om den vore en medlemsmetod av den utökade typen. Den här metoddeklarationen följer också ett standard-idiom där indata- och utdatatyperna är IEnumerable<T>. Den metoden gör att LINQ-metoder kan kopplas samman för att utföra mer komplexa frågor.
Eftersom du delar upp däcket i halvor måste du sätta ihop dessa halvor. I kod innebär det att du räknar upp båda sekvenserna som du skaffade genom Take och Skip på en gång, interfolierar elementen och skapar en sekvens: din nu blandade kortlek. Att skriva en LINQ-metod som fungerar med två sekvenser kräver att du förstår hur IEnumerable<T> fungerar.
IEnumerable<T>-gränssnittet har en metod: GetEnumerator. Objektet som returneras av GetEnumerator har en metod för att flytta till nästa element och en egenskap som hämtar det aktuella elementet i sekvensen. Du använder dessa två medlemmar för att räkna upp samlingen och returnera elementen. Den här Interleave-metoden är en iteratormetod, så i stället för att skapa en samling och returnera samlingen använder du syntaxen yield return som visas i föregående kod.
Här är implementeringen av den metoden:
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;
}
}
Nu när du har skrivit den här metoden går du tillbaka till Main metoden och blandar kortleken en gång:
var shuffledDeck = top.InterleaveSequenceWith(bottom);
foreach (var c in shuffledDeck)
{
Console.WriteLine(c);
}
Jämförelser
Bestäm hur många blandningar av kortleken det tar att sätta kortleken i sin ursprungliga ordning. Du kan ta reda på det genom att skriva en metod som avgör om två sekvenser är lika. När du har den metoden, lägg koden som blandar kortleken i en loop och kontrollera när kortleken är tillbaka i ordning.
Det ska vara enkelt att skriva en metod för att avgöra om de två sekvenserna är lika. Strukturen är liknande den metod du använde för att blanda kortleken. Men den här gången jämför du matchande element i varje sekvens i stället för att använda yield return för varje element. När hela sekvensen räknas upp är sekvenserna samma om varje element matchar:
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;
}
Den här metoden visar ett andra LINQ-formspråk: terminalmetoder. De tar en sekvens som indata (eller i det här fallet två sekvenser) och returnerar ett enda skalärt värde. När du använder terminalmetoder är de alltid den sista metoden i en metodkedja för en LINQ-fråga.
Du kan se detta i praktiken när du använder det för att avgöra när däcket är tillbaka i sin ursprungliga ordning. Placera shuffle-koden i en loop och stoppa när sekvensen är tillbaka i sin ursprungliga ordning genom att använda metoden SequenceEquals(). Du kan se att det alltid skulle vara den sista metoden i en fråga eftersom den returnerar ett enda värde i stället för en sekvens:
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);
Kör den kod som du har skapat hittills och lägg märke till hur kortleken ordnas om vid varje blandning. Efter 8 blandningar (iterationer av do-while-loopen) återgår kortleken till den ursprungliga konfigurationen som den hade när du först skapade den från den ursprungliga LINQ-frågan.
Optimeringar
Exemplet som du har skapat hittills utför en out shuffle, där de översta och nedersta korten förblir desamma vid varje körning. Vi gör en ändring: istället använd en in shuffle, där alla 52 kort ändrar position. För en in-shuffle, interleaver du kortleken så att det första kortet i den nedre halvan blir det första kortet i kortleken. Det innebär att det sista kortet i den övre halvan blir det nedre kortet. Den här ändringen kräver en kodrad. Uppdatera den aktuella shuffle-frågan genom att växla positionerna för Take och Skip. Den här ändringen växlar ordningen på däckets övre och nedre halvor:
shuffledDeck = shuffledDeck.Skip(26).InterleaveSequenceWith(shuffledDeck.Take(26));
Kör programmet igen och du ser att det krävs 52 iterationer för att däcket ska kunna ordna om sig själv. Du ser också en allvarlig prestandaförsämring när programmet fortsätter att köras.
Det finns flera orsaker till att prestandan sjunker. Du kan ta itu med en av de viktigaste orsakerna: ineffektiv användning av lat utvärdering.
Lat utvärdering anger att utvärderingen av ett uttryck inte görs förrän dess värde behövs. LINQ-frågor är instruktioner som utvärderas lättjefullt. Sekvenserna genereras endast när elementen begärs. Vanligtvis är det en stor fördel med LINQ. I ett program som detta orsakar lat utvärdering exponentiell tillväxt i körtiden.
Kom ihåg att du genererade den ursprungliga kortleken med hjälp av en LINQ-fråga. Varje blandning genereras genom att utföra tre LINQ-frågor på den föregående kortleken. Alla dessa frågor utförs fördröjt. Det innebär också att de utförs igen varje gång sekvensen begärs. När du kommer till den 52:a iterationen kommer du att återskapa den ursprungliga kortleken många gånger. Skriv en logg för att demonstrera det här beteendet. När du har samlat in data kan du förbättra prestandan.
Skriv in eller kopiera metoden från följande kodexempel i din Extensions.cs-fil. Den här tilläggsmetoden skapar en ny fil med namnet debug.log i projektkatalogen och registrerar vilken fråga som körs till loggfilen. Lägg till den här tilläggsmetoden i valfri fråga för att markera att frågan kördes.
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;
}
Instrumentera sedan definitionen av varje fråga med ett loggmeddelande:
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);
Observera att du inte loggar varje gång du kommer åt en fråga. Du loggar bara när du skapar den ursprungliga frågan. Det tar fortfarande lång tid att köra programmet, men nu kan du se varför. Om du tappar tålamodet när du kör in shuffle med loggning aktiverat, så växla tillbaka till out shuffle. Du ser fortfarande de lata utvärderingseffekterna. I en körning kör den 2 592 frågor, inklusive värdet och kostymgenereringen.
Du kan förbättra kodens prestanda för att minska antalet körningar som du gör. En enkel åtgärd är att cacha resultatet av den ursprungliga LINQ-frågan som konstruerar kortleken. För närvarande kör du frågorna om och om igen varje gång do-while-loopen går igenom en iteration, rekonstruerar kortleken och omdelar den varje gång. För att cachelagra kortleken använder du LINQ-metoderna ToArray och ToList. När du lägger till dem i frågorna utför de samma åtgärder som du sa till dem, men nu lagrar de resultaten i en matris eller en lista, beroende på vilken metod du väljer att anropa. Lägg till LINQ-metoden ToArray till båda frågorna och kör programmet igen:
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);
Nu är out shuffle nere på 30 frågor. Kör igen med i shuffle och du ser liknande förbättringar: nu körs 162 frågor.
Det här exemplet är utformat för att belysa användningsfall där lat utvärdering kan orsaka prestandaproblem. Även om det är viktigt att se var lat utvärdering kan påverka kodprestanda är det lika viktigt att förstå att inte alla frågeoperationer ska köras ivrigt. Prestandaförlusten du drabbas av utan att använda ToArray beror på att varje nytt arrangemang av kortleken är byggda från föregående arrangemang. Att använda lat utvärdering innebär att varje ny lek-konfiguration skapas från den ursprungliga leken, och även att köra koden som skapade startingDeck. Det orsakar en stor mängd extra arbete.
I praktiken körs vissa algoritmer bra med ivrig utvärdering, och andra körs bra med hjälp av lat utvärdering. För daglig användning är lat utvärdering vanligtvis ett bättre val när datakällan är en separat process, till exempel en databasmotor. För databaser möjliggör fördröjd utvärdering att mer komplexa frågor endast behöver en interaktion med databasprocessen och tillbaka till resten av din kod. LINQ är flexibelt oavsett om du väljer att använda lat eller ivrig utvärdering, så mät dina processer och välj vilken utvärdering som ger dig bästa prestanda.
Slutsats
I det här projektet har du gått igenom:
- Använda LINQ-frågor för att aggregera data i en meningsfull sekvens.
- Skriva tilläggsmetoder för att lägga till anpassade funktioner i LINQ-frågor.
- Hitta områden i kod där LINQ-frågor kan stöta på prestandaproblem som försämrad hastighet.
- Lat och ivrig utvärdering i LINQ-frågor och de konsekvenser de kan få för frågeprestanda.
Förutom LINQ lärde du dig om en teknik som magiker använder för korttricks. Trollkarlar använder faro shuffle eftersom de kan styra var varje kort rör sig i leken. Nu när du vet, förstör det inte för alla andra!
Mer information om LINQ finns i: