Udostępnij za pośrednictwem


Aplikacja konsolowa

W tym samouczku przedstawiono wiele funkcji na platformie .NET i w języku C#. Dowiesz się:

  • Podstawy interfejsu wiersza polecenia platformy .NET
  • Struktura aplikacji konsolowej języka C#
  • Wejście/Wyjście konsoli
  • Podstawowe informacje o interfejsach API do operacji we/wy plików w .NET
  • Podstawy programowania asynchronicznego opartego na zadaniach na platformie .NET

Utworzysz aplikację, która odczytuje plik tekstowy i powtórzy zawartość tego pliku tekstowego w konsoli. Dane wyjściowe do konsoli są zgodne z odczytywaniem go na głos. Możesz przyspieszyć lub spowolnić tempo, naciskając "<" (mniejsze niż) lub ">" (większe niż). Tę aplikację można uruchomić w systemach Windows, Linux, macOS lub w kontenerze platformy Docker.

Ten samouczek zawiera wiele funkcji. Skompilujmy je pojedynczo.

Wymagania wstępne

  • Najnowszy .NET SDK
  • Edytor programu Visual Studio Code
  • Zestaw deweloperski C#

Tworzenie aplikacji

Pierwszym krokiem jest utworzenie nowej aplikacji. Otwórz wiersz polecenia i utwórz nowy katalog dla aplikacji. Utwórz bieżący katalog. Wpisz polecenie dotnet new console w wierszu polecenia. Spowoduje to utworzenie plików startowych dla podstawowej aplikacji "Hello World".

Przed rozpoczęciem wprowadzania modyfikacji uruchomimy prostą aplikację Hello World. Po utworzeniu aplikacji wpisz dotnet run w wierszu polecenia. To polecenie uruchamia proces przywracania pakietów NuGet, tworzy plik wykonywalny aplikacji i uruchamia plik wykonywalny.

Prosty kod aplikacji Hello World znajduje się w Program.cs. Otwórz ten plik przy użyciu ulubionego edytora tekstów. Zastąp kod w Program.cs następującym kodem:

namespace TeleprompterConsole;

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

W górnej części pliku zobacz instrukcję namespace. Podobnie jak w przypadku innych języków zorientowanych na obiekty, język C# używa przestrzeni nazw do organizowania typów. Ten program Hello World nie różni się. Widać, że program znajduje się w przestrzeni nazw o nazwie TeleprompterConsole.

Odczytywanie i echo pliku

Pierwszą funkcją do dodania jest możliwość odczytywania pliku tekstowego i wyświetlania całego tego tekstu w konsoli programu . Najpierw dodajmy plik tekstowy. Skopiuj plik sampleQuotes.txt z repozytorium GitHub dla tego przykładowego do katalogu projektu. Będzie to służyć jako skrypt dla aplikacji. Aby uzyskać informacje na temat pobierania przykładowej aplikacji na potrzeby tego samouczka, zobacz instrukcje w Przykłady i samouczki.

Następnie dodaj następującą metodę w klasie Program (tuż poniżej metody Main):

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

Ta metoda jest specjalnym typem metody języka C# o nazwie metody iteratora . Metody iteracyjne zwracają sekwencje, które są oceniane leniwie. Oznacza to, że każdy element w sekwencji jest generowany, ponieważ jest on żądany przez kod korzystający z sekwencji. Metody iteracyjne to metody, które zawierają co najmniej jedną instrukcję yield return. Obiekt zwrócony przez metodę ReadFrom zawiera kod do wygenerowania każdego elementu w sekwencji. W tym przykładzie obejmuje to odczytywanie następnego wiersza tekstu z pliku źródłowego i zwracanie tego ciągu. Za każdym razem, gdy kod wywołujący żąda następnego elementu z sekwencji, kod odczytuje następny wiersz tekstu z pliku i zwraca go. Gdy plik jest całkowicie odczytywany, sekwencja wskazuje, że nie ma więcej elementów.

Istnieją dwa elementy składni języka C#, które mogą być dla Ciebie nowe. Instrukcja using w tej metodzie zarządza oczyszczaniem zasobów. Zmienna zainicjowana w instrukcji using (readerw tym przykładzie) musi implementować interfejs IDisposable. Ten interfejs definiuje jedną metodę, Dispose, która powinna być wywoływana, gdy należy zwolnić zasób. Kompilator generuje to wywołanie, gdy wykonanie osiągnie zamykający nawias klamrowy instrukcji using. Kod wygenerowany przez kompilator gwarantuje, że zasób zostanie zwolniony, nawet jeśli wystąpi wyjątek w kodzie w bloku zdefiniowanym przez instrukcję using.

Zmienna reader jest definiowana przy użyciu słowa kluczowego var. var definiuje niejawnie typizowanej zmiennej lokalnej. Oznacza to, że typ zmiennej jest określany przez typ czasu kompilacji obiektu przypisanego do zmiennej. W tym miejscu jest to wartość zwracana z metody OpenText(String), która jest obiektem StreamReader.

Teraz wypełnijmy kod, aby odczytać plik w metodzie Main:

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

Uruchom program (przy użyciu dotnet run) i możesz zobaczyć każdy wiersz wyświetlony na konsoli.

Dodawanie opóźnień i formatowanie wyników

To, co masz, jest wyświetlane zbyt szybko, by można było przeczytać to na głos. Teraz musisz dodać opóźnienia w danych wyjściowych. Na początku utworzysz część podstawowego kodu, który umożliwia przetwarzanie asynchroniczne. Jednak te pierwsze kroki będą podążać według kilku antywzorców. Anty-wzorce są wskazywane w komentarzach podczas dodawania kodu, a kod zostanie zaktualizowany w kolejnych krokach.

W tej sekcji są dwa kroki. Najpierw zaktualizujesz metodę iteratora, aby zwracała pojedyncze wyrazy zamiast całych wierszy. Odbywa się to przy użyciu tych modyfikacji. Zastąp instrukcję yield return line; następującym kodem:

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

Następnie należy zmodyfikować sposób odczytu wierszy pliku i dodać opóźnienie po zapisaniu każdego wyrazu. Zastąp instrukcję Console.WriteLine(line) w metodzie Main następującym blokiem:

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

Uruchom przykład i sprawdź dane wyjściowe. Teraz każde pojedyncze słowo jest wyświetlane, a następnie następuje opóźnienie o 200 ms. Wyświetlany wynik pokazuje jednak pewne problemy, ponieważ źródłowy plik tekstowy zawiera kilka wierszy, które mają więcej niż 80 znaków bez złamania linii. To może być trudne do odczytania podczas przewijania. Jest to łatwe do naprawienia. Będziesz śledzić długość każdego wiersza i generować nowy wiersz, gdy długość linii osiągnie określony próg. Zadeklaruj zmienną lokalną po deklaracji words w metodzie ReadFrom, która przechowuje długość wiersza:

var lineLength = 0;

Następnie dodaj następujący kod po instrukcji yield return word + " "; (przed zamykającym nawiasem klamrowym):

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

Uruchom przykład i będziesz mógł czytać na głos we wstępnie skonfigurowanym tempie.

Zadania asynchroniczne

W tym ostatnim kroku dodasz kod do zapisu danych wyjściowych asynchronicznie w jednym zadaniu, a jednocześnie uruchamiasz inne zadanie odczytu danych wejściowych od użytkownika, jeśli chce przyspieszyć lub spowolnić wyświetlanie tekstu, lub całkowicie zatrzymać wyświetlanie tekstu. Obejmuje to kilka kroków i do końca będziesz mieć wszystkie potrzebne aktualizacje. Pierwszym krokiem jest utworzenie asynchronicznej metody, która zwraca Task, reprezentującej kod, który stworzyłeś do tej pory w celu odczytania i wyświetlenia pliku.

Dodaj tę metodę do klasy Program (pochodzi ona z treści metody 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);
        }
    }
}

Zauważysz dwie zmiany. Najpierw, w treści metody, zamiast wywoływać Wait(), aby synchronicznie czekać na zakończenie zadania, ta wersja używa słowa kluczowego await. Aby to zrobić, należy dodać modyfikator async do podpisu metody. Ta metoda zwraca Task. Zwróć uwagę, że nie ma instrukcji zwracających obiekt Task. Zamiast tego obiekt Task jest tworzony przez kod generowany przez kompilator podczas korzystania z operatora await. Można sobie wyobrazić, że ta metoda zwraca wartość, gdy osiągnie await. Zwrócony Task wskazuje, że praca nie została ukończona. Metoda zostanie wznowiona po zakończeniu oczekiwanego zadania. Po pełnym wykonaniu, zwrócony Task wskazuje, że proces został zakończony. Kod wywołujący może śledzić wartość zwróconą jako Task, aby określić, kiedy proces został ukończony.

Dodaj słowo kluczowe await przed wywołaniem metody ShowTeleprompter:

await ShowTeleprompter();

Wymaga to zmiany sygnatury metody Main na:

static async Task Main(string[] args)

Dowiedz się więcej o metodzie async Main w naszej sekcji Podstawy.

Następnie musisz napisać drugą asynchroniczną metodę, aby odczytywać dane z konsoli i monitorować klawisze '<' (mniej niż), '>' (więcej niż) oraz klawisze 'X' lub 'x'. Oto metoda dodana dla tego zadania:

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

Spowoduje to utworzenie wyrażenia lambda reprezentującego delegata Action, który odczytuje klucz z konsoli i modyfikuje zmienną lokalną reprezentującą opóźnienie, gdy użytkownik naciska "<" (mniejsze niż) lub ">" (większe niż). Metoda delegata zostaje zakończona, gdy użytkownik naciśnie klawisze "X" lub "x", to umożliwia użytkownikowi zatrzymanie wyświetlania tekstu w dowolnym momencie. Ta metoda używa ReadKey() do blokowania i oczekiwania, aby użytkownik nacisnął klawisz.

Aby ukończyć tę funkcję, należy utworzyć metodę zwracającą async Task, która uruchomi oba te zadania (GetInput i ShowTeleprompter), a także zarządza wspólnymi danymi między nimi.

Nadszedł czas, aby utworzyć klasę, która może obsługiwać udostępnione dane między tymi dwoma zadaniami. Ta klasa zawiera dwie właściwości publiczne: opóźnienie i flagę Done wskazującą, że plik został całkowicie odczytany:

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

Umieść tę klasę w nowym pliku i uwzględnij tę klasę w przestrzeni nazw TeleprompterConsole, jak pokazano. Należy również dodać instrukcję using static na początku pliku, aby można było odwoływać się do metod Min i Max bez podawania nazw klas ani przestrzeni nazw. Instrukcja using static importuje metody z jednej klasy. Jest to w przeciwieństwie do instrukcji using bez static, która importuje wszystkie klasy z przestrzeni nazw.

using static System.Math;

Następnie należy zaktualizować metody ShowTeleprompter i GetInput, aby użyć nowego obiektu config. Napisz jedną ostateczną metodę Task zwracającą async, aby uruchomić oba zadania i zakończyć działanie, gdy pierwsze z nich się zakończy.

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

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

Nową metodą tutaj jest wywołanie WhenAny(Task[]). Spowoduje to utworzenie Task, który zakończy się, gdy tylko zakończy się dowolne z zadań na liście argumentów.

Następnie należy zaktualizować metody ShowTeleprompter i GetInput, aby użyć obiektu config do opóźnienia.

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

Ta nowa wersja ShowTeleprompter wywołuje nową metodę w klasie TeleprompterConfig. Teraz należy zaktualizować Main, aby wywołać RunTeleprompter zamiast ShowTeleprompter:

await RunTeleprompter();

Podsumowanie

W tym samouczku przedstawiono wiele funkcji dotyczących języka C# i bibliotek platformy .NET Core związanych z pracą w aplikacjach konsolowych. Możesz opierać się na tej wiedzy, aby dowiedzieć się więcej na temat języka i klas wprowadzonych tutaj. Znasz już podstawy operacji we/wy plików i konsoli, blokowanie i nieblokowanie korzystania z programowania asynchronicznego opartego na zadaniach, przewodnik po języku C# i sposób organizowania programów w języku C# oraz interfejsu wiersza polecenia platformy .NET.

Aby uzyskać więcej informacji na temat operacji we/wy plików, zobacz plik i strumień we/wy. Aby uzyskać więcej informacji na temat modelu programowania asynchronicznego używanego w tym samouczku, zobacz Programowanie asynchroniczne oparte na zadaniach i Programowanie asynchroniczne.