Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Introdução
Este tutorial ensina-te funcionalidades em .NET e na linguagem C#. Você aprende a:
- Gere sequências com o LINQ.
- Escrever métodos que possas usar facilmente em consultas LINQ.
- Distinga entre avaliação ávida e preguiçosa.
Aprende-se estas técnicas construindo uma aplicação que demonstra uma das competências básicas de qualquer mágico: o faro shuffle. Um baralhamento faro é uma técnica em que se divide um baralho de cartas exatamente ao meio, e depois as cartas de cada metade são intercaladas para reconstruir o baralho original.
Os mágicos usam esta técnica porque cada carta está em um local conhecido após cada embaralhamento, e a ordem é um padrão repetitivo.
Este tutorial oferece uma visão descontraída sobre como manipular sequências de dados. A aplicação constrói um baralho de cartas, executa uma sequência de embaralhamentos e então escreve a sequência a cada vez. Também compara a encomenda atualizada com a encomenda original.
Este tutorial tem várias etapas. Após cada etapa, você pode executar o aplicativo e ver o progresso. Você também pode ver o exemplo concluído no repositório dotnet/samples do GitHub. Para obter instruções de download, consulte Exemplos e Tutoriais.
Pré-requisitos
- O mais recente SDK do .NET
- Editor de código do Visual Studio
- O Kit de Desenvolvimento C#
Criar o aplicativo
Crie uma nova aplicação. Abra um prompt de comando e crie um novo diretório para seu aplicativo. Defina isso como o diretório atual. Digite o comando dotnet new console -o LinqFaroShuffle no prompt de comando. Este comando cria os ficheiros iniciais para uma aplicação básica "Hello World".
Se você nunca usou C# antes, este tutorial explica a estrutura de um programa C#. Você pode ler isso e depois voltar aqui para saber mais sobre o LINQ.
Criar o conjunto de dados
Sugestão
Para este tutorial, você pode organizar seu código em um namespace chamado LinqFaroShuffle para corresponder ao código de exemplo ou pode usar o namespace global padrão. Se você optar por usar um namespace, certifique-se de que todas as suas classes e métodos estejam consistentemente dentro do mesmo namespace ou adicione instruções apropriadas using conforme necessário.
Considere o que constitui um baralho de cartas. Um baralho de cartas tem quatro naipes, e cada naipe tem 13 valores. Normalmente, podes considerar criar uma Card classe logo de início e preencher uma coleção de Card objetos manualmente. Com o LINQ, podes ser mais conciso do que a forma habitual de criar um baralho de cartas. Em vez de criar uma Card classe, criar duas sequências para representar naipes e valores. Crie um par de métodos iteradores que gerem os valores e naipes como sequências de cadeias:
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";
}
Coloque estes métodos sob a Console.WriteLine declaração no seu Program.cs ficheiro. Ambos os métodos usam a sintaxe yield return para produzir uma sequência enquanto são executados. O compilador constrói um objeto que implementa IEnumerable<T> e gera a sequência de strings conforme são solicitadas.
Agora, use esses métodos iteradores para criar o baralho de cartas. Coloque a consulta LINQ no topo do Program.cs ficheiro. Veja como é:
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);
}
As cláusulas múltiplas from produzem um SelectMany, que cria uma única sequência a partir da combinação de cada elemento na primeira sequência com cada elemento na segunda sequência. A ordem é importante para este exemplo. O primeiro elemento na primeira sequência de origem (Suits) é combinado com todos os elementos na segunda sequência (Ranks). Este processo produz todas as 13 cartas do primeiro naipe. Este processo é repetido com cada elemento na primeira sequência (Suits). O resultado final é um baralho de cartas ordenado por ternos, seguido de valores.
Tenha em mente que, quer escreva o seu LINQ na sintaxe de consulta usada no exemplo anterior ou use a sintaxe do método, é sempre possível passar de uma forma de sintaxe para outra. A consulta anterior, escrita em sintaxe de consulta, pode ser traduzida para a sintaxe de método da seguinte forma:
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => (Suit: suit, Rank: rank )));
O compilador traduz instruções LINQ escritas com sintaxe de consulta na sintaxe de chamada de método equivalente. Portanto, independentemente da sua escolha de sintaxe, as duas versões da consulta produzem o mesmo resultado. Escolhe a sintaxe que melhor funciona para a tua situação. Por exemplo, se estiveres a trabalhar numa equipa onde alguns membros têm dificuldades com a sintaxe dos métodos, tenta preferir usar a sintaxe das consultas.
Executa a amostra que construíste neste ponto. Mostra todas as 52 cartas do baralho. Poderá achar útil correr este exemplo num depurador para observar como os métodos Suits() e Ranks() executam. Pode ver claramente que cada cadeia em cada sequência é gerada apenas quando necessário.
Manipular a ordem
Depois, foca-te em como embaralhas as cartas do baralho. O primeiro passo em qualquer bom embaralhamento é dividir o baralho em dois. Os métodos Take e Skip que fazem parte das APIs do LINQ fornecem essa funcionalidade. Coloque-os seguindo o foreach loop:
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
No entanto, não há um método de embaralhamento para tirar partido na biblioteca padrão, por isso você precisa criar o seu próprio. O método de embaralhamento que você cria ilustra várias técnicas usadas em programas baseados em LINQ, assim, cada parte deste processo é explicada em etapas.
Para adicionar funcionalidade à forma como interage com os IEnumerable<T> resultados das consultas LINQ, escreve-se alguns tipos especiais de métodos chamados métodos de extensão. Um método de extensão é um método estático de propósito especial que adiciona nova funcionalidade a um tipo já existente sem ter de modificar o tipo original ao qual pretende adicionar funcionalidade.
Dê aos seus métodos de extensão uma nova página inicial adicionando um novo arquivo de classe estática ao seu programa chamado Extensions.cse, em seguida, comece a criar o primeiro método de extensão:
public static class CardExtensions
{
extension<T>(IEnumerable<T> sequence)
{
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
// Your implementation goes here
return default;
}
}
}
Observação
Se você estiver usando um editor diferente do Visual Studio (como o Visual Studio Code), talvez seja necessário adicionar using LinqFaroShuffle; à parte superior do arquivo de Program.cs para que os métodos de extensão sejam acessíveis. **
O Visual Studio adiciona automaticamente esta declaração using, mas outros editores podem não.
O extension contentor especifica o tipo a ser estendido. O extension nó declara o tipo e o nome do parâmetro receptor para todos os membros dentro do extension contêiner. Neste exemplo, estás a estender IEnumerable<T>, e o parâmetro é chamado sequence.
As declarações de membros da extensão aparecem como se fossem membros do tipo receptor.
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
Invocas o método como se fosse um método membro do tipo estendido. Esta declaração de método também segue uma linguagem padrão onde os tipos de entrada e saída são IEnumerable<T>. Essa prática permite que os métodos LINQ sejam encadeados para executar consultas mais complexas.
Como divides o baralho em metades, tens de unir essas metades. No código, isso significa enumerar ambas as sequências que adquiriu de uma só vez, TakeSkipentrelaçando os elementos e criando uma sequência: o seu baralho de cartas agora embaralhado. Escrever um método LINQ que funcione com duas sequências requer que você entenda como IEnumerable<T> funciona.
A IEnumerable<T> interface tem um método: GetEnumerator. O objeto devolvido por GetEnumerator tem um método para se mover para o elemento seguinte e uma propriedade que recupera o elemento atual na sequência. Usas esses dois membros para enumerar a coleção e devolver os elementos. Este método Interleave é um método iterador, por isso, em vez de construir uma coleção e devolvê-la, utiliza-se a yield return sintaxe mostrada no código anterior.
Aqui está a implementação desse método:
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;
}
}
Agora que escreveste este método, volta ao método Main e baralha o baralho uma vez.
var shuffledDeck = top.InterleaveSequenceWith(bottom);
foreach (var c in shuffledDeck)
{
Console.WriteLine(c);
}
Comparações
Determina quantas baralhadas são necessárias para voltar a pôr o baralho na ordem original. Para descobrir, escreva um método que determine se duas sequências são iguais. Depois de teres esse método, coloca o código que baralha o baralho num loop e verifica quando o baralho retorna à ordem original.
Escrever um método para determinar se as duas sequências são iguais deve ser simples. É uma estrutura semelhante ao método que tu escreveste para embaralhar o baralho. No entanto, desta vez, em vez de usares yield return para cada elemento, comparas os elementos correspondentes de cada sequência. Quando toda a sequência é enumerada, se todos os elementos coincidirem, as sequências são as mesmas:
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;
}
Este método mostra um segundo idioma LINQ: métodos terminais. Eles recebem uma sequência como entrada (ou, neste caso, duas sequências) e devolvem um único valor escalar. Quando usas métodos terminais, eles são sempre o método final numa cadeia de métodos para uma consulta LINQ.
Você pode ver isso em ação quando você usá-lo para determinar quando o deck está de volta à sua ordem original. Coloque o código aleatório dentro de um loop e pare quando a sequência estiver de volta em sua ordem original aplicando o SequenceEquals() método. Pode ver que seria sempre o método final em qualquer consulta porque devolve um único valor em vez de uma sequência:
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);
Executa o código que construíste até agora e repara como o baralho se reorganiza a cada embaralhamento. Após 8 shuffles (iterações do loop do-while), o deck retorna à configuração original em que estava quando você o criou pela primeira vez a partir da consulta LINQ inicial.
Otimizações
O exemplo que construíste até agora executa um embaralhamento externo, onde as cartas do topo e de baixo ficam as mesmas em cada sequência. Vamos fazer uma alteração: usar in shuffle, onde as 52 cartas mudam de posição. Para um embaralhamento, você intercala o baralho para que a primeira carta na metade inferior se torne a primeira carta do baralho. Isso significa que a última carta na metade superior torna-se a carta inferior. Esta alteração requer uma linha de código. Atualize a consulta de ordenação aleatória atual alternando as posições de Take e Skip. Esta alteração altera a ordem das metades superior e inferior do baralho:
shuffledDeck = shuffledDeck.Skip(26).InterleaveSequenceWith(shuffledDeck.Take(26));
Executa o programa novamente e vês que demora 52 iterações para o baralho se reordenar. Também se nota uma degradação séria de desempenho à medida que o programa continua a correr.
Existem várias razões para esta queda de desempenho. Pode abordar uma das principais causas: o uso ineficiente da avaliação preguiçosa.
A avaliação preguiçosa afirma que a avaliação de uma afirmação não é realizada até que o seu valor seja necessário. As consultas LINQ são declarações que são avaliadas preguiçosamente. As sequências são geradas somente quando os elementos são solicitados. Normalmente, esse é um grande benefício do LINQ. No entanto, num programa como este, a avaliação preguiçosa provoca um crescimento exponencial no tempo de execução.
Lembra-te que geraste o baralho original usando uma consulta LINQ. Cada shuffle é gerado pela execução de três consultas LINQ no deck anterior. Todas estas consultas são feitas de forma preguiçosa. Isto também significa que são repetidos cada vez que a sequência é solicitada. Quando chegas à 52.ª iteração, estás a reformular o baralho original repetidamente. Escreve um registo para demonstrar este comportamento. Depois de recolher dados, pode melhorar o desempenho.
No seu Extensions.cs ficheiro, escreva ou copie o método no exemplo de código seguinte. Esse método de extensão cria um novo arquivo chamado debug.log dentro do diretório do projeto e registra qual consulta está sendo executada no momento para o arquivo de log. Anexe este método de extensão a qualquer consulta para marcar que a consulta foi executada.
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;
}
Em seguida, instrumente a definição de cada consulta com uma mensagem de log:
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);
Observe que você não registra toda vez que acessa uma consulta. Você registra somente quando cria a consulta original. O programa ainda leva muito tempo para ser executado, mas agora você pode ver o porquê. Se ficares sem paciência ao executar o 'in shuffle' com o registo ativado, volta para o 'out shuffle'. Ainda são visíveis os efeitos da avaliação preguiçosa. Numa sequência, executa 2.592 consultas, incluindo a geração do valor e do naipe.
Podes melhorar o desempenho do código para reduzir o número de execuções que fazes. Uma solução simples é armazenar em cache os resultados da consulta LINQ original que constrói o baralho de cartas. Atualmente, estás a executar as consultas repetidamente cada vez que o ciclo do-while passa por uma iteração, reconstruindo o baralho de cartas e baralhando-o sempre. Para armazenar em cache o baralho de cartas, aplique os métodos ToArray LINQ e ToList. Quando os adicionas às consultas, eles realizam as mesmas ações que lhes pediste, mas agora armazenam os resultados num array ou numa lista, dependendo do método que escolheres chamar. Anexe o método ToArray LINQ a ambas as consultas e execute o programa novamente:
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);
Agora, o shuffle de saída é reduzido para 30 consultas. Executa novamente com o in shuffle e vês melhorias semelhantes: agora executa 162 consultas.
Este exemplo foi concebido para destacar os casos de uso em que uma avaliação preguiçosa pode causar dificuldades de desempenho. Embora seja importante ver onde a avaliação preguiçosa pode afetar o desempenho do código, é igualmente importante entender que nem todas as consultas devem ser executadas ansiosamente. O acerto de desempenho que você incorre sem usar ToArray é porque cada novo arranjo do baralho de cartas é construído a partir do arranjo anterior. Usar avaliação preguiçosa significa que cada nova configuração de baralho é construída a partir do baralho original, mesmo executando o código que construiu o startingDeck. Isso causa uma grande quantidade de trabalho extra.
Na prática, alguns algoritmos funcionam bem usando avaliação ansiosa, e outros funcionam bem usando avaliação preguiçosa. Para uso diário, a avaliação preguiçosa geralmente é uma escolha melhor quando a fonte de dados é um processo separado, como um mecanismo de banco de dados. Para bancos de dados, a avaliação preguiçosa permite que consultas mais complexas executem apenas uma viagem de ida e volta ao processo do banco de dados e voltem para o resto do seu código. LINQ é flexível quer opte por uma avaliação tardia ou imediata, por isso meça os seus processos e escolha a avaliação que lhe der melhor desempenho.
Conclusão
Neste projeto, você abrangeu:
- Usar consultas LINQ para agregar dados numa sequência significativa.
- Escrita de métodos de extensão para adicionar funcionalidades personalizadas a consultas LINQ.
- Localizar áreas no código onde as consultas LINQ podem enfrentar problemas de desempenho, como velocidade degradada.
- Avaliação preguiçosa e imediata nas consultas LINQ e as implicações que podem ter no desempenho das consultas.
Para além do LINQ, aprendeste sobre uma técnica que os mágicos usam para truques de cartas. Os magos usam o baralhamento faro porque podem controlar onde cada carta se move no baralho. Agora que já sabes, não o estragues para os outros!
Para obter mais informações sobre o LINQ, consulte: