Anleitung: Schreiben eines benutzerdefinierten String-Interpolations-Handlers
In diesem Tutorial lernen Sie Folgendes:
- Implementieren des Musters für Zeichenfolgeninterpolationshandler
- Interagieren mit dem Empfänger in einem Zeichenfolgeninterpolationsvorgang
- Hinzufügen von Argumenten zum Zeichenfolgeninterpolationshandler
- Verstehen Sie die neuen Bibliotheksfunktionen für die Zeichenfolgeninterpolation
Voraussetzungen
Sie müssen Ihren Computer so einrichten, dass .NET ausgeführt wird. Der C#-Compiler ist mit Visual Studio 2022- oder dem .NET SDK-verfügbar.
In diesem Lernprogramm wird davon ausgegangen, dass Sie mit C# und .NET vertraut sind, einschließlich Visual Studio oder .NET CLI.
Sie können einen benutzerdefinierten Handler für interpolierte Zeichenfolgen schreiben. Ein Handler für interpolierte Zeichenfolgen ist ein Typ, der den Platzhalterausdruck in einer interpolierten Zeichenfolge verarbeitet. Ohne einen benutzerdefinierten Handler werden Platzhalter ähnlich wie String.Format verarbeitet. Jeder Platzhalter ist als Text formatiert, anschließend werden die Komponenten verkettet, um die resultierende Zeichenfolge zu bilden.
Sie können einen Handler für jedes Szenario schreiben, in dem Sie Informationen zur resultierenden Zeichenfolge verwenden. Wird sie verwendet? Welche Einschränkungen gelten für das Format? Einige Beispiele sind:
- Es könnte nötig sein, dass keine der resultierenden Zeichenfolgen größer als ein bestimmter Grenzwert, z. B. 80 Zeichen, ist. Sie können die interpolierten Zeichenfolgen verarbeiten, um einen Puffer mit fester Länge auszufüllen und die Verarbeitung zu beenden, sobald diese Pufferlänge erreicht ist.
- Möglicherweise verfügen Sie über ein tabellarisches Format, und jeder Platzhalter muss eine feste Länge aufweisen. Ein benutzerdefinierter Handler kann dies erzwingen, anstatt die Konformität des gesamten Clientcodes durchzusetzen.
In diesem Tutorial erstellen Sie einen Zeichenfolgeninterpolationshandler für eines der wichtigsten Leistungsszenarien: Protokollierungsbibliotheken. Abhängig von der konfigurierten Protokollebene ist die Arbeit zum Erstellen einer Protokollnachricht nicht erforderlich. Wenn die Protokollierung deaktiviert ist, muss aus dem interpolierten Zeichenfolgenausdruck keine Zeichenfolge erstellt werden. Die Nachricht wird niemals ausgegeben, sodass Zeichenfolgenverkettungen übersprungen werden können. Darüber hinaus müssen in den Platzhaltern verwendete Ausdrücke nicht ausgeführt werden, beispielsweise das Generieren von Stapelüberwachungen.
Ein interpolierter Zeichenfolgenhandler kann bestimmen, ob die formatierte Zeichenfolge verwendet wird, und nur bei Bedarf die erforderliche Arbeit ausführen.
Erste Implementierung
Beginnen wir mit einer einfachen Logger
Klasse, die verschiedene Ebenen unterstützt:
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);
}
}
Dieser Logger
unterstützt sechs verschiedene Ebenen. Wenn eine Nachricht den Protokollebenenfilter nicht übergibt, erfolgt keine Ausgabe. Die öffentliche API für den Logger akzeptiert eine (vollständig formatierte) Zeichenfolge als Nachricht. Alle Arbeiten zum Erstellen der Zeichenfolge wurden bereits ausgeführt.
Implementieren des Handlermusters
Dieser Schritt besteht darin, einen interpolierten Zeichenfolgenhandler zu erstellen, der das aktuelle Verhalten neu erstellt. Ein interpolierter Zeichenfolgenhandler ist ein Typ, der die folgenden Merkmale aufweisen muss:
- Das System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute muss auf den Typ angewendet sein.
- Er muss einen Konstruktor mit zwei
int
-Parametern,literalLength
undformattedCount
, aufweisen. (Weitere Parameter sind zulässig). - Eine öffentliche
AppendLiteral
-Methode mit der Signaturpublic void AppendLiteral(string s)
ist erforderlich. - Eine generische öffentliche
AppendFormatted
-Methode mit der Signaturpublic void AppendFormatted<T>(T t)
ist erforderlich.
Intern erstellt der Generator die formatierte Zeichenfolge und stellt einen Member für einen Client zum Abrufen dieser Zeichenfolge bereit. Der folgende Code zeigt einen LogInterpolatedStringHandler
-Typ, der diese Anforderungen erfüllt:
[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();
}
Sie können jetzt LogMessage
in der Logger
-Klasse eine Überladung hinzufügen, um den neuen Handler für interpolierte Zeichenfolgen zu testen:
public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
Sie müssen die ursprüngliche LogMessage
-Methode nicht entfernen, der Compiler bevorzugt eine Methode mit einem interpolierten Handlerparameter gegenüber einer Methode mit einem string
-Parameter, wenn das Argument ein interpolierter Zeichenfolgenausdruck ist.
Sie können überprüfen, ob der neue Handler als Hauptprogramm mit dem folgenden Code aufgerufen wird.
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.");
Wenn Sie die Anwendung ausführen, wird eine Ausgabe erzeugt, die dem folgenden Text ähnlich ist:
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.
Wenn Sie die Ausgabe durchlaufen, können Sie sehen, wie der Compiler Code hinzufügt, um den Handler aufzurufen und die Zeichenfolge zu erstellen:
- Der Compiler fügt einen Aufruf zum Erstellen des Handlers hinzu. Hierbei werden die Gesamtlänge des Literaltexts in der Formatzeichenfolge und die Anzahl der Platzhalter übergeben.
- Für jeden Abschnitt der Literalzeichenfolge und für jeden Platzhalter fügt der Compiler in
AppendLiteral
undAppendFormatted
Aufrufe hinzu. - Der Compiler ruft die
LogMessage
-Methode mitCoreInterpolatedStringHandler
als Argument auf.
Beachten Sie schließlich, dass die letzte Warnung den interpolierten Zeichenfolgenhandler nicht aufruft. Das Argument ist ein string
, sodass der Aufruf die andere Überladung mit einem Zeichenfolgenparameter aufruft.
Wichtig
Verwenden Sie ref struct
nur für interpolierte Zeichenfolgenhandler, wenn dies unbedingt erforderlich ist. Die Verwendung von ref struct
unterliegt Einschränkungen, da die Handler im Stapel gespeichert werde müssen. Beispielsweise funktionieren sie nicht, wenn eine interpolierte Zeichenfolgenlücke einen await
-Ausdruck enthält, da der Compiler den Handler in der vom Compiler generierten IAsyncStateMachine
-Implementierung speichern muss.
Hinzufügen weiterer Funktionen zum Handler
Die vorherige Version des interpolierten Zeichenfolgenhandlers implementiert das Muster. Um die Verarbeitung jedes einzelnen Platzhalterausdrucks zu vermeiden, benötigen Sie weitere Informationen im Handler. In diesem Abschnitt verbessern Sie den Handler, sodass es weniger Arbeit macht, wenn die konstruierte Zeichenfolge nicht in das Protokoll geschrieben wird. Sie verwenden System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, um eine Zuordnung zwischen den Parametern einer öffentlichen API und den Parametern eines Handlerkonstruktors anzugeben. Dies stellt den Handler mit den erforderlichen Informationen bereit, um festzustellen, ob die interpolierte Zeichenfolge ausgewertet werden soll.
Beginnen wir mit Änderungen am Handler. Fügen Sie zunächst ein Feld hinzu, um nachzuverfolgen, ob der Handler aktiviert ist. Fügen Sie dem Konstruktor zwei Parameter hinzu: eine, um die Protokollebene für diese Nachricht anzugeben, und die andere einen Verweis auf das Protokollobjekt:
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}");
}
Als Nächstes verwenden Sie das Feld, sodass Ihr Handler nur Literale oder formatierte Objekte anfügt, wenn die endgültige Zeichenfolge verwendet wird:
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");
}
Als Nächstes müssen Sie die LogMessage
-Deklaration aktualisieren, damit der Compiler die zusätzlichen Parameter an den Konstruktor des Handlers übergibt. Dies wird über das System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute für das Handlerargument durchgeführt:
public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
Dieses Attribut gibt die Liste der Argumente für LogMessage
an, die den Parametern zugeordnet sind, die auf die erforderlichen Parameter literalLength
und formattedCount
folgen. Die leere Zeichenfolge (""), gibt den Empfänger an. Der Compiler ersetzt den Wert des Logger
-Objekts, das durch this
dargestellt wird, durch das nächste Argument für den Handlerkonstruktor. Der Compiler ersetzt den Wert von level
durch das folgende Argument. Sie können eine beliebige Anzahl von Argumenten für jeden Handler angeben, den Sie schreiben. Die Argumente, die Sie hinzufügen, sind Zeichenfolgenargumente.
Sie können diese Version mit demselben Testcode ausführen. Dieses Mal werden die folgenden Ergebnisse angezeigt:
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.
Sie können sehen, dass die methoden AppendLiteral
und AppendFormat
aufgerufen werden, aber sie funktionieren nicht. Der Handler hat festgestellt, dass die endgültige Zeichenfolge nicht benötigt wird, sodass der Handler sie nicht erstellt. Es gibt noch einige Verbesserungen vorzunehmen.
Zunächst können Sie eine Überladung von AppendFormatted
hinzufügen, die das Argument auf einen Typ einschränkt, der System.IFormattable implementiert. Diese Überladung ermöglicht Aufrufern das Hinzufügen von Formatzeichenfolgen in den Platzhaltern. Während sie diese Änderung vornehmen, ändern wir auch den Rückgabetyp der anderen AppendFormatted
und AppendLiteral
Methoden von void
in bool
(wenn eine dieser Methoden unterschiedliche Rückgabetypen aufweist, erhalten Sie einen Kompilierungsfehler). Diese Änderung ermöglicht einen Kurzschluss. Die Methoden geben false
zurück, um darauf hinzuweisen, dass die Verarbeitung des interpolierten Zeichenfolgenausdrucks beendet werden sollte. Die Rückgabe true
gibt an, dass sie fortgesetzt werden soll. In diesem Beispiel verwenden Sie sie, um die Verarbeitung zu beenden, wenn die resultierende Zeichenfolge nicht benötigt wird. Durch Kurzschließen werden differenziertere Aktionen unterstützt. Sie können die Verarbeitung des Ausdrucks beenden, sobald er eine bestimmte Länge erreicht hat, um Puffer mit fester Länge zu unterstützen. Oder eine Bedingung könnte darauf hinweisen, dass verbleibende Elemente nicht benötigt werden.
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");
}
Mit dieser Ergänzung können Sie Formatzeichenfolgen in Ihrem interpolierten Zeichenfolgenausdruck angeben:
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
in der ersten Nachricht gibt das „kurze Zeitformat“ für die aktuelle Zeit an. Im vorherigen Beispiel wurde eine der Überladungen für die AppendFormatted
-Methode gezeigt, die Sie für Ihren Handler erstellen können. Sie müssen kein generisches Argument für das formatierte Objekt angeben. Möglicherweise verfügen Sie über effizientere Möglichkeiten, die von Ihnen erstellten Typen in Zeichenfolgen zu konvertieren. Sie können Überladungen von AppendFormatted
schreiben, die diese Typen anstelle eines generischen Arguments verwenden. Der Compiler wählt die beste Überladung aus. Die Runtime verwendet diese Technik, um System.Span<T> in Zeichenfolgenausgabe zu konvertieren. Sie können einen ganzzahligen Parameter hinzufügen, um die Ausrichtung der Ausgabe mit oder ohne IFormattable anzugeben. Der in .NET 6 enthaltene System.Runtime.CompilerServices.DefaultInterpolatedStringHandler enthält neun Überladungen von AppendFormatted für verschiedene Zwecke. Sie können sie beim Erstellen eines Handlers für Ihre Zwecke als Referenz verwenden.
Führen Sie das Beispiel jetzt aus. Sie stellen fest, dass für die Trace
-Nachricht nur das erste AppendLiteral
aufgerufen wird:
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.
Sie können am Konstruktor des Handlers ein letztes Update vornehmen, um die Effizienz zu verbessern. Der Handler kann einen abschließenden Parameter out bool
hinzufügen. Das Festlegen dieses Parameters auf false
gibt an, dass der Handler überhaupt nicht aufgerufen werden sollte, um den interpolierten Zeichenfolgenausdruck zu verarbeiten:
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!;
}
Diese Änderung bedeutet, dass Sie das feld enabled
entfernen können. Anschließend können Sie den Rückgabetyp von AppendLiteral
und AppendFormatted
zu void
ändern.
Wenn Sie das Beispiel jetzt ausführen, erhalten Sie folgende Ausgabe:
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.
Die einzige Ausgabe bei Angabe von LogLevel.Trace
ist die Ausgabe des Konstruktors. Der Handler hat angezeigt, dass er nicht aktiviert ist. Daher wurde keine der Append
-Methoden aufgerufen.
Dieses Beispiel veranschaulicht einen wichtigen Aspekt von Handlern für interpolierte Zeichenfolgen, insbesondere bei Verwendung von Protokollierungsbibliotheken. Nebeneffekte in den Platzhaltern treten möglicherweise nicht auf. Fügen Sie ihrem Hauptprogramm den folgenden Code hinzu, und sehen Sie dieses Verhalten in Aktion:
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}");
Sie können sehen, dass die Variable index
in jeder Iteration der Schleife fünfmal inkrementiert wird. Da die Platzhalter nur für die Ebenen Critical
, Error
und Warning
, aber nicht für Information
und Trace
ausgewertet werden, entspricht der endgültige Wert von index
nicht den Erwartungen:
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
Handler für interpolierte Zeichenfolgen bieten eine bessere Kontrolle darüber, wie ein interpolierter Zeichenfolgenausdruck in eine Zeichenfolge konvertiert wird. Das .NET-Laufzeitteam hat dieses Feature verwendet, um die Leistung in mehreren Bereichen zu verbessern. Sie können dieselbe Funktion in Ihren eigenen Bibliotheken verwenden. Sehen Sie sich zur weiteren Erkundung den System.Runtime.CompilerServices.DefaultInterpolatedStringHandler an. Es bietet eine umfassendere Implementierung, als Sie hier erstellt haben. Sie finden viele weitere Überladungen, die für die Append
-Methoden möglich sind.