Självstudie: Skriva en anpassad stränginterpolationshanterare

I den här självstudien får du lära dig att:

  • Implementera mönstret för stränginterpoleringshanteraren
  • Interagera med mottagaren i en stränginterpolation.
  • Lägga till argument i stränginterpolationshanteraren
  • Förstå de nya biblioteksfunktionerna för stränginterpolation

Förutsättningar

Du måste konfigurera datorn för att köra .NET 6, inklusive C# 10-kompilatorn. C# 10-kompilatorn är tillgänglig från och med Visual Studio 2022 eller .NET 6 SDK.

Den här självstudien förutsätter att du är bekant med C# och .NET, inklusive antingen Visual Studio eller .NET CLI.

Ny disposition

C# 10 lägger till stöd för en anpassad interpolerad stränghanterare. En interpolerad stränghanterare är en typ som bearbetar platshållaruttrycket i en interpolerad sträng. Utan en anpassad hanterare bearbetas platshållare som liknar String.Format. Varje platshållare formateras som text och sedan sammanfogas komponenterna för att bilda den resulterande strängen.

Du kan skriva en hanterare för alla scenarier där du använder information om den resulterande strängen. Kommer den att användas? Vilka begränsningar gäller för formatet? Vissa exempel inkluderar:

  • Du kan kräva att ingen av de resulterande strängarna är större än någon gräns, till exempel 80 tecken. Du kan bearbeta de interpolerade strängarna för att fylla en buffert med fast längd och sluta bearbeta när buffertlängden har nåtts.
  • Du kan ha ett tabellformat och varje platshållare måste ha en fast längd. En anpassad hanterare kan framtvinga detta i stället för att tvinga all klientkod att överensstämma.

I den här självstudien skapar du en stränginterpolationshanterare för ett av de viktigaste prestandascenarierna: loggningsbibliotek. Beroende på den konfigurerade loggnivån behövs inte arbetet med att skapa ett loggmeddelande. Om loggningen är inaktiverad behövs inte arbetet med att konstruera en sträng från ett interpolerat stränguttryck. Meddelandet skrivs aldrig ut, så alla sammanlänkningssträngar kan hoppas över. Dessutom behöver inga uttryck som används i platshållarna, inklusive generering av stackspårningar, göras.

En interpolerad stränghanterare kan avgöra om den formaterade strängen ska användas och endast utföra det arbete som krävs om det behövs.

Inledande implementering

Vi börjar från en grundläggande Logger klass som har stöd för olika nivåer:

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

Detta Logger stöder sex olika nivåer. När ett meddelande inte skickar loggnivåfiltret finns det inga utdata. Det offentliga API:et för loggaren accepterar en (fullständigt formaterad) sträng som meddelande. Allt arbete för att skapa strängen har redan utförts.

Implementera hanterarens mönster

Det här steget är att skapa en interpolerad stränghanterare som återskapar det aktuella beteendet. En interpolerad stränghanterare är en typ som måste ha följande egenskaper:

  • Den System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute som tillämpas på typen.
  • En konstruktor som har två int parametrar och literalLengthformattedCount. (Fler parametrar tillåts).
  • En offentlig AppendLiteral metod med signaturen: public void AppendLiteral(string s).
  • En allmän offentlig AppendFormatted metod med signaturen: public void AppendFormatted<T>(T t).

Internt skapar byggaren den formaterade strängen och tillhandahåller en medlem för en klient för att hämta strängen. Följande kod visar en LogInterpolatedStringHandler typ som uppfyller dessa krav:

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

Nu kan du lägga till en överlagring LogMessage i Logger klassen för att prova din nya interpolerade stränghanterare:

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

Du behöver inte ta bort den ursprungliga LogMessage metoden. Kompilatorn föredrar en metod med en interpolerad hanterarparameter framför en metod med en string parameter när argumentet är ett interpolerat stränguttryck.

Du kan kontrollera att den nya hanteraren anropas med hjälp av följande kod som huvudprogram:

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.");

När du kör programmet genereras utdata som liknar följande text:

        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.

Genom att spåra utdata kan du se hur kompilatorn lägger till kod för att anropa hanteraren och skapa strängen:

  • Kompilatorn lägger till ett anrop för att konstruera hanteraren, vilket skickar den totala längden på literaltexten i formatsträngen och antalet platshållare.
  • Kompilatorn lägger till anrop till AppendLiteral och AppendFormatted för varje avsnitt i strängliteralen och för varje platshållare.
  • Kompilatorn anropar LogMessage metoden med argumentet CoreInterpolatedStringHandler som argument.

Observera slutligen att den sista varningen inte anropar den interpolerade stränghanteraren. Argumentet är ett string, så att anropet anropar den andra överlagringen med en strängparameter.

Lägga till fler funktioner i hanteraren

Föregående version av den interpolerade stränghanteraren implementerar mönstret. För att undvika bearbetning av varje platshållaruttryck behöver du mer information i hanteraren. I det här avsnittet förbättrar du hanteraren så att den inte fungerar lika mycket när den konstruerade strängen inte skrivs till loggen. Du använder System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute för att ange en mappning mellan parametrar till ett offentligt API och parametrar till en hanterares konstruktor. Det ger hanteraren den information som behövs för att avgöra om den interpolerade strängen ska utvärderas.

Vi börjar med ändringar i hanteraren. Lägg först till ett fält för att spåra om hanteraren är aktiverad. Lägg till två parametrar i konstruktorn: en för att ange loggnivån för det här meddelandet och den andra en referens till loggobjektet:

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

Använd sedan fältet så att hanteraren endast lägger till literaler eller formaterade objekt när den sista strängen ska användas:

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

Därefter måste du uppdatera deklarationen LogMessage så att kompilatorn skickar de ytterligare parametrarna till hanterarens konstruktor. Det hanteras med hjälp av System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute hanterarens argument:

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

Det här attributet anger listan med argument till LogMessage den mappningen till de parametrar som följer de obligatoriska literalLength parametrarna och formattedCount parametrarna. Den tomma strängen (") anger mottagaren. Kompilatorn ersätter värdet för objektet Logger som representeras av this för nästa argument med hanterarens konstruktor. Kompilatorn ersätter värdet level för för följande argument. Du kan ange valfritt antal argument för alla hanterare som du skriver. Argumenten som du lägger till är strängargument.

Du kan köra den här versionen med samma testkod. Den här gången visas följande resultat:

        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.

Du kan se att AppendLiteral metoderna och AppendFormat anropas, men de utför inget arbete. Hanteraren har fastställt att den sista strängen inte behövs, så hanteraren skapar den inte. Det finns fortfarande ett par förbättringar att göra.

Först kan du lägga till en överlagring av AppendFormatted som begränsar argumentet till en typ som implementerar System.IFormattable. Den här överlagringen gör det möjligt för anropare att lägga till formatsträngar i platshållarna. När vi gör den här ändringen ska vi också ändra returtypen för den andra AppendFormatted och AppendLiteral metoderna, från void till bool (om någon av dessa metoder har olika returtyper får du ett kompileringsfel). Den ändringen möjliggör kortslutning. Metoderna återgår false för att indikera att bearbetningen av det interpolerade stränguttrycket ska stoppas. Returnerar true anger att det ska fortsätta. I det här exemplet använder du det för att sluta bearbeta när den resulterande strängen inte behövs. Kortslutning stöder mer detaljerade åtgärder. Du kan sluta bearbeta uttrycket när det når en viss längd för att stödja buffertar med fast längd. Eller så kan vissa villkor tyda på att återstående element inte behövs.

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

Med det tillägget kan du ange formatsträngar i ditt interpolerade stränguttryck:

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.");

I :t det första meddelandet anges formatet "kort tid" för den aktuella tiden. I föregående exempel visades en av överlagringarna för den AppendFormatted metod som du kan skapa för hanteraren. Du behöver inte ange ett allmänt argument för objektet som formateras. Du kan ha effektivare sätt att konvertera typer som du skapar till sträng. Du kan skriva överlagringar av AppendFormatted dessa typer i stället för ett allmänt argument. Kompilatorn väljer den bästa överbelastningen. Körningen använder den här tekniken för att konvertera System.Span<T> till strängutdata. Du kan lägga till en heltalsparameter för att ange justeringen av utdata, med eller utan en IFormattable. Det System.Runtime.CompilerServices.DefaultInterpolatedStringHandler som levereras med .NET 6 innehåller nio överlagringar av AppendFormatted för olika användningsområden. Du kan använda den som referens när du skapar en hanterare för dina syften.

Kör exemplet nu, så ser du att för Trace meddelandet anropas endast den första AppendLiteral :

        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.

Du kan göra en sista uppdatering av hanterarens konstruktor som förbättrar effektiviteten. Hanteraren kan lägga till en slutlig out bool parameter. Ange parametern till false anger att hanteraren inte ska anropas alls för att bearbeta det interpolerade stränguttrycket:

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

Den ändringen innebär att du kan ta bort fältet enabled . Sedan kan du ändra returtypen för AppendLiteral och AppendFormatted till void. Nu visas följande utdata när du kör exemplet:

        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.

Det enda utdata som angavs är LogLevel.Trace utdata från konstruktorn. Hanteraren angav att den inte är aktiverad, så ingen av Append metoderna anropades.

Det här exemplet illustrerar en viktig punkt för interpolerade stränghanterare, särskilt när loggningsbibliotek används. Eventuella biverkningar i platshållarna kan inte uppstå. Lägg till följande kod i huvudprogrammet och se det här beteendet i praktiken:

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

Du kan se att variabeln index ökas fem gånger varje iteration av loopen. Eftersom platshållarna endast utvärderas för Critical, Error och Warning nivåer, inte för Information och Trace, matchar det slutliga värdet index för inte förväntningarna:

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

Interpolerade stränghanterare ger större kontroll över hur ett interpolerat stränguttryck konverteras till en sträng. .NET-körningsteamet har redan använt den här funktionen för att förbättra prestandan inom flera områden. Du kan använda samma funktion i dina egna bibliotek. Om du vill utforska vidare kan du titta på System.Runtime.CompilerServices.DefaultInterpolatedStringHandler. Det ger en mer fullständig implementering än du skapade här. Du ser många fler överlagringar som är möjliga för Append metoderna.