Application console

Ce didacticiel vous présente un certain nombre de fonctionnalités de .NET et du langage C#. Vous apprendrez ce qui suit :

  • Principes de base du CLI .NET
  • La structure d’une application de console C#
  • E/S console
  • Les principes fondamentaux des API d’E/S de fichier dans .NET
  • Les principes fondamentaux de la programmation asynchrone basée sur des tâches dans .NET

Vous allez générer une application qui lit un fichier texte et retourne le contenu du fichier texte dans la console. La sortie sur la console se fait à un rythme permettant de la lire à haute voix. Vous pouvez accélérer ou ralentir la vitesse en appuyant sur les touches ’<’ (inférieur à) ou ’>’ (supérieur à). Vous pouvez exécuter cette application sur Windows, Linux, macOS ou dans un conteneur Docker.

Il existe un grand nombre de fonctionnalités dans ce didacticiel. Nous allons les construire une par une.

Prérequis

Créer l’application

La première étape consiste à créer une nouvelle application. Ouvrez une invite de commandes et créez un nouveau répertoire pour votre application. Réglez-le comme répertoire actuel. Saisissez la commande dotnet new console à l’invite. Elle crée les fichiers de démarrage d’une application « Hello World » de base.

Avant d’apporter des modifications, exécutons l’application simple Hello World. Après avoir créé l’application, saisissez dotnet run à l’invite de commandes. Cette commande exécute le processus de restauration de package NuGet, crée l’exécutable de l’application et l’exécute.

Le code d’application Hello World simple se trouve intégralement dans le fichier Program.cs. Ouvrez ce fichier avec votre éditeur de texte préféré. Remplacez le code dans Program.cs par le code suivant :

namespace TeleprompterConsole;

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

En haut du fichier, remarquez l’instruction namespace. Comme d’autres langages orientés objet que vous avez peut-être utilisés, C# utilise des espaces de noms pour organiser les types. Ce programme Hello World ne fait pas exception. Vous pouvez voir que le programme est placé dans l’espace de noms avec le nom TeleprompterConsole.

Lecture et affichage du fichier

La première fonctionnalité à ajouter est la capacité à lire un fichier texte et à afficher tout le texte dans la console. Tout d’abord, nous allons ajouter un fichier texte. Copiez le fichier sampleQuotes.txt à partir du dépôt GitHub pour cet exemple dans votre répertoire de projet. Il servira de script pour votre application. Pour plus d’informations sur le téléchargement de l’exemple d’application pour ce didacticiel, consultez les instructions dans Exemples et tutoriels.

Ensuite, ajoutez la méthode suivante dans votre classe Program (juste en dessous de la méthode Main) :

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

Cette méthode est un type spécial de méthode C# appelé méthode d’itérateur. Les méthodes d’itérateur retournent des séquences qui sont évaluées de manière tardive. Cela signifie que chaque élément de la séquence est généré lorsque cela est demandé par le code utilisant la séquence. Les méthodes d’itérateur sont des méthodes qui contiennent une ou plusieurs instructions yield return. L’objet retourné par la méthode ReadFrom contient le code pour générer chaque élément dans la séquence. Dans cet exemple, cela implique la lecture de la ligne de texte suivante à partir du fichier source et le renvoi de cette chaîne. Chaque fois que le code appelant demande l’élément suivant de la séquence, le code lit la ligne suivante du texte à partir du fichier et la renvoie. Quand le fichier est entièrement lu, la séquence indique qu’il n’y a pas d’autres éléments.

Il existe deux éléments de syntaxe de C# qui peuvent être nouveaux pour vous. L’instruction using dans cette méthode gère le nettoyage des ressources. La variable initialisée dans l’instruction using (reader, dans cet exemple) doit implémenter l’interface IDisposable. Cette interface définit une méthode unique, Dispose, qui doit être appelée lorsque les ressources doivent être libérées. Le compilateur génère l’appel lorsque l’exécution atteint l’accolade fermante de l’instruction using. Le code généré par le compilateur garantit que la ressource est libérée même si une exception est levée à partir du code dans le bloc défini par l’instruction using.

La variable reader est définie à l’aide du mot-clé var. var définit une variable locale implicitement typée. Cela signifie que le type de la variable est déterminé par le type au moment de la compilation de l’objet assigné à la variable. Ici, c’est la valeur retournée par la méthode OpenText(String), qui est un objet StreamReader.

À présent, nous allons remplir le code pour lire le fichier dans la méthode Main :

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

Exécutez le programme (à l’aide de dotnet run). Chaque ligne est envoyée sur la console.

Ajout de délais et de mise en forme à la sortie

Ce que vous avez s’affiche beaucoup trop rapidement pour le lire à haute voix. Vous devez maintenant ajouter des délais à la sortie. Lorsque vous commencez, vous créez une partie du code principal qui permet le traitement asynchrone. Toutefois, ces premières étapes suivront quelques anti-modèles. Les anti-modèles sont signalés dans les commentaires lorsque vous ajoutez le code, et le code sera actualisé ultérieurement.

Il existe deux étapes dans cette section. Tout d’abord, vous allez mettre à jour la méthode d’itérateur pour retourner des mots uniques au lieu de lignes entières. Vous le faites avec ces modifications. Remplacez l’instruction yield return line; par le code suivant :

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

Ensuite, vous devez modifier la façon dont vous consommez les lignes du fichier, et ajoutez un délai après chaque mot. Remplacez l’instruction Console.WriteLine(line) dans la méthode Main avec le bloc suivant :

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

Exécutez l’exemple et vérifiez le résultat. À présent, chaque mot unique est affiché séparément, suivi d’un délai de 200 ms. Toutefois, la sortie affichée présente certains problèmes, car le fichier texte source possède plusieurs lignes qui ont plus de 80 caractères sans saut de ligne. Ce qui peut être difficile à lire lors du défilement. Ce problème est facile à résoudre. Vous allez simplement effectuer le suivi de la longueur de chaque ligne et générer une nouvelle ligne chaque fois que la longueur de ligne atteint un certain seuil. Déclarez une variable locale après la déclaration de words dans la méthode ReadFrom qui contient la longueur de ligne :

var lineLength = 0;

Ensuite, ajoutez le code suivant après l’instruction yield return word + " "; (avant l’accolade fermante) :

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

Exécutez l’exemple et vous serez en mesure de lire à haute voix au rythme préconfiguré.

Tâches asynchrones

Dans cette dernière étape, vous allez ajouter le code pour écrire la sortie de façon asynchrone dans une tâche, lorsque vous exécutez également une autre tâche pour lire l’entrée de l’utilisateur s’il souhaite accélérer ou ralentir l’affichage du texte, ou l’arrêter. Cela représente plusieurs étapes et à la fin, vous aurez toutes les mises à jour dont vous avez besoin. La première étape consiste à créer une méthode de retour Task asynchrone qui représente le code que vous avez créé jusqu'à présent pour lire et afficher le fichier.

Ajoutez cette méthode à votre classe Program (elle est extraite du corps de votre méthode 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);
        }
    }
}

Vous remarquerez deux modifications. Tout d’abord, dans le corps de la méthode, au lieu d’appeler Wait() pour attendre de manière synchrone qu’une tâche se termine, cette version utilise le mot-clé await. Pour ce faire, vous devez ajouter le modificateur async pour la signature de méthode. Cette méthode renvoie une Task. Notez qu’il n’y a aucune instruction de retour renvoyant un objet Task. Au lieu de cela, cet objet Task est créé par le code que le compilateur génère lorsque vous utilisez l’opérateur await. Vous pouvez imaginer que cette méthode renvoie une valeur lorsqu’elle atteint un await. La Task renvoyée indique que la tâche n’a pas été effectuée. La méthode reprend lorsque la tâche attendue se termine. Lorsqu’elle a terminé son exécution, la Task renvoyée indique qu’elle est terminée. Le code appelant peut surveiller cette Task renvoyée pour déterminer si elle est terminée.

Ajoutez un mot clé await avant l’appel à ShowTeleprompter :

await ShowTeleprompter();

Pour cela, vous devez modifier la signature de la méthode Main en  :

static async Task Main(string[] args)

Pour en savoir plus sur la async Main méthode, consultez la section relative aux notions de base.

Ensuite, vous devez écrire la seconde méthode asynchrone pour lire à partir de la console et chercher les touches '<’ (inférieur à), ’>' (supérieur à) et 'X' ou 'x’. Voici la méthode que vous ajoutez pour cette tâche :

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

Cela crée une expression lambda pour représenter un délégué Action qui lit une touche de la console et modifie une variable locale représentant le délai lorsque l’utilisateur appuie sur les touches '<’ (inférieur à) et ’>' (supérieur à). La méthode déléguée se termine lorsque l’utilisateur appuie sur les touches 'X' ou 'x’, ce qui autorise l’utilisateur à arrêter l’affichage du texte à tout moment. Cette méthode utilise ReadKey() pour bloquer et attendre que l’utilisateur appuie sur une touche.

Pour terminer cette fonctionnalité, vous devez créer une nouvelle méthode de retour async Task qui démarre ces deux tâches (GetInput et ShowTeleprompter) et gère également les données partagées entre ces deux tâches.

Il est temps de créer une classe qui peut gérer les données partagées entre ces deux tâches. Cette classe contient deux propriétés publiques : le délai et un indicateur Done pour indiquer que le fichier a été entièrement lu :

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

Placez cette classe dans un nouveau fichier et incluez-la à l’espace de noms TeleprompterConsole, comme indiqué. Vous devrez également ajouter une instruction using static en haut du fichier pour pouvoir référencer les méthodes Min et Max sans les noms de la classe ou de l’espace de noms qui les contient. Une instruction using static importe les méthodes d’une classe. Cela contraste avec l’instruction using sans static, qui importe toutes les classes à partir d’un espace de noms.

using static System.Math;

Ensuite, vous devez mettre à jour les méthodes ShowTeleprompter et GetInput pour utiliser le nouvel objet config. Écrivez une dernière Task renvoyant une méthode async pour démarrer les deux tâches et quitter lorsque la première tâche est terminée :

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

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

Ici, la nouvelle méthode est l’appel WhenAny(Task[]). Elle crée une Task qui se termine dès que les tâches de sa liste d’arguments se terminent.

Ensuite, vous devez mettre à jour les méthodes ShowTeleprompter et GetInput pour utiliser l’objet config pour le délai :

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

Cette nouvelle version de ShowTeleprompter appelle une nouvelle méthode dans la classe TeleprompterConfig. À présent, vous devez mettre à jour Main pour appeler RunTeleprompter au lieu de ShowTeleprompter :

await RunTeleprompter();

Conclusion

Ce didacticiel vous a montré certaines des fonctionnalités du langage C# et les bibliothèques .NET Core liées au travail dans les applications console. Vous pouvez utiliser ces connaissances pour explorer davantage le langage ainsi que les classes présentées ici. Vous avez vu les principes de base des E/S de fichier et de console, l’utilisation bloquante et non bloquante de la programmation asynchrone basée sur des tâches, une présentation du langage C# et de la façon dont les programmes C# sont organisés, et le CLI .NET.

Pour plus d’informations sur les E/S de fichier, consultez Fichier et flux de données E/S. Pour plus d’informations sur le modèle de programmation asynchrone utilisé dans ce didacticiel, consultez Programmation asynchrone basée sur les tâches et Asynchronous programming.