Dela via


Förbättrade interpolerade strängar

Anteckning

Den här artikeln är en funktionsspecifikation. Specifikationen fungerar som designdokument för funktionen. Den innehåller föreslagna specifikationsändringar, tillsammans med information som behövs under utformningen och utvecklingen av funktionen. Dessa artiklar publiceras tills de föreslagna specifikationsändringarna har slutförts och införlivats i den aktuella ECMA-specifikationen.

Det kan finnas vissa skillnader mellan funktionsspecifikationen och den slutförda implementeringen. Dessa skillnader samlas in i de relevanta LDM-anteckningar (Language Design Meeting).

Du kan läsa mer om processen för att införa funktionsspecifikationer i C#-språkstandarden i artikeln om specifikationerna.

Champion-fråga: https://github.com/dotnet/csharplang/issues/4487

Sammanfattning

Vi introducerar ett nytt mönster för att skapa och använda interpolerade stränguttryck för att möjliggöra effektiv formatering och användning i både allmänna string scenarier och mer specialiserade scenarier som loggningsramverk, utan onödiga allokeringar från formatering av strängen i ramverket.

Motivation

Idag sänks stränginterpolationen främst till ett anrop till string.Format. Detta kan, även om det är allmänt syfte, vara ineffektivt av flera orsaker:

  1. Den kapslar in alla struct-argument, om inte körmiljön råkar ha infört en överlagring av string.Format som tar exakt rätt typer av argument i exakt rätt ordning.
    • Den här ordningen är anledningen till att körningsmiljön är tveksam till att introducera generiska versioner av metoden, eftersom det skulle leda till en kombinatorisk explosion av generiska instanseringar av en mycket vanlig metod.
  2. Den måste allokera en matris för argumenten i de flesta fall.
  3. Det finns ingen möjlighet att undvika att skapa instansen om den inte behövs. Loggningsramverk rekommenderar till exempel att du undviker stränginterpolation eftersom det gör att en sträng realiseras som kanske inte behövs, beroende på programmets aktuella loggnivå.
  4. Den kan aldrig använda Span eller andra referensstruktureringstyper i dag, eftersom referensstrukturer inte tillåts som generiska typparametrar, vilket innebär att om en användare vill undvika att kopiera till mellanliggande platser måste de manuellt formatera strängar.

Internt har runtime en typ som kallas ValueStringBuilder för att hantera de två första av dessa scenarier. De skickar en stackallokerad buffert till konstruktören, anropar upprepade gånger AppendFormat med varje del och får sedan ut en slutlig sträng. Om den resulterande strängen går förbi stackbuffertens gränser kan de sedan flytta till ett fält i heap-minnet. Den här typen är dock farlig att exponera direkt, eftersom felaktig användning kan leda till att en uthyrd matris frigörs två gånger, vilket sedan orsakar alla typer av oförutsägbart beteende i programmet eftersom två ställen tror att de har ensam åtkomst till den uthyrda matrisen. Det här förslaget skapar ett sätt att använda den här typen på ett säkert sätt från inbyggd C#-kod genom att bara skriva en interpolerad strängliteral, vilket lämnar den skrivna koden oförändrad samtidigt som varje interpolerad sträng som en användare skriver förbättras. Det utökar också det här mönstret så att interpolerade strängar skickas som argument till andra metoder för att använda ett hanteringsmönster, definierat av metodens mottagare, som gör att saker som loggningsramverk kan undvika allokering av strängar som aldrig kommer att behövas och ge C#-användare välbekant, bekväm interpoleringssyntax.

Detaljerad design

Hanterarmönstret

Vi introducerar ett nytt hanteringsmönster som kan representera en interpolerad sträng som skickas som ett argument till en metod. Mönstrets enkla engelska beskrivning är följande:

När en interpolated_string_expression skickas som ett argument till en metod tittar vi på parametertypen. Om parametertypen har en konstruktor som kan anropas med 2 int-parametrar, literalLength och formattedCount, kan du välja att ta ytterligare parametrar som anges av ett attribut för den ursprungliga parametern, om du vill ha en ut boolesk avslutande parameter och typen av den ursprungliga parametern har instans AppendLiteral och AppendFormatted metoder som kan anropas för varje del av den interpolerade strängen, sedan sänker vi interpolationen med det, i stället för till ett traditionellt anrop till string.Format(formatStr, args). Ett mer konkret exempel är användbart för att föreställa sig detta:

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

Här, eftersom TraceLoggerParamsInterpolatedStringHandler har en konstruktor med rätt parametrar, säger vi att den interpolerade strängen har en implicit hanterarekonvertering till den parametern, och den sänks till det mönster som visas ovan. Specifikationen som behövs för detta är lite komplicerad och expanderas nedan.

Resten av detta förslag kommer att använda Append... för att referera till antingen AppendLiteral eller AppendFormatted i de fall då båda är tillämpliga.

Nya attribut

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

Det här attributet används av kompilatorn för att avgöra om en typ är en giltig interpolerad stränghanterartyp.

Kompilatorn känner också igen 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; }
    }
}

Det här attributet används på parametrar för att informera kompilatorn om hur du sänker ett interpolerat stränghanterarmönster som används i en parameterposition.

Konvertering för hantering av interpolerade strängar

Typ T sägs vara en tillämplig interpolerad stränghanterartyp om den är attribuerad med System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Det finns en implicit interpolated_string_handler_conversion att T från en interpolated_string_expression, eller en additive_expression som helt består av _interpolated_string_expression_s och endast använder + operatorer.

För enkelhetens skull i resten av den här specifikationen refererar interpolated_string_expression till både en enkel interpolated_string_expressionoch en additive_expression som helt består av _interpolated_string_expression_s och endast använder + operatorer.

Observera att den här konverteringen alltid finns, oavsett om det uppstår senare fel när du faktiskt försöker sänka interpolationen med hjälp av hanterarens mönster. Detta görs för att säkerställa att det finns förutsägbara och användbara fel och att körningsbeteendet inte ändras baserat på innehållet i en interpolerad sträng.

Tillämpliga funktionsmedlemsjusteringar

Vi justerar formuleringen för den tillämpliga funktionsmedlemsalgoritmen (§12.6.4.2) enligt följande (en ny underpunkt läggs till i varje avsnitt, i fetstil):

En funktionsmedlem sägs vara en tillämplig funktionsmedlem med avseende på en argumentlista A när allt följande är sant:

  • Varje argument i A motsvarar en parameter i funktionsmedlemsdeklarationen enligt beskrivningen i Motsvarande parametrar (§12.6.2.2), och alla parametrar som inget argument motsvarar är en valfri parameter.
  • För varje argument i Aär parameterns överföringsläge för argumentet (dvs. värdet, refeller out) identiskt med parameterns överföringsläge för motsvarande parameter, och
    • för en värdeparameter eller en parametermatris finns en implicit konvertering (§10.2) från argumentet till typen av motsvarande parameter, eller
    • för en ref-parameter vars typ är av strukturen finns det en implicit interpolerad_sträng_handler_konvertering från argumentet till typen av motsvarande parameter, eller
    • för en ref- eller out-parameter är argumentets typ identisk med typen av motsvarande parameter. En ref- eller out-parameter är trots allt ett alias för argumentet som skickas.

För en funktionsmedlem som innehåller en parametermatris, om funktionsmedlemmen är tillämplig enligt ovanstående regler, sägs den vara tillämplig i dess normala formulär. Om en funktionsmedlem som innehåller en parametermatris inte är tillämplig i sin normala form kan funktionsmedlemmen i stället vara tillämplig i dess expanderade formulär:

  • Det expanderade formuläret skapas genom att parametermatrisen i funktionsmedlemsdeklarationen ersätts med noll eller fler värdeparametrar av elementtypen för parametermatrisen så att antalet argument i argumentlistan A matchar det totala antalet parametrar. Om A har färre argument än antalet fasta parametrar i funktionsmedlemsdeklarationen kan inte funktionsmedlemmens utökade form konstrueras och är därför inte tillämplig.
  • I annat fall gäller det expanderade formuläret om för varje argument i A parameteröverföringsläget för argumentet är identiskt med parameterns överföringsläge för motsvarande parameter, och
    • för en parameter med fast värde eller en värdeparameter som skapats av expansionen, finns en implicit konvertering (§10.2) från typen av argumentet till typen av motsvarande parameter, eller
    • för en ref-parameter vars typ är en structtyp, finns det en implicit interpolated_string_handler_conversion från argumentet till typen av den motsvarande parametern, eller
    • för en ref- eller out-parameter är argumentets typ identisk med typen av motsvarande parameter.

Viktigt: Detta innebär att om det finns 2 annars likvärdiga överlagringar, som bara skiljer sig beroende på vilken typ av applicable_interpolated_string_handler_type, kommer dessa överlagringar att betraktas som tvetydiga. Eftersom vi inte ser igenom explicita avgjutningar är det dessutom möjligt att det kan uppstå ett olösligt scenario där både tillämpliga överlagringar använder InterpolatedStringHandlerArguments och är helt oåtkomliga utan att manuellt utföra hanterarens sänkningsmönster. Vi kan eventuellt göra ändringar i algoritmen för bättre funktionsmedlem för att lösa detta om vi så väljer, men det här scenariot kommer sannolikt inte att inträffa och är inte en prioritet att åtgärda.

Bättre konvertering från uttrycksjusteringar

Vi ändrar den bättre konverteringen från uttryck (§12.6.4.5) avsnitt till följande:

Med en implicit konvertering C1 som konverterar från ett uttryck E till en typ T1och en implicit konvertering C2 som konverteras från ett uttryck E till en typ T2är C1 en bättre konvertering än C2 om:

  1. E är en icke-konstant interpolated_string_expression, och C1 är en implicit_string_handler_conversion, T1 är en applicable_interpolated_string_handler_type, och C2 är inte en implicit_string_handler_conversion, eller
  2. E matchar inte exakt T2 och minst något av följande gäller:

Detta innebär att det finns vissa potentiellt icke-uppenbara regler för överlagringsupplösning, beroende på om den aktuella interpolerade strängen är ett konstant uttryck eller inte. Till exempel:

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

Detta introduceras så att saker som helt enkelt kan genereras som konstanter gör det och inte medför några omkostnader, medan saker som inte kan vara konstanta använder hanterarens mönster.

InterpolatedStringHandler och användning

I System.Runtime.CompilerServicesintroducerar vi en ny typ: DefaultInterpolatedStringHandler. Detta är en ref struct med många av samma semantiker som ValueStringBuilder, avsedd för att användas direkt av C#-kompilatorn. Den här structen skulle se ut ungefär så här:

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

Vi gör en liten ändring av reglerna för innebörden av en interpolated_string_expression (§12.8.3):

Om typen av en interpolerad sträng är string och typen System.Runtime.CompilerServices.DefaultInterpolatedStringHandler finns, och den aktuella kontexten stöder användning av den typen, sänks strängenmed hjälp av hanterarens mönster. Det slutliga string värdet hämtas sedan genom att anropa ToStringAndClear() på hanteringstypen.Annars, om typen av en interpolerad sträng är System.IFormattable eller System.FormattableString [resten är oförändrat]

Regeln "och den aktuella kontexten stöder användning av den typen" är avsiktligt vag för att ge kompilatorn spelrum när det gäller att optimera användningen av det här mönstret. Hanterartypen är sannolikt en referensstruktureringstyp och ref struct-typer tillåts normalt inte i asynkrona metoder. I det här fallet skulle kompilatorn tillåtas använda hanteraren om inget av interpolationshålen innehåller ett await uttryck, eftersom vi statiskt kan fastställa att hanterartypen används på ett säkert sätt utan ytterligare komplicerad analys eftersom hanteraren kommer att tas bort när det interpolerade stränguttrycket utvärderas.

Öppna fråga:

Vill vi i stället bara få kompilatorn att känna till DefaultInterpolatedStringHandler och hoppa över string.Format-anropet helt och hållet? Det skulle göra det möjligt för oss att dölja en metod som vi inte nödvändigtvis vill sätta i människors ansikten när de manuellt anropar string.Format.

Svara: Ja.

Öppna fråga:

Vill vi ha hanterare för System.IFormattable och System.FormattableString också?

Svara: Nej.

Kodgen för hanteringsmönster

I det här avsnittet avser metodanropslösning de steg som anges i §12.8.10.2.

Konstruktorlösning

Givet en applicable_interpolated_string_handler_typeT och en interpolated_string_expressioni, utförs metodanropslösning och validering för en giltig konstruktor på T på följande sätt:

  1. Sökning efter medlemmar för instanskonstruktorer utförs på T. Den resulterande metodgruppen kallas M.
  2. Argumentlistan A konstrueras på följande sätt:
    1. De första två argumenten är heltalskonstanter som representerar literallängden för ioch antalet interpolering komponenter i irespektive .
    2. Om i används som ett argument för någon parameter pi i metoden M1, och parametern pi tillskrivs System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, matchar kompilatorn för varje namn Argx i Arguments-matrisen för attributet den till en parameter px som har samma namn. Den tomma strängen matchas med mottagaren för M1.
      • Om någon Argx inte kan matchas med en parameter i M1, eller om en Argx begär mottagaren av M1, och M1 är en statisk metod, genereras ett fel och inga ytterligare åtgärder vidtas.
      • Annars läggs typen av varje löst px till i argumentlistan, i den ordning som anges av Arguments matrisen. Varje px skickas med samma ref semantik som anges i M1.
    3. Det sista argumentet är en bool, som skickas som en out parameter.
  3. Traditionell metodanropsmatchning utförs med metodgruppen M och argumentlistan A. Vid slutlig validering av metodanrop behandlas kontexten för M som en member_access genom typen T.
    • Om en enda bästa konstruktor F hittades blir resultatet av överbelastningsupplösningen F.
    • Om inga tillämpliga konstruktorer hittades görs ett nytt försök i steg 3, vilket tar bort den sista bool-parametern från A. Om det här återförsöket inte heller hittar några tillämpliga medlemmar genereras ett fel och inga ytterligare åtgärder vidtas.
    • Om ingen entydigt bäst metod hittades är resultatet av överbelastningslösning tvetydigt, ett fel genereras och inga ytterligare åtgärder vidtas.
  4. Slutlig validering på F utförs.
    • Om någon del av A inträffat lexikalt efter igenereras ett fel och inga ytterligare åtgärder vidtas.
    • Om någon A begär mottagaren av Foch F används som en initializer_target i en member_initializerrapporteras ett fel och inga ytterligare åtgärder vidtas.

Obs! Lösningen här inte använda de faktiska uttryck som skickas som andra argument för Argx element. Vi överväger bara typerna efter konverteringen. Detta säkerställer att vi inte har problem med dubbelkonvertering eller oväntade fall där en lambda är bunden till en delegattyp när den skickas till M1 och är bunden till en annan delegattyp när den skickas till M.

Observera: Vi meddelar ett fel för indexerare som används som medlemsinitierare på grund av utvärderingsordningen för kapslade medlemsinitierare. Överväg det här kodfragmentet:


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

Argumenten till __c1.C2[] utvärderas innan mottagaren av indexeraren. Även om vi kan komma med en sänkning som fungerar för det här scenariot (antingen genom att skapa en temp för __c1.C2 och dela den över båda indexerarens anrop, eller bara använda den för den första indexerarens anrop och dela argumentet mellan båda anropen) tror vi att en sänkning skulle vara förvirrande för vad vi tror är ett patologiskt scenario. Därför förbjuder vi scenariot helt och hållet.

Öppen fråga:

Om vi använder en konstruktor i stället för Createskulle vi förbättra runtime codegen på bekostnad av att begränsa mönstret lite.

Answer: Vi kommer att begränsa oss till konstruktorer för tillfället. Vi kan gå tillbaka till att lägga till en allmän Create-metod senare om scenariot uppstår.

Append... metodöverbelastningsmatchning

Med en applicable_interpolated_string_handler_typeT och en interpolated_string_expressioniutförs överlagringslösning för en uppsättning giltiga Append... metoder på T enligt följande sätt:

  1. Om det finns några interpolated_regular_string_character-komponenter i i:
    1. Sökning efter medlem på T med namnet AppendLiteral utförs. Den resulterande metodgruppen kallas Ml.
    2. Argumentlistan Al är konstruerad med en värdeparameter av typen string.
    3. Traditionell metodanropsmatchning utförs med metodgruppen Ml och argumentlistan Al. Vid slutlig validering av metodanrop behandlas kontexten för Ml som en member_access via en instans av T.
      • Om en enda bästa metod Fi hittas och inga fel har uppstått, är resultatet av metodens anropsresolution Fi.
      • Annars rapporteras ett fel.
  2. För varje interpoleringix komponenten i i:
    1. Utförs en medlemssökning på T med namnet AppendFormatted. Den resulterande metodgruppen kallas Mf.
    2. Argumentlistan Af är konstruerad:
      1. Den första parametern är expression för ix, passerad som värde.
      2. Om ix direkt innehåller en constant_expression komponent läggs en heltalsvärdeparameter till med namnet alignment angivet.
      3. Om ix följs direkt av en interpolation_formatläggs en strängvärdeparameter till med namnet format angivet.
    3. Traditionell metodanropsmatchning utförs med metodgruppen Mf och argumentlistan Af. Vid slutlig validering av metodanrop behandlas kontexten för Mf som en member_access via en instans av T.
      • Om en enda bästa metod Fi hittas, är resultatet av metodanropslösningen Fi.
      • Annars rapporteras ett fel.
  3. Slutligen utförs slutlig validering för varje Fi som identifieras i steg 1 och 2:
    • Om något Fi inte returnerar bool som värde eller voidrapporteras ett fel.
    • Om alla Fi inte returnerar samma typ rapporteras ett fel.

Observera att dessa regler inte tillåter tilläggsmetoder för Append...-anrop. Vi kan överväga att aktivera det om vi väljer, men det här är detsamma som uppräkningsmönstret, där vi tillåter att GetEnumerator är en tilläggsmetod, men inte Current eller MoveNext().

Dessa regler gör tillåter standardparametrar för Append...-anrop, vilket fungerar med saker som CallerLineNumber eller CallerArgumentExpression (när det stöds av språket).

Vi har separata regler för överbelastningssökning för baselement jämfört med interpoleringshål eftersom vissa hanterare vill kunna förstå skillnaden mellan de komponenter som interpolerades och de komponenter som ingick i bassträngen.

Öppna fråga

Vissa scenarier, till exempel strukturerad loggning, vill kunna ange namn för interpoleringselement. I dag kan ett loggningsanrop till exempel se ut som Log("{name} bought {itemCount} items", name, items.Count);. Namnen i {} ange viktig strukturinformation för loggare som hjälper till att säkerställa att utdata är konsekventa och enhetliga. Vissa fall kanske kan återanvända :format komponenten i ett interpolationshål för detta, men många loggare förstår redan formatspecificerare och har ett befintligt beteende för utdataformatering baserat på den här informationen. Finns det någon syntax som vi kan använda för att möjliggöra placering av dessa namngivna specificerare?

Vissa fall kan komma undan med CallerArgumentExpression, förutsatt att supporten landar i C# 10. Men för fall som anropar en metod/egenskap kanske det inte räcker.

Svar:

Även om det finns några intressanta delar i mallsträngar som vi kan utforska i en ortogonal språkfunktion, tror vi inte att en specifik syntax här har mycket nytta jämfört med lösningar som att använda en tupler: $"{("StructuredCategory", myExpression)}".

Utföra konverteringen

Med tanke på en applicable_interpolated_string_handler_typeT och en interpolated_string_expressioni som hade en giltig konstruktor Fc och Append... metoder Fa lösta, genomförs sänkningen för i på följande vis:

  1. Argumenten till Fc som kommer lexikalt före i utvärderas och lagras i tillfälliga variabler i lexikal ordning. Om i inträffat som en del av ett större uttryck ekommer även eventuella komponenter i e som inträffade före i att utvärderas i lexikal ordning för att bevara lexikal ordning.
  2. Fc anropas med längden på de interpolerade strängliterala komponenterna, antalet interpolering hål, eventuella tidigare utvärderade argument och ett bool ut-argument (om Fc löstes med en som den sista parametern). Resultatet lagras i ett tillfälligt värde ib.
    1. Längden på de literala komponenterna beräknas när du har ersatt alla open_brace_escape_sequence med en enda {och alla close_brace_escape_sequence med en enda }.
  3. Om Fc slutade med ett bool ut-argument genereras en kontroll av det bool värdet. Om sant anropas metoderna i Fa. Annars anropas de inte.
  4. För varje Fax i Faanropas Faxib med antingen den aktuella literalkomponenten eller interpolation uttryck, beroende på vad som är lämpligt. Om Fax returnerar en boolkombineras resultatet logiskt med alla tidigare Fax-anrop.
    1. Om Fax är ett anrop till AppendLiteral, avkodas literalkomponenten genom att ersätta alla open_brace_escape_sequence med ett enda {och alla close_brace_escape_sequence med ett enda }.
  5. Resultatet av konverteringen är ib.

Observera återigen att argument som skickas till Fc och argument som skickas till e är samma temp. Konverteringar kan ske ovanpå temp för att konvertera till ett formulär som Fc kräver, men till exempel kan lambdas inte bindas till en annan delegattyp mellan Fc och e.

Öppna Fråga

Den här sänkningen innebär att efterföljande delar av den interpolerade strängen efter ett falskt returnerande Append...-anrop inte utvärderas. Detta kan potentiellt vara mycket förvirrande, särskilt om formathålet är sidoeffekterande. Vi kan i stället utvärdera alla formathål först och sedan upprepade gånger anropa Append... med resultatet och stoppa om det returnerar falskt. Detta säkerställer att alla uttryck utvärderas som man kan förvänta sig, men vi anropar så få metoder som vi behöver. Även om den partiella utvärderingen kan vara önskvärd för vissa mer avancerade fall, är det kanske inte intuitivt för det allmänna fallet.

Ett annat alternativ, om vi alltid vill utvärdera alla formathål, är att ta bort den Append... versionen av API:et och bara göra upprepade Format anrop. Hanteraren kan spåra om det bara ska ignorera argumentet och omedelbart återgå i denna version.

Answer: Vi kommer att ha villkorlig utvärdering av hålen.

Öppna fråga

Behöver vi ta bort disponibla hanterartyper och omsluta anrop med try/finally för att säkerställa att Dispose kallas? Till exempel kan den interpolerade stränghanteraren i bcl ha en hyrd matris inuti sig, och om ett av interpolationshålen utlöser ett undantag under utvärderingen kan den hyrda matrisen läckas om den inte tas bort.

Svara: Nej. hanterare kan tilldelas till lokalbefolkningen (till exempel MyHandler handler = $"{MyCode()};), och livslängden för sådana hanterare är oklar. Till skillnad från foreach-uppräknare, där livslängden är uppenbar och ingen användardefinierad lokal skapas för uppräknaren.

Påverkan på referenstyper som kan ogiltigförklaras

För att minimera implementeringens komplexitet har vi några begränsningar för hur vi utför nullbar analys på interpolerade stränghanterarkonstruktorer som används som argument till en metod eller indexerare. I synnerhet flödar vi inte information från konstruktorn tillbaka till de ursprungliga platserna med parametrar eller argument från den ursprungliga kontexten, och vi använder inte konstruktorparametertyper för att informera om allmän typinferens för typparametrar i den innehållande metoden. Ett exempel på var detta kan påverka är:

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

Andra överväganden

Tillåt att string typer kan konverteras även till hanterare

För enkelhetens skull kan vi överväga att tillåta att uttryck av typen string kan implicit konverteras till applicable_interpolated_string_handler_types. Som vi föreslår i dag måste författarna förmodligen överbelasta både den hanterartypen och vanliga string typer, så att användarna inte behöver förstå skillnaden. Detta kan vara en irriterande och icke-uppenbar overhead, eftersom ett string uttryck kan ses som en interpolation med expression.Length förfylld längd och 0 hål som ska fyllas.

Detta skulle göra det möjligt för nya API:er att endast exponera en hanterare, utan att även behöva exponera en stringsom accepterar överbelastning. Det kan dock inte eliminera behovet av förändringar för bättre konvertering från uttryck, så även om det skulle fungera kan belastningen vara onödig.

Svar:

Vi tror att detta kan bli förvirrande och det finns en enkel lösning för anpassade hanteringstyper: lägga till en användardefinierad konvertering från strängen.

Inkorporera intervall för heap-fria strängar

ValueStringBuilder som det finns idag har 2 konstruktorer: en som tar en räkning, och allokerar på heap ivrigt, och en som tar en Span<char>. Span<char> har vanligtvis en fast storlek i körmiljökodbasen, cirka 250 element i genomsnitt. För att verkligen ersätta den typen bör vi överväga ett tillägg till denna där vi också känner igen GetInterpolatedString-metoder som tar en Span<char>, i stället för bara räkningsversionen. Vi ser dock några potentiella svåra fall att lösa här:

  • Vi vill inte stackalloc upprepade gånger i en frekvent loop. Om vi skulle göra det här tillägget till funktionen skulle vi förmodligen vilja dela stackalloc'd-intervallet mellan loop-iterationer. Vi vet att detta är säkert eftersom Span<T> är en referensstruktur som inte kan lagras på högen, och användarna måste vara ganska listiga för att kunna extrahera en referens till den Span (till exempel genom att skapa en metod som accepterar en sådan hanterare och sedan avsiktligt hämta Span från hanteraren och returnera den till anroparen). Att allokera i förväg ger dock andra frågor:
    • Ska vi ivrigt stackalloc? Vad händer om loopen aldrig gås in i eller avslutas innan den behöver utrymmet?
    • Innebär det att vi introducerar en dold gren i varje loop om vi inte ivrigt använder stackalloc? De flesta loopar bryr sig förmodligen inte om detta, men det kan påverka vissa snäva loopar som inte vill betala kostnaden.
  • Vissa strängar kan vara ganska stora, och den lämpliga mängden till stackalloc beror på flera faktorer, inklusive körningstidens faktorer. Vi vill inte att C#-kompilatorn och specifikationen ska behöva fastställa detta i förväg, så vi vill lösa https://github.com/dotnet/runtime/issues/25423 och lägga till ett API för kompilatorn att anropa i dessa fall. Det lägger också till fler för- och nackdelar till punkterna från föregående loop, där vi vill undvika att potentiellt allokera stora matriser i heapminnet flera gånger eller innan det verkligen behövs.

Svar:

Detta ligger utanför omfånget för C# 10. Vi kan se på detta generellt när vi tittar på den mer generella params Span<T>-funktionen.

Ej testversion av API:et

För enkelhetens skull föreslår den här specifikationen för närvarande bara att känna igen en Append...-metod, och saker som alltid lyckas (som InterpolatedStringHandler) skulle alltid returnera sant från metoden. Detta gjordes för att stödja partiella formateringsscenarier där användaren vill sluta formatera om ett fel inträffar eller om det är onödigt, till exempel loggningsfallet, men potentiellt kan introducera en massa onödiga grenar i standardinterpolerad stränganvändning. Vi kan överväga ett tillägg där vi bara använder FormatX metoder om det inte finns någon Append... metod, men det ger frågor om vad vi gör om det finns en blandning av både Append...- och FormatX-anrop.

Svar:

Vi vill ha icke-try-versionen av API:et. Förslaget har uppdaterats för att återspegla detta.

Skicka tidigare argument till hanteraren

Det finns för närvarande en olycklig brist på symmetri i förslaget: att anropa en tilläggsmetod i reducerad form ger olika semantik än att anropa tilläggsmetoden i normal form. Detta skiljer sig från de flesta andra platser i språket, där reducerad form bara är ett socker. Vi föreslår att du lägger till ett attribut i ramverket som vi känner igen när vi binder en metod, som informerar kompilatorn om att vissa parametrar ska skickas till konstruktorn på hanteraren. Användningen ser ut så här:

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

Användningen av detta är då:

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 frågor vi behöver besvara:

  1. Gillar vi det här mönstret i allmänhet?
  2. Vill vi tillåta att dessa argument kommer efter hanteringsparametern? Vissa befintliga mönster i BCL, till exempel Utf8Formatter, placerar värdet som ska formateras innan det som behövs för att formatera till. För att passa in bäst med dessa mönster skulle vi förmodligen vilja tillåta detta, men vi måste bestämma om den här oordnade utvärderingen är okej.

Svar:

Vi vill stödja detta. Specifikationen har uppdaterats för att återspegla detta. Argumenten kommer att behöva anges i lexikal ordning på anropsplatsen, och om ett nödvändigt argument för skapametoden anges efter den interpolerade strängliteralen, genereras ett fel.

await användning i interpoleringshål

Eftersom $"{await A()}" är ett giltigt uttryck idag måste vi rationalisera interpoleringshål med await. Vi kan lösa detta med några regler:

  1. Om en interpolerad sträng som används som string, IFormattableeller FormattableString har en await i ett interpolationshål, ska vi återgå till den gamla formatteraren.
  2. Om en interpolerad sträng omfattas av en implicit_string_handler_conversion och applicable_interpolated_string_handler_type är en ref structfår await inte användas i formathålen.

I grund och botten skulle den här avtäcken kunna använda en referensstruktur i en asynkron metod så länge vi garanterar att ref struct inte behöver sparas i heap, vilket bör vara möjligt om vi förbjuder awaiti interpolationshålen.

Alternativt kan vi helt enkelt göra alla hanteringstyper till icke-ref-structs, inklusive ramverkshanteraren för interpolerade strängar. Detta skulle dock hindra oss från att en dag känna igen en Span version som inte behöver allokera något ledigt utrymme alls.

Svar:

Vi behandlar interpolerade stränghanterare på samma sätt som andra typer: det innebär att om hanterartypen är en referens-struct och den aktuella kontexten inte tillåter användning av referensstrukturer är det olagligt att använda hanteraren här. Specifikationen kring sänkning av strängliteraler som används som strängar är avsiktligt vag för att kompilatorn ska kunna bestämma vilka regler den anser lämpliga, men för anpassade hanteringstyper måste de följa samma regler som resten av språket.

Hanterare som referensparametrar

Vissa hanterare kanske vill skickas som referensparametrar (antingen in eller ref). Ska vi tillåta något av dem? Och i så fall, hur kommer en ref hanterare att se ut? ref $"" är förvirrande, eftersom du faktiskt inte skickar själva strängen som referens, utan du skickar en hanterare som skapats från referensen och detta medför liknande potentiella problem med asynkrona metoder.

Svar:

Vi vill stödja detta. Specifikationen har uppdaterats för att återspegla detta. Reglerna bör återspegla samma regler som gäller för tilläggsmetoder för värdetyper.

Interpolerade strängar via binära uttryck och konverteringar

Eftersom det här förslaget gör kontexten för interpolerade strängar känslig vill vi att kompilatorn ska kunna behandla ett binärt uttryck som helt består av interpolerade strängar eller en interpolerad sträng som utsätts för en gjuten strängliteral i syfte att lösa överbelastningen. Anta till exempel följande 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

Detta skulle vara tvetydigt, vilket kräver en typkonvertering till antingen Handler1 eller Handler2 för att lösa det. Men när vi gör denna typkonvertering skulle vi potentiellt kasta bort informationen om att det finns kontext från metodmottagaren, vilket innebär att typkonverteringen skulle misslyckas eftersom det inte finns något att fylla i informationen i c. Ett liknande problem uppstår med binär konkatenering av strängar: användaren kan vilja formatera strängliteralen över flera rader för att undvika radbrytning, men skulle inte kunna göra det eftersom det inte längre skulle vara en interpolerad strängliteral konvertibel till hanterarens typ.

För att lösa dessa fall gör vi följande ändringar:

  • En additive_expression som helt består av interpolated_string_expressions och endast använder + operatorer betraktas som en interpolated_string_literal för omvandlingar och överlagringslösning. Den slutliga interpolerade strängen skapas genom att logiskt sammanfoga alla enskilda interpolated_string_expression komponenter, från vänster till höger.
  • En cast_expression eller en relational_expression med operatorn as vars operande är en interpolated_string_expressions anses vara en interpolated_string_expressions för konverteringar och överlagringsmatchning.

Öppna frågor:

Vill vi göra det här? Vi gör inte detta för System.FormattableString, till exempel, men det kan delas upp på en annan linje, medan detta kan vara kontextberoende och därför inte kunna delas upp i en annan linje. Det finns heller inga problem med hantering av överbelastning med FormattableString och IFormattable.

Svar:

Vi anser att detta är ett giltigt användningsfall för additiva uttryck, men att den gjutna versionen inte är tillräckligt övertygande just nu. Vi kan lägga till den senare om det behövs. Specifikationen har uppdaterats för att återspegla det här beslutet.

Andra användningsfall

Se https://github.com/dotnet/runtime/issues/50635 för exempel på föreslagna hanterar-API:er med det här mönstret.