Tutorial: Schreiben eines benutzerdefinierten Zeichenfolgeninterpolationshandlers

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
  • Grundlegendes zu den neuen Bibliotheksfunktionen für die Zeichenfolgeninterpolation

Voraussetzungen

Sie müssen Ihren Computer für die Ausführung von .NET 6 einrichten, einschließlich des C# 10-Compilers. Der C# 10-Compiler steht ab Visual Studio 2022 oder mit .NET SDK 6 zur Verfügung.

In diesem Tutorial wird davon ausgegangen, dass Sie mit C# und .NET vertraut sind (einschließlich Visual Studio oder der .NET-CLI).

Neue Gliederung

C# 10 fügt Unterstützung für einen benutzerdefinierten Handler für interpolierte Zeichenfolgen hinzu. 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 wird 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 über die resultierende Zeichenfolge verwenden. Wird sie verwendet? Welche Einschränkungen gelten für das Format? Beispiele hierfür sind:

  • Keine der resultierenden Zeichenfolgen darf einen bestimmten Grenzwert überschreiten, z. B. 80 Zeichen. Sie können die interpolierten Zeichenfolgen so verarbeiten, dass sie einen Puffer fester Länge ausfüllen und die Verarbeitung beendet wird, sobald diese Pufferlänge erreicht ist.
  • Sie verwenden 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 Erstellung 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 Handler für interpolierte Zeichenfolgen kann ermitteln, ob die formatierte Zeichenfolge verwendet wird, und nur bei Bedarf die erforderlichen Aufgaben 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 die Protokollierung akzeptiert eine (vollständig formatierte) Zeichenfolge als Nachricht. Alle Arbeiten zum Erstellen der Zeichenfolge wurden bereits ausgeführt.

Implementieren des Handlermusters

In diesem Schritt wird ein Handler für interpolierte Zeichenfolgen erstellt, der das aktuelle Verhalten neu erstellt. Ein Handler für interpolierte Zeichenfolgen 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 und formattedCount, aufweisen. (Weitere Parameter sind zulässig.)
  • Eine öffentliche AppendLiteral-Methode mit der Signatur public void AppendLiteral(string s) ist erforderlich.
  • Eine generische öffentliche AppendFormatted-Methode mit der Signatur public 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 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();
}

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.

Mithilfe des folgenden Codes als Hauptprogramm können Sie sicherstellen, dass der neue Handler 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.");

Beim Ausführen der Anwendung wird eine Ausgabe ähnlich dem folgenden Text erzeugt:

        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 verfolgen, sehen Sie, wie der Compiler Code zum Aufrufen des Handlers und zum Erstellen der Zeichenfolge hinzufügt:

  • 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 und AppendFormatted Aufrufe hinzu.
  • Der Compiler ruft die LogMessage-Methode auf und verwendet CoreInterpolatedStringHandler als Argument.

Beachten Sie abschließend, dass die letzte Warnung den Handler für interpolierte Zeichenfolgen nicht aufruft. Das Argument ist ein string, sodass der Aufruf die andere Überladung mit einem Zeichenfolgenparameter aufruft.

Hinzufügen weiterer Funktionen zum Handler

Die vorherige Version des Handlers für interpolierte Zeichenfolgen implementiert das Muster. Um die Verarbeitung jedes einzelnen Platzhalterausdrucks zu vermeiden, benötigen Sie weitere Informationen im Handler. In diesem Abschnitt verbessern Sie Ihren Handler so, dass er weniger Arbeiten ausführt, wenn die erstellte Zeichenfolge nicht in das Protokoll geschrieben wird. Sie verwenden System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, um eine Zuordnung zwischen Parametern für eine öffentliche API und Parametern für den Konstruktor eines Handlers anzugeben. Dadurch erhält der Handler die Informationen, mit deren Hilfe bestimmt wird, 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: einen zur Angabe der Protokollebene für diese Nachricht und den anderen als 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");
}

Danach 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 werden, 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 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 sehen, dass die Methoden AppendLiteral und AppendFormat aufgerufen werden, aber sie führen keine Aufgaben aus. Der Handler hat festgestellt, dass die endgültige Zeichenfolge nicht benötigt wird, daher wird sie nicht erstellt. Es müssen noch einige Verbesserungen vorgenommen werden.

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. Bei dieser Änderung ändern wir auch den Rückgabetyp der anderen AppendFormatted- und AppendLiteral-Methoden aus void in bool (wenn eine dieser Methoden unterschiedliche Rückgabetypen enthält, 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 fester Länge zu unterstützen. Außerdem kann eine Bedingung 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 zu formatierende 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. Durch Festlegen dieses Parameters auf false wird angegeben, dass der Handler überhaupt nicht aufgerufen werden soll, 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. Dann ändern Sie den Rückgabetyp von AppendLiteral und AppendFormatted in void. Wenn Sie das Beispiel jetzt ausführen, wird folgende Ausgabe angezeigt:

        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 dem Hauptprogramm den folgenden Code hinzu, und sehen Sie sich dieses Verhalten in Aktion an:

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 sehen, dass die Variable index bei jedem Durchlaufen der Schleife fünfmal erhöht 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 Team der .NET-Runtime hat dieses Feature bereits verwendet, um die Leistung in verschiedenen Bereichen zu verbessern. Sie können dieselbe Funktion in Ihren eigenen Bibliotheken nutzen. Sehen Sie sich zur weiteren Erkundung den System.Runtime.CompilerServices.DefaultInterpolatedStringHandler an. Er bietet eine vollständigere Implementierung, als Sie an dieser Stelle erstellt haben. Sie finden viele weitere Überladungen, die für die Append-Methoden möglich sind.