Teilen über


Anleitung: Schreiben eines benutzerdefinierten String-Interpolations-Handlers

In diesem Tutorial lernen Sie Folgendes:

  • Implementieren Sie das Zeichenfolgeninterpolationshandlermuster.
  • Interagieren mit dem Empfänger in einem Zeichenfolgeninterpolationsvorgang
  • Fügen Sie dem Zeichenfolgeninterpolationshandler Argumente hinzu.
  • Verständnis der neuen Bibliotheksfunktionen für die Zeichenfolgeninterpolation.

Voraussetzungen

Richten Sie Ihren Computer so ein, dass .NET ausgeführt wird. Der C#-Compiler ist über Visual Studio oder das .NET SDK verfügbar.

In diesem Lernprogramm wird davon ausgegangen, dass Sie mit C# und .NET vertraut sind, einschließlich Visual Studio oder Visual Studio Code und C# DevKit.

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 verarbeitet das System Platzhalter ähnlich wie String.Format. 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. Stellen Sie Fragen wie: 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 diese Einschränkung durchsetzen, statt allen Clientcode zur Anpassung zu zwingen.

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 alle Ausdrücke, die in den Platzhaltern verwendet werden, einschließlich des Generierens von Stapelablaufverfolgungen, nicht ausgeführt werden.

Ein interpolierter Zeichenfolgenhandler kann bestimmen, ob die formatierte Zeichenfolge verwendet wird, und nur bei Bedarf die erforderliche Arbeit ausführen.

Erste Implementierung

Beginnen Sie 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 Filter auf Protokollebene nicht übergibt, erzeugt der Logger keine Ausgabe. Die öffentliche API für den Logger akzeptiert eine vollständig formatierte Zeichenfolge als Nachricht. Der Aufrufer erledigt die gesamte Arbeit, um die Zeichenfolge zu erstellen.

Implementieren des Handlermusters

In diesem Schritt erstellen Sie einen interpolierten Zeichenfolgenhandler , 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 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 ein Element für einen Client bereit, um diese Zeichenfolge abzurufen. 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");
    }

    public override string ToString() => builder.ToString();
}

Hinweis

Wenn der interpolierte Zeichenfolgenausdruck eine Kompilierungszeitkonstante ist (d. h. es hat keine Platzhalter), verwendet der Compiler den Zieltyp string , anstatt einen benutzerdefinierten interpolierten Zeichenfolgenhandler aufzugeben. Dieses Verhalten bedeutet, dass konstant interpolierte Zeichenfolgen benutzerdefinierte Handler vollständig umgehen.

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

Sie müssen die ursprüngliche LogMessage Methode nicht entfernen. Wenn das Argument ein interpolierter Zeichenfolgenausdruck ist, bevorzugt der Compiler eine Methode mit einem interpolierten Handlerparameter gegenüber einer Methode mit einem string Parameter.

Sie können überprüfen, ob der neue Handler aufgerufen wird, indem Sie den folgenden Code als Hauptprogramm verwenden:

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 und AppendFormatted Aufrufe hinzu.
  • Der Compiler ruft die LogMessage-Methode mit CoreInterpolatedStringHandler 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. ref struct Typen haben Einschränkungen, da sie im Stapel gespeichert werden müssen. Sie funktionieren beispielsweise nicht, wenn ein interpoliertes Zeichenfolgenloch einen await Ausdruck enthält, da der Compiler den Handler in der compilergenerierten 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. Diese Zuordnung stellt den Handler mit den erforderlichen Informationen bereit, um festzustellen, ob die interpolierte Zeichenfolge ausgewertet werden soll.

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

Verwenden Sie als Nächstes das Feld, damit Ihr Handler nur Literale oder formatierte Objekte anhängt, 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");
}

Aktualisieren Sie als Nächstes die LogMessage Deklaration, damit der Compiler die zusätzlichen Parameter an den Konstruktor des Handlers übergibt. Behandeln Sie diesen Schritt mithilfe des System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute Arguments des Handlers.

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

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.

Hinweis

Wenn die InterpolatedStringHandlerArgumentAttribute Konstruktorargumentliste leer ist, entspricht das Verhalten dem vollständigen Auslassen des Attributs.

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 AppendLiteral Methoden AppendFormat aufgerufen werden, aber sie erledigen keine Arbeit. 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. Ändern Sie beim Vornehmen dieser Änderung auch den Rückgabetyp der anderen AppendFormatted Und AppendLiteral Methoden von void zu 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");
}

public void AppendFormatted<T>(T t, int alignment, string format) where T : IFormattable
{
    Console.WriteLine($"\tAppendFormatted (IFormattable version) called: {t} with alignment {alignment} and format {{{format}}} is of type {typeof(T)},");
    var formatString =$"{alignment}:{format}";
    builder.Append(string.Format($"{{0,{formatString}}}", t));
    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 angegeben, dass er nicht aktiviert ist, sodass keine der Append Methoden aufgerufen wird.

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 {index++}");
    numberOfIncrements++;
}
Console.WriteLine($"Value of index {index}, value of numberOfIncrements: {numberOfIncrements}");

Sie können sehen, dass die index Variable bei jeder Iteration der Schleife inkrementiert wird. Da die Platzhalter nur für die Ebenen Critical, Error und Warning ausgewertet werden, nicht aber für Information und Trace, entspricht der endgültige Wert von index nicht der Erwartung.

Critical
Critical: Increment index 0
Error
Error: Increment index 1
Warning
Warning: Increment index 2
Information
Trace
Value of index 3, value of numberOfIncrements: 5

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.