App console

Questa esercitazione illustra numerose funzionalità in .NET e nel linguaggio C#. Si apprenderà:

  • Nozioni di base dell'interfaccia della riga di comando .NET
  • Struttura di un'applicazione console in C#
  • Input/output della console
  • Nozioni di base sulle API di I/O dei file in .NET
  • Nozioni di base sul modello di programmazione asincrona delle attività in .NET

Si creerà un'applicazione che legge un file di testo ed echi il contenuto del file di testo nella console. La velocità di riproduzione dell'output della console verrà quindi configurata in modo da consentirne la lettura ad alta voce. È possibile velocizzare o rallentare il ritmo premendo "" (minore di) o "<>" (maggiore di) tasti. È possibile eseguire questa applicazione in Windows, Linux, macOS o in un contenitore Docker.

In questa esercitazione verranno create anche Creiamoli uno per uno.

Prerequisiti

Creare l'app

Il primo passaggio consiste nel creare una nuova applicazione. Aprire un prompt dei comandi e creare una nuova directory per l'applicazione, impostandola come directory corrente. Digitare il comando dotnet new console al prompt dei comandi Questa operazione crea i file iniziali per un'applicazione "Hello World" di base.

Prima di iniziare a apportare modifiche, eseguire la semplice applicazione Hello World. Dopo aver creato l'applicazione, digitare dotnet run al prompt dei comandi. Questo comando esegue il processo di ripristino del pacchetto NuGet, crea l'eseguibile dell'applicazione ed esegue il file eseguibile.

Il codice dell'applicazione Hello World semplice è tutto in Program.cs. Aprire il file con un editor di testo Sostituire il codice in Program.cs con il codice seguente:

namespace TeleprompterConsole;

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

Nella parte superiore del file vedere un'istruzione namespace . Come altri linguaggi orientati agli oggetti, anche C# ricorre agli spazi dei nomi per organizzare i tipi. Il programma Hello World non è diverso. È possibile notare che il programma si trova nello spazio dei nomi con il nome TeleprompterConsole.

Lettura e restituzione del file

La prima funzionalità da aggiungere è la capacità di leggere un file di testo e visualizzare tutto il testo nella console. Prima di tutto, aggiungiamo un file di testo. Copiare il file sampleQuotes.txt dal repository GitHub di questo esempio alla directory del progetto. Questo file verrà usato come script per l'applicazione. Per informazioni su come scaricare l'app di esempio per questa esercitazione, vedere le istruzioni riportate in Esempi ed esercitazioni.

Aggiungere quindi il metodo seguente alla classe Program (immediatamente sotto il metodo Main):

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

Questo metodo è un tipo speciale di metodo C# denominato metodo iteratore. I metodi iteratore restituiscono sequenze valutate in modo pigri. In altre parole, ogni elemento della sequenza viene generato nel momento in cui viene richiesto dal codice che utilizza la sequenza. I metodi iteratore sono metodi che contengono una o più yield return istruzioni. L'oggetto restituito dal metodo ReadFrom contiene il codice per generare ogni elemento della sequenza. In questo esempio, ciò consiste nella lettura della riga di testo successiva dal file di origine e nella restituzione della stringa. Ogni volta che il codice chiamante richiede l'elemento successivo della sequenza, il codice legge la riga di testo successiva dal file e la restituisce. Quando il file è stato letto completamente, la sequenza indica che non sono presenti altri elementi.

Esistono due elementi di sintassi C# che potrebbero essere nuovi. L'istruzione using gestisce la pulizia delle risorse in questo metodo. La variabile inizializzata nell'istruzione using (reader, in questo esempio) deve implementare l'interfaccia IDisposable. Tale interfaccia definisce un singolo metodo, Dispose, che deve essere chiamato quando deve essere rilasciata la risorsa. Il compilatore genera la chiamata quando l'esecuzione raggiunge la parentesi graffa di chiusura dell'istruzione using. Il codice generato dal compilatore assicura che la risorsa venga rilasciata anche se viene generata un'eccezione dal codice nel blocco definito tramite l'istruzione using.

La variabile reader viene definita tramite la parola chiave var. var definisce una variabile locale tipizzata in modo implicito, ovvero il tipo della variabile è determinato dal tipo in fase di compilazione dell'oggetto assegnato alla variabile. In questo caso corrisponde al valore restituito dal metodo OpenText(String), ovvero a un oggetto StreamReader.

A questo punto, compilare il codice per leggere il file nel Main metodo:

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

Eseguire il programma, usando dotnet run in modo da poter visualizzare ogni riga visualizzata nella console.

Aggiunta di ritardi e formattazione dell'output

Il testo restituito viene visualizzato troppo velocemente per potere essere letto a voce alta. È quindi necessario aggiungere ritardi nell'output. Come si inizia, si creerà un certo codice di base che consente l'elaborazione asincrona. Questi primi passaggi dovranno tuttavia seguire alcuni anti-pattern, evidenziati nei commenti mentre si aggiunge il codice, e il codice verrà aggiornato nei passaggi successivi.

Questa sezione è articolata in due fasi. Prima di tutto, si aggiornerà il metodo iteratore per restituire singole parole anziché tutte le righe. Questa operazione viene eseguita con queste modifiche. Sostituire la funzione yield return line; con il codice seguente:

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

Sarà quindi necessario modificare il modo in cui vengono usate le righe del file e aggiungere un ritardo dopo la scrittura di ogni parola. Sostituire l'istruzione Console.WriteLine(line) nel metodo Main con il blocco seguente:

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

Eseguire l'esempio e verificare l'output. Ogni singola parola viene ora visualizzata, seguita da un ritardo di 200 ms. L'output visualizzato mostra tuttavia alcuni problemi perché il file di testo di origine presenta più righe con oltre 80 caratteri senza interruzione di riga, che possono risultare difficili da leggere durante lo scorrimento. Questo è facile da correggere. Tenere traccia della lunghezza di ogni riga e generare una nuova riga ogni volta che la lunghezza della linea raggiunge una determinata soglia. Dichiarare una variabile locale dopo la dichiarazione di words nel metodo ReadFrom che contiene la lunghezza di riga:

var lineLength = 0;

Aggiungere quindi il codice seguente dopo l'istruzione yield return word + " "; (prima della parentesi graffa di chiusura):

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

Eseguire l'esempio e sarà possibile leggere ad alta voce al suo ritmo preconfigurato.

Attività asincrone

In questo passaggio finale si aggiungerà il codice per scrivere l'output in modo asincrono in un'attività, eseguendo anche un'altra attività per leggere l'input dall'utente se vogliono velocizzare o rallentare la visualizzazione del testo o arrestare completamente la visualizzazione del testo. Questo ha alcuni passaggi in esso e alla fine, si avranno tutti gli aggiornamenti necessari. Il primo passaggio consiste nel creare un metodo di restituzione asincrono Task che rappresenta il codice creato finora per leggere e visualizzare il file.

Aggiungere questo metodo alla Program classe (è tratto dal corpo del Main metodo):

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

Si noteranno due modifiche. Nel corpo del metodo, anziché chiamare Wait() per attendere in modo sincrono il completamento di un'attività, questa versione usa la parola chiave await. A questo scopo, è necessario aggiungere il modificatore async alla firma del metodo. Il metodo restituisce Task. Osservare inoltre come non siano presenti istruzioni return che restituiscono un oggetto Task. L'oggetto Task, infatti, viene creato dal codice che il compilatore genera quando si usa l'operatore await. È possibile immaginare quindi che il metodo restituisca l'oggetto quando raggiunge la parola chiave await. L'oggetto Task restituito indica che il lavoro non è stato completato. Il metodo riprende l'esecuzione quando viene completata l'attività attesa. Al termine dell'esecuzione, l'oggetto Task restituito indica che il lavoro è stato completato. Il codice chiamante può monitorare l'oggetto Task restituito per determinare quando è stato completato.

Aggiungere una await parola chiave prima della chiamata a ShowTeleprompter:

await ShowTeleprompter();

È quindi necessario modificare la firma del Main metodo in:

static async Task Main(string[] args)

Altre informazioni sul async Main metodo sono disponibili nella sezione Concetti fondamentali.

Successivamente, è necessario scrivere il secondo metodo asincrono per leggere dalla console e watch per le chiavi '' (minore di), '<>' (maggiore di) e 'X' o 'x'. Ecco il metodo aggiunto per l'attività:

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

Viene creata un'espressione lambda per rappresentare un Action delegato che legge una chiave dalla console e modifica una variabile locale che rappresenta il ritardo quando l'utente preme i tasti '<' (minore di) o '>' (maggiore di). Il metodo delegato termina quando l'utente preme i tasti 'X' o 'x', che consentono all'utente di arrestare la visualizzazione del testo in qualsiasi momento. Questo metodo usa ReadKey() per bloccare l'operazione e attendere che l'utente prema un tasto.

Per completare questa funzionalità, è necessario creare un nuovo metodo di restituzione async Task in grado di avviare entrambe le attività (GetInput e ShowTeleprompter) e gestire i dati condivisi tra di esse.

È ora di creare una classe in grado di gestire i dati condivisi tra queste due attività. Questa classe contiene due proprietà pubbliche: il ritardo e un flag Done per indicare che il file è stato letto interamente:

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

Inserire tale classe in un nuovo file e includere tale classe nello TeleprompterConsole spazio dei nomi, come illustrato. Sarà anche necessario aggiungere un'istruzione using static all'inizio del file in modo da poter fare riferimento ai Min metodi e Max senza i nomi della classe o dello spazio dei nomi che lo racchiude. Un'istruzione using static importa i metodi da una classe. In contrasto con l'istruzione using senza static, che importa tutte le classi da uno spazio dei nomi.

using static System.Math;

Sarà quindi necessario aggiornare i metodi ShowTeleprompter e GetInput affinché usino il nuovo oggetto config. Scrivere un ultimo metodo Task di restituzione dell'oggetto async, in grado di avviare entrambe le attività e chiudere la prima attività nel momento in cui viene completata:

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

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

Il nuovo metodo qui è la chiamata WhenAny(Task[]). In questo modo si crea un oggetto Task che termina non appena viene completata un'attività inclusa nel relativo elenco di argomenti.

È ora necessario aggiornare i metodi ShowTeleprompter e GetInput affinché usino l'oggetto config per il ritardo:

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

Questa nuova versione di ShowTeleprompter chiama un nuovo metodo nella classe TeleprompterConfig. A questo punto, è necessario aggiornare Main affinché chiami RunTeleprompter anziché ShowTeleprompter:

await RunTeleprompter();

Conclusione

In questa esercitazione sono state illustrate diverse funzionalità del linguaggio C# e presentate le librerie .NET Core correlate all'uso di applicazioni console. A partire da queste informazioni è possibile approfondire la conoscenza del linguaggio e delle classi presentate nell'esercitazione. Sono state apprese le nozioni di base di I/O di File e console, il blocco e l'uso non bloccante della programmazione asincrona basata su attività, una panoramica del linguaggio C# e la modalità di organizzazione dei programmi C# e l'interfaccia della riga di comando di .NET.

Per altre informazioni sull'I/O dei file, vedere I/O di file e flusso. Per altre informazioni sul modello di programmazione asincrona usato in questa esercitazione, vedere Programmazione asincrona basata su attività e Programmazione asincrona.