Поделиться через


Консольное приложение

В этом руководстве вы узнаете о нескольких функциях в .NET и языке C#. Вы узнаете:

  • Основы интерфейса командной строки .NET
  • Структура консольного приложения C#
  • Ввод-вывод консоли
  • Основы API-интерфейсов ввода-вывода файлов в .NET
  • Основы асинхронного программирования на основе задач в .NET

Вы создадите приложение, которое считывает текстовый файл и отражает содержимое этого текстового файла в консоли. Вывод на консоль синхронизирован так, чтобы соответствовать ритму чтения вслух. Вы можете ускорить или замедлить темп, нажав клавиши "<" (меньше) или ">" (больше). Это приложение можно запустить в Windows, Linux, macOS или в контейнере Docker.

В этом руководстве представлено много особенностей. Давайте создадим их по одному.

Предпосылки

Создание приложения

Первым шагом является создание нового приложения. Откройте командную строку и создайте новый каталог для приложения. Сделайте это текущим каталогом. Введите команду dotnet new console в командной строке. При этом создаются начальные файлы для базового приложения Hello World.

Прежде чем приступить к внесению изменений, давайте запустите простое приложение Hello World. После создания приложения введите dotnet run в командной строке. Эта команда запускает процесс восстановления пакета NuGet, создает исполняемый файл приложения и запускает исполняемый файл.

Простой код приложения Hello World находится в Program.cs. Откройте этот файл с помощью избранного текстового редактора. Замените код в Program.cs следующим кодом:

namespace TeleprompterConsole;

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

В верхней части файла см. инструкцию namespace. Как и другие языки, ориентированные на объекты, которые вы могли использовать, C# использует пространства имен для упорядочивания типов. Эта программа "Hello World" ничем не отличается. Вы можете увидеть, что программа находится в пространстве имен с именем TeleprompterConsole.

Чтение и эхо файла

Первая функция, которую необходимо добавить, — это возможность считывания текстового файла и отображения всего этого текста в консоли. Сначала добавим текстовый файл. Скопируйте файл sampleQuotes.txt из репозитория GitHub для этого примера в каталог проекта. Это будет служить скриптом для приложения. Сведения о том, как скачать образец приложения для этого руководства, см. в инструкциях Примеры и учебники.

Затем добавьте следующий метод в класс Program (справа под методом Main):

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

Этот метод представляет собой особый тип метода C#, который называется методом итератора. Методы итератора возвращают последовательности, которые вычисляются лениво. Это означает, что каждый элемент в последовательности создается по мере запроса кода, потребляющего последовательность. Методы итератора — это методы, содержащие один или более операторов yield return. Объект, возвращаемый методом ReadFrom, содержит код для создания каждого элемента в последовательности. В этом примере предполагается чтение следующей строки текста из исходного файла и возвращение этой строки. Каждый раз, когда вызывающий код запрашивает следующий элемент из последовательности, код считывает следующую строку текста из файла и возвращает его. Когда файл полностью считывается, последовательность указывает, что больше элементов нет.

Существует два элемента синтаксиса C#, которые могут быть новыми для вас. Инструкция using в этом методе управляет очисткой ресурсов. Переменная, инициализированная в инструкции using (reader, в этом примере) должна реализовать интерфейс IDisposable. Этот интерфейс определяет один метод, Dispose, который должен вызываться при выпуске ресурса. Компилятор создает этот вызов, когда выполнение достигает закрывающей скобки оператора using. Код, сгенерированный компилятором, гарантирует, что ресурс освобождается, даже если из кода внутри блока, определяемого оператором using, происходит выброс исключения.

Переменная reader определяется с помощью ключевого слова var. var определяет неявно типизированную локальную переменную. Это означает, что тип переменной определяется типом времени компиляции объекта, назначенного переменной. Здесь возвращается значение из метода OpenText(String), который является объектом StreamReader.

Теперь давайте заполним код для чтения файла в методе Main:

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

Запустите программу (с помощью dotnet run) и вы увидите каждую строку, напечатанную в консоли.

Добавление задержек и форматирование вывода

То, что у вас отображается, слишком быстро, чтобы читать вслух. Теперь необходимо добавить задержки в выходных данных. При запуске вы создадите некоторый основной код, который обеспечивает асинхронную обработку. Однако эти первые шаги будут следовать нескольким антипаттернам. Антишаблоны указываются в комментариях при добавлении кода, и код будет обновлен в последующих шагах.

В этом разделе описано два шага. Во-первых, вы обновите метод итератора, чтобы возвращать отдельные слова вместо целых строк. Это сделано с этими изменениями. Замените инструкцию yield return line; следующим кодом:

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

Затем необходимо изменить способ использования строк файла и добавить задержку после написания каждого слова. Замените инструкцию Console.WriteLine(line) в методе Main следующим блоком:

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();
}

Запустите пример и проверьте выходные данные. Теперь каждое слово печатается, за которым следует задержка в 200 мс. Однако отображаемые выходные данные показывают некоторые проблемы, так как исходный текстовый файл содержит несколько строк с более чем 80 символами без разрыва строки. Это может быть трудно прочитать, пока текст прокручивается. Это легко исправить. Вы просто следите за длиной каждой строки и создадите новую строку, когда длина строки достигает определенного порогового значения. Объявите локальную переменную после объявления words в методе ReadFrom, которая будет хранить длину строки.

var lineLength = 0;

Затем добавьте следующий код после конструкции yield return word + " "; (перед закрывающей скобкой):

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

Запустите пример, и вы сможете читать вслух с заранее настроенной скоростью.

Асинхронные задачи

На этом последнем шаге вы добавите код для асинхронной записи выходных данных в одной задаче, а также выполнение другой задачи для чтения входных данных от пользователя, если они хотят ускорить или замедлить отображение текста, или полностью остановить отображение текста. Это включает несколько шагов, и к концу у вас будут все необходимые обновления. Первым шагом является создание асинхронного метода Task, который возвращает код, созданный для чтения и отображения файла.

Добавьте этот метод в класс Program (он взят из текста метода 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);
        }
    }
}

Вы заметите два изменения. Во-первых, в тексте метода вместо вызова Wait() для синхронного ожидания завершения задачи эта версия использует ключевое слово await. Для этого необходимо добавить модификатор async в сигнатуру метода. Этот метод возвращает Task. Обратите внимание, что ни один оператор return не возвращает объект Task. Вместо этого этот объект Task создается кодом, который компилятор создает при использовании оператора await. Вы можете представить, что этот метод возвращается, когда он достигает await. Возвращенный Task указывает, что работа не завершена. Метод возобновляется, когда ожидается выполнение задачи. После полного выполнения возвращаемое значение Task указывает, что оно завершено. Вызывающий код может отслеживать возвращаемый Task, чтобы определить момент его завершения.

Добавьте ключевое слово await перед вызовом ShowTeleprompter:

await ShowTeleprompter();

Для этого необходимо изменить сигнатуру метода Main на:

static async Task Main(string[] args)

Узнайте больше о методе async Main в разделе основы.

Затем необходимо написать второй асинхронный метод для чтения из консоли и следить за клавишами '<' (меньше), '>' (больше) и 'X' или 'x'. Ниже приведен метод, который вы добавили для этой задачи:

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);
}

Это создает лямбда-выражение для представления делегата Action, который считывает ключ из консоли и изменяет локальную переменную, представляющую задержку, когда пользователь нажимает клавиши "<" (меньше) или ">" (больше). Метод делегата завершается, когда пользователь нажимает клавиши X или X, что позволяет пользователю останавливать отображение текста в любое время. Этот метод использует ReadKey() для блокировки и ожидания нажатия клавиши пользователем.

Чтобы завершить эту функцию, необходимо создать новый метод возврата async Task, который запускает обе эти задачи (GetInput и ShowTeleprompter), а также управляет общими данными между этими двумя задачами.

Пришло время создать класс, который может обрабатывать общие данные между этими двумя задачами. Этот класс содержит два общедоступных свойства: задержку и флаг Done, чтобы указать, что файл полностью считывается:

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;
    }
}

Поместите этот класс в новый файл и включите этот класс в пространство имен TeleprompterConsole, как показано ниже. Кроме того, необходимо добавить инструкцию using static в верхней части файла, чтобы можно было ссылаться на методы Min и Max без вложенных имен классов или пространств имен. Инструкция using static импортирует методы из одного класса. Это отличается от инструкции using без static, которая импортирует все классы из пространства имен.

using static System.Math;

Затем необходимо обновить методы ShowTeleprompter и GetInput для использования нового объекта config. Напишите финальный метод Task, который возвращает async, чтобы запустить обе задачи и завершить выполнение, когда первая задача завершится.

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

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

Одним из новых методов является вызов WhenAny(Task[]). Это создает Task, который завершает выполнение как только любая из задач в списке его аргументов завершится.

Затем необходимо обновить методы ShowTeleprompter и GetInput, чтобы использовать объект config для задержки:

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);
}

Эта новая версия ShowTeleprompter вызывает новый метод в классе TeleprompterConfig. Теперь необходимо обновить Main для вызова RunTeleprompter вместо ShowTeleprompter:

await RunTeleprompter();

Заключение

В этом руководстве показано несколько функций языка C# и библиотек .NET Core, связанных с работой в консольных приложениях. Вы можете опираться на эти знания, чтобы узнать больше о языке и классах, представленных здесь. Вы узнали основы ввода-вывода файлов и консоли, освоили блокированное и неблокированное использование асинхронного программирования на основе задач, получили обзор возможностей языка C#, узнали, как организованы программы на C#, и познакомились с интерфейсом командной строки .NET.

Дополнительные сведения о файловом вводе-выводе см. в File and Stream I/O. Дополнительные сведения об асинхронной модели программирования, используемой в этом руководстве, см. в асинхронного программирования на основе задач и асинхронного программирования.