Aplikacja konsolowa

Ten samouczek zawiera informacje o wielu funkcjach platformy .NET i języku C#. Dowiesz się:

  • Podstawy interfejsu wiersza polecenia platformy .NET
  • Struktura aplikacji konsolowej języka C#
  • We/wy konsoli
  • Podstawy interfejsów API we/wy plików na platformie .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 odczytem na głos. Możesz przyspieszyć lub spowolnić tempo, naciskając klawisze "<" (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

Tworzenie aplikacji

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

Przed rozpoczęciem wprowadzania modyfikacji uruchommy prostą aplikację Hello world. Po utworzeniu aplikacji wpisz dotnet run polecenie 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 pliku Program.cs. Otwórz ten plik za pomocą ulubionego edytora tekstów. Zastąp kod w pliku 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. Najpierw dodajmy plik tekstowy. Skopiuj plik sampleQuotes.txt z repozytorium GitHub dla tego przykładu do katalogu projektu. Będzie to służyć jako skrypt aplikacji. Aby uzyskać informacje na temat pobierania przykładowej aplikacji na potrzeby tego samouczka, zobacz instrukcje w temacie Przykłady i samouczki.

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

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# nazywanym metodą iteratora. Metody iteracyjne zwracają sekwencje, które są oceniane z opóźnieniem. 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ą yield return instrukcję. Obiekt zwrócony przez metodę ReadFrom zawiera kod do wygenerowania każdego elementu w sekwencji. W tym przykładzie obejmuje to odczytanie następnego wiersza tekstu z pliku źródłowego i zwrócenie 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 odczytany, 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ć IDisposable interfejs. Ten interfejs definiuje jedną metodę , Disposektóra powinna być wywoływana, gdy zasób powinien zostać zwolniony. 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 zostanie zgłoszony wyjątek z kodu w bloku zdefiniowanym przez instrukcję using.

Zmienna jest definiowana reader 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 OpenText(String) metody , 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 polecenia dotnet run) i zobaczysz każdy wiersz wydrukowany w konsoli.

Dodawanie opóźnień i danych wyjściowych formatowania

To, co masz, jest wyświetlane zbyt szybko, aby przeczytać 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ą wykonywane w kilku antywzór. Anty-patterns są wskazywane w komentarzach podczas dodawania kodu, a kod zostanie zaktualizowany w kolejnych krokach.

W tej sekcji znajdują się 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 korzystania z 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żdy pojedynczy wyraz jest drukowany, po którym następuje opóźnienie 200 ms. Wyświetlane dane wyjściowe pokazują jednak pewne problemy, ponieważ źródłowy plik tekstowy zawiera kilka wierszy zawierających więcej niż 80 znaków bez podziału wiersza. To może być trudne do odczytania podczas przewijania. To łatwe do naprawienia. Będziesz śledzić długość każdego wiersza i generować nową linię za każdym razem, 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 w stanie odczytać go na głos we wstępnie skonfigurowanym tempie.

Zadania asynchroniczne

W tym ostatnim kroku dodasz kod, aby zapisać dane wyjściowe asynchronicznie w jednym zadaniu, a także uruchomić 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, a do końca będziesz mieć wszystkie potrzebne aktualizacje. Pierwszym krokiem jest utworzenie asynchronicznej Task metody zwracanej, która reprezentuje kod, który został utworzony do tej pory w celu odczytania i wyświetlenia pliku.

Dodaj tę metodę do Program klasy (pobraną z treści Main metody):

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. Po pierwsze, w treści metody, zamiast wywoływać Wait() synchronicznie czekać na zakończenie zadania, ta wersja używa słowa kluczowego await . Aby to zrobić, należy dodać async modyfikator do podpisu metody. Ta metoda zwraca Taskwartość . Zwróć uwagę, że nie ma instrukcji return, które zwracają Task obiekt. Zamiast tego ten Task obiekt jest tworzony przez kod, który kompilator generuje podczas korzystania z await operatora . Można sobie wyobrazić, że ta metoda zwraca wartość, gdy osiągnie awaitwartość . Zwrócony Task element wskazuje, że praca nie została ukończona. Metoda zostanie wznowiona po zakończeniu oczekiwanego zadania. Po wykonaniu do ukończenia zwrócony element Task wskazuje, że jest ukończony. Wywoływanie kodu może monitorować, który został zwrócony Task w celu określenia, kiedy został ukończony.

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

await ShowTeleprompter();

Wymaga to zmiany podpisu metody na Main :

static async Task Main(string[] args)

Dowiedz się więcej o metodzieasync Main w naszej sekcji podstaw.

Następnie należy napisać drugą metodę asynchroniczną, aby odczytać z konsoli i watch dla kluczy "<" (mniejsze niż), ">" (większe niż) i "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 Action delegata, który odczytuje klucz z konsoli i modyfikuje zmienną lokalną reprezentującą opóźnienie, gdy użytkownik naciska klawisze "<" (mniejsze niż) lub ">" (większe niż). Metoda delegata kończy się, gdy użytkownik naciska klawisze "X" lub "x", co umożliwia użytkownikowi zatrzymanie wyświetlania tekstu w dowolnym momencie. Ta metoda używa ReadKey() metody do blokowania i oczekiwania na naciśnięcie klawisza przez użytkownika.

Aby ukończyć tę funkcję, należy utworzyć nową async Task metodę zwracaną, która uruchamia oba te zadania (GetInput i ShowTeleprompter), a także zarządza udostępnionymi danymi między tymi dwoma zadaniami.

Nadszedł czas, aby utworzyć klasę, która może obsłużyć 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ę TeleprompterConsole w przestrzeni nazw, jak pokazano poniżej. Należy również dodać instrukcję using static w górnej części pliku, aby można było odwoływać się Min do metod i Max bez nazw otaczającej klasy lub przestrzeni nazw. Instrukcja using static importuje metody z jednej klasy. Jest to sprzeczne z instrukcją using bez static, która importuje wszystkie klasy z przestrzeni nazw.

using static System.Math;

Następnie należy zaktualizować ShowTeleprompter metody i GetInput , aby użyć nowego config obiektu. Napisz jedną ostatnią Task zwracaną metodę, async aby uruchomić zarówno zadania, jak i zakończyć działanie po zakończeniu pierwszego zadania:

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

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

Jedną z nowych metod jest wywołanie WhenAny(Task[]) . Spowoduje to utworzenie obiektu Task , który zakończy się zaraz po zakończeniu któregokolwiek z zadań na liście argumentów.

Następnie należy zaktualizować zarówno ShowTeleprompter metody , jak i GetInput w celu użycia config obiektu w celu 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 metody ShowTeleprompter wywołuje nową metodę TeleprompterConfig w klasie . Teraz musisz zaktualizować Main metodę , aby wywołać metodę RunTeleprompterShowTeleprompterzamiast :

await RunTeleprompter();

Podsumowanie

W tym samouczku przedstawiono szereg funkcji dotyczących języka C# i bibliotek platformy .NET Core związanych z pracą w aplikacjach konsolowych. Możesz wykorzystać tę wiedzę, 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 We/Wy plików i strumienia. Aby uzyskać więcej informacji na temat asynchronicznego modelu programowania używanego w tym samouczku, zobacz Programowanie asynchroniczne oparte na zadaniach i Programowanie asynchroniczne.