Delen via


Verbeterde geïnterpoleerde tekenreeksen

Notitie

Dit artikel is een functiespecificatie. De specificatie fungeert als het ontwerpdocument voor de functie. Het bevat voorgestelde specificatiewijzigingen, samen met informatie die nodig is tijdens het ontwerp en de ontwikkeling van de functie. Deze artikelen worden gepubliceerd totdat de voorgestelde specificaties zijn voltooid en opgenomen in de huidige ECMA-specificatie.

Er kunnen enkele verschillen zijn tussen de functiespecificatie en de voltooide implementatie. Deze verschillen worden vastgelegd in de relevante notities van de LDM (Language Design Meeting).

Meer informatie over het proces voor het aannemen van functiespeclets in de C#-taalstandaard vindt u in het artikel over de specificaties.

Probleem met kampioen: https://github.com/dotnet/csharplang/issues/4487

Samenvatting

We introduceren een nieuw patroon voor het maken en gebruiken van geïnterpoleerde tekenreeksexpressies om efficiënte opmaak en gebruik mogelijk te maken in zowel algemene string-scenario's als meer gespecialiseerde scenario's, zoals frameworks voor logboekregistratie, zonder onnodige toewijzingen van de tekenreeks in het framework te maken.

Motivatie

Tegenwoordig komt tekenreeksinterpolatie voornamelijk neer op een aanroep naar string.Format. Dit kan om een aantal redenen inefficiënt zijn, hoewel het een algemeen doel dient.

  1. Het verpakt structarguments, tenzij de runtime toevallig een overload van string.Format heeft die precies de juiste typen argumenten in precies de juiste volgorde aanneemt.
    • Deze volgorde is waarom de runtime aarzelt om algemene versies van de methode te introduceren, omdat dit zou leiden tot combinatorische explosie van algemene instantiëringen van een zeer gangbare methode.
  2. In de meeste gevallen moet een matrix worden toegewezen voor de argumenten.
  3. Er is geen mogelijkheid om te voorkomen dat de instantie wordt geïnstantieerd als dat niet nodig is. Frameworks voor logboekregistratie raden u bijvoorbeeld aan om tekenreeksinterpolatie te voorkomen, omdat hierdoor een tekenreeks wordt gerealiseerd die mogelijk niet nodig is, afhankelijk van het huidige logboekniveau van de toepassing.
  4. Het kan vandaag niet Span of andere verw-struct-typen gebruiken, omdat verw-structs niet zijn toegestaan als generieke typeparameters, wat betekent dat als een gebruiker wil voorkomen dat hij kopieert naar tussenliggende locaties, hij strings handmatig moet formatteren.

Intern is er in de runtime een type met de naam ValueStringBuilder om te helpen bij het omgaan met de eerste twee van deze scenario's. Ze geven een via stackalloc toegewezen buffer door aan de builder, roepen herhaaldelijk AppendFormat aan voor elk onderdeel, en krijgen vervolgens een definitieve tekenreeks. Als de resulterende tekenreeks de grenzen van de stackbuffer overschrijdt, kunnen ze vervolgens naar een array op de heap verplaatsen. Dit type is echter gevaarlijk om direct bloot te stellen, omdat onjuist gebruik kan leiden tot een gehuurde matrix die tweemaal wordt verwijderd, wat dan allerlei ongedefinieerd gedrag in het programma zal veroorzaken, omdat twee locaties denken dat ze exclusief toegang hebben tot de gehuurde matrix. Dit voorstel maakt een manier om dit type veilig te gebruiken vanuit systeemeigen C#-code door alleen een letterlijke geïnterpoleerde tekenreeks te schrijven, waardoor geschreven code ongewijzigd blijft terwijl elke geïnterpoleerde tekenreeks wordt verbeterd die een gebruiker schrijft. Het breidt dit patroon ook uit om geïnterpoleerde tekenreeksen toe te staan die als argumenten worden doorgegeven aan andere methoden voor het gebruik van een handlerpatroon, gedefinieerd door de ontvanger van de methode, zodat zaken zoals frameworks voor logboekregistratie kunnen worden gebruikt om te voorkomen dat tekenreeksen worden toegewezen die nooit nodig zijn en C#-gebruikers vertrouwde, handige interpolatiesyntaxis te geven.

Gedetailleerd ontwerp

Het handlerpatroon

We introduceren een nieuw handlerpatroon dat een geïnterpoleerde tekenreeks kan voorstellen die als argument aan een methode wordt doorgegeven. Het eenvoudige Engels van het patroon is als volgt:

Wanneer een interpolated_string_expression wordt doorgegeven als argument aan een methode, kijken we naar het type parameter. Als het parametertype een constructor heeft die kan worden aangeroepen met 2 int-parameters, literalLength en formattedCount, en optioneel extra parameters accepteert die zijn opgegeven door een kenmerk op de oorspronkelijke parameter, een booleaanse volgparameter heeft, en het type van de oorspronkelijke parameter beschikt over instantie AppendLiteral- en AppendFormatted-methoden die voor elk deel van de geïnterpoleerde tekenreeks kunnen worden aangeroepen, dan passen we de interpolatie aan met die methode, in plaats van over te gaan tot een traditionele aanroep van string.Format(formatStr, args). Een concreter voorbeeld is handig om dit te bekijken:

// 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);

Omdat TraceLoggerParamsInterpolatedStringHandler een constructor met de juiste parameters heeft, zeggen we dat de geïnterpoleerde tekenreeks een impliciete handlerconversie naar die parameter heeft en dat deze lager is dan het bovenstaande patroon. De specificaties die hiervoor nodig zijn, zijn een beetje ingewikkeld en worden hieronder uitgebreid.

In de rest van dit voorstel wordt Append... gebruikt om te verwijzen naar een van de AppendLiteral of AppendFormatted in gevallen waarin beide van toepassing zijn.

Nieuwe kenmerken

De compiler herkent de 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()
        {
        }
    }
}

Dit kenmerk wordt door de compiler gebruikt om te bepalen of een type een geldig geïnterpoleerd tekenreekshandlertype is.

De compiler herkent ook de 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; }
    }
}

Dit kenmerk wordt gebruikt voor parameters om de compiler te informeren over het verlagen van een geïnterpoleerd tekenreekshandlerpatroon dat wordt gebruikt in een parameterpositie.

Conversie van geïnterpoleerde tekenreeksverwerker

Type T wordt een applicable_interpolated_string_handler_type als het wordt toegeschreven aan System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Er bestaat een impliciete interpolated_string_handler_conversion naar T vanuit een interpolated_string_expression, ofwel een additive_expression die volledig bestaat uit _interpolated_string_expression_s en alleen + operators gebruikt.

Voor het gemak in de rest van deze speclet verwijst interpolated_string_expression naar zowel een eenvoudige interpolated_string_expressionals naar een additive_expression die volledig uit _interpolated_string_expression_s bestaat en alleen + operatoren gebruikt.

Houd er rekening mee dat deze conversie altijd bestaat, ongeacht of er later fouten optreden bij het daadwerkelijk verlagen van de interpolatie met behulp van het handler-patroon. Dit wordt gedaan om ervoor te zorgen dat er voorspelbare en nuttige fouten zijn en dat runtimegedrag niet verandert op basis van de inhoud van een geïnterpoleerde tekenreeks.

Toepasselijke aanpassingen van functieleden

We passen de formulering van het toepasselijke algoritme voor functieleden (§12.6.4.2) als volgt aan (er wordt een nieuw sub-opsommingsteken toegevoegd aan elke sectie, vetgedrukt):

Een functielid wordt geacht een van toepassing functielid te zijn met betrekking tot een lijst met argumenten A wanneer alle volgende waar zijn:

  • Elk argument in A komt overeen met een parameter in de declaratie van het functielid, zoals beschreven in corresponderende parameters (§12.6.2.2) en een parameter waarvoor geen argument overeenkomt, is een optionele parameter.
  • Voor elk argument in Ais de parameterdoorgiftemodus van het argument (dat wil bijvoorbeeld waarde, refof out) identiek zijn aan de parameterdoorgiftemodus van de bijbehorende parameter en
    • voor een waardeparameter of een parametermatrix bestaat een impliciete conversie (§10.2) van het argument tot het type van de bijbehorende parameter, of
    • voor een ref parameter waarvan het type een structtype is, bestaat er een impliciete interpolated_string_handler_conversion van het argument naar het type van de bijbehorende parameter of
    • voor een ref- of out parameter is het type van het argument identiek aan het type van de bijbehorende parameter. Een ref of out parameter is immers een alias voor het argument dat is doorgegeven.

Voor een functielid dat een parametermatrix bevat, als het functielid van toepassing is volgens de bovenstaande regels, wordt gezegd dat het van toepassing is in de normale vorm. Als een functielid dat een parametermatrix bevat, niet van toepassing is in de normale vorm, kan het functielid in plaats daarvan van toepassing zijn in de uitgevouwen vorm:

  • Het uitgevouwen formulier wordt samengesteld door de parametermatrix in de functieliddeclaratie te vervangen door nul of meer waardeparameters van het elementtype van de parametermatrix, zodat het aantal argumenten in de argumentenlijst A overeenkomt met het totale aantal parameters. Als A minder argumenten heeft dan het aantal vaste parameters in de declaratie van het functielid, kan de uitgevouwen vorm van het functielid niet worden samengesteld en is dus niet van toepassing.
  • Anders is het uitgevouwen formulier van toepassing als voor elk argument in A de parameterdoorgiftemodus van het argument identiek is aan de parameterdoorgiftemodus van de bijbehorende parameter en
    • voor een parameter met vaste waarde of een waardeparameter die door de uitbreiding is gemaakt, bestaat er een impliciete conversie (§10.2) van het type argument tot het type van de bijbehorende parameter, of
    • voor een ref parameter waarvan het type een structtype is, bestaat er een impliciete interpolated_string_handler_conversion van het argument naar het type van de bijbehorende parameter, of
    • voor een ref- of out parameter is het type van het argument identiek aan het type van de bijbehorende parameter.

Belangrijke opmerking: dit betekent dat als er 2 andere equivalente overbelastingen zijn, die alleen verschillen per type van de applicable_interpolated_string_handler_type, deze overbelastingen als dubbelzinnig worden beschouwd. Omdat we niet door expliciete casts heen kijken, kan er een onoplosbaar scenario ontstaan waarbij beide toepasselijke overloads InterpolatedStringHandlerArguments gebruiken en volledig ongebruikbaar zijn zonder het handler-verlagingspatroon handmatig uit te voeren. We kunnen mogelijk wijzigingen aanbrengen in het algoritme van het betere functielid om dit op te lossen als we dit zo kiezen, maar dit scenario is waarschijnlijk niet mogelijk en is geen prioriteit om aan te pakken.

Betere conversie van expressieaanpassingen

We wijzigen de betere conversie van expressie (§12.6.4.5) in het volgende:

Gezien een impliciete conversie C1 die wordt geconverteerd van een expressie E naar een type T1en een impliciete conversie C2 die wordt geconverteerd van een expressie E naar een type T2, is C1 een betere conversie dan C2 als:

  1. E is een niet-constante interpolated_string_expression, C1 is een implicit_string_handler_conversion, T1 is een applicable_interpolated_string_handler_type, en C2 is geen implicit_string_handler_conversion, of
  2. E komt niet exact overeen met T2 en ten minste één van de volgende is waar:

Dit betekent dat er mogelijk niet-voor de hand liggende regels voor overbelastingsoplossing zijn, afhankelijk van of de geïnterpoleerde tekenreeks een constante expressie is of niet. Bijvoorbeeld:

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

Dit wordt geïntroduceerd, zodat dingen die eenvoudigweg als constanten kunnen worden uitgevoerd dit doen en geen overhead veroorzaken, terwijl dingen die niet constant kunnen zijn, het handlerpatroon gebruiken.

InterpolatedStringHandler en Gebruik

We introduceren een nieuw type in System.Runtime.CompilerServices: DefaultInterpolatedStringHandler. Dit is een ref-struct met veel van dezelfde semantiek als ValueStringBuilder, bedoeld voor direct gebruik door de C#-compiler. Deze struct ziet er ongeveer als volgt uit:

// 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);
    }
}

We brengen een kleine wijziging aan in de regels voor de betekenis van een interpolated_string_expression (§12.8.3):

Als het type geïnterpoleerde tekenreeks string is en het type System.Runtime.CompilerServices.DefaultInterpolatedStringHandler bestaat en de huidige context het gebruik van dat type ondersteunt, wordt de tekenreeksverlaagd met behulp van het handlerpatroon. De uiteindelijke string waarde wordt vervolgens verkregen door ToStringAndClear() aan te roepen op het handlertype.Anders, als het type geïnterpoleerde tekenreeks wordt System.IFormattable of System.FormattableString [de rest is ongewijzigd]

De regel 'en de huidige context ondersteunt het gebruik van dat type' is opzettelijk vaag om de compiler ruimte te geven voor het optimaliseren van het gebruik van dit patroon. Het handlertype is waarschijnlijk een ref struct-type, en ref struct-typen zijn normaal gesproken niet toegestaan in asynchrone methoden. Voor dit specifieke geval zou de compiler de handler mogen gebruiken als geen van de interpolatiegaten een await-expressie bevat, omdat we statisch kunnen bepalen dat het handlertype veilig wordt gebruikt zonder extra gecompliceerde analyse, omdat de handler wordt verwijderd nadat de geïnterpoleerde tekenreeksexpressie is geëvalueerd.

vraag openen:

Willen we in plaats daarvan de compiler alleen laten weten over DefaultInterpolatedStringHandler en de string.Format-aanroep volledig overslaan? Hiermee kunnen we een methode verbergen die we niet per se in de gezichten van mensen willen plaatsen wanneer ze handmatig string.Formataanroepen.

Antwoord: Ja.

vraag openen:

Willen we ook handlers hebben voor System.IFormattable en System.FormattableString?

Antwoord: Nee.

Handler-patrooncodegen

In deze paragraaf verwijst de methode-aanroepresolutie naar de stappen in §12.8.10.2.

Resolutie van de constructor

Gezien een applicable_interpolated_string_handler_typeT en een interpolated_string_expressioni, wordt methodeaanroepoplossing en -validatie voor een geldige constructor op T als volgt uitgevoerd:

  1. Ledenopzoeking voor exemplaarconstructors wordt uitgevoerd op T. De resulterende methodegroep wordt Mgenoemd.
  2. De lijst met argumenten A wordt als volgt samengesteld:
    1. De eerste twee argumenten zijn gehele getallen, die de letterlijke lengte van ivertegenwoordigen, en het aantal interpolatie onderdelen in i, respectievelijk.
    2. Als i wordt gebruikt als argument voor een parameter pi in methode M1, en parameter pi is toegeschreven met System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, dan stemt de compiler voor elke naam Argx in de Arguments array van dat attribuut deze overeen met een parameter px die dezelfde naam heeft. De lege tekenreeks komt overeen met de ontvanger van M1.
      • Als een Argx niet kan worden gekoppeld aan een parameter van M1, of als een Argx de ontvanger van M1 aanvraagt en M1 een statische methode is, wordt er een fout gegenereerd en worden er geen verdere stappen ondernomen.
      • Anders wordt het type van elke opgeloste px toegevoegd aan de lijst met argumenten, in de volgorde die is opgegeven door de Arguments matrix. Elke px wordt doorgegeven met dezelfde ref semantiek als die is opgegeven in M1.
    3. Het laatste argument is een bool, doorgegeven als een out parameter.
  3. Traditionele methode-aanroepoplossing wordt uitgevoerd met methodegroep M en lijst met argumenten A. Voor de definitieve validatie van methode-aanroep wordt de context van M behandeld als een member_access via het type T.
    • Als er een beste enkele constructor F is gevonden, wordt het resultaat van de overbelastingoplossing F.
    • Als er geen toepasselijke constructors zijn gevonden, wordt stap 3 opnieuw geprobeerd, de laatste bool parameter uit Ate verwijderen. Als deze nieuwe poging ook geen toepasselijke leden vindt, wordt er een fout gegenereerd en worden er geen verdere stappen ondernomen.
    • Als er geen enkele methode is gevonden, is het resultaat van overbelastingsresolutie niet eenduidig, wordt er een fout gegenereerd en worden er geen verdere stappen ondernomen.
  4. Definitieve validatie op F wordt uitgevoerd.
    • Als er een element van A lexisch is opgetreden na i, wordt er een fout gegenereerd en worden er geen verdere stappen ondernomen.
    • Als een A de ontvanger van Faanvraagt en F een indexeerfunctie is die wordt gebruikt als een initializer_target in een member_initializer, wordt er een fout gerapporteerd en worden er geen verdere stappen ondernomen.

Opmerking: de oplossing hier opzettelijk niet de werkelijke expressies gebruiken die worden doorgegeven als andere argumenten voor Argx elementen. We houden alleen rekening met de typen na conversie. Dit zorgt ervoor dat er geen problemen zijn met dubbele conversie, of onverwachte gevallen waarbij een lambda is gebonden aan één gedelegeerdetype wanneer deze wordt doorgegeven aan M1 en is gebonden aan een ander type gemachtigde wanneer deze wordt doorgegeven aan M.

Opmerking: Er wordt een fout gerapporteerd voor indexeerfuncties die worden gebruikt als initialisatieprogramma's voor leden vanwege de volgorde van evaluatie voor geneste initializers van leden. Houd rekening met dit codefragment:


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 { }
    }
}

De argumenten voor __c1.C2[] worden geëvalueerd voordat de ontvanger van de indexeerfunctie. Hoewel we een verlaging kunnen bedenken die geschikt is voor dit scenario (ofwel door een temp te maken voor __c1.C2 en deze te delen tussen beide indexeerfuncties, of alleen gebruiken voor de eerste aanroep van de indexeerfunctie en het delen van het argument tussen beide aanroepen) denken we dat elke verlaging verwarrend zou zijn voor wat we geloven een pathologisch scenario is. Daarom verbieden we het scenario volledig.

Vraag openen:

Als we een constructor gebruiken in plaats van Create, verbeteren we de runtime codegen, zij het ten koste van het patroon enigszins in te perken.

Antwoord: We zullen ons voorlopig beperken tot constructors. We kunnen later opnieuw een algemene Create methode toevoegen als het scenario zich voordoet.

Append... methode overbelastingsresolutie

Gezien een applicable_interpolated_string_handler_typeT en een interpolated_string_expressioni, wordt overbelastingsresolutie voor een set geldige Append... methoden op T als volgt uitgevoerd:

  1. Als er interpolated_regular_string_character onderdelen in izijn:
    1. Het opzoeken van leden op T met de naam AppendLiteral wordt uitgevoerd. De resulterende methodegroep wordt Mlgenoemd.
    2. De lijst met argumenten Al wordt samengesteld met één waardeparameter van het type string.
    3. Traditionele methode-aanroepoplossing wordt uitgevoerd met methodegroep Ml en lijst met argumenten Al. Voor de definitieve validatie van methodeaanroepen wordt de context van Ml behandeld als een member_access via een exemplaar van T.
      • Als een enkele beste methode Fi wordt gevonden en er geen fouten zijn geproduceerd, is het resultaat van de resolutie van de methode-aanroep Fi.
      • Anders wordt er een fout gerapporteerd.
  2. Voor elke interpolatieix onderdeel van i:
    1. Het opzoeken van leden op T met de naam AppendFormatted wordt uitgevoerd. De resulterende methodegroep wordt Mfgenoemd.
    2. De lijst met argumenten Af is samengesteld:
      1. De eerste parameter is de expression van ix, doorgegeven door waarde.
      2. Als ix rechtstreeks een constant_expression onderdeel bevat, wordt er een parameter met een geheel getal toegevoegd, waarbij de naam alignment opgegeven.
      3. Als ix direct wordt gevolgd door een interpolation_format, wordt er een parameter voor de tekenreekswaarde toegevoegd met de naam format opgegeven.
    3. Traditionele methode-aanroepoplossing wordt uitgevoerd met methodegroep Mf en lijst met argumenten Af. Voor de definitieve validatie van methodeaanroepen wordt de context van Mf behandeld als een member_access via een exemplaar van T.
      • Als er een beste methode Fi wordt gevonden, wordt het resultaat van methode-aanroepresolutie Fi.
      • Anders wordt er een fout gerapporteerd.
  3. Ten slotte wordt voor elke Fi die in stap 1 en 2 is gedetecteerd, de definitieve validatie uitgevoerd:
    • Als een Fi niet bool als waarde of voidretourneert, wordt er een fout gerapporteerd.
    • Als alle Fi niet hetzelfde type retourneren, wordt er een fout gerapporteerd.

Houd er rekening mee dat deze regels geen uitbreidingsmethoden toestaan voor de Append... aanroepen. We kunnen overwegen dat in te schakelen als we kiezen, maar dit is vergelijkbaar met het enumeratorpatroon, waarbij we toestaan dat GetEnumerator een uitbreidingsmethode zijn, maar niet Current of MoveNext().

Deze regels standaardparameters toestaan voor de Append...-aanroepen, die werken met zaken zoals CallerLineNumber of CallerArgumentExpression (wanneer deze worden ondersteund door de taal).

We hebben afzonderlijke opzoekregels voor overbelasting voor basiselementen versus interpolatiegaten, omdat sommige handlers het verschil willen kunnen begrijpen tussen de onderdelen die zijn geïnterpoleerd en de onderdelen die deel uitmaakten van de basistekenreeks.

vraag openen

Sommige scenario's, zoals gestructureerde logboekregistratie, willen namen kunnen opgeven voor interpolatie-elementen. Vandaag kan een logboekaanroep er bijvoorbeeld uitzien als Log("{name} bought {itemCount} items", name, items.Count);. De namen in de {} voorzien loggers van belangrijke structuurinformatie, wat helpt om ervoor te zorgen dat de uitvoer consistent en uniform is. In sommige gevallen kan het :format onderdeel van een interpolatiegat hiervoor opnieuw worden gebruikt, maar veel loggers begrijpen indelingsaanduidingen al en hebben bestaand gedrag voor uitvoeropmaak op basis van deze informatie. Is er enige syntaxis die we kunnen gebruiken om deze genoemde specificaties toe te voegen?

Sommige gevallen kunnen misschien volstaan met CallerArgumentExpression, mits ondersteuning in C# 10 terechtkomt. Maar voor gevallen die een methode/eigenschap aanroepen, is dat mogelijk niet voldoende.

Antwoord:

Hoewel er enkele interessante onderdelen voor sjabloontekenreeksen zijn die we kunnen verkennen in een orthogonale taalfunctie, denken we niet dat een specifieke syntaxis hier veel voordeel heeft van oplossingen zoals het gebruik van een tuple: $"{("StructuredCategory", myExpression)}".

De conversie uitvoeren

Gezien een applicable_interpolated_string_handler_typeT en een interpolated_string_expressioni met een geldige constructor Fc en opgeloste Append... methoden Fa, wordt de verlaging van i als volgt uitgevoerd:

  1. Argumenten voor Fc die vóór i lexicaal optreden, worden geëvalueerd en in lexicale volgorde opgeslagen in tijdelijke variabelen. Om lexicale volgorde te behouden, als i heeft plaatsgevonden als onderdeel van een grotere expressie e, worden alle onderdelen van e die plaatsvonden vóór i ook in lexicale volgorde geëvalueerd.
  2. Fc wordt aangeroepen met de lengte van de letterlijke onderdelen van de geïnterpoleerde tekenreeks, het aantal interpolatie gaten, eerder geëvalueerde argumenten en een bool argument (als Fc is opgelost met één als laatste parameter). Het resultaat wordt opgeslagen in een tijdelijke waarde ib.
    1. De lengte van de letterlijke onderdelen wordt berekend na het vervangen van een open_brace_escape_sequence door één {en alle close_brace_escape_sequence door één }.
  3. Als Fc eindigde met een bool-uitgangsargument, wordt er een controle uitgevoerd op die bool-waarde. Indien waar, worden de methoden in Fa aangeroepen. Anders worden ze niet gebeld.
  4. Voor elke Fax in Fawordt Fax aangeroepen op ib met het huidige letterlijke onderdeel of interpolatie expressie, indien van toepassing. Als Fax een boolretourneert, wordt het resultaat logisch en gekoppeld aan alle voorgaande Fax aanroepen.
    1. Als Fax een aanroep tot AppendLiteralis, wordt de escape van het letterlijke onderdeel opgeheven door elke open_brace_escape_sequence te vervangen door een enkele {, en elke close_brace_escape_sequence door een enkele }.
  5. Het resultaat van de conversie is ib.

Houd er ook rekening mee dat argumenten die zijn doorgegeven aan Fc en argumenten die worden doorgegeven aan e dezelfde temp zijn. Conversies kunnen boven op de temp plaatsvinden om te converteren naar een formulier dat Fc vereist, maar lambdas kan bijvoorbeeld niet worden gebonden aan een ander type gemachtigde tussen Fc en e.

vraag openen

Deze verlaging betekent dat de volgende onderdelen van de geïnterpoleerde tekenreeks na een vals geretourneerde Append...-aanroep niet worden geëvalueerd. Dit kan erg verwarrend zijn, vooral als het formaatgat bijwerkingen veroorzaakt. We kunnen in plaats daarvan eerst alle opmaakgaten evalueren en vervolgens herhaaldelijk Append... aanroepen met de resultaten; we stoppen als het onwaar retourneert. Dit zorgt ervoor dat alle expressies worden geëvalueerd zoals verwacht, maar we roepen zo weinig methoden aan als nodig is. Hoewel de gedeeltelijke evaluatie wenselijk kan zijn voor sommige geavanceerdere gevallen, is het misschien niet intuïtief voor de algemene zaak.

Een ander alternatief, als we altijd alle opmaakgaten willen evalueren, is de Append... versie van de API te verwijderen en gewoon herhaalde Format aanroepen uit te voeren. De handler kan bijhouden of het argument alleen moet worden laten vallen en onmiddellijk moet terugkeren in deze versie.

Answer: we zullen de gaten voorwaardelijk evalueren.

vraag openen

Moeten we wegwerphandlertypen verwijderen en oproepen verpakken met try/finally om ervoor te zorgen dat Verwijdering wordt aangeroepen? De geïnterpoleerde tekenreekshandler in de bcl kan bijvoorbeeld een gehuurde matrix bevatten en als een van de interpolatiegaten een uitzondering genereert tijdens de evaluatie, kan die gehuurde matrix worden gelekt als deze niet is verwijderd.

Antwoord: Nee. handlers kunnen worden toegewezen aan de lokale bevolking (zoals MyHandler handler = $"{MyCode()};) en de levensduur van dergelijke handlers is onduidelijk. In tegenstelling tot foreach-enumerators, waarbij de levensduur duidelijk is en er geen door de gebruiker gedefinieerde lokale variabele voor de enumerator wordt gemaakt.

Invloed op nullbare referentietypen

Om de complexiteit van de systeemimplementatie te minimaliseren, hebben we enkele beperkingen voor het uitvoeren van nul-waarde-analyse van geïnterpoleerde string handlerconstructors die worden gebruikt als argumenten in een methode of indexeerder. Met name doen we geen gegevens terugvloeien van de constructor naar de oorspronkelijke posities van parameters of argumenten uit de oorspronkelijke context en gebruiken we geen constructorparametertypen voor de bepaling van generieke type-afleiding van typeparameters in de bevattende methode. Een voorbeeld van waar dit invloed kan hebben is:

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()
    {
    }
}

Andere overwegingen

Sta toe dat string typen ook kunnen worden omgezet in handlers

Voor de eenvoud van de auteur van het type kunnen we overwegen om expressies van het type string impliciet om te zetten in applicable_interpolated_string_handler_types. Zoals vandaag voorgesteld, moeten auteurs waarschijnlijk overload toepassen op zowel deze handlertype als de reguliere typen string, waardoor hun gebruikers het verschil niet hoeven te begrijpen. Dit kan een vervelende en niet voor de hand liggende overhead zijn, omdat een string expressie kan worden gezien als een interpolatie met expression.Length voorgevulde lengte en 0 gaten die opgevuld moeten worden.

Hierdoor kunnen nieuwe API's alleen een handler beschikbaar maken, zonder ook een string-overbelasting te hoeven accepteren. Het zal echter het probleem van de noodzaak van wijzigingen om de conversie van de expressie te verbeteren niet oplossen, dus hoewel het zou werken, kan dit onnodige overhead zijn.

Antwoord:

We denken dat dit verwarrend kan zijn en er is een eenvoudige tijdelijke oplossing voor aangepaste handlertypen: voeg een door de gebruiker gedefinieerde conversie vanuit een tekenreeks toe.

Integratie van spans voor heaploze strings

ValueStringBuilder zoals het nu bestaat, heeft 2 constructors: een die een telling neemt, en die gretig toewijst aan de heap, en een die een Span<char>neemt. Deze Span<char> is meestal een vaste grootte in de runtimecodebase, gemiddeld ongeveer 250 elementen. Om dat type echt te vervangen, moeten we een uitbreiding overwegen waarbij we ook GetInterpolatedString-methoden herkennen die een Span<char>gebruiken, in plaats van alleen de telversie. We zien echter een paar potentiële lastige gevallen die hier kunnen worden opgelost:

  • We willen niet herhaaldelijk stackalloc in een kritieke lus. Als we deze uitbreiding voor de functionaliteit zouden doorvoeren, willen we waarschijnlijk de stackalloc-gealloceerde reeks tussen de lusiteraties delen. We weten dat dit veilig is, omdat Span<T> een refstruct is die niet kan worden opgeslagen op de heap, en gebruikers zouden behoorlijk devief moeten zijn om een verwijzing naar die Span te extraheren (zoals het maken van een methode die een dergelijke handler accepteert, de Span bewust ophalen van de handler en het terugsturen naar de beller). Het toewijzen van tevoren levert echter andere vragen op:
    • Moeten we graag stackalloc? Wat gebeurt er als de lus nooit wordt ingevoerd of wordt afgesloten voordat deze de ruimte nodig heeft?
    • Als we niet graag stackalloc gebruiken, betekent dit dan dat we een verborgen vertakking op elke lus introduceren? De meeste lussen zullen dit waarschijnlijk niet schelen, maar dit kan van invloed zijn op enkele strakke lussen die de kosten niet willen betalen.
  • Sommige tekstreeksen kunnen behoorlijk groot zijn, en de juiste hoeveelheid voor stackalloc hangt af van een aantal factoren, inclusief factoren tijdens de uitvoering. We willen niet dat de C#-compiler en -specificatie dit van tevoren moeten bepalen, dus we willen https://github.com/dotnet/runtime/issues/25423 oplossen en een API toevoegen voor de compiler om in deze gevallen aan te roepen. Het voegt ook meer voor- en nadelen toe aan de punten uit de vorige lus, waarbij we niet mogelijk grote arrays op de heap vaak willen toewijzen of voordat dat nodig is.

Antwoord:

Dit valt buiten het bereik voor C# 10. We kunnen dit in het algemeen bekijken wanneer we kijken naar de meer algemene params Span<T> functie.

Niet-testversie van de API

Ter vereenvoudiging stelt deze specificatie momenteel alleen voor om een Append... methode te herkennen en dingen die altijd slagen (zoals InterpolatedStringHandler) zouden altijd waar retourneren uit de methode. Dit is gedaan ter ondersteuning van gedeeltelijke opmaakscenario's waarbij de gebruiker de opmaak wil stoppen als er een fout optreedt of als dit niet nodig is, zoals de logboekregistratiecase, maar mogelijk een aantal onnodige vertakkingen zou kunnen introduceren in standaard geïnterpoleerd tekenreeksgebruik. We kunnen een addendum overwegen waarbij we gewoon FormatX methoden gebruiken als er geen Append... methode aanwezig is, maar er vragen worden gesteld over wat we doen als er een combinatie is van zowel Append... als FormatX aanroepen.

Antwoord:

We willen de niet-probeerversie van de API. Het voorstel is bijgewerkt om dit te weerspiegelen.

Vorige argumenten doorgeven aan de handler

Er is helaas geen symmetrie in het voorstel op dit moment: het aanroepen van een uitbreidingsmethode in gereduceerde vorm produceert verschillende semantiek dan het aanroepen van de extensiemethode in normale vorm. Dit verschilt van de meeste andere locaties in de taal, waarbij verminderde vorm slechts een suiker is. We stellen voor om een kenmerk toe te voegen aan het framework dat we zullen herkennen bij het binden van een methode, die de compiler informeert dat bepaalde parameters moeten worden doorgegeven aan de constructor op de handler. Gebruik ziet er als volgt uit:

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; }
    }
}

Het gebruik hiervan is dan:

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);

De vragen die we moeten beantwoorden:

  1. Vinden we dit patroon in het algemeen leuk?
  2. Willen we toestaan dat deze argumenten afkomstig zijn van na de handlerparameter? Sommige bestaande patronen in de BCL, zoals Utf8Formatter, plaatsen de waarde die moet worden opgemaakt voordat het ding waarin moet worden opgemaakt. Om het beste aan deze patronen te voldoen, willen we dit waarschijnlijk toestaan, maar we moeten beslissen of deze out-of-order evaluatie acceptabel is.

Antwoord:

We willen dit steunen. De specificatie is bijgewerkt om dit weer te geven. Argumenten moeten in lexicale volgorde worden opgegeven op de plek van de aanroep, en als een vereist argument voor de create-methode wordt opgegeven na de geïnterpoleerde tekenreeks, wordt er een fout gegenereerd.

gebruik van await in interpolatiegaten

Omdat $"{await A()}" vandaag een geldige expressie is, moeten we interpolatiegaten met behulp van await optimaliseren. We kunnen dit oplossen met een paar regels:

  1. Als een geïnterpoleerde tekenreeks die als een string, IFormattableof FormattableString wordt gebruikt, een await in een interpolatiegat heeft, val dan terug op een formatter van het oude type.
  2. Als een geïnterpoleerde tekenreeks onderworpen is aan een implicit_string_handler_conversion en applicable_interpolated_string_handler_type een ref structis, mag await niet worden gebruikt in de opmaakgaten.

In wezen kan deze herleiding een ref struct in een asynchrone methode gebruiken zolang we garanderen dat de ref struct niet op de heap hoeft te worden opgeslagen, wat mogelijk moet zijn als we await’s in de interpolatiegaten verbieden.

Alternatief kunnen we alle handlertypen niet-ref structs maken, inclusief de framework-handler voor geïnterpoleerde tekenreeksen. Dit zou echter voorkomen dat we ooit een Span versie herkennen die helemaal geen scratchruimte hoeft toe te wijzen.

Antwoord:

We behandelen geïnterpoleerde tekenreekshandlers hetzelfde als elk ander type: dit betekent dat als het handlertype een verw-struct is en de huidige context het gebruik van verw-structs niet toestaat, het hier ongeldig is om handler te gebruiken. De specificatie rond het verlagen van letterlijke tekenreeksen die worden gebruikt als tekenreeksen, is opzettelijk vaag, zodat de compiler kan bepalen welke regels het geschikt acht, maar voor aangepaste handlertypen moeten ze dezelfde regels volgen als de rest van de taal.

Handlers als referentieparameters

Sommige handlers kunnen worden doorgegeven als refparameters (in of ref). Moeten we dat ook toestaan? En zo ja, hoe ziet een ref handler eruit? ref $"" is verwarrend, omdat u niet daadwerkelijk de string bij referentie doorgeeft. U geeft de handler, die is gemaakt op basis van de referentie, bij referentie door. Dit heeft vergelijkbare potentiele problemen met asynchrone methoden.

Antwoord:

We willen dit steunen. De specificatie is bijgewerkt om dit weer te geven. De regels moeten dezelfde regels weerspiegelen die van toepassing zijn op extensiemethoden voor waardetypen.

Geïnterpoleerde tekenreeksen via binaire expressies en conversies

Omdat dit voorstel geïnterpoleerde tekenreeksen contextgevoelig maakt, willen we de compiler toestaan om een binaire expressie te behandelen die volledig bestaat uit geïnterpoleerde tekenreeksen, of een geïnterpoleerde tekenreeks die wordt onderworpen aan een cast, als een letterlijke tekst voor geïnterpoleerde tekenreeksen voor het doel van overbelastingsresolutie. Neem bijvoorbeeld het volgende scenario:

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

Dit zou dubbelzinnig zijn, waardoor een cast naar Handler1 of Handler2 nodig is om dit op te lossen. Bij het maken van die cast zouden we echter mogelijk de informatie weggooien dat er context komt van de methodeontvanger, wat betekent dat de cast zou mislukken omdat er niets is om de informatie van caan te vullen. Er doet zich een vergelijkbaar probleem voor bij de binaire samenvoeging van tekenreeksen: de gebruiker zou de letterlijke tekst over meerdere regels willen formatteren om regelterugloop te vermijden, maar dat zou niet meer kunnen omdat het dan geen letterlijk geïnterpoleerde tekenreeks meer zou zijn die kan worden omgezet naar het handlertype.

Om deze gevallen op te lossen, brengen we de volgende wijzigingen aan:

  • Een additive_expression die volledig uit interpolated_string_expressions bestaat en alleen + operators gebruikt, wordt beschouwd als een interpolated_string_literal voor conversies en overbelastingsresolutie. De uiteindelijke geïnterpoleerde tekenreeks wordt gemaakt door alle afzonderlijke interpolated_string_expression onderdelen logisch samen te voegen, van links naar rechts.
  • Een cast_expression of een relational_expression met operator as waarvan de operand een interpolated_string_expressions is, wordt beschouwd als een interpolated_string_expressions voor conversies en overbelastingsresolutie.

Vragen openen:

Willen we dit doen? Dit doen we bijvoorbeeld niet voor System.FormattableString, maar dat kan worden uitgesplitsd op een andere regel, terwijl dit contextafhankelijk kan zijn en daarom niet in een andere regel kan worden onderverdeeld. Er zijn ook geen problemen met betrekking tot overbelastingsresolutie met FormattableString en IFormattable.

Antwoord:

We denken dat dit een geldig gebruiksscenario is voor additieve expressies, maar dat de cast-versie op dit moment niet overtuigend genoeg is. We kunnen het later toevoegen indien nodig. De specificatie is bijgewerkt om deze beslissing weer te geven.

Andere gebruiksvoorbeelden

Zie https://github.com/dotnet/runtime/issues/50635 voor voorbeelden van voorgestelde handler-API's met behulp van dit patroon.