Partilhar via


Aplicação de consola

Este tutorial ensina vários recursos no .NET e na linguagem C#. Irá aprender:

  • Noções básicas da CLI do .NET
  • A estrutura de um aplicativo de console C#
  • E/S da consola
  • Noções básicas de APIs de E/S de arquivo no .NET
  • Noções básicas da programação assíncrona baseada em tarefas no .NET

Você criará um aplicativo que lê um arquivo de texto e ecoa o conteúdo desse arquivo de texto para o console. A saída para o console é cadenciada para corresponder à leitura em voz alta. Você pode acelerar ou diminuir o ritmo pressionando as teclas '<' (menor que) ou '>' (maior que). Você pode executar este aplicativo no Windows, Linux, macOS ou em um contêiner do Docker.

Há muitos recursos neste tutorial. Vamos construí-los um a um.

Pré-requisitos

Criar a aplicação

O primeiro passo é criar um novo aplicativo. 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 no prompt de comando. Isso cria os arquivos iniciais para um aplicativo básico "Hello World".

Antes de começar a fazer modificações, vamos executar o aplicativo Hello World simples. Depois de criar o aplicativo, digite dotnet run no prompt de comando. Este comando executa o processo de restauração do pacote NuGet, cria o executável do aplicativo e executa o executável.

O código simples do aplicativo Hello World está todo em Program.cs. Abra esse arquivo com seu editor de texto favorito. Substitua o código no Program.cs pelo seguinte código:

namespace TeleprompterConsole;

internal class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

Na parte superior do ficheiro, veja uma namespace instrução. Como outras linguagens orientadas a objetos que você pode ter usado, C# usa namespaces para organizar tipos. Este programa Hello World não é diferente. Você pode ver que o programa está no namespace com o nome TeleprompterConsole.

Lendo e ecoando o arquivo

O primeiro recurso a ser adicionado é a capacidade de ler um arquivo de texto e exibir todo esse texto no console. Primeiro, vamos adicionar um arquivo de texto. Copie o arquivo sampleQuotes.txt do repositório GitHub para este exemplo no diretório do projeto. Isso servirá como o script para seu aplicativo. Para obter informações sobre como baixar o aplicativo de exemplo para este tutorial, consulte as instruções em Exemplos e tutoriais.

Em seguida, adicione o seguinte método em sua Program classe (logo abaixo do Main método):

static IEnumerable<string> ReadFrom(string file)
{
    string? line;
    using (var reader = File.OpenText(file))
    {
        while ((line = reader.ReadLine()) != null)
        {
            yield return line;
        }
    }
}

Este método é um tipo especial de método C# chamado um método iterador. Os métodos iteradores retornam sequências que são avaliadas preguiçosamente. Isso significa que cada item na sequência é gerado conforme é solicitado pelo código que consome a sequência. Métodos iteradores são métodos que contêm uma ou mais yield return instruções. O objeto retornado pelo ReadFrom método contém o código para gerar cada item na sequência. Neste exemplo, isso envolve ler a próxima linha de texto do arquivo de origem e retornar essa cadeia de caracteres. Cada vez que o código de chamada solicita o próximo item da sequência, o código lê a próxima linha de texto do arquivo e a retorna. Quando o arquivo é completamente lido, a sequência indica que não há mais itens.

Há dois elementos de sintaxe C# que podem ser novos para você. A using instrução neste método gerencia a limpeza de recursos. A variável que é inicializada na using instrução (reader, neste exemplo) deve implementar a IDisposable interface. Essa interface define um único método, Dispose, que deve ser chamado quando o recurso deve ser liberado. O compilador gera essa chamada quando a execução atinge a chave de fechamento da using instrução. O código gerado pelo compilador garante que o recurso seja liberado mesmo que uma exceção seja lançada do código contido no bloco definido pela instrução using.

A reader variável é definida usando a var palavra-chave. var define uma variável local digitada implicitamente. Isso significa que o tipo da variável é determinado pelo tipo de tempo de compilação do objeto atribuído à variável. Aqui, esse é o valor de retorno do OpenText(String) método, que é um StreamReader objeto.

Agora, vamos preencher o código para ler o arquivo no método Main.

var lines = ReadFrom("sampleQuotes.txt");
foreach (var line in lines)
{
    Console.WriteLine(line);
}

Execute o programa (usando dotnet run) e você pode ver todas as linhas impressas no console.

Adicionando atrasos e formatando a saída

O que tu tens está a ser exibido rápido demais para ser lido em voz alta. Agora você precisa adicionar os atrasos na saída. Ao começar, você criará parte do código principal que permite o processamento assíncrono. No entanto, estes primeiros passos seguirão algumas práticas inadequadas. Os anti-padrões são apontados nos comentários à medida que se adiciona o código, e o código será atualizado nas etapas seguintes.

Há duas etapas para esta seção. Primeiro, você atualizará o método iterador para retornar palavras únicas em vez de linhas inteiras. Isso é feito com essas modificações. Substitua a yield return line; instrução pelo seguinte código:

var words = line.Split(' ');
foreach (var word in words)
{
    yield return word + " ";
}
yield return Environment.NewLine;

Em seguida, precisas modificar a forma como consomes as linhas do ficheiro e adicionar um atraso após escreveres cada palavra. Substitua a declaração Console.WriteLine(line) no método Main pelo seguinte bloco:

Console.Write(line);
if (!string.IsNullOrWhiteSpace(line))
{
    var pause = Task.Delay(200);
    // Synchronously waiting on a task is an
    // anti-pattern. This will get fixed in later
    // steps.
    pause.Wait();
}

Execute o exemplo e verifique a saída. Agora, cada palavra é impressa, seguida por um atraso de 200 ms. No entanto, a saída exibida mostra alguns problemas porque o arquivo de texto de origem tem várias linhas que têm mais de 80 caracteres sem uma quebra de linha. Isso pode ser difícil de ler enquanto está rolando. Isso é fácil de corrigir. Você apenas controlará o comprimento de cada linha e gerará uma nova linha sempre que o comprimento da linha atingir um determinado limite. Declare uma variável local após a declaração de words no método ReadFrom que contém o comprimento da linha.

var lineLength = 0;

Em seguida, adicione o seguinte código após a instrução yield return word + " "; (antes do fecho):

lineLength += word.Length + 1;
if (lineLength > 70)
{
    yield return Environment.NewLine;
    lineLength = 0;
}

Execute a amostra e você poderá ler em voz alta em seu ritmo pré-configurado.

Tarefas assíncronas

Nesta etapa final, você adicionará o código para gravar a saída de forma assíncrona em uma tarefa, enquanto também executa outra tarefa para ler a entrada do usuário se ele quiser acelerar ou diminuir a exibição de texto, ou interromper a exibição de texto completamente. Isso tem algumas etapas e, no final, você terá todas as atualizações de que precisa. A primeira etapa é criar um método de retorno assíncrono Task que represente o código que você criou até agora para ler e exibir o arquivo.

Adicione este método à sua Program classe (ele é retirado do corpo do seu Main método):

private static async Task ShowTeleprompter()
{
    var words = ReadFrom("sampleQuotes.txt");
    foreach (var word in words)
    {
        Console.Write(word);
        if (!string.IsNullOrWhiteSpace(word))
        {
            await Task.Delay(200);
        }
    }
}

Você notará duas alterações. Primeiro, no corpo do método, em vez de chamar Wait() para aguardar de forma síncrona a conclusão de uma tarefa, esta versão usa a await palavra-chave. Para fazer isso, você precisa adicionar o async modificador à assinatura do método. Este método retorna um Task. Observe que não há instruções de retorno que retornam um Task objeto. Em vez disso, esse objeto é criado pelo código que o compilador gera quando o operador await é utilizado. Você pode imaginar que este método retorna quando atinge um await. O retorno Task indica que o trabalho não foi concluído. O método é retomado quando a tarefa aguardada é concluída. Quando tiver sido executado até a conclusão, o retorno Task indica que ele está completo. O código de chamada pode monitorizar o que retornou Task para determinar quando estiver concluído.

Adicione uma await palavra-chave antes da chamada para ShowTeleprompter:

await ShowTeleprompter();

Isso requer que você altere a assinatura do Main método para:

static async Task Main(string[] args)

Saiba mais sobre o async Main método em nossa seção de fundamentos.

Em seguida, precisas escrever o segundo método assíncrono para ler do Console e observar as teclas '<' (menor que), '>' (maior que) e 'X' ou 'x'. Aqui está o método que você adiciona para essa tarefa:

private static async Task GetInput()
{
    var delay = 200;
    Action work = () =>
    {
        do {
            var key = Console.ReadKey(true);
            if (key.KeyChar == '>')
            {
                delay -= 10;
            }
            else if (key.KeyChar == '<')
            {
                delay += 10;
            }
            else if (key.KeyChar == 'X' || key.KeyChar == 'x')
            {
                break;
            }
        } while (true);
    };
    await Task.Run(work);
}

Isso cria uma expressão lambda para representar um Action delegado que lê uma tecla do Console e modifica uma variável local que representa o atraso quando o usuário pressiona as teclas '<' (menor que) ou '>' (maior que). O método de delegação termina quando o usuário pressiona as teclas 'X' ou 'x', que permitem ao usuário parar a exibição de texto a qualquer momento. Este método usa ReadKey() para bloquear e esperar que o usuário pressione uma tecla.

Para concluir esse recurso, você precisa criar um novo async Task método de retorno que inicie ambas as tarefas (GetInput e ShowTeleprompter) e também gerencie os dados compartilhados entre essas duas tarefas.

É hora de criar uma classe que possa lidar com os dados compartilhados entre essas duas tarefas. Essa classe contém duas propriedades públicas: o atraso e um sinalizador Done para indicar que o arquivo foi completamente lido:

namespace TeleprompterConsole;

internal class TelePrompterConfig
{
    public int DelayInMilliseconds { get; private set; } = 200;
    public void UpdateDelay(int increment) // negative to speed up
    {
        var newDelay = Min(DelayInMilliseconds + increment, 1000);
        newDelay = Max(newDelay, 20);
        DelayInMilliseconds = newDelay;
    }
    public bool Done { get; private set; }
    public void SetDone()
    {
        Done = true;
    }
}

Coloque essa classe em um novo arquivo e inclua essa classe no TeleprompterConsole namespace, conforme mostrado. Você também precisará adicionar uma using static instrução na parte superior do arquivo para que consiga referenciar os métodos Min e Max sem os nomes de classe nem de namespace que os envolvem. Uma using static instrução importa os métodos de uma classe. Isso contrasta com a declaração using sem static, que importa todas as classes de um namespace.

using static System.Math;

Em seguida, você precisa atualizar os ShowTeleprompter métodos e GetInput para usar o novo config objeto. Escreva um método final `Task` que retorne `async` para iniciar ambas as tarefas e sair quando a primeira tarefa terminar:

private static async Task RunTeleprompter()
{
    var config = new TelePrompterConfig();
    var displayTask = ShowTeleprompter(config);

    var speedTask = GetInput(config);
    await Task.WhenAny(displayTask, speedTask);
}

O método novo aqui é a chamada WhenAny(Task[]). Isso cria um Task que termina assim que qualquer uma das tarefas em sua lista de argumentos for concluída.

Em seguida, você precisa atualizar os métodos ShowTeleprompter e GetInput para usar o objeto config para o atraso.

private static async Task ShowTeleprompter(TelePrompterConfig config)
{
    var words = ReadFrom("sampleQuotes.txt");
    foreach (var word in words)
    {
        Console.Write(word);
        if (!string.IsNullOrWhiteSpace(word))
        {
            await Task.Delay(config.DelayInMilliseconds);
        }
    }
    config.SetDone();
}

private static async Task GetInput(TelePrompterConfig config)
{
    Action work = () =>
    {
        do {
            var key = Console.ReadKey(true);
            if (key.KeyChar == '>')
                config.UpdateDelay(-10);
            else if (key.KeyChar == '<')
                config.UpdateDelay(10);
            else if (key.KeyChar == 'X' || key.KeyChar == 'x')
                config.SetDone();
        } while (!config.Done);
    };
    await Task.Run(work);
}

Esta nova versão do ShowTeleprompter chama um novo método na TeleprompterConfig classe. Agora, você precisa atualizar Main para chamar RunTeleprompter em vez de ShowTeleprompter:

await RunTeleprompter();

Conclusão

Este tutorial mostrou vários recursos em torno da linguagem C# e das bibliotecas do .NET Core relacionadas ao trabalho em aplicativos de console. Você pode aproveitar esse conhecimento para explorar mais sobre o idioma e as aulas apresentadas aqui. Você viu os conceitos básicos de E/S de arquivos e consoles, o uso bloqueado e não bloqueado da programação assíncrona baseada em tarefas, um tour pela linguagem C# e como os programas C# são organizados, além da ferramenta CLI do .NET.

Para obter mais informações sobre E/S de arquivo, consulte E/S de arquivo e fluxo. Para obter mais informações sobre o modelo de programação assíncrona usado neste tutorial, consulte Programação assíncrona baseada em tarefas e Programação assíncrona.