Freigeben über


Verbesserte interpolierte Zeichenfolgen

Anmerkung

Dieser Artikel ist eine Featurespezifikation. Die Spezifikation dient als Designdokument für das Feature. Es enthält vorgeschlagene Spezifikationsänderungen sowie Informationen, die während des Entwurfs und der Entwicklung des Features erforderlich sind. Diese Artikel werden veröffentlicht, bis die vorgeschlagenen Spezifikationsänderungen abgeschlossen und in die aktuelle ECMA-Spezifikation aufgenommen werden.

Es kann einige Abweichungen zwischen der Featurespezifikation und der abgeschlossenen Implementierung geben. Diese Unterschiede werden in den entsprechenden Hinweisen zum Language Design Meeting (LDM) erfasst.

Weitere Informationen zum Einführen von Featurespezifikationen in den C#-Sprachstandard finden Sie im Artikel zu den Spezifikationen.

Champion-Problem: https://github.com/dotnet/csharplang/issues/4487

Zusammenfassung

Wir stellen ein neues Muster zur Erstellung und Nutzung von interpolierten Zeichenfolgenausdrücken vor, um eine effiziente Formatierung zu ermöglichen. Es kann sowohl in allgemeinen string-Szenarien als auch in spezialisierteren Szenarien wie Protokollierungsframeworks eingesetzt werden, ohne unnötige Zuordnungen durch die Formatierung der Zeichenfolge im Framework zu verursachen.

Motivation

Heute besteht die Zeichenfolgeninterpolation hauptsächlich aus einem Aufruf von string.Format. Dies kann, obwohl es allgemein verwendet wird, aus verschiedenen Gründen ineffizient sein:

  1. Dabei werden alle Strukturargumente verpackt, sofern die Laufzeit nicht zufällig eine Überladung von string.Format eingeführt hat, die genau die richtigen Typen von Argumenten in genau der richtigen Reihenfolge akzeptiert.
    • Diese Sortierung ist der Grund, warum die Laufzeit zögerlich ist, generische Versionen der Methode einzuführen, da sie zu einer kombinatorischen Explosion generischer Instanziierungen einer sehr gängigen Methode führen würde.
  2. In den meisten Fällen muss es ein Array für die Argumente allokieren.
  3. Es gibt keine Möglichkeit, die Instanz zu instanziieren, wenn sie nicht erforderlich ist. Protokollierungsframeworks werden beispielsweise empfehlen, die String-Interpolation zu vermeiden, da dadurch eine Zeichenfolge generiert wird, die je nach der aktuellen Protokollebene der Anwendung möglicherweise nicht benötigt wird.
  4. Es kann heute niemals Span oder andere Verweisstrukturtypen verwenden, da Refstrukturen nicht als generische Typparameter zulässig sind. Das bedeutet, wenn ein Benutzer das Kopieren an Zwischenablagen vermeiden möchte, muss er Zeichenfolgen manuell formatieren.

Intern hat die Laufzeit einen Typ namens ValueStringBuilder, um die ersten 2 dieser Szenarien zu bewältigen. Sie übergeben einen mithilfe von „stackalloc“ erstellten Puffer an den Builder, rufen wiederholt AppendFormat mit jedem Teil auf und erhalten schließlich eine endgültige Zeichenfolge. Wenn die resultierende Zeichenfolge die Grenzen des Stapelpuffers überschreitet, dann können sie auf ein Array im Heap zugreifen. Es ist jedoch gefährlich, diesen Typ direkt zugänglich zu machen, denn eine falsche Nutzung könnte dazu führen, dass ein gemietetes Array doppelt verworfen wird. Das kann dann zu unterschiedlichsten undefinierten Verhaltensweisen im Programm führen, weil zwei Speicherorte glauben, dass sie alleinigen Zugriff auf das gemietete Array haben. Dieser Vorschlag bietet die Möglichkeit, diesen Typ sicher aus systemeigenem C#-Code zu verwenden, indem nur ein interpoliertes Zeichenfolgenliteral geschrieben wird. Der geschriebene Code bleibt dabei unverändert und gleichzeitig wird jede interpolierte Zeichenfolge verbessert, die ein Benutzer schreibt. Außerdem wird dieses Muster erweitert, um interpolierte Zeichenfolgen zuzulassen, die als Argumente an andere Methoden übergeben werden, wobei ein Handlermuster verwendet wird, das durch den Empfänger der Methode definiert wird. Dadurch können z. B. Protokollierungsframeworks vermeiden, Zeichenfolgen zuzuordnen, die niemals benötigt werden, und C#-Benutzern eine vertraute und bequeme Interpolationssyntax bieten.

Detailentwurf

Das Handlermuster

Wir stellen ein neues Handlermuster vor, das eine interpolierte Zeichenfolge darstellen kann, die als Argument an eine Methode übergeben wird. Das einfache Englisch des Musters lautet wie folgt:

Wenn ein interpolated_string_expression als Argument an eine Methode übergeben wird, betrachten wir den Typ des Parameters. Wenn der Parametertyp über einen Konstruktor verfügt, der mit 2 int-Parametern literalLength und formattedCount aufgerufen werden kann, optional zusätzliche Parameter annimmt, die durch ein Attribut des ursprünglichen Parameters angegeben werden, optional über einen ausgehenden booleschen nachgestellten Parameter verfügt, und der Typ des ursprünglichen Parameters die Instanzmethoden AppendLiteral und AppendFormatted besitzt, die für jeden Teil der interpolierten Zeichenfolge aufgerufen werden können, dann nutzen wir diese Möglichkeiten zur Interpolation anstatt eines herkömmlichen Aufrufs von string.Format(formatStr, args). Ein konkretes Beispiel ist hilfreich, um sich dies vorzustellen.

// The handler that will actually "build" the interpolated string"
[InterpolatedStringHandler]
public ref struct TraceLoggerParamsInterpolatedStringHandler
{
    // Storage for the built-up string

    private bool _logLevelEnabled;

    public TraceLoggerParamsInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, out bool handlerIsValid)
    {
        if (!logger._logLevelEnabled)
        {
            handlerIsValid = false;
            return;
        }

        handlerIsValid = true;
        _logLevelEnabled = logger.EnabledLevel;
    }

    public void AppendLiteral(string s)
    {
        // Store and format part as required
    }

    public void AppendFormatted<T>(T t)
    {
        // Store and format part as required
    }
}

// The logger class. The user has an instance of this, accesses it via static state, or some other access
// mechanism
public class Logger
{
    // Initialization code omitted
    public LogLevel EnabledLevel;

    public void LogTrace([InterpolatedStringHandlerArguments("")]TraceLoggerParamsInterpolatedStringHandler handler)
    {
        // Impl of logging
    }
}

Logger logger = GetLogger(LogLevel.Info);

// Given the above definitions, usage looks like this:
var name = "Fred Silberberg";
logger.LogTrace($"{name} will never be printed because info is < trace!");

// This is converted to:
var name = "Fred Silberberg";
var receiverTemp = logger;
var handler = new TraceLoggerParamsInterpolatedStringHandler(literalLength: 47, formattedCount: 1, receiverTemp, out var handlerIsValid);
if (handlerIsValid)
{
    handler.AppendFormatted(name);
    handler.AppendLiteral(" will never be printed because info is < trace!");
}
receiverTemp.LogTrace(handler);

Da TraceLoggerParamsInterpolatedStringHandler einen Konstruktor mit den richtigen Parametern aufweist, sagen wir, dass die interpolierte Zeichenfolge eine implizite Handlerkonvertierung in diesen Parameter aufweist und auf das oben gezeigte Muster reduziert wird. Die für dies erforderliche Spezifikation ist ein bisschen kompliziert und wird weiter unten erweitert.

Der Rest dieses Vorschlags wird Append... verwenden, um auf eine der AppendLiteral oder AppendFormatted in Fällen zu verweisen, in denen beide anwendbar sind.

Neue Attribute

Der Compiler erkennt die System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute:

using System;
namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
    public sealed class InterpolatedStringHandlerAttribute : Attribute
    {
        public InterpolatedStringHandlerAttribute()
        {
        }
    }
}

Dieses Attribut wird vom Compiler verwendet, um zu bestimmen, ob ein Typ ein gültiger interpolierter Zeichenfolgenhandlertyp ist.

Der Compiler erkennt auch die System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public sealed class InterpolatedStringHandlerArgumentAttribute : Attribute
    {
        public InterpolatedHandlerArgumentAttribute(string argument);
        public InterpolatedHandlerArgumentAttribute(params string[] arguments);

        public string[] Arguments { get; }
    }
}

Dieses Attribut wird für Parameter verwendet, um den Compiler darüber zu informieren, wie sie ein Muster von Handlern für interpolierte Zeichenfolgen verringern, das in einer Parameterposition verwendet wird.

Handlerkonvertierung einer interpolierten Zeichenfolge

Der Typ T wird als applicable_interpolated_string_handler_type bezeichnet, wenn er mit System.Runtime.CompilerServices.InterpolatedStringHandlerAttributeversehen ist. Es gibt eine implizite interpolated_string_handler_conversion (Handlerkonvertierung einer interpolierten Zeichenfolge) zu T, die von einem interpolated_string_expression (interpolierten Zeichenfolgenausdruck) herrührt oder aus einem additive_expression (additiven Ausdruck), der vollständig aus „_interpolated_string_expression_s“ besteht und nur +-Operatoren verwendet.

Der Einfachheit halber bezieht sich interpolated_string_expression sowohl auf eine einfache interpolated_string_expression als auch auf einen additive_expression, der vollständig aus „_interpolated_string_expression_s“ besteht und nur +-Operatoren verwendet.

Beachten Sie, dass diese Konvertierung immer vorhanden ist, unabhängig davon, ob später Fehler auftreten, wenn sie tatsächlich versuchen, die Interpolation mithilfe des Handlermusters zu verringern. Dies geschieht, um sicherzustellen, dass vorhersehbare und nützliche Fehler vorhanden sind und dass sich das Laufzeitverhalten basierend auf dem Inhalt einer interpolierten Zeichenfolge nicht ändert.

Anwendbare Funktionsmitgliedsanpassungen

Wir passen den Wortlaut des anwendbaren Funktionselementalgorithmus (§12.6.4.2) wie folgt an (ein neues Unterzeichen wird jedem Abschnitt fett hinzugefügt):

Ein Funktionselement wird als anwendbares Funktionselement in Bezug auf eine Argumentliste A bezeichnet, wenn alle folgenden Bedingungen zutreffen.

  • Jedes Argument in A entspricht einem Parameter in der Funktionsmitglieddeklaration, wie in Entsprechende Parameter beschrieben (§12.6.2.2), und alle Parameter, denen kein Argument entspricht, sind optionale Parameter.
  • Für jedes Argument in Aist der Parameterübergabemodus des Arguments (d. h. Wert, refoder out) identisch mit dem Parameterübergabemodus des entsprechenden Parameters und
    • für einen Wertparameter oder ein Parameterarray existiert eine implizite Konvertierung (§10.2) vom Argument zum Typ des entsprechenden Parameters oder
    • für einen ref-Parameter, dessen Typ ein Strukturtyp ist, ist eine implizite interpolated_string_handler_conversion vom Argument zum Typ des entsprechenden Parameters vorhanden, oder
    • für einen ref- oder out-Parameter ist der Typ des Arguments identisch mit dem Typ des entsprechenden Parameters. Schließlich ist ein ref- oder out-Parameter ein Alias für das übergebene Argument.

Für einen Funktionsmember, der ein Parameterarray enthält, gilt, dass falls er nach den oben genannten Regeln anwendbar ist, in seiner Normalform anwendbar ist. Wenn ein Funktionselement, das ein Parameterarray enthält, nicht in seiner normalen Form anwendbar ist, kann das Funktionselement stattdessen in seiner erweiterten Formanwendbar sein:

  • Das erweiterte Formular wird erstellt, indem das Parameterarray in der Funktionselementdeklaration durch null oder mehr Wertparameter des Elementtyps des Parameterarrays ersetzt wird, sodass die Anzahl der Argumente in der Argumentliste A der Gesamtzahl der Parameter entspricht. Wenn A weniger Argumente als die Anzahl der festen Parameter in der Funktionsmememmdeklaration aufweist, kann die erweiterte Form des Funktionsmemems nicht erstellt werden und ist somit nicht anwendbar.
  • Andernfalls gilt das erweiterte Formular, wenn für jedes Argument in A der Parameterübergabemodus des Arguments mit dem Parameterübergabemodus des entsprechenden Parameters identisch ist, und
    • für einen festen Wertparameter oder einen Wertparameter, der durch die Erweiterung erstellt wird, ist eine implizite Konvertierung (§10.2) vom Typ des Arguments bis zum Typ des entsprechenden Parameters vorhanden, oder
    • für einen ref-Parameter, dessen Typ ein Strukturtyp ist, ist eine implizite interpolated_string_handler_conversion vom Argument zum Typ des entsprechenden Parameters vorhanden, oder
    • für einen ref- oder out-Parameter ist der Typ des Arguments identisch mit dem Typ des entsprechenden Parameters.

Wichtiger Hinweis: Dies bedeutet, dass, wenn 2 andernfalls gleichwertige Überladungen vorhanden sind, die sich nur vom Typ der applicable_interpolated_string_handler_typeunterscheiden, diese Überladungen als mehrdeutig betrachtet werden. Da wir explizite Umwandlungen nicht sehen, ist es möglich, dass es ein nicht auflösbares Szenario geben könnte, in dem beide anwendbare Überladungen InterpolatedStringHandlerArguments verwenden und überhaupt nicht aufgerufen werden können, ohne das Muster zur Handler-Verringerung auszuführen. Wir könnten möglicherweise Änderungen am besseren Funktionsmitgliedalgorithmus vornehmen, um dies zu beheben, wenn wir dies wollen, aber dieses Szenario ist unwahrscheinlich, und es ist keine Priorität, die angegangen werden muss.

Anpassungen für bessere Konvertierung aus Ausdruck

Wir ändern den Abschnitt „Bessere Konvertierung aus Ausdruck“ (§12.6.4.5) wie folgt:

Aufgrund einer impliziten Konvertierung C1, die von einem Ausdruck E in einen Typ T1konvertiert wird, und einer impliziten Konvertierung C2, die von einem Ausdruck E in einen Typ T2konvertiert wird, ist C1 eine bessere Konvertierung als C2 wenn:

  1. E ist ein nicht konstanter interpolated_string_expression, C1 ist eine implicit_string_handler_conversion (Handlerkonvertierung einer impliziten Zeichenfolge), T1 ist ein applicable_interpolated_string_handler_type (anwendbarer Handler für implizite Zeichenfolgen), und C2 ist keine implicit_string_handler_conversion, oder
  2. E stimmt nicht genau mit T2 überein und mindestens eine der folgenden Bedingungen trifft zu:

Dies bedeutet, dass es einige potenziell nicht offensichtliche Überladungsauflösungsregeln gibt, je nachdem, ob es sich bei der betreffenden interpolierten Zeichenfolge um einen konstanten Ausdruck handelt oder nicht. Zum Beispiel:

void Log(string s) { ... }
void Log(TraceLoggerParamsInterpolatedStringHandler p) { ... }

Log($""); // Calls Log(string s), because $"" is a constant expression
Log($"{"test"}"); // Calls Log(string s), because $"{"test"}" is a constant expression
Log($"{1}"); // Calls Log(TraceLoggerParamsInterpolatedStringHandler p), because $"{1}" is not a constant expression

Dies wird eingeführt, sodass Dinge, die einfach als Konstanten ausgeführt werden können, dies tun und keinen Aufwand verursachen, während Dinge, die nicht konstant sein können, das Handlermuster verwenden.

InterpolatedStringHandler und Verwendung

Wir führen einen neuen Typ in System.Runtime.CompilerServices ein: DefaultInterpolatedStringHandler. Dies ist eine Referenzstruktur mit vielen der gleichen Semantik wie ValueStringBuilder, die für die direkte Verwendung durch den C#-Compiler vorgesehen ist. Diese Struktur würde ungefähr wie folgt aussehen:

// API Proposal issue: https://github.com/dotnet/runtime/issues/50601
namespace System.Runtime.CompilerServices
{
    [InterpolatedStringHandler]
    public ref struct DefaultInterpolatedStringHandler
    {
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount);
        public string ToStringAndClear();

        public void AppendLiteral(string value);

        public void AppendFormatted<T>(T value);
        public void AppendFormatted<T>(T value, string? format);
        public void AppendFormatted<T>(T value, int alignment);
        public void AppendFormatted<T>(T value, int alignment, string? format);

        public void AppendFormatted(ReadOnlySpan<char> value);
        public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null);

        public void AppendFormatted(string? value);
        public void AppendFormatted(string? value, int alignment = 0, string? format = null);

        public void AppendFormatted(object? value, int alignment = 0, string? format = null);
    }
}

Wir ändern die Regeln für die Bedeutung von einem interpolated_string_expression (§12.8.3):

Wenn der Typ einer interpolierten Zeichenfolge string ist und der Typ System.Runtime.CompilerServices.DefaultInterpolatedStringHandler vorhanden ist und der aktuelle Kontext diesen Typ unterstützt, wird die Zeichenfolge mit dem Handlermuster verringert. Der letzte string-Wert wird dann abgerufen, indem ToStringAndClear() für den Handlertyp aufgerufen wird.Andernfalls, falls der Typ einer interpolierten Zeichenfolge System.IFormattable oder System.FormattableString ist [bleibt der Rest unverändert].

Die Regel "und der aktuelle Kontext unterstützt die Verwendung dieses Typs" ist absichtlich vage, um dem Compiler spielraum bei der Optimierung der Verwendung dieses Musters zu geben. Der Handlertyp ist wahrscheinlich ein ref struct Typ, und ref struct Typen sind normalerweise in asynchronen Methoden nicht zulässig. In diesem speziellen Fall kann der Compiler den Handler verwenden, wenn keine der Interpolationslöcher einen await Ausdruck enthält, da wir statisch bestimmen können, dass der Handlertyp ohne zusätzliche komplizierte Analyse sicher verwendet wird, da der Handler nach der Auswertung des interpolierten Zeichenfolgenausdrucks abgelegt wird.

Öffnen Frage:

Möchten wir stattdessen nur den Compiler über DefaultInterpolatedStringHandler informieren und den string.Format Aufruf vollständig überspringen? Das würde uns ermöglichen, eine Methode auszublenden, die nicht unbedingt sichtbar sein sollte, wenn manuell string.Format aufgerufen wird.

Antwort: Ja.

Öffnen Frage:

Möchten wir auch Handler für System.IFormattable und System.FormattableString haben?

Antwort: Nein.

Handlermuster-Codegenerierung

In diesem Abschnitt bezieht sich die Methodenaufrufauflösung auf die in §12.8.10.2aufgeführten Schritte.

Konstruktorauflösung

Bei einem applicable_interpolated_string_handler_typeT und einem interpolated_string_expressioni wird die Auflösung und Validierung des Methodenaufrufs für einen gültigen Konstruktor von T so durchgeführt:

  1. Die Membersuche für Instanzkonstruktoren wird für T ausgeführt. Die resultierende Methodengruppe wird Mgenannt.
  2. Die Argumentliste A wird wie folgt erstellt:
    1. Die ersten beiden Argumente sind ganzzahlige Konstanten, die die Literallänge i bzw. die Anzahl der Interpolations-Komponenten in i darstellen.
    2. Wenn i als Argument für einen Parameter pi in der Methode M1verwendet wird und der Parameter pi mit System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttributeversehen ist, stimmt der Compiler für jeden Namen Argx im Array Arguments dieses Attributs mit einem Parameter px überein, der denselben Namen hat. Die leere Zeichenfolge wird dem Empfänger von M1 zugeordnet.
      • Wenn ein Argx nicht mit einem Parameter von M1abgeglichen werden kann oder ein Argx den Empfänger von M1 anfordert und M1 eine statische Methode ist, wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.
      • Andernfalls wird der Typ aller aufgelösten px der Argumentliste in der durch das Arguments-Array angegebenen Reihenfolge hinzugefügt. Jede px wird mit derselben ref-Semantik übergeben, wie sie in M1angegeben ist.
    3. Das letzte Argument ist ein bool, das als out-Parameter übergeben wird.
  3. Die herkömmliche Methodenaufrufauflösung wird mit Methodengruppe M und Argumentliste Aausgeführt. Zur endgültigen Überprüfung der Methodenausführung wird der Kontext von M als member_access durch den Typ T behandelt.
    • Wenn ein bester einzelner Konstruktor F gefunden wurde, ist F das Ergebnis der Überladungsauflösung.
    • Wenn keine anwendbaren Konstruktoren gefunden wurden, wird Schritt 3 wiederholt, um den endgültigen bool-Parameter aus Azu entfernen. Wenn dieser Wiederholungsversuch auch keine anwendbaren Mitglieder findet, wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.
    • Wenn keine einzige beste Methode gefunden wurde, ist das Ergebnis der Überladungsauflösung mehrdeutig, ein Fehler wird erzeugt, und es werden keine weiteren Schritte ausgeführt.
  4. Die endgültige Überprüfung auf F wird ausgeführt.
    • Wenn ein Element von A lexikalisch nach iaufgetreten ist, wird ein Fehler erzeugt, und es werden keine weiteren Schritte ausgeführt.
    • Wenn ein A den Empfänger von Fanfordert und F ein Indexer ist, der als initializer_target in einem member_initializerverwendet wird, wird ein Fehler gemeldet, und es werden keine weiteren Schritte ausgeführt.

Hinweis: Die hier angegebene Auflösung verwendet absichtlich nicht die tatsächlichen Ausdrücke, die als andere Argumente für Argx-Elemente übergeben werden. Wir betrachten nur die Typen nach der Konvertierung. Dadurch wird sichergestellt, dass es keine Probleme bei der Doppeltkonvertierung gibt oder unerwartete Fälle auftreten, in denen eine Lambda-Funktion an einen Delegatentyp gebunden ist, wenn sie an M1 übergeben und bei der Übergabe an M einen anderen Delegatentyp gebunden wird.

Hinweis: Wir melden einen Fehler für Indexer, die als Member-Initialisierer verwendet werden, aufgrund der Auswertungsreihenfolge für geschachtelte Member-Initialisierer. Betrachten Sie diesen Codeausschnitt:


var x1 = new C1 { C2 = { [GetString()] = { A = 2, B = 4 } } };

/* Lowering:
__c1 = new C1();
string argTemp = GetString();
__c1.C2[argTemp][1] = 2;
__c1.C2[argTemp][3] = 4;

Prints:
GetString
get_C2
get_C2
*/

string GetString()
{
    Console.WriteLine("GetString");
    return "";
}

class C1
{
    private C2 c2 = new C2();
    public C2 C2 { get { Console.WriteLine("get_C2"); return c2; } set { } }
}

class C2
{
    public C3 this[string s]
    {
        get => new C3();
        set { }
    }
}

class C3
{
    public int A
    {
        get => 0;
        set { }
    }
    public int B
    {
        get => 0;
        set { }
    }
}

Die Argumente für __c1.C2[] werden ausgewertet, bevor der Indexer-Empfänger zugeordnet wird. Obwohl wir eine Verringerung erzielen könnten, die für dieses Szenario funktioniert (entweder durch das Erstellen eines temporären Elements für __c1.C2 und die gemeinsame Nutzung bei beiden Indexer-Aufrufen oder nur beim ersten Indexer-Aufruf und die gemeinsame Nutzung des Arguments bei beiden Aufrufen), denken wir, dass jede Verringerung für das, was wir als ein schädliches Szenario betrachten, verwirrend wäre. Daher verbieten wir das Szenario vollständig.

Offene Frage:

Wenn wir einen Konstruktor anstelle von Create verwenden, würden wir die Codegenerierung zur Laufzeit verbessern, dadurch aber das Muster geringfügig eingrenzen.

Antwort: Wir beschränken uns vorerst auf Konstruktoren. Wir können später eine allgemeine Create-Methode hinzufügen, wenn das Szenario auftritt.

Auflösung der Append...-Methodenüberladung

Bei einem applicable_interpolated_string_handler_typeT und einem interpolated_string_expressioni erfolgt die Auflösung der Überladung für eine Gruppe gültiger Append...-Methoden für T wie folgt:

  1. Wenn interpolated_regular_string_character-Komponenten in i vorhanden sind:
    1. Die Membersuche in T mit dem Namen AppendLiteral wird durchgeführt. Die resultierende Methodengruppe wird Mlbenannt.
    2. Die Argumentliste Al wird mit einem Wertparameter vom Typ stringerstellt.
    3. Die herkömmliche Methodenaufrufauflösung wird mit Methodengruppe Ml und Argumentliste Alausgeführt. Zur endgültigen Überprüfung der Methodenausführung wird der Kontext von Ml als member_access durch eine Instanz von T behandelt.
      • Wenn eine einzelne beste Methode Fi gefunden wird und dabei keine Fehler aufgetreten sind, ist Fi das Ergebnis der Auflösung des Methodenaufrufs.
      • Andernfalls wird ein Fehler gemeldet.
  2. Für jede Interpolations-ix-Komponente von i:
    1. Die Membersuche in T mit dem Namen AppendFormatted wird durchgeführt. Die resultierende Methodengruppe wird Mfgenannt.
    2. Die Argumentliste Af wird erstellt:
      1. Der erste Parameter ist expression von ix, übergeben als Wert.
      2. Wenn ix direkt eine constant_expression Komponente enthält, wird ein ganzzahliger Wertparameter hinzugefügt, wobei der Name alignment angegeben ist.
      3. Wenn ix direkt auf ein interpolation_format folgt, wird ein Zeichenfolgenwert-Parameter hinzugefügt, wobei der Name format angegeben wird.
    3. Die herkömmliche Methodenaufrufauflösung wird mit Methodengruppe Mf und Argumentliste Afausgeführt. Zur endgültigen Überprüfung der Methodenausführung wird der Kontext von Mf als member_access durch eine Instanz von T behandelt.
      • Wenn eine einzelne beste Methode Fi gefunden wird, ist Fi das Ergebnis der Auflösung des Methodenaufrufs.
      • Andernfalls wird ein Fehler gemeldet.
  3. Schließlich wird für jede in den Schritten 1 und 2 ermittelte Fi die endgültige Überprüfung durchgeführt:
    • Wenn Fi nicht bool nach Wert oder void zurückgibt, wird ein Fehler gemeldet.
    • Wenn alle Fi nicht denselben Typ zurückgeben, wird ein Fehler gemeldet.

Beachten Sie, dass diese Regeln keine Erweiterungsmethoden für die Append... Aufrufe zulassen. Wir könnten dies in Betracht ziehen, wenn wir uns dafür entscheiden, aber dies ist analog zum Enumeratormuster, bei dem wir erlauben, dass GetEnumerator eine Erweiterungsmethode ist, aber nicht Current oder MoveNext().

Diese Regeln erlauben Standardparameter für die Append... Aufrufe, die mit Dingen wie CallerLineNumber oder CallerArgumentExpression funktionieren (wenn von der Sprache unterstützt).

Es gibt separate Überladungssuchregeln für Basiselemente im Vergleich zu Interpolationslöchern, da einige Handler den Unterschied zwischen den komponenten verstehen möchten, die interpoliert wurden, und den Komponenten, die Teil der Basiszeichenfolge waren.

Offene Frage

Einige Szenarien, z. B. die strukturierte Protokollierung, möchten Namen für Interpolationselemente bereitstellen können. Beispielsweise könnte ein Protokollierungsaufruf heute wie Log("{name} bought {itemCount} items", name, items.Count); aussehen. Die Namen innerhalb der {} stellen wichtige Strukturinformationen für Logger bereit, mit denen sichergestellt wird, dass die Ausgabe konsistent und einheitlich ist. In einigen Fällen kann die :format Komponente eines Interpolationslochs dafür wiederverwendet werden, aber viele Logger verstehen bereits Formatbezeichner und verfügen über vorhandenes Verhalten für die Ausgabeformatierung basierend auf diesen Informationen. Gibt es eine Syntax, mit der wir diese benannten Bezeichner einfügen können?

In einigen Fällen reicht möglicherweise CallerArgumentExpression aus, vorausgesetzt, dass die Unterstützung in C# 10 verfügbar ist. Aber für Fälle, die eine Methode/Eigenschaft aufrufen, reicht dies möglicherweise nicht aus.

Antwort:

Obwohl es einige interessante Teile für vorlagenbasierte Zeichenfolgen gibt, die wir in einem orthogonalen Sprachfeature untersuchen könnten, sind wir nicht der Ansicht, dass eine bestimmte Syntax hier viele Vorteile gegenüber Lösungen wie der Verwendung eines Tupels hat: $"{("StructuredCategory", myExpression)}".

Durchführen der Konvertierung

Bei einem applicable_interpolated_string_handler_typeT und einer interpolated_string_expressioni, für die ein gültiger Konstruktor Fc und die Append...-Methoden Fa aufgelöst wurden, erfolgt die Verringerung für i wie folgt:

  1. Alle Argumente für Fc, die lexikalisch vor i auftreten, werden ausgewertet und in der lexikalischen Reihenfolge in temporäre Variablen gespeichert. Um die lexikalische Reihenfolge beizubehalten, wenn i als Teil eines größeren Ausdrucks eaufgetreten ist, werden alle Komponenten von e, die vor i aufgetreten sind, ebenfalls in lexikalischer Reihenfolge ausgewertet.
  2. Fc wird mit der Länge der Komponenten des interpolierten Zeichenfolgenliterals, der Anzahl der Interpolations-Lücken, allen zuvor ausgewerteten Argumenten und einem bool-out-Argument aufgerufen (wenn Fc mit einem Argument als letzter Parameter aufgelöst wurde). Das Ergebnis wird in einem temporären Wert ibgespeichert.
    1. Die Länge der Literalkomponenten wird berechnet, nachdem jede open_brace_escape_sequence durch ein einzelnes {-Element und jede close_brace_escape_sequence durch ein einzelnes }-Element ersetzt wurde.
  3. Wenn Fc mit einem bool-out-Argument beendet wurde, wird eine Überprüfung des bool-Wertes generiert. Wenn wahr, werden die Methoden in Fa aufgerufen. Andernfalls werden sie nicht aufgerufen.
  4. Für jede Fax in Fa wird für ib je nach Bedarf Fax entweder mit der aktuellen Literalkomponente oder dem Interpolations-Ausdruck aufgerufen. Wenn Fax einen bool-Wert zurückgibt, wird das Ergebnis logisch mit allen vorherigen Fax-Aufrufen per UND-Operator verbunden.
    1. Wenn Fax ein Aufruf von AppendLiteral ist, wird die Literalkomponente entschlüsselt, indem jede open_brace_escape_sequence durch ein einzelnes {-Element und jede close_brace_escape_sequence durch ein einzelnes }-Element ersetzt wird.
  5. Das Ergebnis der Konvertierung ist ib.

Beachten Sie erneut, dass die an Fc und e übergebenen Argumente dasselbe temporäre Element sind. Konvertierungen können basierend auf diesem temporären Element erfolgen, um sie in ein von Fc benötigtes Format zu bringen, aber beispielsweise können Lambdas nicht an einen anderen Delegatentyp zwischen Fc und e gebunden werden.

Offene Frage

Das bedeutet, dass nachfolgende Teile der interpolierten Zeichenfolge nach einem falsch zurückgegebenen Append...-Aufruf nicht ausgewertet werden. Dies könnte möglicherweise sehr verwirrend sein, insbesondere wenn das Formatloch Nebenwirkungen hat. Stattdessen könnten wir zuerst alle Formatlöcher auswerten und dann wiederholt Append... mit den Ergebnissen aufrufen und beenden, wenn "false" zurückgegeben wird. Dadurch wird sichergestellt, dass alle Ausdrücke wie erwartet ausgewertet werden, aber wir rufen so wenige Methoden wie nötig auf. Obwohl die Teilbewertung für einige komplexere Fälle wünschenswert sein könnte, ist dies für den allgemeinen Fall vielleicht nicht intuitiv.

Eine weitere Alternative, wenn wir immer alle Formatlöcher auswerten möchten, besteht darin, die Append... Version der API zu entfernen und nur wiederholte Format Aufrufe auszuführen. Der Handler kann nachverfolgen, ob er einfach das Argument ablegen und sofort für diese Version zurückgeben soll.

Antwort: Wir werden die Lücken bedingt bewerten.

Offene Frage

Müssen wir verfügbare Handlertypen löschen und Aufrufe mit „try/finally“ umschließen, um sicherzustellen, dass „Dispose“ aufgerufen wird? Beispielsweise könnte der interpolierte Zeichenfolgenhandler in der Basisklassenbibliothek (BCL) über ein gemietetes Array verfügen, und wenn eine der Interpolationslücken während der Auswertung eine Ausnahme auslöst, könnte dieses gemietete Array verloren gehen, wenn es nicht gelöscht wurde.

Antwort: Nein. Handler können lokal zugewiesen werden (z. B. MyHandler handler = $"{MyCode()};), und die Lebensdauer solcher Handler ist unklar. Das steht im Gegensatz zu einem Foreach-Enumerator, bei dem die Lebensdauer offensichtlich ist und keine benutzerdefinierte lokale Variable für den Enumerator erstellt wird.

Auswirkungen auf Verweistypen, die Null-Werte zulassen

Um die Komplexität der Implementierung zu minimieren, haben wir einige Einschränkungen, wie wir nullable Analysen für interpolierte Zeichenfolgenhandlerkonstruktoren ausführen, die als Argumente für eine Methode oder einen Indexer verwendet werden. Insbesondere leiten wir keine Informationen vom Konstruktor zurück an die ursprünglichen Slots von Parametern oder Argumenten des ursprünglichen Kontexts. Außerdem verwenden wir keine Konstruktorparametertypen, um generische Typableitungen für Typparameter in der enthaltenden Methode heranzuziehen. Ein Beispiel dafür, wo sich dies auswirken kann, ist:

string s = "";
C c = new C();
c.M(s, $"", c.ToString(), s.ToString()); // No warnings on c.ToString() or s.ToString(), as the `MaybeNull` does not flow back.

public class C
{
    public void M(string s1, [InterpolatedStringHandlerArgument("", "s1")] CustomHandler c1, string s2, string s3) { }
}

[InterpolatedStringHandler]
public partial struct CustomHandler
{
    public CustomHandler(int literalLength, int formattedCount, [MaybeNull] C c, [MaybeNull] string s) : this()
    {
    }
}
string? s = null;
M(s, $""); // Infers `string` for `T` because of the `T?` parameter, not `string?`, as flow analysis does not consider the unannotated `T` parameter of the constructor

void M<T>(T? t, [InterpolatedStringHandlerArgument("s1")] CustomHandler<T> c) { }

[InterpolatedStringHandler]
public partial struct CustomHandler<T>
{
    public CustomHandler(int literalLength, int formattedCount, T t) : this()
    {
    }
}

Weitere Überlegungen

Zulassen, dass string-Typen auch in Handler konvertiert werden können

Aus Gründen der Einfachheit des Auftragstyps könnten wir erwägen, dass Ausdrücke vom Typ string implizit in applicable_interpolated_string_handler_types konvertiert werden können. Wie heute vorgeschlagen, müssen Autoren wahrscheinlich sowohl diesen Handlertyp als auch reguläre string-Typen überladen, damit ihre Benutzer den Unterschied nicht kennen müssen. Dies kann ein lästiger und nicht offensichtlicher Aufwand sein, weil ein string-Ausdruck als Interpolation mit vorab ausgefüllter Länge expression.Length und 0 auszufüllenden Lücken angesehen werden kann.

Dies würde es neuen APIs ermöglichen, nur einen Handler bereitzustellen, ohne auch eine Überladung, die string akzeptiert, verfügbar machen zu müssen. Es bleibt jedoch weiter notwendig, Änderungen für eine bessere Konvertierung von Ausdrücken vorzunehmen, sodass es möglicherweise unnötiger Aufwand ist.

Antwort:

Wir denken, dass dies verwirrend sein könnte, und es gibt eine einfache Lösung für benutzerdefinierte Handlertypen: Fügen Sie eine benutzerdefinierte Umwandlung von einem String hinzu.

Integrieren von span-Elementen für Zeichenfolgen ohne Heap

ValueStringBuilder, wie er heute existiert, verfügt über 2 Konstruktoren: einen, der eine Anzahl akzeptiert und vorzeitig im Heap speichert, sowie einen, der ein Span<char> akzeptiert. Dieses Span<char>-Element ist in der Regel eine feste Größe in der Laufzeit-Codebasis, etwa 250 Elemente im Durchschnitt. Um diesen Typ wirklich zu ersetzen, sollten wir eine Erweiterung darauf in Betracht ziehen, bei der wir auch GetInterpolatedString-Methoden berücksichtigen, die ein Span<char>-Element verwenden, anstatt nur die Zählversion. Hier sehen wir jedoch einige potenzielle knifflige Fälle.

  • Wir möchten in einer Schleife in einer heißen Ebene nicht wiederholt „stackalloc“ verwenden. Wenn diese Erweiterung für das Feature ausgeführt wird, würden wir wahrscheinlich die mithilfe von „stackalloc“ erstellte Spanne zwischen Schleifeniterationen teilen. Wir wissen, dass dies sicher ist, da Span<T> eine Referenzstruktur ist, die nicht auf dem Heap gespeichert werden kann, und Benutzer müssten ziemlich unabsichtig sein, um einen Verweis auf diese Span zu extrahieren (z. B. das Erstellen einer Methode, die einen solchen Handler akzeptiert, und dann absichtlich den Span aus dem Handler abruft und an den Aufrufer zurückgibt). Die Zuweisung vor der Zeit führt jedoch zu anderen Fragen:
    • Sollten wir stackalloc bereitwillig verwenden? Was geschieht, wenn die Schleife nie aufgerufen oder beendet wird, bevor sie den Platz benötigt?
    • Wenn wir „stackalloc“ nicht vorzeitig verwenden, bedeutet das dann, dass wir in jeder Schleife eine verdeckte Verzweigung einführen? Für die meisten Schleifen ist dies wahrscheinlich nicht relevant, aber es könnte sich auf einige enge Schleifen auswirken, die die Kosten nicht tragen möchten.
  • Einige Zeichenfolgen können ziemlich groß sein, und der entsprechende Betrag für stackalloc hängt von einer Reihe von Faktoren ab, einschließlich Laufzeitfaktoren. Wir möchten nicht, dass der C#-Compiler und die Spezifikation diese vorab ermitteln müssen. Daher möchten wir https://github.com/dotnet/runtime/issues/25423 auflösen und eine API für den Compiler hinzufügen, die in diesen Fällen aufgerufen werden soll. Zudem werden den Punkten aus der vorherigen Schleife weitere Vor- und Nachteile hinzugefügt. Dabei möchten wir gegebenenfalls vermeiden, mehrfach große Arrays dem Heap zuzuweisen, insbesondere bevor einer davon benötigt wird.

Antwort:

Dies liegt außerhalb des Gültigkeitsbereichs für C# 10. Wir können dies im Allgemeinen sehen, wenn wir das allgemeinere params Span<T>-Feature betrachten.

Nicht-Testversion der API

Aus Gründen der Einfachheit schlägt diese Spezifikation derzeit nur vor, eine Append...-Methode zu erkennen, und Elemente, die immer erfolgreich sind (wie InterpolatedStringHandler), würden aus der Methode immer den Wert „true“ zurückgeben. Dies wurde getan, um Teilformatierungsszenarien zu unterstützen, in denen der Benutzer die Formatierung beenden möchte, wenn ein Fehler auftritt oder wenn er unnötig ist, z. B. der Protokollierungsfall, aber möglicherweise eine Reihe unnötiger Verzweigungen in der standardmäßigen interpolierten Zeichenfolgenverwendung einführen könnte. Wir könnten einen Zusatz in Betracht ziehen, bei dem wir nur FormatX Methoden verwenden, wenn keine Append... Methode vorhanden ist, aber es stellt Fragen darüber dar, was wir tun, wenn eine Mischung aus Append... und FormatX Aufrufen vorhanden ist.

Antwort:

Wir möchten die Nicht-Try-Version der API. Der Vorschlag wurde aktualisiert, um dies widerzuspiegeln.

Übergeben vorheriger Argumente an den Handler

Es besteht zurzeit ein unglücklicher Mangel an Symmetrie im Vorschlag: Das Aufrufen einer Erweiterungsmethode in reduzierter Form erzeugt eine andere Semantik als das Aufrufen der Erweiterungsmethode in normaler Form. Dies unterscheidet sich von den meisten anderen Orten in der Sprache, wo reduzierte Form nur ein Zucker ist. Wir schlagen vor, dem Framework ein Attribut hinzuzufügen, das beim Binden einer Methode erkannt wird, die den Compiler darüber informiert, dass bestimmte Parameter an den Konstruktor im Handler übergeben werden sollen. Die Verwendung sieht wie folgt aus:

namespace System.Runtime.CompilerServices
{
    [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)]
    public sealed class InterpolatedStringHandlerArgumentAttribute : Attribute
    {
        public InterpolatedStringHandlerArgumentAttribute(string argument);
        public InterpolatedStringHandlerArgumentAttribute(params string[] arguments);

        public string[] Arguments { get; }
    }
}

Die Verwendung ist dann:

namespace System
{
    public sealed class String
    {
        public static string Format(IFormatProvider? provider, [InterpolatedStringHandlerArgument("provider")] ref DefaultInterpolatedStringHandler handler);
        …
    }
}

namespace System.Runtime.CompilerServices
{
    public ref struct DefaultInterpolatedStringHandler
    {
        public DefaultInterpolatedStringHandler(int baseLength, int holeCount, IFormatProvider? provider); // additional factory
        …
    }
}

var formatted = string.Format(CultureInfo.InvariantCulture, $"{X} = {Y}");

// Is lowered to

var tmp1 = CultureInfo.InvariantCulture;
var handler = new DefaultInterpolatedStringHandler(3, 2, tmp1);
handler.AppendFormatted(X);
handler.AppendLiteral(" = ");
handler.AppendFormatted(Y);
var formatted = string.Format(tmp1, handler);

Die Fragen, die wir beantworten müssen:

  1. Gefällt uns dieses Muster im Allgemeinen?
  2. Möchten wir zulassen, dass diese Argumente nach dem Handlerparameter stammen? Einige vorhandene Muster in der BCL, z. B. Utf8Formatter, positionieren den Wert, der formatiert werden soll, vor dem Element, in das formatiert werden soll. Um sich am besten an diese Muster anzupassen, würden wir dies wahrscheinlich zulassen, aber wir müssen entscheiden, ob diese Bewertung außerhalb der Reihenfolge in Ordnung ist.

Antwort:

Wir wollen dies unterstützen. Die Spezifikation wurde aktualisiert, um dies widerzuspiegeln. Argumente müssen in lexikalischer Reihenfolge an der Aufrufstelle angegeben werden, und wenn ein erforderliches Argument für die Methode zum Erstellen nach dem interpolierten Zeichenfolgenliteral angegeben wird, wird ein Fehler angezeigt.

await-Verwendung in Interpolationslücken

Da $"{await A()}" heute ein gültiger Ausdruck ist, müssen wir Interpolationslücken mit „await“ rationalisieren. Wir könnten dies mit einigen Regeln lösen:

  1. Wenn eine interpolierte Zeichenfolge, die als string, IFormattable oder FormattableString verwendet wird, await in einer Interpolationslücke aufweist, gibt es ein Fallback auf den Formatierer im alten Stil.
  2. Wenn eine interpolierte Zeichenfolge einer implicit_string_handler_conversion unterliegt und applicable_interpolated_string_handler_type ein ref struct ist, darf await nicht in den Formatlücken verwendet werden.

Grundsätzlich könnte dieses Desugaring eine Referenzstruktur in einer asynchronen Methode verwenden, solange sichergestellt ist, dass ref struct nicht im Heap gespeichert werden muss. Das sollte möglich sein, wenn awaits in den Interpolationslücken verboten werden.

Alternativ können wir einfach alle Handlertypen erstellen, die keine Verweisstruktur aufweisen, einschließlich des Frameworkhandlers für interpolierte Zeichenfolgen. Dies würde uns jedoch daran hindern, eines Tages eine Span-Version zu erkennen, die überhaupt keinen Zwischenspeicher zuweisen muss.

Antwort:

Interpolierte Zeichenfolgenhandler werden wie alle anderen Typen behandelt: Dies bedeutet, dass die Verwendung von Handlern hier unzulässig ist, wenn der Handlertyp eine Verweisstruktur ist und der aktuelle Kontext die Verwendung von Verweisstrukturs nicht zulässt. Die Spezifikation zum Verringern von Zeichenfolgenliteralen, die als Zeichenfolgen verwendet werden, ist absichtlich vage. Denn so kann der Compiler entscheiden, welche Regeln er als angemessen erachtet, aber für benutzerdefinierte Handlertypen muss er dieselben Regeln wie die restliche Sprache befolgen.

Handler als Referenzparameter

Einige Handler sollten unter Umständen als Referenzparameter übergeben werden (entweder in oder ref). Sollten wir eine der beiden zulassen? Und wenn ja, wie sieht ein ref-Handler aus? ref $"" ist verwirrend, da Sie die Zeichenfolge nicht tatsächlich per Verweis übergeben. Stattdessen wird der Handler übergeben, der aus dem Verweis erstellt wird, und dieser hat ähnliche potenzielle Probleme bei asynchronen Methoden.

Antwort:

Wir wollen dies unterstützen. Die Spezifikation wurde aktualisiert, um dies widerzuspiegeln. Die Regeln sollten dieselben Regeln enthalten, die für Erweiterungsmethoden für Werttypen gelten.

Interpolierte Zeichenfolgen unter Verwendung binärer Ausdrücke und Konvertierungen

Weil dieser Vorschlag interpolierte Zeichenfolgen kontextabhängig macht, möchten wir dem Compiler erlauben, entweder einen binären Ausdruck, der vollständig aus interpolierten Zeichenfolgen besteht, oder eine interpolierte Zeichenfolge, die einer Umwandlung unterliegt, als ein interpoliertes Zeichenfolgenliteral für die Überladungsauflösung zu behandeln. Nehmen Sie sich beispielsweise das folgende Szenario an:

struct Handler1
{
    public Handler1(int literalLength, int formattedCount, C c) => ...;
    // AppendX... methods as necessary
}
struct Handler2
{
    public Handler2(int literalLength, int formattedCount, C c) => ...;
    // AppendX... methods as necessary
}

class C
{
    void M(Handler1 handler) => ...;
    void M(Handler2 handler) => ...;
}

c.M($"{X}"); // Ambiguous between the M overloads

Dies wäre mehrdeutig, was eine Umwandlung entweder zu Handler1 oder Handler2 erfordert, um das Problem zu lösen. Wenn wir jedoch diese Umwandlung vornehmen, würden wir die Informationen, die es im Zusammenhang mit dem Methodenempfänger gibt, möglicherweise wegwerfen. Das bedeutet, dass die Umwandlung scheitert, weil es nichts gibt, um die Informationen von c auszufüllen. Ein ähnliches Problem tritt bei der binären Verkettung von Zeichenfolgen auf: Der Benutzer möchte das Literal über mehrere Zeilen hinweg formatieren, um einen Zeilenumbruch zu vermeiden, kann das aber nicht, weil dies kein interpoliertes Zeichenfolgenliteral ist, das in den Handlertyp konvertierbar ist.

Um diese Fälle zu beheben, nehmen wir die folgenden Änderungen vor:

  • Ein additive_expression, der ausschließlich aus interpolated_string_expressions besteht und nur +-Operatoren verwendet, gilt als interpolated_string_literal für Konvertierungen und die Überladungsauflösung. Die endgültige interpolierte Zeichenfolge wird erstellt, indem alle einzelnen interpolated_string_expression-Komponenten logisch von links nach rechts verkettet werden.
  • Ein cast_expression oder ein relational_expression mit Operator as, dessen Operand interpolated_string_expressions ist, gilt als interpolated_string_expressions für Konvertierungen und die Überladungsauflösung.

Offene Fragen:

Möchten wir dies tun? Wir tun dies nicht für System.FormattableString, aber das kann auf eine andere Zeile aufgeteilt werden, während dies kontextabhängig sein kann und daher nicht in eine andere Zeile unterteilt werden kann. Es gibt auch keine Bedenken hinsichtlich der Überlastungslösung bei FormattableString und IFormattable.

Antwort:

Wir glauben, dass dies ein gültiger Anwendungsfall für additive Ausdrücke ist, aber dass die Umwandlungsversion derzeit nicht überzeugend genug ist. Wir können es später bei Bedarf hinzufügen. Die Spezifikation wurde aktualisiert, um diese Entscheidung widerzuspiegeln.

Andere Anwendungsfälle

Beispiele für vorgeschlagene Handler-APIs mit diesem Muster finden Sie unter https://github.com/dotnet/runtime/issues/50635.