Zelfstudie: Een aangepaste tekenreeksinterpolatiehandler schrijven

In deze zelfstudie leert u het volgende:

  • Het tekenreeksinterpolatiehandlerpatroon implementeren
  • Interactie met de ontvanger in een tekenreeksinterpolatiebewerking.
  • Argumenten toevoegen aan de handler voor tekenreeksinterpolatie
  • Inzicht in de nieuwe bibliotheekfuncties voor tekenreeksinterpolatie

Vereisten

U moet uw computer instellen om .NET 6 uit te voeren, inclusief de C# 10-compiler. De C# 10-compiler is beschikbaar vanaf Visual Studio 2022 of .NET 6 SDK.

In deze zelfstudie wordt ervan uitgegaan dat u bekend bent met C# en .NET, met inbegrip van Visual Studio of de .NET CLI.

Nieuw overzicht

C# 10 voegt ondersteuning toe voor een aangepaste geïnterpoleerde tekenreekshandler. Een geïnterpoleerde tekenreekshandler is een type dat de tijdelijke expressie in een geïnterpoleerde tekenreeks verwerkt. Zonder een aangepaste handler worden tijdelijke aanduidingen verwerkt zoals String.Format. Elke tijdelijke aanduiding wordt opgemaakt als tekst en vervolgens worden de onderdelen samengevoegd om de resulterende tekenreeks te vormen.

U kunt een handler schrijven voor elk scenario waarin u informatie over de resulterende tekenreeks gebruikt. Wordt het gebruikt? Welke beperkingen gelden voor de indeling? Enkele voorbeelden:

  • Mogelijk hebt u geen van de resulterende tekenreeksen nodig die groter zijn dan een bepaalde limiet, zoals 80 tekens. U kunt de geïnterpoleerde tekenreeksen verwerken om een buffer met vaste lengte te vullen en de verwerking stoppen zodra die bufferlengte is bereikt.
  • Mogelijk hebt u een tabelvorm en moet elke tijdelijke aanduiding een vaste lengte hebben. Een aangepaste handler kan dat afdwingen, in plaats van dat alle clientcode moet voldoen.

In deze zelfstudie maakt u een tekenreeksinterpolatiehandler voor een van de belangrijkste prestatiescenario's: logboekregistratiebibliotheken. Afhankelijk van het geconfigureerde logboekniveau is het werk om een logboekbericht te maken niet nodig. Als logboekregistratie is uitgeschakeld, is het werk voor het maken van een tekenreeks van een geïnterpoleerde tekenreeksexpressie niet nodig. Het bericht wordt nooit afgedrukt, dus elke tekenreekssamenvoeging kan worden overgeslagen. Daarnaast hoeven alle expressies die worden gebruikt in de tijdelijke aanduidingen, waaronder het genereren van stacktraceringen, niet te worden uitgevoerd.

Een geïnterpoleerde tekenreekshandler kan bepalen of de opgemaakte tekenreeks wordt gebruikt en alleen het benodigde werk uitvoeren als dat nodig is.

Eerste implementatie

Laten we beginnen met een basisklasse Logger die verschillende niveaus ondersteunt:

public enum LogLevel
{
    Off,
    Critical,
    Error,
    Warning,
    Information,
    Trace
}

public class Logger
{
    public LogLevel EnabledLevel { get; init; } = LogLevel.Error;

    public void LogMessage(LogLevel level, string msg)
    {
        if (EnabledLevel < level) return;
        Console.WriteLine(msg);
    }
}

Dit Logger ondersteunt zes verschillende niveaus. Wanneer een bericht het filter op logboekniveau niet doorgeeft, is er geen uitvoer. De openbare API voor de logger accepteert een (volledig opgemaakte) tekenreeks als het bericht. Al het werk voor het maken van de tekenreeks is al voltooid.

Het handlerpatroon implementeren

Deze stap is het bouwen van een geïnterpoleerde tekenreekshandler waarmee het huidige gedrag opnieuw wordt gemaakt. Een geïnterpoleerde tekenreekshandler is een type dat de volgende kenmerken moet hebben:

  • De System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute toegepast op het type.
  • Een constructor met twee int parameters en literalLengthformattedCount. (Er zijn meer parameters toegestaan).
  • Een openbare AppendLiteral methode met de handtekening: public void AppendLiteral(string s).
  • Een algemene openbare AppendFormatted methode met de handtekening: public void AppendFormatted<T>(T t).

Intern maakt de opbouwfunctie de opgemaakte tekenreeks en biedt een lid voor een client om die tekenreeks op te halen. De volgende code toont een LogInterpolatedStringHandler type dat voldoet aan deze vereisten:

[InterpolatedStringHandler]
public ref struct LogInterpolatedStringHandler
{
    // Storage for the built-up string
    StringBuilder builder;

    public LogInterpolatedStringHandler(int literalLength, int formattedCount)
    {
        builder = new StringBuilder(literalLength);
        Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
    }

    public void AppendLiteral(string s)
    {
        Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
        
        builder.Append(s);
        Console.WriteLine($"\tAppended the literal string");
    }

    public void AppendFormatted<T>(T t)
    {
        Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");

        builder.Append(t?.ToString());
        Console.WriteLine($"\tAppended the formatted object");
    }

    internal string GetFormattedText() => builder.ToString();
}

U kunt nu een overbelasting toevoegen aan LogMessage de Logger klasse om uw nieuwe geïnterpoleerde tekenreekshandler uit te proberen:

public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
    if (EnabledLevel < level) return;
    Console.WriteLine(builder.GetFormattedText());
}

U hoeft de oorspronkelijke LogMessage methode niet te verwijderen. De compiler geeft de voorkeur aan een methode met een geïnterpoleerde handlerparameter voor een methode met een string parameter wanneer het argument een geïnterpoleerde tekenreeksexpressie is.

U kunt controleren of de nieuwe handler wordt aangeroepen met behulp van de volgende code als hoofdprogramma:

var logger = new Logger() { EnabledLevel = LogLevel.Warning };
var time = DateTime.Now;

logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time}. This won't be printed.");
logger.LogMessage(LogLevel.Warning, "Warning Level. This warning is a string, not an interpolated string expression.");

Als u de toepassing uitvoert, wordt uitvoer geproduceerd die vergelijkbaar is met de volgende tekst:

        literal length: 65, formattedCount: 1
        AppendLiteral called: {Error Level. CurrentTime: }
        Appended the literal string
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: {. This is an error. It will be printed.}
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
        AppendLiteral called: {Trace Level. CurrentTime: }
        Appended the literal string
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: {. This won't be printed.}
        Appended the literal string
Warning Level. This warning is a string, not an interpolated string expression.

Door de uitvoer te traceren, kunt u zien hoe de compiler code toevoegt om de handler aan te roepen en de tekenreeks te bouwen:

  • De compiler voegt een aanroep toe om de handler samen te stellen, waarbij de totale lengte van de letterlijke tekst in de notatietekenreeks en het aantal tijdelijke aanduidingen wordt doorgegeven.
  • De compiler voegt aanroepen toe aan AppendLiteral en AppendFormatted voor elke sectie van de letterlijke tekenreeks en voor elke tijdelijke aanduiding.
  • De compiler roept de LogMessage methode aan met behulp van het CoreInterpolatedStringHandler argument.

Ten slotte ziet u dat de laatste waarschuwing de geïnterpoleerde tekenreekshandler niet aanroept. Het argument is een string, zodat de aanroep de andere overbelasting aanroept met een tekenreeksparameter.

Meer mogelijkheden toevoegen aan de handler

De voorgaande versie van de geïnterpoleerde tekenreekshandler implementeert het patroon. Als u wilt voorkomen dat elke tijdelijke expressie wordt verwerkt, hebt u meer informatie in de handler nodig. In deze sectie gaat u de handler verbeteren, zodat deze minder werkt wanneer de samengestelde tekenreeks niet naar het logboek wordt geschreven. U gebruikt System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute om een toewijzing op te geven tussen parameters voor een openbare API en parameters voor de constructor van een handler. Hiermee beschikt de handler over de informatie die nodig is om te bepalen of de geïnterpoleerde tekenreeks moet worden geëvalueerd.

Laten we beginnen met wijzigingen in de handler. Voeg eerst een veld toe om bij te houden of de handler is ingeschakeld. Voeg twee parameters toe aan de constructor: een om het logboekniveau voor dit bericht op te geven en de andere een verwijzing naar het logboekobject:

private readonly bool enabled;

public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel logLevel)
{
    enabled = logger.EnabledLevel >= logLevel;
    builder = new StringBuilder(literalLength);
    Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
}

Gebruik vervolgens het veld zodat uw handler alleen letterlijke waarden of opgemaakte objecten toevoegt wanneer de uiteindelijke tekenreeks wordt gebruikt:

public void AppendLiteral(string s)
{
    Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
    if (!enabled) return;

    builder.Append(s);
    Console.WriteLine($"\tAppended the literal string");
}

public void AppendFormatted<T>(T t)
{
    Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");
    if (!enabled) return;

    builder.Append(t?.ToString());
    Console.WriteLine($"\tAppended the formatted object");
}

Vervolgens moet u de LogMessage declaratie bijwerken zodat de compiler de aanvullende parameters doorgeeft aan de constructor van de handler. Dit wordt verwerkt met behulp van het System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute argument voor de handler:

public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
    if (EnabledLevel < level) return;
    Console.WriteLine(builder.GetFormattedText());
}

Dit kenmerk geeft de lijst met argumenten aan LogMessage die worden toegewezen aan de parameters die volgen op de vereiste literalLength en formattedCount parameters. De lege tekenreeks (""), geeft de ontvanger op. De compiler vervangt de waarde van het Logger object dat wordt vertegenwoordigd door this het volgende argument aan de constructor van de handler. De compiler vervangt de waarde van level het volgende argument. U kunt een willekeurig aantal argumenten opgeven voor elke handler die u schrijft. De argumenten die u toevoegt, zijn tekenreeksargumenten.

U kunt deze versie uitvoeren met dezelfde testcode. Deze keer ziet u de volgende resultaten:

        literal length: 65, formattedCount: 1
        AppendLiteral called: {Error Level. CurrentTime: }
        Appended the literal string
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: {. This is an error. It will be printed.}
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
        AppendLiteral called: {Trace Level. CurrentTime: }
        AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
        AppendLiteral called: {. This won't be printed.}
Warning Level. This warning is a string, not an interpolated string expression.

U kunt zien dat de AppendLiteral en AppendFormat methoden worden aangeroepen, maar dat ze geen werk doen. De handler heeft vastgesteld dat de uiteindelijke tekenreeks niet nodig is, dus de handler bouwt deze niet. Er zijn nog een aantal verbeteringen die u moet aanbrengen.

Ten eerste kunt u een overbelasting van AppendFormatted dat argument toevoegen aan een type dat wordt geïmplementeerd System.IFormattable. Met deze overbelasting kunnen bellers opmaaktekenreeksen toevoegen in de tijdelijke aanduidingen. Terwijl u deze wijziging aanbrengt, gaan we ook het retourtype van de andere AppendFormatted en AppendLiteral methoden wijzigen van void in bool (als een van deze methoden verschillende retourtypen heeft, krijgt u een compilatiefout). Deze wijziging maakt kortsluiting mogelijk. De methoden worden geretourneerd false om aan te geven dat de verwerking van de geïnterpoleerde tekenreeksexpressie moet worden gestopt. Retourneert true geeft aan dat deze moet worden voortgezet. In dit voorbeeld gebruikt u deze om de verwerking te stoppen wanneer de resulterende tekenreeks niet nodig is. Kortsluiting ondersteunt meer verfijnde acties. U kunt de verwerking van de expressie stoppen zodra deze een bepaalde lengte heeft bereikt, om buffers met vaste lengte te ondersteunen. Of een bepaalde voorwaarde kan aangeven dat resterende elementen niet nodig zijn.

public void AppendFormatted<T>(T t, string format) where T : IFormattable
{
    Console.WriteLine($"\tAppendFormatted (IFormattable version) called: {t} with format {{{format}}} is of type {typeof(T)},");

    builder.Append(t?.ToString(format, null));
    Console.WriteLine($"\tAppended the formatted object");
}

Met deze toevoeging kunt u opmaaktekenreeksen opgeven in de geïnterpoleerde tekenreeksexpressie:

var time = DateTime.Now;

logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. The time doesn't use formatting.");
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time:t}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time:t}. This won't be printed.");

In :t het eerste bericht wordt de 'korte tijdnotatie' voor de huidige tijd opgegeven. In het vorige voorbeeld is een van de overbelastingen getoond aan de AppendFormatted methode die u voor uw handler kunt maken. U hoeft geen algemeen argument op te geven voor het object dat wordt opgemaakt. Mogelijk hebt u efficiëntere manieren om typen te converteren die u maakt naar een tekenreeks. U kunt overbelastingen van AppendFormatted die typen schrijven in plaats van een algemeen argument. De compiler kiest de beste overbelasting. De runtime gebruikt deze techniek om te converteren System.Span<T> naar tekenreeksuitvoer. U kunt een parameter voor een geheel getal toevoegen om de uitlijning van de uitvoer op te geven, met of zonder een IFormattable. De System.Runtime.CompilerServices.DefaultInterpolatedStringHandler die wordt geleverd met .NET 6 bevat negen overbelastingen voor AppendFormatted verschillende toepassingen. U kunt deze gebruiken als referentie tijdens het bouwen van een handler voor uw doeleinden.

Voer het voorbeeld nu uit en u ziet dat alleen de eerste AppendLiteral wordt aangeroepen voor het Trace bericht:

        literal length: 60, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted called: 10/20/2021 12:18:29 PM is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: . The time doesn't use formatting.
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:18:29 PM. The time doesn't use formatting.
        literal length: 65, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted (IFormattable version) called: 10/20/2021 12:18:29 PM with format {t} is of type System.DateTime,
        Appended the formatted object
        AppendLiteral called: . This is an error. It will be printed.
        Appended the literal string
Error Level. CurrentTime: 12:18 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
        AppendLiteral called: Trace Level. CurrentTime:
Warning Level. This warning is a string, not an interpolated string expression.

U kunt één laatste update uitvoeren voor de constructor van de handler die de efficiëntie verbetert. De handler kan een laatste out bool parameter toevoegen. Als u deze parameter instelt om aan te false geven dat de handler helemaal niet mag worden aangeroepen om de geïnterpoleerde tekenreeksexpressie te verwerken:

public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel level, out bool isEnabled)
{
    isEnabled = logger.EnabledLevel >= level;
    Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
    builder = isEnabled ? new StringBuilder(literalLength) : default!;
}

Deze wijziging betekent dat u het enabled veld kunt verwijderen. Vervolgens kunt u het retourtype AppendLiteral van en AppendFormatted naar void. Wanneer u het voorbeeld uitvoert, ziet u nu de volgende uitvoer:

        literal length: 60, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted called: 10/20/2021 12:19:10 PM is of type System.DateTime
        Appended the formatted object
        AppendLiteral called: . The time doesn't use formatting.
        Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. The time doesn't use formatting.
        literal length: 65, formattedCount: 1
        AppendLiteral called: Error Level. CurrentTime:
        Appended the literal string
        AppendFormatted (IFormattable version) called: 10/20/2021 12:19:10 PM with format {t} is of type System.DateTime,
        Appended the formatted object
        AppendLiteral called: . This is an error. It will be printed.
        Appended the literal string
Error Level. CurrentTime: 12:19 PM. This is an error. It will be printed.
        literal length: 50, formattedCount: 1
Warning Level. This warning is a string, not an interpolated string expression.

De enige uitvoer wanneer LogLevel.Trace is opgegeven, is de uitvoer van de constructor. De handler heeft aangegeven dat deze niet is ingeschakeld, dus geen van de Append methoden is aangeroepen.

In dit voorbeeld ziet u een belangrijk punt voor geïnterpoleerde tekenreekshandlers, met name wanneer logboekregistratiebibliotheken worden gebruikt. Eventuele bijwerkingen in de tijdelijke aanduidingen kunnen niet optreden. Voeg de volgende code toe aan uw hoofdprogramma en bekijk dit gedrag in actie:

int index = 0;
int numberOfIncrements = 0;
for (var level = LogLevel.Critical; level <= LogLevel.Trace; level++)
{
    Console.WriteLine(level);
    logger.LogMessage(level, $"{level}: Increment index a few times {index++}, {index++}, {index++}, {index++}, {index++}");
    numberOfIncrements += 5;
}
Console.WriteLine($"Value of index {index}, value of numberOfIncrements: {numberOfIncrements}");

U kunt zien dat de index variabele vijf keer wordt verhoogd bij elke iteratie van de lus. Omdat de tijdelijke aanduidingen alleen worden geëvalueerd voor Criticalen ErrorWarning niveaus, niet voor Information en Trace, komt de uiteindelijke waarde index niet overeen met de verwachting:

Critical
Critical: Increment index a few times 0, 1, 2, 3, 4
Error
Error: Increment index a few times 5, 6, 7, 8, 9
Warning
Warning: Increment index a few times 10, 11, 12, 13, 14
Information
Trace
Value of index 15, value of numberOfIncrements: 25

Geïnterpoleerde tekenreekshandlers bieden meer controle over hoe een geïnterpoleerde tekenreeksexpressie wordt geconverteerd naar een tekenreeks. Het .NET Runtime-team heeft deze functie al gebruikt om de prestaties op verschillende gebieden te verbeteren. U kunt gebruikmaken van dezelfde mogelijkheid in uw eigen bibliotheken. Als u verder wilt verkennen, bekijkt u de System.Runtime.CompilerServices.DefaultInterpolatedStringHandler. Het biedt een completere implementatie dan u hier hebt gebouwd. U ziet veel meer overbelastingen die mogelijk zijn voor de Append methoden.