Freigeben über


Konsolen-App

In diesem Lernprogramm lernen Sie eine Reihe von Features in .NET und der C#-Sprache kennen. Sie lernen Folgendes:

  • Die Grundlagen der .NET CLI
  • Die Struktur einer C#-Konsolenanwendung
  • Konsolen-E/A
  • Die Grundlagen von Datei-E/A-APIs in .NET
  • Die Grundlagen der aufgabenbasierten asynchronen Programmierung in .NET

Sie erstellen eine Anwendung, die eine Textdatei liest, und gibt den Inhalt dieser Textdatei an die Konsole an. Das Tempo der Ausgabe in der Konsole ist so festgelegt, dass ein lautes Mitlesen möglich ist. Sie können das Tempo beschleunigen oder verlangsamen, indem Sie die Tasten "<" (kleiner als) oder ">" (größer als) drücken. Sie können diese Anwendung unter Windows, Linux, macOS oder in einem Docker-Container ausführen.

In diesem Lernprogramm gibt es viele Features. Lassen Sie uns sie einzeln erstellen.

Voraussetzungen

Erstellen der App

Der erste Schritt besteht darin, eine neue Anwendung zu erstellen. Öffnen Sie eine Eingabeaufforderung, und erstellen Sie ein neues Verzeichnis für Ihre Anwendung. Legen Sie das Verzeichnis als aktuelles Verzeichnis fest. Geben Sie den Befehl dotnet new console an der Eingabeaufforderung ein. Dadurch werden die Startdateien für eine einfache "Hello World"-Anwendung erstellt.

Bevor Sie mit dem Vornehmen von Änderungen beginnen, führen wir die einfache Hello World-Anwendung aus. Geben Sie nach dem Erstellen der Anwendung an der Eingabeaufforderung dotnet run ein. Dieser Befehl führt den NuGet-Paketwiederherstellungsprozess aus, erstellt die ausführbare Anwendung und führt die ausführbare Datei aus.

Der einfache Hello World-Anwendungscode ist in Program.csenthalten. Öffnen Sie diese Datei mit Ihrem bevorzugten Text-Editor. Ersetzen Sie den Code in Program.cs durch den folgenden Code:

namespace TeleprompterConsole;

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

Am Anfang der Datei sehen Sie eine namespace-Anweisung. Wie andere objektorientierte Sprachen, die Sie möglicherweise verwendet haben, verwendet C# Namespaces zum Organisieren von Typen. Dieses Hello World-Programm unterscheidet sich nicht. Sie können sehen, dass sich das Programm im Namespace mit dem Namen TeleprompterConsolebefindet.

Lesen und Ausgeben der Datei

Das erste Hinzuzufügende Feature ist die Möglichkeit, eine Textdatei zu lesen und den gesamten Text in der Konsole anzuzeigen. Als Erstes fügen wir eine Textdatei hinzu. Kopieren Sie die sampleQuotes.txt Datei aus dem GitHub-Repository für dieses Beispiel in Ihr Projektverzeichnis. Dies dient als Skript für Ihre Anwendung. Informationen zum Herunterladen der Beispiel-App für dieses Lernprogramm finden Sie in den Anweisungen in Beispielen und Lernprogrammen.

Fügen Sie als Nächstes die folgende Methode in Ihrer Program Klasse hinzu (direkt unterhalb der Main-Methode):

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

Diese Methode ist ein spezieller Typ von C#-Methode, die eine Iteratormethodegenannt wird. Iteratormethoden geben Sequenzen zurück, die verzögert ausgewertet werden. Dies bedeutet, dass jedes Element in der Sequenz generiert wird, da es vom Code angefordert wird, der die Sequenz verbraucht. Iteratormethoden sind Methoden, die eine oder mehrere yield return-Anweisungen enthalten. Das von der ReadFrom-Methode zurückgegebene Objekt enthält den Code, um jedes Element in der Sequenz zu generieren. In diesem Beispiel umfasst dies das Lesen der nächsten Textzeile aus der Quelldatei und das Zurückgeben dieser Zeichenfolge. Jedes Mal, wenn der aufrufende Code das nächste Element aus der Sequenz anfordert, liest der Code die nächste Textzeile aus der Datei und gibt es zurück. Wenn die Datei vollständig gelesen wird, gibt die Sequenz an, dass keine weiteren Elemente vorhanden sind.

Es gibt zwei C#-Syntaxelemente, die Ihnen möglicherweise neu sind. Die using-Anweisung in dieser Methode verwaltet die Ressourcenbereinigung. Die Variable, die in der using-Anweisung initialisiert wird (reader, in diesem Beispiel), muss die IDisposable Schnittstelle implementieren. Diese Schnittstelle definiert eine einzelne Methode, Dispose, die aufgerufen werden soll, wenn die Ressource freigegeben werden soll. Der Compiler generiert diesen Aufruf, wenn die Ausführung die schließende geschweifte Klammer der using-Anweisung erreicht. Der vom Compiler generierte Code stellt sicher, dass die Ressource freigegeben wird, auch wenn eine Ausnahme aus dem Code im durch die using-Anweisung definierten Block ausgelöst wird.

Die reader Variable wird mithilfe des schlüsselworts var definiert. var definiert eine implizit typisierte lokale Variable. Das bedeutet, dass der Typ der Variablen durch den Kompilierungszeittyp des Objekts bestimmt wird, das der Variablen zugewiesen ist. Dies ist der Rückgabewert aus der OpenText(String)-Methode, bei der es sich um ein StreamReader-Objekt handelt.

Jetzt füllen wir den Code aus, um die Datei in der Main-Methode zu lesen:

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

Führen Sie das Programm (unter Verwendung von dotnet run) aus. Jede Zeile wird einzeln an der Konsole ausgegeben.

Hinzufügen von Verzögerungen und Formatieren der Ausgabe

Was Sie haben, wird viel zu schnell angezeigt, um es laut vorzulesen. Jetzt müssen Sie die Verzögerungen in der Ausgabe hinzufügen. Als Erstes erstellen Sie einen Teil des Kerncodes, der die asynchrone Verarbeitung ermöglicht. Diese ersten Schritte folgen jedoch einigen Antimustern. Die Antimuster werden in Kommentaren beim Hinzufügen des Codes hervorgehoben, und der Code wird in späteren Schritten aktualisiert.

Dieser Abschnitt enthält zwei Schritte. Zuerst aktualisieren Sie die Iteratormethode so, dass einzelne Wörter anstelle von ganzen Zeilen zurückgegeben werden. Dies geschieht mit diesen Änderungen. Ersetzen Sie die yield return line;-Anweisung durch den folgenden Code:

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

Als Nächstes müssen Sie die Art und Weise ändern, wie Sie die Zeilen der Datei einlesen, und nach dem Schreiben jedes Wortes eine Verzögerung hinzufügen. Ersetzen Sie die Console.WriteLine(line)-Anweisung in der Main-Methode durch den folgenden Block:

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

Führen Sie das Beispiel aus, und überprüfen Sie die Ausgabe. Nun wird jedes einzelne Wort gedruckt, gefolgt von einer Verzögerung von 200 ms. Die angezeigte Ausgabe zeigt jedoch einige Probleme, da die Quelltextdatei mehrere Zeilen mit mehr als 80 Zeichen ohne Zeilenumbruch enthält. Der Text ist möglicherweise schwer zu lesen, wenn er ohne Umbrüche angezeigt wird. Das ist einfach zu beheben. Sie verfolgen einfach die Länge jeder Zeile und generieren eine neue Zeile, wenn die Zeilenlänge einen bestimmten Schwellenwert erreicht. Deklarieren Sie eine lokale Variable nach der Deklaration von words in der ReadFrom-Methode, die die Zeilenlänge enthält:

var lineLength = 0;

Fügen Sie dann den folgenden Code nach der yield return word + " ";-Anweisung hinzu (vor der schließenden Klammer):

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

Führen Sie das Beispiel aus. Jetzt sollten Sie in der Lage sein, den Text im festgelegten Tempo laut mitzulesen.

Asynchrone Aufgaben

In diesem letzten Schritt fügen Sie den Code hinzu, um die Ausgabe asynchron in einer Aufgabe zu schreiben, während sie auch eine andere Aufgabe ausführen, um eingaben vom Benutzer zu lesen, wenn sie die Textanzeige beschleunigen oder verlangsamen möchten, oder beenden Sie die Textanzeige vollständig. Dies enthält einige Schritte, und am Ende haben Sie alle benötigten Updates. Der erste Schritt besteht darin, eine asynchrone Task Rückgabemethode zu erstellen, die den code darstellt, den Sie bisher erstellt haben, um die Datei zu lesen und anzuzeigen.

Fügen Sie diese Methode Ihrer Program-Klasse hinzu (diese stammt aus dem Textkörper Ihrer Main-Methode):

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

Sie werden zwei Änderungen bemerken. Zuerst verwendet diese Version im Textkörper der Methode das Schlüsselwort await, anstatt Wait() zu verwenden, um synchron auf den Abschluss einer Aufgabe zu warten. Dazu müssen Sie der Methodensignatur den async Modifizierer hinzufügen. Diese Methode gibt einen Task zurück. Beachten Sie, dass es keine return-Anweisungen gibt, die ein Task-Objekt zurückgeben. Stattdessen wird dieses Task Objekt durch Code erstellt, den der Compiler generiert, wenn Sie den await-Operator verwenden. Sie können sich dies so vorstellen, dass die Methode eine Rückgabe durchführt, wenn sie ein await-Schlüsselwort erreicht. Die zurückgegebene Task gibt an, dass die Arbeit nicht abgeschlossen wurde. Die Methode wird fortgesetzt, wenn die erwartete Aufgabe abgeschlossen ist. Wenn sie zum Abschluss ausgeführt wurde, gibt die zurückgegebene Task an, dass sie abgeschlossen ist. Der aufrufende Code kann den zurückgegebenen Task überwachen, um zu ermitteln, wann dieser abgeschlossen ist.

Fügen Sie ein Schlüsselwort await vor dem Aufruf von ShowTeleprompterhinzu:

await ShowTeleprompter();

Dazu müssen Sie die Main Methodensignatur wie folgt ändern:

static async Task Main(string[] args)

Erfahren Sie mehr über die async Main Methode in unserem Abschnitt "Grundlagen".

Als Nächstes müssen Sie die zweite asynchrone Methode schreiben, um aus der Konsole zu lesen und auf die Tasten "<" (kleiner als), ">" (größer als) und "X" oder "x" zu achten. Hier ist die Methode, die Sie für diese Aufgabe hinzufügen:

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

Dadurch wird ein Lambda-Ausdruck erstellt, der einen Action Delegaten darstellt, der eine Taste aus der Konsole liest und eine lokale Variable ändert, die die Verzögerung darstellt, wenn der Benutzer die Tasten "<" (kleiner als) oder ">" (größer als) drückt. Die Delegatmethode wird beendet, wenn der Benutzer die Tasten "X" oder "x" drückt, wodurch der Benutzer die Textanzeige jederzeit beenden kann. Diese Methode verwendet ReadKey(), um zu blockieren und zu warten, bis der Benutzer eine Taste drückt.

Um dieses Feature abzuschließen, müssen Sie eine neue async Task-Rückgabe-Methode erstellen, die beide Aufgaben (GetInput und ShowTeleprompter) startet und die gemeinsamen Daten zwischen diesen beiden Aufgaben verwaltet.

Es ist an der Zeit, eine Klasse zu erstellen, die die gemeinsamen Daten zwischen diesen beiden Aufgaben verarbeiten kann. Diese Klasse enthält zwei öffentliche Eigenschaften: die Verzögerung und ein Flag Done, um anzugeben, dass die Datei vollständig gelesen wurde:

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

Fügen Sie diese Klasse in eine neue Datei ein, und fügen Sie diese Klasse wie dargestellt in den TeleprompterConsole Namespace ein. Außerdem müssen Sie oben in der Datei eine using static-Anweisung hinzufügen, damit Sie auf die methoden Min und Max ohne die eingeschlossenen Klassen- oder Namespacenamen verweisen können. Eine using static-Anweisung importiert die Methoden aus einer Klasse. Dies ist im Gegensatz zur using-Anweisung ohne static, die alle Klassen aus einem Namespace importiert.

using static System.Math;

Als Nächstes müssen Sie die methoden ShowTeleprompter und GetInput aktualisieren, um das neue config objekt zu verwenden. Schreiben Sie einen finalen Task, der die async-Methode zurückgibt, um beide Tasks zu starten und den Vorgang zu beenden, sobald der erste Task beendet wird:

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

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

Die neue Methode hier ist der WhenAny(Task[])-Aufruf. Hiermit wird ein Task erstellt, der abgeschlossen wird, sobald einer der Tasks in dieser Argumentliste beendet ist.

Als Nächstes müssen Sie sowohl die methoden ShowTeleprompter als auch GetInput aktualisieren, um das config-Objekt für die Verzögerung zu verwenden:

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

Diese neue Version von ShowTeleprompter ruft eine neue Methode in der TeleprompterConfig Klasse auf. Jetzt müssen Sie Main aktualisieren, um RunTeleprompter anstelle von ShowTeleprompteraufzurufen:

await RunTeleprompter();

Schlussfolgerung

In diesem Lernprogramm wurde Ihnen eine Reihe von Features zu der C#-Sprache und den .NET Core-Bibliotheken für die Arbeit mit Konsolenanwendungen gezeigt. Sie können auf diesem Wissen aufbauen, um mehr über die Sprache und die hier eingeführten Kurse zu erfahren. Sie haben die Grundlagen von Datei- und Konsolen-E/A, die blockierende und nicht blockierende Verwendung der aufgabenbasierten asynchronen Programmierung, einen Überblick über die C#-Sprache und die Organisation von C#-Programmen sowie die .NET-CLI kennengelernt.

Weitere Informationen zur Datei-E/A finden Sie unter Datei- und Stream-E/A. Weitere Informationen zum in diesem Lernprogramm verwendeten asynchronen Programmierungsmodell finden Sie unter Aufgabenbasierte asynchrone Programmierung und asynchrone Programmierung.