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:
- 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.
- Den måste allokera en matris för argumenten i de flesta fall.
- 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å.
- 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,ref
ellerout
) 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
- ellerout
-parameter är argumentets typ identisk med typen av motsvarande parameter. Enref
- ellerout
-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. OmA
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
- ellerout
-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 T1
och en implicit konvertering C2
som konverteras från ett uttryck E
till en typ T2
är C1
en bättre konvertering än C2
om:
-
E
är en icke-konstant interpolated_string_expression, ochC1
är en implicit_string_handler_conversion,T1
är en applicable_interpolated_string_handler_type, ochC2
är inte en implicit_string_handler_conversion, eller -
E
matchar inte exaktT2
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.CompilerServices
introducerar 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:
- Sökning efter medlemmar för instanskonstruktorer utförs på
T
. Den resulterande metodgruppen kallasM
. - Argumentlistan
A
konstrueras på följande sätt:- De första två argumenten är heltalskonstanter som representerar literallängden för
i
och antalet interpolering komponenter ii
respektive . - Om
i
används som ett argument för någon parameterpi
i metodenM1
, och parameternpi
tillskrivsSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute
, matchar kompilatorn för varje namnArgx
iArguments
-matrisen för attributet den till en parameterpx
som har samma namn. Den tomma strängen matchas med mottagaren förM1
.- Om någon
Argx
inte kan matchas med en parameter iM1
, eller om enArgx
begär mottagaren avM1
, ochM1
ä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 avArguments
matrisen. Varjepx
skickas med sammaref
semantik som anges iM1
.
- Om någon
- Det sista argumentet är en
bool
, som skickas som enout
parameter.
- De första två argumenten är heltalskonstanter som representerar literallängden för
- Traditionell metodanropsmatchning utförs med metodgruppen
M
och argumentlistanA
. Vid slutlig validering av metodanrop behandlas kontexten förM
som en member_access genom typenT
.- Om en enda bästa konstruktor
F
hittades blir resultatet av överbelastningsupplösningenF
. - Om inga tillämpliga konstruktorer hittades görs ett nytt försök i steg 3, vilket tar bort den sista
bool
-parametern frånA
. 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.
- Om en enda bästa konstruktor
- Slutlig validering på
F
utförs.- Om någon del av
A
inträffat lexikalt efteri
genereras ett fel och inga ytterligare åtgärder vidtas. - Om någon
A
begär mottagaren avF
ochF
används som en initializer_target i en member_initializerrapporteras ett fel och inga ytterligare åtgärder vidtas.
- Om någon del av
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 Create
skulle 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_expressioni
utförs överlagringslösning för en uppsättning giltiga Append...
metoder på T
enligt följande sätt:
- Om det finns några interpolated_regular_string_character-komponenter i
i
:- Sökning efter medlem på
T
med namnetAppendLiteral
utförs. Den resulterande metodgruppen kallasMl
. - Argumentlistan
Al
är konstruerad med en värdeparameter av typenstring
. - Traditionell metodanropsmatchning utförs med metodgruppen
Ml
och argumentlistanAl
. Vid slutlig validering av metodanrop behandlas kontexten förMl
som en member_access via en instans avT
.- Om en enda bästa metod
Fi
hittas och inga fel har uppstått, är resultatet av metodens anropsresolutionFi
. - Annars rapporteras ett fel.
- Om en enda bästa metod
- Sökning efter medlem på
- För varje interpolering
ix
komponenten ii
:- Utförs en medlemssökning på
T
med namnetAppendFormatted
. Den resulterande metodgruppen kallasMf
. - Argumentlistan
Af
är konstruerad:- Den första parametern är
expression
förix
, passerad som värde. - Om
ix
direkt innehåller en constant_expression komponent läggs en heltalsvärdeparameter till med namnetalignment
angivet. - Om
ix
följs direkt av en interpolation_formatläggs en strängvärdeparameter till med namnetformat
angivet.
- Den första parametern är
- Traditionell metodanropsmatchning utförs med metodgruppen
Mf
och argumentlistanAf
. Vid slutlig validering av metodanrop behandlas kontexten förMf
som en member_access via en instans avT
.- Om en enda bästa metod
Fi
hittas, är resultatet av metodanropslösningenFi
. - Annars rapporteras ett fel.
- Om en enda bästa metod
- Utförs en medlemssökning på
- Slutligen utförs slutlig validering för varje
Fi
som identifieras i steg 1 och 2:- Om något
Fi
inte returnerarbool
som värde ellervoid
rapporteras ett fel. - Om alla
Fi
inte returnerar samma typ rapporteras ett fel.
- Om något
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:
- Argumenten till
Fc
som kommer lexikalt förei
utvärderas och lagras i tillfälliga variabler i lexikal ordning. Omi
inträffat som en del av ett större uttrycke
kommer även eventuella komponenter ie
som inträffade förei
att utvärderas i lexikal ordning för att bevara lexikal ordning. -
Fc
anropas med längden på de interpolerade strängliterala komponenterna, antalet interpolering hål, eventuella tidigare utvärderade argument och ettbool
ut-argument (omFc
löstes med en som den sista parametern). Resultatet lagras i ett tillfälligt värdeib
.- 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}
.
- Längden på de literala komponenterna beräknas när du har ersatt alla open_brace_escape_sequence med en enda
- Om
Fc
slutade med ettbool
ut-argument genereras en kontroll av detbool
värdet. Om sant anropas metoderna iFa
. Annars anropas de inte. - För varje
Fax
iFa
anropasFax
ib
med antingen den aktuella literalkomponenten eller interpolation uttryck, beroende på vad som är lämpligt. OmFax
returnerar enbool
kombineras resultatet logiskt med alla tidigareFax
-anrop.- Om
Fax
är ett anrop tillAppendLiteral
, avkodas literalkomponenten genom att ersätta alla open_brace_escape_sequence med ett enda{
och alla close_brace_escape_sequence med ett enda}
.
- Om
- 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 string
som 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 denSpan
(till exempel genom att skapa en metod som accepterar en sådan hanterare och sedan avsiktligt hämtaSpan
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:
- Gillar vi det här mönstret i allmänhet?
- 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:
- Om en interpolerad sträng som används som
string
,IFormattable
ellerFormattableString
har enawait
i ett interpolationshål, ska vi återgå till den gamla formatteraren. - Om en interpolerad sträng omfattas av en implicit_string_handler_conversion och applicable_interpolated_string_handler_type är en
ref struct
fårawait
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 await
i 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.
C# feature specifications