Partilhar via


Aplicação de consola

Este tutorial ensina-lhe várias funcionalidades no .NET e na linguagem C#. Irá aprender:

  • Noções básicas da CLI de .NET
  • A estrutura de uma Aplicação de Consola C#
  • E/S da Consola
  • As noções básicas das APIs de E/S de Ficheiros no .NET
  • As noções básicas da Programação Assíncrona baseada em tarefas no .NET

Irá criar uma aplicação que lê um ficheiro de texto e faz eco do conteúdo desse ficheiro de texto para a consola do . A saída para a consola é acelerada para corresponder à leitura em voz alta. Pode acelerar ou abrandar o ritmo ao premir as teclas "<" (menor que) ou ">" (maior que). Pode executar esta aplicação no Windows, Linux, macOS ou num contentor do Docker.

Existem muitas funcionalidades neste tutorial. Vamos compilá-los um a um.

Pré-requisitos

Criar a aplicação

O primeiro passo é criar uma nova aplicação. Abra uma linha de comandos e crie um novo diretório para a sua aplicação. Torne-o no diretório atual. Escreva o comando dotnet new console na linha de comandos. Isto cria os ficheiros de arranque para uma aplicação "Hello World" básica.

Antes de começar a fazer modificações, vamos executar a aplicação Hello World simples. Depois de criar a aplicação, escreva dotnet run na linha de comandos. Este comando executa o processo de restauro do pacote NuGet, cria o executável da aplicação e executa o executável.

O código da aplicação Hello World simples está tudo no Program.cs. Abra esse ficheiro com o 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 ficheiro, veja uma namespace instrução. Tal como outros idiomas orientados para objetos que possa ter utilizado, O C# utiliza espaços de nomes para organizar tipos. Este programa Hello World não é diferente. Pode ver que o programa está no espaço de nomes com o nome TeleprompterConsole.

Ler e Ecoar o Ficheiro

A primeira funcionalidade a adicionar é a capacidade de ler um ficheiro de texto e apresentar todo esse texto na consola do . Primeiro, vamos adicionar um ficheiro de texto. Copie o ficheiro desampleQuotes.txt do repositório do GitHub para este exemplo para o diretório do projeto. Isto servirá de script para a sua aplicação. Para obter informações sobre como transferir a aplicação de exemplo para este tutorial, veja as instruções em Exemplos e Tutoriais.

Em seguida, adicione o seguinte método na sua Program classe (imediatamente 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# denominado método iterador. Os métodos de iterador devolvem sequências que são avaliadas preguiçosamente. Isto significa que cada item na sequência é gerado à medida que é pedido pelo código que consome a sequência. Os métodos de iterador são métodos que contêm uma ou mais yield return instruções. O objeto devolvido 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 a partir do ficheiro de origem e devolver essa cadeia. Sempre que o código de chamada pedir o item seguinte a partir da sequência, o código lê a linha de texto seguinte do ficheiro e devolve-o. Quando o ficheiro é completamente lido, a sequência indica que não existem mais itens.

Existem dois elementos de sintaxe C# que podem ser novos para si. A using instrução neste método gere a limpeza de recursos. A variável que é inicializada na using instrução (readerneste exemplo) tem de implementar a IDisposable interface. Essa interface define um único método, Dispose, que deve ser chamado quando o recurso deve ser libertado. O compilador gera essa chamada quando a execução atinge a chaveta de fecho da using instrução . O código gerado pelo compilador garante que o recurso é libertado mesmo que seja emitida uma exceção a partir do código no bloco definido pela instrução using.

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

Agora, vamos preencher o código para ler o ficheiro no Main método :

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

Execute o programa (com dotnet run) e pode ver todas as linhas impressas na consola.

Adicionar Atrasos e Formatar saída

O que tem está a ser apresentado demasiado rápido para ler em voz alta. Agora, tem de adicionar os atrasos na saída. À medida que começa, vai criar alguns dos códigos principais que permitem o processamento assíncrono. No entanto, estes primeiros passos seguirão alguns anti-padrões. Os anti-padrões são apontados nos comentários à medida que adiciona o código e o código será atualizado em passos posteriores.

Existem dois passos para esta secção. Primeiro, irá atualizar o método de iterador para devolver palavras simples em vez de linhas inteiras. Isto é feito com estas 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, tem de modificar a forma como consome as linhas do ficheiro e adicionar um atraso depois de escrever cada palavra. Substitua a Console.WriteLine(line) instrução no Main método 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 de um atraso de 200 ms. No entanto, a saída apresentada mostra alguns problemas porque o ficheiro de texto de origem tem várias linhas com mais de 80 carateres sem uma quebra de linha. Pode ser difícil de ler enquanto se desloca. É fácil de resolver. Basta controlar o comprimento de cada linha e gerar uma nova linha sempre que o comprimento da linha atingir um determinado limiar. Declare uma variável local após a declaração de words no ReadFrom método que contém o comprimento da linha:

var lineLength = 0;

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

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

Execute o exemplo e poderá ler em voz alta ao ritmo pré-configurado.

Tarefas Assíncronas

Neste passo final, irá adicionar o código para escrever o resultado de forma assíncrona numa tarefa, ao mesmo tempo que executa outra tarefa para ler entradas do utilizador se quiser acelerar ou abrandar a apresentação de texto ou parar completamente a apresentação do texto. Tem alguns passos e, no final, terá todas as atualizações de que precisa. O primeiro passo é criar um método de retorno assíncrono Task que representa o código que criou até agora para ler e apresentar o ficheiro.

Adicione este método à sua Program classe (é 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);
        }
    }
}

Irá reparar em duas alterações. Primeiro, no corpo do método, em vez de chamar Wait() para aguardar síncronamente a conclusão de uma tarefa, esta versão utiliza a await palavra-chave. Para tal, tem de adicionar o async modificador à assinatura do método. Este método devolve um Task. Repare que não existem instruções de devolução que devolvam um Task objeto. Em vez disso, esse Task objeto é criado pelo código que o compilador gera quando utiliza o await operador . Pode imaginar que este método é devolvido quando atinge um await. O devolvido Task indica que o trabalho não foi concluído. O método é retomado quando a tarefa aguardada for concluída. Quando for executado até à conclusão, o devolvido Task indica que está concluído. O código de chamada pode monitorizar o que foi devolvido Task para determinar quando foi concluído.

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

await ShowTeleprompter();

Isto requer que altere a assinatura do Main método para:

static async Task Main(string[] args)

Saiba mais sobre o async Main método na nossa secção de noções básicas.

Em seguida, tem de escrever o segundo método assíncrono para ler a partir da Consola e watch para as teclas "<" (menor que), ">maior que" e "X" ou "x". Eis o método que 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);
}

Esta ação cria uma expressão lambda para representar um Action delegado que lê uma chave da Consola e modifica uma variável local que representa o atraso quando o utilizador prime as teclas "<" (menor que) ou ">" (maior que). O método delegado é concluído quando o utilizador prime as teclas "X" ou "x", que permitem ao utilizador parar a apresentação de texto a qualquer momento. Este método utiliza ReadKey() para bloquear e aguardar que o utilizador prima uma chave.

Para concluir esta funcionalidade, tem de criar um novo async Task método de devolução que inicia ambas as tarefas (GetInput e ) e ShowTelepromptertambém gere os dados partilhados entre estas duas tarefas.

Está na altura de criar uma classe que consiga processar os dados partilhados entre estas duas tarefas. Esta classe contém duas propriedades públicas: o atraso e um sinalizador Done para indicar que o ficheiro 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 num novo ficheiro e inclua essa classe no TeleprompterConsole espaço de nomes, conforme mostrado. Também terá de adicionar uma using static instrução na parte superior do ficheiro para que possa referenciar os Min métodos e Max sem os nomes da classe ou espaço de nomes. Uma using static instrução importa os métodos de uma classe. Isto contrasta com a using instrução sem static, que importa todas as classes de um espaço de nomes.

using static System.Math;

Em seguida, tem de atualizar os ShowTeleprompter métodos e GetInput para utilizar o novo config objeto. Escreva um método de async retorno final Task 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 único método novo aqui é a WhenAny(Task[]) chamada. Isto cria um Task que é concluído assim que qualquer uma das tarefas na sua lista de argumentos for concluída.

Em seguida, tem de atualizar os ShowTeleprompter métodos e GetInput para utilizar o config objeto 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 de ShowTeleprompter chama um novo método na TeleprompterConfig classe. Agora, tem de atualizar Main para ligar RunTeleprompter em vez de ShowTeleprompter:

await RunTeleprompter();

Conclusão

Este tutorial mostrou-lhe várias funcionalidades em torno da linguagem C# e das bibliotecas .NET Core relacionadas com o trabalho em Aplicações de consola. Pode basear-se neste conhecimento para explorar mais sobre o idioma e as classes introduzidas aqui. Viu as noções básicas de E/S de Ficheiros e Consolas, o bloqueio e a utilização não bloqueada da programação assíncrona baseada em tarefas, uma apresentação da linguagem C# e a forma como os programas C# são organizados e a CLI .NET.

Para obter mais informações sobre a E/S de Ficheiro, consulte Ficheiro e E/S de Transmissão em Fluxo. Para obter mais informações sobre o modelo de programação assíncrono utilizado neste tutorial, veja Programação Assíncrona baseada em tarefas e programação assíncrona.