Självstudie: Skriva en anpassad stränginterpolationshanterare
I den här handledningen lär du dig hur man:
- Implementera mönstret för stränginterpoleringshanteraren
- Interagera med mottagaren i en operation med 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. C#-kompilatorn är tillgänglig med Visual Studio 2022 eller .NET SDK.
Den här självstudien förutsätter att du är bekant med C# och .NET, inklusive antingen Visual Studio eller .NET CLI.
Du kan skriva 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 på liknande sätt som 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? Några exempel är:
- Du kanske inte behöver någon av de resulterande strängarna som ä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 något 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å all strängkonkatenering kan hoppas över. Dessutom behöver inga uttryck som används i platshållarna, inklusive generering av stackspårningar, utfö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 med en grundläggande Logger
-klass som stöder 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);
}
}
Den här Logger
stöder sex olika nivåer. När ett meddelande inte passerar loggnivåfiltret, finns det ingen 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 tillämpas på typen.
- En konstruktor som har två
int
parametrar,literalLength
ochformattedCount
. (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 sett skapar byggaren den formaterade strängen och tillhandahåller en medlem åt en klient att hämta strängen. Följande kod visar en LogInterpolatedStringHandler
typ som uppfyller dessa krav:
[InterpolatedStringHandler]
public 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 till LogMessage
i klassen Logger
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 hanterareparameter 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
ochAppendFormatted
för varje avsnitt i literalsträngen och för varje platshållare. - Kompilatorn anropar metoden
LogMessage
med hjälp avCoreInterpolatedStringHandler
som argument.
Observera slutligen att den sista varningen inte anropar den interpolerade stränghanteraren. Argumentet är en string
, så att anropet anropar den andra överlagringen med en strängparameter.
Viktig
Använd ref struct
endast för interpolerade stränghanterare om det är absolut nödvändigt. Användning av ref struct
har begränsningar eftersom de måste lagras på stacken. De fungerar till exempel inte om ett interpolerat stränghål innehåller ett await
uttryck eftersom kompilatorn måste lagra hanteraren i den kompilatorgenererade IAsyncStateMachine
implementeringen.
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 fungerar mindre 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 LogMessage
-deklarationen så att kompilatorn skickar de ytterligare parametrarna till hanterarens konstruktor. Det hanteras med hjälp av System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute på hanteringsargumentet:
public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
Det här attributet anger listan över argument till LogMessage
som mappas till de parametrar som följer de obligatoriska parametrarna literalLength
och formattedCount
. Den tomma strängen ("") anger mottagaren. Kompilatorn ersätter värdet för det Logger
objekt som representeras av this
för nästa argument till hanterarens konstruktor. Kompilatorn ersätter värdet för level
med 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 metoderna AppendLiteral
och AppendFormat
anropas, men de utför inget arbete. Hanteraren fastställde 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 överbelastningen gör det möjligt för anropare att lägga till formatsträngar i platshållare. När vi gör den här ändringen ska vi också ändra returtypen för de 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 gör det möjligt för att kortsluta. Metoderna returnerar false
för att indikera att bearbetningen av det interpolerade stränguttrycket ska stoppas. Att returnera true
indikerar att processen 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.");
:t
i det första meddelandet anger "kort tidsformat" för den aktuella tiden. I föregående exempel visades en av överlagringarna för metoden AppendFormatted
som du kan skapa för din hanterare. 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
som tar dessa typer istället för ett generiskt argument. Kompilatorn väljer den bästa överbelastningen. Körmiljön använder den här tekniken för att konvertera System.Span<T> till strängutgång. Du kan lägga till en heltalsparameter för att ange justering av utdata, med eller utan en IFormattable. Den 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 och du ser 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 slutlig uppdatering av hanterarens konstruktor som förbättrar effektiviteten. Hanteraren kan lägga till en slutlig out bool
parameter. Om du anger parametern till false
betyder det 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
.
När du kör exemplet ser du nu följande utdata:
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 när LogLevel.Trace
angavs är utdata från konstruktorn. Hanteraren angav att den inte är aktiverad, så ingen av de 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 kanske inte inträffar. 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 nivåerna Critical
, Error
och Warning
, inte för Information
och Trace
, matchar det slutliga värdet för index
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 använde 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 mer 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 överbelastningar som är möjliga för Append
-metoderna.