Delen via


console-applicatie

In deze zelfstudie leert u een aantal functies in .NET en de C#-taal. U leert het volgende:

  • De basisbeginselen van de .NET CLI
  • De structuur van een C#-consoletoepassing
  • Console I/O
  • De basisprincipes van Bestands-I/O-API's in .NET
  • De basisprincipes van op taken gebaseerde asynchrone programmering in .NET

U bouwt een toepassing die een tekstbestand leest en de inhoud van dat tekstbestand weerklinkt naar de console. De uitvoer van de console is zodanig getimed dat het overeenkomt met het hardop voorlezen. U kunt het tempo versnellen of vertragen door op de toetsen '<' (kleiner dan) of '>' (groter dan) te drukken. U kunt deze toepassing uitvoeren in Windows, Linux, macOS of in een Docker-container.

Deze handleiding bevat veel mogelijkheden. Laten we ze één voor één bouwen.

Vereiste voorwaarden

De app maken

De eerste stap is het maken van een nieuwe toepassing. Open een opdrachtprompt en maak een nieuwe map voor uw toepassing. Maak het de huidige directory. Typ de opdracht dotnet new console bij de opdrachtprompt. Voorbeeld:

E:\development\VSprojects>mkdir teleprompter
E:\development\VSprojects>cd teleprompter
E:\development\VSprojects\teleprompter>dotnet new console
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on E:\development\VSprojects\teleprompter\teleprompter.csproj...
  Determining projects to restore...
  Restored E:\development\VSprojects\teleprompter\teleprompter.csproj (in 78 ms).
Restore succeeded.

Hiermee worden de startersbestanden gemaakt voor een eenvoudige 'Hallo wereld'-toepassing.

Voordat u wijzigingen gaat aanbrengen, gaan we de eenvoudige Hello World-toepassing uitvoeren. Nadat u de toepassing hebt gemaakt, typt u dotnet run bij de opdrachtprompt. Met deze opdracht wordt het herstelproces van het NuGet-pakket uitgevoerd, het uitvoerbare bestand van de toepassing gemaakt en het uitvoerbare bestand uitgevoerd.

De eenvoudige Hello World-toepassingscode bevindt zich allemaal in Program.cs. Open dat bestand met uw favoriete teksteditor. Vervang de code in Program.cs door de volgende code:

namespace TeleprompterConsole;

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

Zie een namespace instructie boven aan het bestand. Net als andere objectgeoriënteerde talen die u mogelijk hebt gebruikt, gebruikt C# naamruimten om typen te organiseren. Dit Hello World-programma is niet anders. U kunt zien dat het programma zich in de naamruimte bevindt met de naam TeleprompterConsole.

Het bestand lezen en echoën

De eerste functie die moet worden toegevoegd, is de mogelijkheid om een tekstbestand te lezen en al die tekst weer te geven aan de console. Eerst gaan we een tekstbestand toevoegen. Kopieer het sampleQuotes.txt-bestand uit de GitHub-opslagplaats voor dit voorbeeld naar uw projectmap. Dit fungeert als het script voor uw toepassing. Zie de instructies in Voorbeelden en zelfstudies voor meer informatie over het downloaden van de voorbeeld-app voor deze zelfstudie.

Voeg vervolgens de volgende methode toe aan uw Program klasse (direct onder de Main methode):

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

Deze methode is een speciaal type C#-methode genaamd een iterator-methode. Iterator-methoden retourneren reeksen die op een luie manier worden geëvalueerd. Dat betekent dat elk item in de reeks wordt gegenereerd omdat het wordt aangevraagd door de code die de reeks gebruikt. Iterator-methoden zijn methoden die een of meer yield return instructies bevatten. Het object dat door de ReadFrom methode wordt geretourneerd, bevat de code om elk item in de reeks te genereren. In dit voorbeeld moet u de volgende regel tekst uit het bronbestand lezen en die tekenreeks retourneren. Telkens wanneer de aanroepende code het volgende item uit de reeks aanvraagt, leest de code de volgende tekstregel uit het bestand en retourneert deze. Wanneer het bestand volledig is gelezen, geeft de reeks aan dat er geen items meer zijn.

Er zijn twee C#-syntaxiselementen die mogelijk nieuw voor u zijn. De using instructie in deze methode beheert het opschonen van resources. De variabele die is geïnitialiseerd in de using instructie (readerin dit voorbeeld) moet de IDisposable interface implementeren. Deze interface definieert één methode, Disposedie moet worden aangeroepen wanneer de resource moet worden vrijgegeven. De compiler genereert die aanroep wanneer de uitvoering het sluitende accolade van de using instructie bereikt. De door de compiler gegenereerde code zorgt ervoor dat de resource wordt vrijgegeven, zelfs als er een uitzondering wordt gegenereerd op basis van de code in het blok dat is gedefinieerd door de using-instructie.

De reader variabele wordt gedefinieerd met behulp van het var trefwoord. var definieert een impliciet getypte lokale variabele. Dit betekent dat het type van de variabele wordt bepaald door het compileertijdtype van het object dat is toegewezen aan de variabele. Hier is dat de retourwaarde van de OpenText(String) methode, een StreamReader object.

Laten we nu de code invullen om het bestand in de Main methode te lezen:

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

Voer het programma uit (met behulp van dotnet run) en u kunt elke regel zien die in de console wordt afgedrukt.

Vertragingen toevoegen en uitvoer opmaken

Wat u hebt, wordt veel te snel weergegeven om hardop voor te lezen. Nu moet u de vertragingen in de uitvoer toevoegen. Wanneer u begint, bouwt u een deel van de kerncode die asynchrone verwerking mogelijk maakt. Deze eerste stappen volgen echter enkele antipatronen. De antipatronen worden in opmerkingen aangegeven wanneer u de code toevoegt en de code wordt in latere stappen bijgewerkt.

Deze sectie bestaat uit twee stappen. Eerst werkt u de iteratormethode bij om enkele woorden te retourneren in plaats van hele regels. Dat is gedaan met deze wijzigingen. Vervang de yield return line; instructie door de volgende code:

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

Vervolgens moet u wijzigen hoe u de regels van het bestand gebruikt en een vertraging toevoegen nadat u elk woord hebt geschreven. Vervang de Console.WriteLine(line) opdracht in de Main methode door het volgende blok:

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

Voer het voorbeeld uit en controleer de uitvoer. Nu wordt elk woord afgedrukt, gevolgd door een vertraging van 200 ms. De weergegeven uitvoer toont echter enkele problemen omdat het brontekstbestand meerdere regels heeft met meer dan 80 tekens zonder een regeleinde. Dat kan moeilijk te lezen zijn terwijl het voorbij scrolt. Dat is makkelijk op te lossen. U houdt alleen de lengte van elke regel bij en genereert een nieuwe regel wanneer de lijnlengte een bepaalde drempelwaarde bereikt. Declareer een lokale variabele na de declaratie van words in de ReadFrom methode die de regellengte bevat:

var lineLength = 0;

Voeg vervolgens de volgende code toe na de yield return word + " "; instructie (vóór de afsluitende accolade):

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

Voer het voorbeeld uit en u kunt hardop voorlezen in het vooraf geconfigureerde tempo.

Asynchrone taken

In deze laatste stap voegt u de code toe om de uitvoer asynchroon in de ene taak te schrijven, terwijl u ook een andere taak uitvoert om invoer van de gebruiker te lezen als ze de tekstweergave willen versnellen of vertragen, of de tekstweergave helemaal willen stoppen. Dit bevat een paar stappen en aan het einde hebt u alle updates die u nodig hebt. De eerste stap is het maken van een asynchrone Task retourmethode die de code vertegenwoordigt die u tot nu toe hebt gemaakt om het bestand te lezen en weer te geven.

Voeg deze methode toe aan uw Program klas (deze is afkomstig uit de hoofdtekst van uw 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);
        }
    }
}

U ziet twee wijzigingen. Ten eerste, in de hoofdtekst van de methode, in plaats van aan te roepen Wait() om synchroon te wachten tot een taak is voltooid, gebruikt deze versie het await trefwoord. Hiervoor moet u de async wijzigingsfunctie toevoegen aan de methodehandtekening. Deze methode retourneert een Task. Merk op dat er geen returnstatements zijn die een Task object retourneren. In plaats daarvan wordt dat Task object gemaakt met code die de compiler genereert wanneer u de await operator gebruikt. U kunt zich voorstellen dat deze methode terugkeert wanneer er een await wordt bereikt. De geretourneerde Task waarde geeft aan dat het werk niet is voltooid. De methode wordt hervat wanneer de wachtende taak is voltooid. Wanneer het is uitgevoerd tot voltooiing, geeft de geretourneerde Task waarde aan dat het is voltooid. Aanroepende code kan de geretourneerde Task bijhouden om te bepalen wanneer deze is voltooid.

Voeg een await trefwoord toe vóór de aanroep naar ShowTeleprompter:

await ShowTeleprompter();

Hiervoor moet u de Main methodehandtekening wijzigen in:

static async Task Main(string[] args)

Meer informatie over de async Main methode in de sectie Grondbeginselen.

Vervolgens moet u de tweede asynchrone methode schrijven om uit de Console te lezen en te kijken naar de sleutels '<' (kleiner dan), '>' (groter dan) en 'X' of 'x'. Dit is de methode die u voor die taak toevoegt:

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

Hiermee maakt u een lambda-expressie die een Action gemachtigde vertegenwoordigt die een sleutel uit de console leest en een lokale variabele wijzigt die de vertraging aangeeft wanneer de gebruiker op de toetsen '<' (kleiner dan) of '>' (groter dan) drukt. De gedelegeerde methode wordt voltooid wanneer de gebruiker op de toetsen X of x drukt, zodat de gebruiker de tekstweergave op elk gewenst moment kan stoppen. Deze methode gebruikt ReadKey() om te blokkeren en te wachten totdat de gebruiker op een toets drukt.

Het is tijd om een klasse te maken die de gedeelde gegevens tussen deze twee taken kan verwerken. Deze klasse bevat twee openbare eigenschappen: de vertraging en een vlag Done om aan te geven dat het bestand volledig is gelezen:

using static System.Math;

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

Een nieuw bestand maken; dit kan elke naam zijn die eindigt op .cs. Bijvoorbeeld TelePrompterConfig.cs. Plak de klassecode TelePrompterConfig, sla deze op en sluit deze. Plaats die klasse in de TeleprompterConsole naamruimte zoals weergegeven. Met de using static instructie kunt u verwijzen naar de Min en Max methodes zonder de omhullende klassen- of naamruimtenamen. Met een using static instructie worden de methoden uit één klasse geïmporteerd. Dit is in tegenstelling tot de using instructie zonder static, waarmee alle klassen uit een naamruimte worden geïmporteerd.

Vervolgens moet u de ShowTeleprompter en GetInput methoden bijwerken om het nieuwe config object te gebruiken. Als u deze functie wilt voltooien, moet u een nieuwe async Task retourmethode maken waarmee beide taken (GetInput en ShowTeleprompter) worden gestart en ook de gedeelde gegevens tussen deze twee taken worden beheerd. Maak een RunTelePrompter-taak om beide taken te starten en af te sluiten wanneer de eerste taak is voltooid:

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

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

De ene nieuwe methode hier is de WhenAny(Task[]) aanroep. Dit creëert een Task dat voltooid is zodra een van de taken in de lijst met argumenten wordt afgerond.

Vervolgens moet u zowel de ShowTeleprompter als de GetInput methoden bijwerken om het config object voor de vertraging in te stellen. Het configuratieobject wordt als parameter doorgegeven aan deze methoden. Gebruik hier copy/paste om de methoden volledig te vervangen door de nieuwe code. U kunt zien dat de code kenmerken gebruikt en methoden aanroept vanuit het configuratieobject:

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

Nu moet u bijwerken Main om aan te roepen RunTeleprompter in plaats van ShowTeleprompter:

await RunTeleprompter();

Conclusie

In deze zelfstudie hebt u een aantal functies rond de C#-taal en de .NET Core-bibliotheken getoond die betrekking hebben op het werken in Console-toepassingen. U kunt op deze kennis voortbouwen om meer te weten te komen over de taal en de klassen die hier worden geïntroduceerd. U hebt de basisbeginselen van Bestands- en console-I/O gezien, het blokkeren en niet-blokkeren van het gebruik van asynchroon programmeren op basis van taken, een rondleiding door de C#-taal en hoe C#-programma's zijn georganiseerd en de .NET CLI.

Zie File and Stream I/O voor meer informatie over Bestands-I/O. Zie Taakgebaseerde Asynchrone Programmering en Asynchrone programmering voor meer informatie over het asynchrone programmeringsmodel dat in deze zelfstudie wordt gebruikt.