Zelfstudie: Een aangepaste tekenreeksinterpolatiehandler schrijven
In deze zelfstudie leert u het volgende:
- Het tekenreeksinterpolatiehandlerpatroon implementeren
- Voer een interactie uit met de ontvanger tijdens een bewerking van tekenreeksinterpolatie.
- Argumenten toevoegen aan de handler voor tekenreeksinterpolatie
- Begrijp de nieuwe bibliotheekfuncties voor tekenreeksinterpolatie
Voorwaarden
U moet uw computer instellen om .NET uit te voeren. De C#-compiler is beschikbaar met Visual Studio 2022- of de .NET SDK-.
In deze zelfstudie wordt ervan uitgegaan dat u bekend bent met C# en .NET, met inbegrip van Visual Studio of de .NET CLI.
U kunt een aangepaste, geïnterpoleerde tekenreekshandler schrijven. Een interpolatiestring-handler is een type dat de plaatsaanduidende expressie in een geïnterpoleerde tekenreeks verwerkt. Zonder een aangepaste handler worden tijdelijke aanduidingen verwerkt die vergelijkbaar zijn met 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 zijn:
- 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 tabelindeling 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 tutorial maakt u een string-interpolatiehandler voor een van de kernprestatiescenario's: loggingbibliotheken. Afhankelijk van het geconfigureerde logboekniveau is het werk om een logboekbericht te maken niet nodig. Als logging is uitgeschakeld, is het construeren van een tekenreeks uit een geïnterpoleerde expressie 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 eenvoudige Logger
klasse 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);
}
}
Deze Logger
ondersteunt zes verschillende niveaus. Wanneer een bericht niet door het filter op logboekniveau komt, vindt er geen uitvoer plaats. 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 om het huidige gedrag te reproduceren. Een geïnterpoleerde tekenreekshandler is een type dat de volgende kenmerken moet hebben:
- System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute toegepast op het type.
- Een constructor met twee
int
parameters,literalLength
enformattedCount
. (Er zijn meer parameters toegestaan). - Een openbare
AppendLiteral
methode met de signatuur:public void AppendLiteral(string s)
. - Een publieke
AppendFormatted
methode met de signatuur:public void AppendFormatted<T>(T t)
.
Intern maakt de bouwer de opgemaakte tekenreeks en biedt een methode voor een client om die tekenreeks op te halen. De volgende code toont een LogInterpolatedStringHandler
type dat voldoet aan deze vereisten:
[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();
}
Je kunt nu een overloadfunctie toevoegen aan LogMessage
in de Logger
-klasse om je nieuwe interpolatiestring-handler 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
enAppendFormatted
voor elk deel van de letterlijke string en voor elke tijdelijke aanduiding. - De compiler roept de
LogMessage
methode aan met behulp van deCoreInterpolatedStringHandler
als argument.
Ten slotte ziet u dat de laatste waarschuwing de geïnterpoleerde tekenreekshandler niet aanroept. Het argument is een string
, waardoor de aanroep de andere overload met een stringparameter activeert.
Belangrijk
Gebruik ref struct
alleen voor geïnterpoleerde tekenreekshandlers als dat absoluut noodzakelijk is. Het gebruik van ref struct
heeft beperkingen omdat ze op de stack moeten worden opgeslagen. Ze werken bijvoorbeeld niet als een geïnterpoleerd tekenreeksgat een await
-expressie bevat, omdat de compiler de handler moet opslaan in de door de compiler gegenereerde IAsyncStateMachine
-implementatie.
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 verbetert u de handler 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 van een openbare API en parameters van 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 aan 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 declaratie LogMessage
bijwerken, zodat de compiler de aanvullende parameters doorgeeft aan de constructor van de handler. Dit wordt verwerkt met behulp van de System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute op het handlerargument.
public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
Met dit kenmerk specificeert u de lijst met argumenten voor LogMessage
die overeenkomen met de parameters, waarna de vereiste parameters literalLength
en formattedCount
volgen. De lege tekenreeks (""), geeft de ontvanger op. De compiler vervangt de waarde van het Logger
-object dat wordt vertegenwoordigd door this
voor het volgende argument aan de constructor van de handler. De compiler vervangt de waarde van level
door 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.
Eerst kunt u een overbelasting van AppendFormatted
toevoegen waarmee het argument wordt beperkt tot een type dat System.IFormattableimplementeert. Met deze overbelasting kunnen bellers opmaaktekenreeksen toevoegen in de tijdelijke aanduidingen. Tijdens het aanbrengen van deze wijziging wijzigen we ook het retourtype van de andere AppendFormatted
en AppendLiteral
methoden, van void
tot bool
(als een van deze methoden verschillende retourtypen heeft, krijgt u een compilatiefout). Deze wijziging maakt kortsluitingmogelijk. De methoden retourneren false
om aan te geven dat de verwerking van de geïnterpoleerde tekenreeksexpressie moet worden gestopt. Het retourneren van true
geeft aan dat het moet doorgaan. 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.");
De :t
in het eerste bericht specificeert het 'korte tijdformaat' voor de huidige tijd. In het vorige voorbeeld werd een van de overloads van de AppendFormatted
-methode getoond die je voor je 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 die u maakt, te converteren naar tekenreeks. U kunt overbelastingen schrijven van AppendFormatted
die in plaats van een algemeen argument deze typen gebruiken. De compiler kiest de beste overbelasting. De runtime gebruikt deze techniek om System.Span<T> te converteren naar string-uitvoer. U kunt een parameter voor een geheel getal toevoegen om de uitlijning op te geven van de uitvoer, met of zonder een IFormattable. De System.Runtime.CompilerServices.DefaultInterpolatedStringHandler die wordt geleverd met .NET 6 bevat negen overbelastingen van AppendFormatted voor 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 voor het Trace
bericht alleen de eerste AppendLiteral
wordt aangeroepen:
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 op false
geeft aan dat de handler helemaal niet moet 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 van AppendLiteral
en AppendFormatted
wijzigen in 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 geeft aan dat deze niet is ingeschakeld, dus geen van de Append
methoden zijn aangeroepen.
In dit voorbeeld ziet u een belangrijk punt voor geïnterpoleerde tekenreekshandlers, met name wanneer logboekregistratiebibliotheken worden gebruikt. Eventuele bijwerkingen in de placeholders kunnen mogelijk 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 Critical
, Error
- en Warning
niveaus, niet voor Information
en Trace
, komt de uiteindelijke waarde van 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 gebruikt om de prestaties op verschillende gebieden te verbeteren. U kunt gebruikmaken van dezelfde mogelijkheid in uw eigen bibliotheken. Kijk naar de System.Runtime.CompilerServices.DefaultInterpolatedStringHandlerom verder te ontdekken. Het biedt een completere implementatie dan u hier hebt gebouwd. U ziet veel meer overbelastingen die mogelijk zijn voor de Append
methoden.