Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Introdução
Este tutorial ensina recursos no .NET e na linguagem C#. Você aprenderá como:
- Gerar sequências com LINQ.
- Escreva métodos que você pode usar facilmente em consultas LINQ.
- Distinguir entre avaliação ansiosa e lenta.
Você aprende essas técnicas ao criar um aplicativo que demonstra uma das habilidades básicas de qualquer mágico: o embaralhamento de faro. Um embaralhamento de faro é uma técnica onde você divide um baralho de cartas exatamente ao meio e, em seguida, o embaralhamento intercala cada carta de uma metade com a outra para reconstruir o baralho original.
Os mágicos usam essa técnica porque cada carta está em um local conhecido após cada embaralhamento, e a ordem é um padrão repetitivo.
Este tutorial oferece um olhar leve sobre a manipulação de sequências de dados. O aplicativo constrói um baralho de cartas, executa uma sequência de embaralhamentos e registra a sequência cada vez. Ele também compara a ordem atualizada com a ordem 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 GitHub dotnet/samples. Para obter instruções de download, consulte Exemplos e Tutoriais.
Pré-requisitos
- O .NET SDK mais recente
- Editor do Visual Studio Code
- O DevKit C#
Criar o aplicativo
Crie um novo aplicativo. Abra um prompt de comando e crie um novo diretório para seu aplicativo. Faça disso o diretório atual. Digite o comando dotnet new console -o LinqFaroShuffle no prompt de comando. Esse comando cria os arquivos iniciais para um aplicativo "Hello World" básico.
Se você nunca usou c# antes, este tutorial explica a estrutura de um programa C#. Você pode ler isso e retornar aqui para saber mais sobre LINQ.
Criar o conjunto de dados
Dica
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, verifique se todas as classes e métodos estão 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 cartas. Normalmente, você pode considerar criar uma Card classe imediatamente e preencher uma coleção de Card objetos manualmente. Com o LINQ, você pode ser mais conciso do que a maneira usual de criar um baralho de cartas. Em vez de criar uma Card classe, crie duas sequências para representar ternos e classificações. Crie um par de métodos de iterador que geram as classificações e os fatos como IEnumerable<T>s de cadeias de caracteres:
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 esses métodos sob a Console.WriteLine instrução em seu Program.cs arquivo. Esses dois métodos usam a yield return sintaxe para produzir uma sequência conforme são executados. O compilador cria um objeto que implementa IEnumerable<T> e gera a sequência de cadeias de caracteres conforme são solicitadas.
Agora, use esses métodos de iteração para criar o baralho. Coloque a consulta LINQ na parte superior do Program.cs arquivo. É assim que parece:
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 múltiplas from cláusulas produzem um SelectMany, que cria uma sequência única combinando 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 cada elemento na segunda sequência (Classificações). Esse processo produz todas as 13 cartas do primeiro naipe. Esse processo é repetido com cada elemento na primeira sequência (Suits). O resultado final é um baralho de cartões ordenados por ternos, seguido por valores.
Tenha em mente que, se você escrever o LINQ na sintaxe de consulta usada no exemplo anterior ou usar a sintaxe do método, é sempre possível alternar de uma forma de sintaxe para outra. A consulta anterior escrita na sintaxe de consulta pode ser escrita na sintaxe do método como:
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => (Suit: suit, Rank: rank )));
O compilador converte 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. Escolha a sintaxe que funciona melhor para sua situação. Por exemplo, se você estiver trabalhando em uma equipe em que alguns membros tenham dificuldade com a sintaxe do método, tente preferir usar a sintaxe de consulta.
Execute o exemplo que você criou neste ponto. Ele exibe todas as 52 cartas no baralho. Você pode achar útil executar este exemplo em um depurador para observar como os métodos Suits() e Ranks() são executadas. Você pode ver claramente que cada cadeia de caracteres em cada sequência é gerada somente conforme necessário.
Manipular a ordem
Em seguida, concentre-se em como você embaralha as cartas no baralho. O primeiro passo em qualquer bom embaralhamento é dividir o baralho em dois. Os métodos Take e Skip que fazem parte das APIs LINQ fornecem esse recurso. Coloque-os seguindo o foreach loop:
var top = startingDeck.Take(26);
var bottom = startingDeck.Skip(26);
No entanto, não há nenhum método de embaralhamento para usar na biblioteca padrão, portanto, você precisa desenvolver o seu próprio. O método shuffle criado ilustra várias técnicas que você usa com programas baseados em LINQ, portanto, cada parte desse processo é explicada em etapas.
Para adicionar funcionalidade à forma como você interage com os IEnumerable<T> resultados das consultas LINQ, você escreve alguns tipos especiais de métodos chamados métodos de extensão. Um método de extensão é um método estático de finalidade especial que adiciona novas funcionalidades a um tipo já existente sem precisar modificar o tipo original ao qual você deseja adicionar funcionalidade.
Dê aos métodos de extensão uma nova casa adicionando um novo arquivo de classe estático ao programa chamado Extensions.cse, em seguida, comece a compilar 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 Program.cs para que os métodos de extensão sejam acessíveis. O Visual Studio adiciona automaticamente esta declaração de uso, mas outros editores podem não fazê-lo.
O extension contêiner especifica o tipo que está sendo 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, você está estendendo IEnumerable<T>e o parâmetro é nomeado sequence.
As declarações de membro de extensão aparecem como se fossem membros do tipo receptor:
public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
Você chama o método como se fosse um método membro do tipo estendido. Essa declaração de método também segue um idioma padrão em que 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 você dividiu o baralho em metades, você precisa unir essas metades. No código, isso significa que você enumera ambas as sequências adquiridas por meio Take e Skip de uma vez, intercalando os elementos e criando uma sequência: seu baralho de cartas agora embaralhado. Escrever um método LINQ que funciona com duas sequências requer que você entenda como IEnumerable<T> funciona.
A IEnumerable<T> interface tem um método: GetEnumerator. O objeto retornado por GetEnumerator tem um método para mover para o próximo elemento e uma propriedade que recupera o elemento atual na sequência. Use esses dois membros para enumerar a coleção e retornar os elementos. Esse método Interleave é um método iterador, portanto, em vez de criar uma coleção e retornar a coleção, use 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 você escreveu este método, retorne ao método Main e embaralhe o baralho uma vez.
var shuffledDeck = top.InterleaveSequenceWith(bottom);
foreach (var c in shuffledDeck)
{
Console.WriteLine(c);
}
Comparações
Determine quantas vezes é preciso embaralhar o baralho para que ele retorne à sua ordem original. Para descobrir, escreva um método que determine se duas sequências são iguais. Depois de ter esse método, coloque o código que embaralha o baralho em um loop e verifique se o deck está de volta em ordem.
Escrever um método para determinar se as duas sequências são iguais deve ser simples. É uma estrutura semelhante à do método que você escreveu para embaralhar o baralho. No entanto, desta vez, em vez de usar yield return para cada elemento, você compara os elementos correspondentes de cada sequência. Quando toda a sequência é enumerada, se cada elemento corresponder, as sequências serã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;
}
Esse método mostra um segundo idioma LINQ: métodos terminais. Eles tomam uma sequência como entrada (ou, nesse caso, duas sequências) e retornam um único valor escalar. Quando você usa métodos terminais, eles são sempre o método final em uma cadeia de métodos para uma consulta LINQ.
Você pode observar isso em ação quando o utiliza para determinar quando o baralho retorna à sua ordem original. Coloque o código de embaralhamento dentro de um loop e pare quando a sequência estiver de volta em sua ordem original aplicando a função SequenceEquals(). Você pode ver que ele sempre seria o método final em qualquer consulta porque ele retorna 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);
Execute o código que você criou até agora e observe como o baralho se reorganiza a cada embaralhamento. Depois de 8 vezes embaralhado (iterações do loop do-while), o baralho retorna à configuração original em que estava ao ser criado pela consulta LINQ inicial.
Optimizations
O exemplo que você criou até agora executa um embaralhamento externo, em que as cartas superior e inferior permanecem as mesmas em cada execução. Vamos fazer uma alteração: use um embaralhamento no local, em que todas as 52 cartas mudam de posição. Para um embaralhamento, você intercala o baralho de forma que a primeira carta da metade inferior se torne a primeira carta do baralho. Isso significa que o último cartão na metade superior se torna o cartão inferior. Essa alteração requer uma linha de código. Atualize a consulta de embaralhamento atual alternando as posições de Take e Skip. Essa mudança troca a ordem das metades superior e inferior do baralho.
shuffledDeck = shuffledDeck.Skip(26).InterleaveSequenceWith(shuffledDeck.Take(26));
Execute o programa novamente e você verá que são necessárias 52 iterações para que o baralho se reordene. Você também percebe alguma degradação séria de desempenho à medida que o programa continua a ser executado.
Há vários motivos para essa queda de desempenho. Você pode enfrentar uma das principais causas: o uso ineficiente da avaliação lenta.
A avaliação preguiçosa indica que a avaliação de uma expressão não é executada até que seu valor seja necessário. As consultas LINQ são instruções que são avaliadas de forma preguiçosa. As sequências são geradas somente conforme os elementos são solicitados. Normalmente, isso é um grande benefício do LINQ. No entanto, em um programa como este, a avaliação lenta causa um crescimento exponencial no tempo de execução.
Lembre-se de que você gerou o deck original usando uma consulta LINQ. Cada embaralhamento é gerado realizando três consultas LINQ no baralho anterior. Todas essas consultas são executadas de forma preguiçosa. Isso também significa que eles são executados novamente sempre que a sequência é solicitada. Quando você chega à 52ª iteração, você está regenerando o baralho original várias vezes. Escreva um log para demonstrar esse comportamento. Depois de coletar dados, você pode melhorar o desempenho.
No seu arquivo Extensions.cs, digite ou copie o método no exemplo de código a seguir. Esse método de extensão cria um novo arquivo chamado debug.log no diretório do projeto e registra qual consulta está sendo executada no momento no arquivo de log. Acrescente esse 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 faz logon 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 você ficar sem paciência executando o embaralhamento interno com o registro de log ativado, alterne de volta para o embaralhamento externo. Você ainda percebe os efeitos da avaliação preguiçosa. Em uma execução, são realizadas 2.592 consultas, incluindo a geração de valor e naipe.
Você pode melhorar o desempenho do código para reduzir o número de execuções que você faz. Uma correção simples é armazenar em cache os resultados da consulta LINQ original que constrói o baralho de cartões. Atualmente, você está executando as consultas várias vezes sempre que o loop do-while passa por uma iteração, reconstruindo o baralho de cartas e ressoando-o todas as vezes. Para armazenar em cache o baralho de cartões, aplique os métodos ToArray LINQ e ToList. Quando você as acrescenta às consultas, elas executam as mesmas ações que você lhes disse, mas agora elas armazenam os resultados em uma matriz ou uma lista, dependendo de qual método você escolher chamar. Acrescente 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 "out shuffle" foi reduzido para 30 consultas. Execute novamente no modo embaralhar e você verá melhorias semelhantes: agora ele executa 162 consultas.
Este exemplo foi projetado para realçar os casos de uso em que a avaliação lenta pode causar dificuldades de desempenho. Embora seja importante ver onde a avaliação lenta pode afetar o desempenho do código, é igualmente importante entender que nem todas as consultas devem ser executadas ansiosamente. O impacto no desempenho que você enfrenta sem usar ToArray ocorre porque cada nova disposição das cartas do baralho é criada com base na disposição anterior. Usar a avaliação lenta significa que cada nova configuração de deck é criada a partir do deck original, até mesmo executando o código que criou o startingDeck. Isso causa uma grande quantidade de trabalho extra.
Na prática, alguns algoritmos são executados bem usando a avaliação ansiosa e outros são executados bem usando a avaliação lenta. Para uso diário, a avaliação lenta geralmente é uma melhor opção quando a fonte de dados é um processo separado, como um mecanismo de banco de dados. Para bancos de dados, a avaliação lenta permite que consultas mais complexas executem apenas uma viagem de ida e volta ao processo de banco de dados e retornem ao restante do código. O LINQ é flexível se você optar por usar uma avaliação lenta ou ansiosa, portanto, meça seus processos e escolha qualquer avaliação que lhe dê o melhor desempenho.
Conclusion
Neste projeto, você abordou:
- Usando consultas LINQ para agregar dados em uma sequência significativa.
- Escrevendo métodos de extensão para adicionar funcionalidade personalizada a consultas LINQ.
- Localizar áreas no código em que as consultas LINQ podem ter problemas de desempenho, como velocidade degradada.
- Avaliação lenta e ansiosa em consultas LINQ e as implicações que elas podem ter sobre o desempenho da consulta.
Além do LINQ, você aprendeu sobre uma técnica que os mágicos usam para truques com cartas. Mágicos usam o embaralhamento de faro porque podem controlar onde cada carta do baralho se move. Agora que você sabe, não estrague a surpresa para os outros!
Para obter mais informações sobre LINQ, consulte: