Compartilhar via


Aplicativo de console

Este tutorial ensina vários recursos no .NET e na linguagem C#. O que você aprenderá:

  • As noções básicas da CLI do .NET
  • A estrutura de um aplicativo de console em C#
  • Entrada/Saída do console
  • As noções básicas das APIs de E/S de Arquivo no .NET
  • As noções básicas da programação assíncrona baseada em tarefa no .NET

Você criará um aplicativo que lê um arquivo de texto e ecoará o conteúdo desse arquivo de texto no console. A saída para o console é conduzida a fim de corresponder à leitura em voz alta. Você pode acelerar ou diminuir o ritmo pressionando as teclas '<' (menor que) ou '>' (maior que). Você pode executar esse 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 o aplicativo

A primeira etapa é criar um novo aplicativo. Abra um prompt de comando e crie um novo diretório para seu aplicativo. Torne ele o diretório atual. Digite o comando dotnet new console no prompt de comando. Isso cria os arquivos iniciais para um aplicativo básico "Olá, Mundo!".

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. Esse comando executa o processo de restauração do pacote NuGet, cria o executável do aplicativo e executa o executável.

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

namespace TeleprompterConsole;

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

Na parte superior do arquivo, observe uma instrução namespace. Como outras linguagens orientadas a objetos que você pode ter usado, o 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.

Como ler e exibir 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 de no seu 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 na classe Program (logo abaixo do método Main):

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

Esse método é um tipo especial de método C# chamado método iterador. Os métodos de iterador retornam sequências que são avaliadas lentamente. Isso significa que cada item na sequência é gerado conforme solicitado pelo código que consome a sequência. Métodos de iterador são métodos que contêm uma ou mais declarações yield return. O objeto retornado pelo método ReadFrom contém o código para gerar cada item na sequência. Neste exemplo, isso envolve a leitura da próxima linha de texto do arquivo de origem e o retorno dessa cadeia de caracteres. Sempre 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 o 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 instrução using neste método gerencia a limpeza de recursos. A variável inicializada na instrução using (reader, neste exemplo) deve implementar a interface IDisposable. 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 atingir a chave de fechamento da instrução using. O código gerado pelo compilador garante que o recurso seja liberado, mesmo se uma exceção for lançada do código no bloco definido pela instrução using.

A variável reader é definida usando a palavra-chave var. 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 retornado do método OpenText(String), que é um objeto StreamReader.

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.

Adicionar atrasos e formatar a saída

O que você possui está sendo exibido muito rápido para permitir a leitura em voz alta. Agora você precisa adicionar os atrasos na saída. Ao começar, você criará parte do código principal que habilita o processamento assíncrono. No entanto, essas primeiras etapas seguirão alguns antipadrões. Os antipadrões são apontados em comentários à medida que você adiciona o código e o código será atualizado em etapas posteriores.

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

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

Em seguida, será necessário modificar a forma como você consume as linhas do arquivo, e adicionar um atraso depois de escrever cada palavra. Substitua a instruçã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 durante a rolagem da tela. Isso é fácil de corrigir. Você apenas manterá o controle do 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 da chave de fechamento):

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

Execute o exemplo 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, ao mesmo tempo em que executará outra tarefa para ler a entrada do usuário se ele quiser acelerar ou diminuir a velocidade da exibição de texto ou interromper completamente a exibição de texto. Isso tem algumas etapas e, no final, você terá todas as atualizações necessárias. A primeira etapa é criar um método de retorno de Task assíncrono que representa o código que você criou até agora para ler e exibir o arquivo.

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

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ê observará 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, essa versão usa a palavra-chave await. Para fazer isso, você precisa adicionar o modificador async à assinatura do método. Esse método retorna um Task. Observe que não há instruções de retorno que retornem um objeto Task. Em vez disso, esse objeto Task é criado por código que o compilador gera quando você usa o operador await. Você pode imaginar que esse método retorna quando atinge um await. O Task retornado indica que o trabalho não foi concluído. O método é retomado quando a tarefa aguardada é concluída. Quando ele tiver sido executado para conclusão, o Task retornado indica que ele está concluído. O código de chamada pode monitorar essa Task retornada para determinar quando ela foi concluída.

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

await ShowTeleprompter();

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

static async Task Main(string[] args)

Saiba mais sobre o método async Main em nossa seção de conceitos básicos.

Em seguida, é necessário escrever o segundo método assíncrono a ser lido no Console e ficar atento às teclas "<" (menor que), ">" (maior que) e "X" ou "x". Este é 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 representante Action que lê uma chave 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 delegado é concluído quando o usuário pressiona as teclas 'X' ou 'x', que permitem que o usuário interrompa a exibição de texto a qualquer momento. Esse método usa ReadKey() para bloquear e aguardar o usuário pressionar uma tecla.

Para concluir esta funcionalidade, você precisa criar um novo método de retorno async Task que inicie ambas essas tarefas (GetInput e ShowTeleprompter) e também gerencie os dados compartilhados pelas 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 namespace TeleprompterConsole, conforme mostrado. Você também precisará adicionar uma instrução using static no início do arquivo para poder referenciar os métodos Min e Max sem os nomes de classe ou namespace. Uma instrução using static importa os métodos de uma classe. Isso contrasta com a instrução using sem static, que importa todas as classes de um namespace.

using static System.Math;

Em seguida, você precisa atualizar os métodos ShowTeleprompter e GetInput para usar o novo objeto config. Escreva um método final async de retorno de Task para iniciar as duas tarefas e sair quando a primeira tarefa for concluída:

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

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

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

Depois, atualize 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 classe TeleprompterConfig. 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 criar esse conhecimento para explorar mais sobre o idioma e as classes introduzidas aqui. Você já viu os fundamentos de E/S do Arquivo e do Console, uso com bloqueio e sem bloqueio da programação assíncrona controlada por tarefa, um tour pela linguagem C# e como os programas em C# são organizados, além da CLI do .NET.

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