Konsolapp

I den här självstudien lär du dig ett antal funktioner i .NET och C#-språket. Du får lära dig detta:

  • Grunderna i .NET CLI
  • Strukturen för ett C#-konsolprogram
  • Konsol-I/O
  • Grunderna i fil-I/O-API:er i .NET
  • Grunderna i aktivitetsbaserad asynkron programmering i .NET

Du skapar ett program som läser en textfil och skickar innehållet i textfilen till konsolen. Utdata till konsolen går i takt för att matcha läsningen högt. Du kan öka eller minska takten genom att trycka på tangenterna '<' (mindre än) eller '>' (större än). Du kan köra det här programmet på Windows, Linux, macOS eller i en Docker-container.

Det finns många funktioner i den här självstudien. Låt oss bygga dem en i taget.

Krav

Skapa appen

Det första steget är att skapa ett nytt program. Öppna en kommandotolk och skapa en ny katalog för ditt program. Gör det till den aktuella katalogen. Skriv kommandot dotnet new console i kommandotolken. Detta skapar startfilerna för ett grundläggande "Hello World"-program.

Innan du börjar göra ändringar ska vi köra det enkla Hello World programmet. När du har skapat programmet skriver dotnet run du i kommandotolken. Det här kommandot kör återställningsprocessen för NuGet-paketet, skapar programmets körbara fil och kör den körbara filen.

Den enkla Hello World programkoden finns i Program.cs. Öppna filen med valfri textredigerare. Ersätt koden i Program.cs med följande kod:

namespace TeleprompterConsole;

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

Överst i filen finns en namespace -instruktion. Precis som andra objektorienterade språk som du kanske har använt använder C# namnområden för att organisera typer. Det här Hello World programmet är inte annorlunda. Du kan se att programmet finns i namnområdet med namnet TeleprompterConsole.

Läsa och upprepa filen

Den första funktionen att lägga till är möjligheten att läsa en textfil och visa all text i konsolen. Först lägger vi till en textfil. Kopiera sampleQuotes.txt-filen från GitHub-lagringsplatsen för det här exemplet till projektkatalogen. Detta fungerar som skript för ditt program. Information om hur du laddar ned exempelappen för den här självstudien finns i anvisningarna i Exempel och Självstudier.

Lägg sedan till följande metod i klassen Program (precis under Main metoden):

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

Den här metoden är en särskild typ av C#-metod som kallas en iteratormetod. Iteratormetoder returnerar sekvenser som utvärderas lazily. Det innebär att varje objekt i sekvensen genereras eftersom det begärs av koden som använder sekvensen. Iteratormetoder är metoder som innehåller en eller flera yield return -instruktioner. Objektet som returneras av ReadFrom metoden innehåller koden för att generera varje objekt i sekvensen. I det här exemplet innebär det att läsa nästa rad med text från källfilen och returnera strängen. Varje gång den anropande koden begär nästa objekt från sekvensen läser koden nästa textrad från filen och returnerar den. När filen är helt läst indikerar sekvensen att det inte finns några fler objekt.

Det finns två C#-syntaxelement som kan vara nya för dig. Instruktionen using i den här metoden hanterar resursrensning. Variabeln som initieras i -instruktionen using (readeri det här exemplet) måste implementera IDisposable gränssnittet. Gränssnittet definierar en enda metod, Dispose, som ska anropas när resursen ska släppas. Kompilatorn genererar det anropet när körningen når den avslutande klammerparentesen för -instruktionen using . Den kompilatorgenererade koden säkerställer att resursen släpps även om ett undantag genereras från koden i blocket som definieras av using-instruktionen.

Variabeln reader definieras med hjälp av nyckelordet var . var definierar en implicit typifierad lokal variabel. Det innebär att variabeltypen bestäms av kompileringstidstypen för objektet som tilldelats variabeln. Här är det returvärdet från OpenText(String) metoden , som är ett StreamReader -objekt.

Nu ska vi fylla i koden för att läsa filen i Main metoden :

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

Kör programmet (med ) dotnet runså kan du se varje rad som skrivs ut till konsolen.

Lägga till fördröjningar och formateringsutdata

Det du har visas alldeles för snabbt för att läsas upp. Nu måste du lägga till fördröjningarna i utdata. När du börjar skapar du en del av den kärnkod som möjliggör asynkron bearbetning. Dessa första steg följer dock några antimönster. Antimönstren visas i kommentarer när du lägger till koden, och koden uppdateras i senare steg.

Det finns två steg i det här avsnittet. Först uppdaterar du iteratormetoden så att den returnerar enkla ord i stället för hela rader. Det är gjort med de här ändringarna. Ersätt -instruktionen yield return line; med följande kod:

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

Därefter måste du ändra hur du använder raderna i filen och lägga till en fördröjning efter att du har skrivit varje ord. Ersätt -instruktionen Console.WriteLine(line)Main i -metoden med följande 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();
}

Kör exemplet och kontrollera utdata. Nu skrivs varje enskilt ord ut, följt av en fördröjning på 200 ms. De utdata som visas visar dock vissa problem eftersom källtextfilen har flera rader med fler än 80 tecken utan radbrytning. Det kan vara svårt att läsa medan det rullar förbi. Det är lätt att fixa. Du håller bara reda på längden på varje rad och genererar en ny rad när linjelängden når ett visst tröskelvärde. Deklarera en lokal variabel efter deklarationen av words i metoden ReadFrom som innehåller radlängden:

var lineLength = 0;

Lägg sedan till följande kod efter -instruktionen yield return word + " "; (före den avslutande klammerparentesen):

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

Kör exemplet så kan du läsa upp det i förkonfigurerad takt.

Asynkrona uppgifter

I det här sista steget lägger du till koden för att skriva utdata asynkront i en aktivitet, samtidigt som du kör en annan uppgift för att läsa indata från användaren om de vill göra textvisningen snabbare eller långsammare, eller stoppa textvisningen helt och hållet. Detta har några steg i den och i slutet har du alla uppdateringar som du behöver. Det första steget är att skapa en asynkron Task returmetod som representerar den kod som du har skapat hittills för att läsa och visa filen.

Lägg till den här metoden i klassen Program (den hämtas från metodens brödtext 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);
        }
    }
}

Du ser två ändringar. I metodens brödtext använder den här versionen först nyckelordet i stället för att anropa Wait() för att synkront vänta på att en uppgift ska slutföras await . För att göra det måste du lägga async till modifieraren i metodsignaturen. Den här metoden returnerar en Task. Observera att det inte finns några returinstruktioner som returnerar ett Task -objekt. I stället skapas objektet med kod som Task kompilatorn genererar när du använder operatorn await . Du kan tänka dig att den här metoden returneras när den når en await. Den returnerade Task anger att arbetet inte har slutförts. Metoden återupptas när den väntade aktiviteten har slutförts. När den har körts tills den har slutförts anger den returnerade Task att den är klar. Anropande kod kan övervaka som returneras Task för att avgöra när den har slutförts.

Lägg till ett await nyckelord före anropet till ShowTeleprompter:

await ShowTeleprompter();

Detta kräver att du ändrar metodsignaturen Main till:

static async Task Main(string[] args)

Läs mer om async Main metoden i avsnittet grunderna.

Därefter måste du skriva den andra asynkrona metoden för att läsa från konsolen och watch för nycklarna '<' (mindre än), '>' (större än) och 'X' eller 'x'. Här är den metod som du lägger till för den uppgiften:

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

Detta skapar ett lambda-uttryck som representerar ett Action ombud som läser en nyckel från konsolen och ändrar en lokal variabel som representerar fördröjningen när användaren trycker på nycklarna '<' (mindre än) eller '>' (större än). Ombudsmetoden slutförs när användaren trycker på X- eller x-tangenterna, vilket gör att användaren kan stoppa textvisningen när som helst. Den här metoden använder ReadKey() för att blockera och vänta tills användaren trycker på en tangent.

För att slutföra den här funktionen måste du skapa en ny async Task returneringsmetod som startar båda dessa uppgifter (GetInput och ShowTeleprompter), och även hanterar delade data mellan dessa två uppgifter.

Det är dags att skapa en klass som kan hantera delade data mellan dessa två uppgifter. Den här klassen innehåller två offentliga egenskaper: fördröjningen och en flagga Done som anger att filen har lästs helt:

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

Placera den klassen i en ny fil och inkludera den klassen i TeleprompterConsole namnområdet som visas. Du måste också lägga till en using static -instruktion överst i filen så att du kan referera till Min metoderna och Max utan de omslutande klass- eller namnområdesnamnen. En using static instruktion importerar metoderna från en klass. Detta står i kontrast till instruktionen using utan static, som importerar alla klasser från ett namnområde.

using static System.Math;

Därefter måste du uppdatera ShowTeleprompter metoderna och GetInput för att använda det nya config objektet. Skriv en sista Task returmetod async för att starta båda aktiviteterna och avsluta när den första aktiviteten är klar:

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

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

Den enda nya metoden här är anropet WhenAny(Task[]) . Det skapar en Task som slutförs så snart någon av aktiviteterna i argumentlistan har slutförts.

Därefter måste du uppdatera både ShowTeleprompter metoderna och GetInput för att använda config objektet för fördröjningen:

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

Den här nya versionen av ShowTeleprompter anropar en ny metod i TeleprompterConfig klassen. Nu måste du uppdatera Main för att anropa RunTeleprompter i stället ShowTeleprompterför :

await RunTeleprompter();

Slutsats

Den här självstudien visade dig ett antal funktioner kring C#-språket och .NET Core-biblioteken som rör arbete i konsolprogram. Du kan bygga vidare på den här kunskapen för att utforska mer om språket och de klasser som introduceras här. Du har sett grunderna i Fil- och konsol-I/O, blockerande och icke-blockerande användning av aktivitetsbaserad asynkron programmering, en genomgång av C#-språket och hur C#-program organiseras samt .NET CLI.

Mer information om fil-I/O finns i Fil- och stream-I/O. Mer information om asynkron programmeringsmodell som används i den här självstudien finns i Uppgiftsbaserad asynkron programmering och asynkron programmering.