Vylepšené interpolované řetězce
Poznámka
Tento článek je specifikace funkce. Specifikace slouží jako návrhový dokument pro funkci. Zahrnuje navrhované změny specifikace spolu s informacemi potřebnými při návrhu a vývoji funkce. Tyto články se publikují, dokud nebudou navrhované změny specifikace finalizovány a začleněny do aktuální specifikace ECMA.
Mezi specifikací funkce a dokončenou implementací může docházet k nějakým nesrovnalostem. Tyto rozdíly jsou zachyceny v příslušných poznámkách ze schůzky jazykového návrhu (LDM).
Další informace o procesu přijetí specifikací funkcí do jazyka C# najdete v článku o specifikacích .
Problém šampiona: https://github.com/dotnet/csharplang/issues/4487
Shrnutí
Zavádíme nový vzor pro vytváření a používání interpolovaných řetězcových výrazů, který umožňuje efektivní formátování a použití v obecných string
scénářích i ve specializovaných scénářích, jako jsou například protokolovací rámce, aniž by docházelo k zbytečnému přidělování při formátování řetězce v rámci.
Motivace
Dnes se interpolace textu převážně redukuje na volání string.Format
. To, i když obecné účely, může být neefektivní z mnoha důvodů:
- Zapouzdřuje všechny argumenty struktury, pokud by modul runtime náhodou nezavedl přetížení
string.Format
, které by přijímalo přesně správné typy argumentů ve správném pořadí.- Toto řazení je důvodem, proč modul runtime váhá zavádět obecné verze metody, protože by to vedlo ke kombinatorické explozi obecné instanciace velmi běžné metody.
- Ve většině případů musí přidělit pole pro argumenty.
- Neexistuje žádná příležitost vyhnout se vytvoření instance, pokud není potřeba. Protokolovací architektury například doporučují vyhnout se interpolaci řetězců, protože způsobí, že se zjistí řetězec, který nemusí být potřeba, v závislosti na aktuální úrovni protokolu aplikace.
- V současné době nemůže používat
Span
ani jiné typy referenčních struktur, protože referenční struktury nejsou povoleny jako obecné typové parametry, což znamená, že pokud chce uživatel zabránit kopírování do mezilehlých míst, musí ručně formátovat řetězce.
Modul runtime má interně typ označovaný jako ValueStringBuilder
, který pomáhá při řešení prvních 2 těchto scénářů. Předají vyrovnávací paměť vytvořenou pomocí stackalloc tvůrci, opakovaně volají AppendFormat
s každou částí a pak získají konečný řetězec. Pokud výsledný řetězec překročí hranice vyrovnávací paměti zásobníku, mohou se pak přesunout na pole v haldě. Tento typ je ale nebezpečný k přímému vystavení, protože nesprávné použití může vést k dvojitému uvolnění pronajatého pole, což způsobí různé druhy nedefinovaného chování v programu, protože dvě místa si myslí, že mají výhradní přístup k pronajatému poli. Tento návrh vytvoří způsob, jak tento typ bezpečně použít z nativního kódu jazyka C# tak, že jednoduše napíše interpolovaný řetězcový literál, přičemž psaný kód zůstane beze změny a zároveň zlepší každý interpolovaný řetězec, který uživatel zapíše. Tento model rozšiřuje své použití tak, že interpolované řetězce předávané jako argumenty jiným metodám mohou využívat vzor obslužné rutiny, který je definován příjemcem metody. Tím se například zabrání tomu, aby protokolovací rámce přidělovaly řetězce, které nikdy nebudou potřeba, a uživatelům jazyka C# poskytne známou a pohodlnou syntaxi interpolace.
Podrobný návrh
Vzor obslužné rutiny
Zavádíme nový vzor obslužné rutiny, který může představovat interpolovaný řetězec předaný jako argument metodě. Jednoduchá angličtina vzoru je následující:
Když se interpolated_string_expression předá metodě jako argument, podíváme se na typ parametru. Pokud má typ parametru konstruktor, který lze vyvolat se dvěma parametry typu int, literalLength
a formattedCount
, a případně přijímá další parametry určené atributem původního parametru, případně má volitelný následný logický parametr, a typ původního parametru má metody instancí AppendLiteral
a AppendFormatted
, které lze vyvolat pro každou část interpolovaného řetězce, pak interpolaci upravíme pomocí tohoto postupu namísto tradičního volání string.Format(formatStr, args)
. Konkrétnější příklad je užitečný pro nakreslování tohoto:
// 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);
Zde, protože TraceLoggerParamsInterpolatedStringHandler
má konstruktor se správnými parametry, říkáme, že interpolovaný řetězec má implicitní konverzi obslužné rutiny k tomuto parametru a redukuje se na vzor uvedený výše. Potřebná specifika jsou trochu složitá a jejich rozbor naleznete níže.
Zbytek tohoto návrhu použije Append...
k odkazování na některý z AppendLiteral
nebo AppendFormatted
v případech, kdy je obojí možné.
Nové atributy
Kompilátor rozpozná 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()
{
}
}
}
Tento atribut používá kompilátor k určení, zda je typ platný jako obslužná rutina interpolovaného řetězce.
Kompilátor také rozpozná 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; }
}
}
Tento atribut se používá u parametrů, aby kompilátor informoval, jak snížit interpolovaný vzor obslužné rutiny řetězce použitý v pozici parametru.
Převod obsluhy interpolace řetězců
Typ T
se označuje jako applicable_interpolated_string_handler_type, pokud je přiřazený System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute
.
Existuje implicitní interpolated_string_handler_conversion pro T
z interpolated_string_expressionnebo z additive_expression, který je složen zcela z _interpolated_string_expression_ a používá pouze +
operátory.
Pro zjednodušení ve zbytku této specifikace, interpolovaný_řetězcový_výraz odkazuje jak na jednoduchý interpolovaný_řetězcový_výraz, tak na sčítací_výraz složený výhradně z interpolovaných_řetězcových_výrazů a pouze s použitím +
operátorů.
Mějte na paměti, že tento převod vždy existuje bez ohledu na to, zda dojde k pozdějším chybám při skutečném pokusu o snížení interpolace pomocí vzoru obslužné rutiny. To vám pomůže zajistit, aby existovaly předvídatelné a užitečné chyby a že chování modulu runtime se nemění na základě obsahu interpolovaného řetězce.
Použitelné úpravy členů funkce
Upravíme formulaci příslušného algoritmu člena funkce (§12.6.4.2) následujícím způsobem (do každé části se přidá nová dílčí odrážka, tučně):
Člen funkce se označuje jako použitelný člen funkce s ohledem na seznam argumentů A
, pokud jsou splněny všechny následující podmínky:
- Každý argument v
A
odpovídá parametru v deklaraci členu funkce, jak je popsáno v odpovídajících parametrech (§12.6.2.2) a jakýkoli parametr, ke kterému žádný argument neodpovídá, je volitelný parametr. - Pro každý argument v
A
je režim předávání parametru argumentu (tj. hodnota,ref
neboout
) identický s režimem předávání parametrů odpovídajícího parametru a- pro parametr hodnoty nebo pole parametrů existuje implicitní převod (§10.2) z argumentu na typ odpovídajícího parametru, nebo
-
pro parametr
ref
, jehož typ je typ struktury, existuje implicitní interpolated_string_handler_conversion z argumentu na typ odpovídajícího parametru nebo - pro
ref
neboout
parametr je typ argumentu shodný s typem odpovídajícího parametru. Koneckonců,ref
neboout
parametr je alias pro předaný argument.
Pro člen funkce, který obsahuje pole parametrů, pokud je člen funkce použitelný podle výše uvedených pravidel, je uvedeno, že je použitelné v jeho normálním formátu. Pokud člen funkce, který obsahuje pole parametrů, není použitelný v normální podobě, může být člen funkce místo toho použitelný ve svém rozšířeném formuláři:
- Rozšířená forma je vytvořena nahrazením pole parametrů v deklaraci člena funkce nulou nebo více hodnotovými parametry typu prvku pole parametrů tak, aby počet argumentů v seznamu argumentů
A
odpovídal celkovému počtu parametrů. PokudA
obsahuje méně argumentů než počet pevných parametrů v deklaraci členu funkce, nelze zvětšovanou formu členu funkce vytvořit, a proto ji nelze použít. - V opačném případě se rozbalený formulář použije, pokud je pro každý argument v
A
režim předávání parametru argumentu stejný jako režim předávání parametrů odpovídajícího parametru a- parametr pevné hodnoty nebo parametr hodnoty vytvořený rozšířením, implicitní převod (§10.2) existuje z typu argumentu na typ odpovídajícího parametru, nebo
-
pro parametr
ref
, jehož typ je struktura, existuje implicitní konverze interpolovaného řetězcového handleru z argumentu na typ odpovídajícího parametru nebo - pro
ref
neboout
parametr je typ argumentu shodný s typem odpovídajícího parametru.
Důležitá poznámka: to znamená, že pokud existují 2 jinak ekvivalentní přetížení, které se liší pouze podle typu applicable_interpolated_string_handler_type, tyto přetížení budou považovány za nejednoznačné. Navíc, protože nevidíme explicitní přetypování, je možné, že může dojít k nesolvovatelnému scénáři, kdy obě příslušné přetížení používají InterpolatedStringHandlerArguments
a jsou zcela nevolitelné bez ručního provádění modelu snížení obslužné rutiny. Mohli bychom potenciálně provést změny v algoritmu člena lepší funkce, abychom to vyřešili, pokud tak zvolíme, ale tento scénář pravděpodobně nenastane a není prioritou k vyřešení.
Lepší konverze z úprav výrazů
Lepší převod z výrazu (§ 12.6.4.5) změníme na následující:
Vzhledem k implicitnímu převodu C1
, který převádí z výrazu E
na typ T1
, a implicitnímu převodu C2
, který převádí z výrazu E
na typ T2
, je C1
lepší převod než C2
, pak:
-
E
není konstantní interpolated_string_expression,C1
je implicit_string_handler_conversion,T1
je applicable_interpolated_string_handler_typeaC2
není implicit_string_handler_conversionnebo -
E
přesně neodpovídáT2
a alespoň jedno z následujícího platí:-
E
přesně odpovídáT1
(§12.6.4.5) -
T1
je lepší cíl převodu nežT2
(§ 12.6.4.6)
-
To znamená, že existují některá potenciálně nejevná pravidla řešení přetížení v závislosti na tom, zda je interpolovaný řetězec v daném případě konstantní výraz nebo ne. Například:
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
To je zavedeno tak, aby věci, které mohou být jednoduše vyjádřeny jako konstanty, nevytvářely žádnou režii, zatímco věci, které nemohou být konstantní, používají vzor obslužné rutiny.
InterpolatedStringHandler a jeho použití
Zavádíme nový typ v System.Runtime.CompilerServices
: DefaultInterpolatedStringHandler
. Jedná se o ref strukturu s mnoha stejnými sémantikou jako ValueStringBuilder
, určená pro přímé použití kompilátorem jazyka C#. Tato struktura by vypadala přibližně takto:
// 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);
}
}
Mírně změníme pravidla pro význam interpolated_string_expression (§12.8.3):
Pokud je typ interpolovaného řetězce string
, existuje typ System.Runtime.CompilerServices.DefaultInterpolatedStringHandler
a aktuální kontext podporuje použití tohoto typu, řetězecse sníží pomocí vzoru obslužné rutiny. Konečné hodnoty string
se pak dosáhne voláním ToStringAndClear()
na typu obslužné rutiny.Jinak pokud typ interpolovaného řetězce je System.IFormattable
nebo System.FormattableString
[zbytek se nezmění]
Pravidlo "a aktuální kontext podporuje použití tohoto typu" je záměrně vágní, aby kompilátoru dal prostor pro optimalizaci použití tohoto modelu. Typ obslužné rutiny je pravděpodobně typ struktury 'ref' a typy struktur 'ref' obvykle nejsou povoleny v asynchronních metodách. V tomto konkrétním případě by kompilátor mohl obslužnou rutinu použít, pokud žádný z interpolačních otvorů neobsahuje výraz await
, protože můžeme staticky určit, že typ obslužné rutiny se bezpečně použije bez další složité analýzy, protože obslužná rutina se po vyhodnocení interpolovaného řetězcového výrazu zahodí.
otevřít otázky:
Chceme místo toho, aby kompilátor věděl o DefaultInterpolatedStringHandler
a úplně přeskočil volání string.Format
? To by nám umožnilo skrýt metodu, kterou nemusíme nutně chtít umístit do tváří lidí, když ručně volají string.Format
.
Odpověď: Ano.
Otevřít Otázka:
Chceme mít také obslužné rutiny pro System.IFormattable
a System.FormattableString
?
Odpověď: Ne.
Vzor obslužné rutiny – codegen
V této části se řešení vyvolání metody týká kroků uvedených v §12.8.10.2.
Rozlišení konstruktoru
Je-li k dispozici applicable_interpolated_string_handler_typeT
a interpolated_string_expressioni
, provádí se následujícím způsobem řešení a ověření vyvolání metody pro platný konstruktor na T
.
- Vyhledávání členů pro konstruktory instancí se provádí na
T
. Výsledná skupina metod se nazýváM
. - Seznam argumentů
A
je vytvořen následujícím způsobem:- První dva argumenty jsou celočíselné konstanty představující délku literálu
i
a počet interpolace komponent vi
. - Pokud se
i
používá jako argument k nějakému parametrupi
v metoděM1
a parametrpi
je přiřazen atributemSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute
, pak pro každý názevArgx
v poliArguments
tohoto atributu kompilátor přiřazuje jméno k parametrupx
, který má stejný název. Prázdný řetězec se shoduje s přijímačemM1
.- Pokud některý
Argx
není možné spárovat s parametremM1
, neboArgx
požaduje příjemceM1
aM1
je statická metoda, vytvoří se chyba a neprovedou se žádné další kroky. - V opačném případě je typ každého vyřešeného
px
přidán do seznamu argumentů v pořadí určeném polemArguments
. Každýpx
se předává se stejnou sémantikouref
, jak je uvedeno vM1
.
- Pokud některý
- Posledním argumentem je
bool
, předaný jakoout
parametr.
- První dva argumenty jsou celočíselné konstanty představující délku literálu
- Proces vyhodnocení zevního volání tradiční metody se provádí s využitím skupiny metod
M
a seznamu argumentůA
. Pro účely konečného ověření při volání metody je kontextM
považován za member_access prostřednictvím typuT
.- Pokud byl nalezen jeden nejlepší konstruktor
F
, výsledek vyhodnocení přetížení jeF
. - Pokud nebyly nalezeny žádné použitelné konstruktory, krok 3 se opakuje a odebere konečný parametr
bool
zA
. Pokud tento opakovaný pokus nenajde žádné použitelné členy, vytvoří se chyba a neprovedou se žádné další kroky. - Pokud nebyla nalezena žádná metoda s jednou nejlepší metodou, je výsledek řešení přetížení nejednoznačný, vytvoří se chyba a neprovedou se žádné další kroky.
- Pokud byl nalezen jeden nejlepší konstruktor
- Konečné ověření pro
F
je provedeno.- Pokud se jakýkoli prvek
A
objeví lexikálně poi
, dojde k chybě a žádné další kroky se neprovedou. - Pokud některý
A
požádá příjemceF
aF
je indexer používaný jako initializer_target v member_initializer, zobrazí se chyba a neprovedou se žádné další kroky.
- Pokud se jakýkoli prvek
Poznámka: Toto řešení záměrně nepoužívejte skutečné výrazy předané jako jiné argumenty pro prvky Argx
. Zvažujeme pouze typy po převodu. To zajišťuje, že nemáme problémy s dvojitou konverzí nebo neočekávané případy, kdy je lambda vázána na jeden typ delegáta při předání do M1
a na jiný typ delegáta při předání do M
.
Poznámka: Ohlašujeme chybu u indexerů, které se používají jako inicializátory členů, kvůli pořadí vyhodnocování u inicializátorů vnořených členů. Vezměte v úvahu tento fragment kódu:
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 { }
}
}
Argumenty __c1.C2[]
jsou vyhodnoceny před, než je spuštěn indexátor. I když bychom mohli přijít se snížením, které by fungovalo v tomto scénáři (buď vytvořením dočasné proměnné pro __c1.C2
a jejím sdílením v rámci obou vyvolání indexéru, nebo pouze jejím použitím pro první vyvolání indexéru a sdílením argumentu napříč oběma vyvoláními), myslíme si, že jakékoli snížení by bylo matoucí pro to, co považujeme za patologický scénář. Proto scénář úplně zakážeme.
otevřítotázky:
Pokud místo Create
použijeme konstruktor, vylepšíme generování kódu za běhu, za cenu mírného zúžení vzoru.
Odpověď: Prozatím se zaměříme na konstruktory. Později se můžeme vrátit k přidání obecné metody Create
, pokud k tomu nastane příležitost.
rozlišení přetížení metody Append...
Pokud existuje applicable_interpolated_string_handler_typeT
a interpolated_string_expressioni
, rozlišení přetížení pro sadu platných metod Append...
na T
se provádí následujícím způsobem:
- Pokud jsou v
i
nějaké komponenty interpolated_regular_string_character:- Provádí se vyhledávání člena na
T
s názvemAppendLiteral
. Výsledná skupina metod se nazýváMl
. - Seznam argumentů
Al
je vytvořen s jedním parametrem hodnoty typustring
. - Tradiční vyhodnocení volání metody je prováděno skupinou metod
Ml
a seznamem argumentůAl
. Pro účely vyvolání metody konečného ověření je kontextMl
považován za přístup člena přes instanciT
.- Pokud je nalezena jedna nejlepší metoda
Fi
a nebyly vytvořeny žádné chyby, výsledek řešení vyvolání metody jeFi
. - V opačném případě se zobrazí chyba.
- Pokud je nalezena jedna nejlepší metoda
- Provádí se vyhledávání člena na
- Pro každou součást interpolace
ix
i
:- Provede se vyhledávání členů na
T
s názvemAppendFormatted
. Výsledná skupina metod se nazýváMf
. - Seznam argumentů
Af
je vytvořen:- Prvním parametrem je
expression
ix
, předaný hodnotou. - Když
ix
přímo obsahuje komponentu vyjádření konstanta, přidá se celočíselná hodnota parametru se zadaným názvemalignment
. - Pokud je
ix
přímo následován interpolation_format, přidá se parametr řetězcové hodnoty s názvemformat
.
- Prvním parametrem je
- Řešení volání tradiční metody se provádí se skupinou metod
Mf
a seznamem argumentůAf
. Pro účely konečného ověření při vyvolání metody se kontextMf
považuje za member_access prostřednictvím instanceT
.- Pokud se najde jedna nejlepší metoda
Fi
, výsledek řešení vyvolání metody jeFi
. - V opačném případě se zobrazí chyba.
- Pokud se najde jedna nejlepší metoda
- Provede se vyhledávání členů na
- Nakonec se pro každou
Fi
zjištěnou v krocích 1 a 2 provede konečné ověření:- Pokud některý
Fi
nevracíbool
podle hodnoty nebovoid
, zobrazí se chyba. - Pokud všechny
Fi
nevrátí stejný typ, zobrazí se chyba.
- Pokud některý
Všimněte si, že tato pravidla nepovolují rozšiřující metody pro volání Append...
. Můžeme zvážit povolení této možnosti, pokud si to zvolíme, ale je to podobné vzoru enumerátoru, kde může být GetEnumerator
rozšiřující metodou, ale Current
nebo MoveNext()
nikoli.
Tato pravidla umožňují povolit výchozí parametry pro volání Append...
, které budou fungovat s věcmi, jako jsou CallerLineNumber
nebo CallerArgumentExpression
(pokud je jazyk podporovaný).
Máme samostatná pravidla vyhledávání přetížení pro základní prvky a interpolační díry, protože některé obslužné programy budou chtít chápat rozdíl mezi interpolovanými komponentami a komponentami, které byly součástí základního řetězce.
Otevřená otázka
Některé scénáře, jako je strukturované protokolování, chtějí mít možnost zadat názvy prvků interpolace. Například dnes může protokolovací volání vypadat jako Log("{name} bought {itemCount} items", name, items.Count);
. Názvy uvnitř {}
poskytují důležité informace o struktuře pro protokolovací nástroje, které pomáhají zajistit, aby byl výstup konzistentní a jednotný. V některých případech může být možné znovu použít komponentu :format
interpolační mezery, ale mnoho loggerů již rozumí formátovacím specifikátorům a mají již zavedené chování pro formátování výstupu na základě těchto informací. Existuje nějaká syntaxe, pomocí které můžeme povolit vkládání těchto pojmenovaných specifikátorů?
Některé případy mohou vystačit si s CallerArgumentExpression
za předpokladu, že podpora bude zahrnuta v C# 10. Ale v případech, které vyvolávají metodu nebo vlastnost, nemusí být dostatečné.
Odpověď:
I když existují některé zajímavé části řetězců s šablonami, které bychom mohli prozkoumat v ortogonální znakové funkci, nemyslíme si, že konkrétní syntaxe zde má velkou výhodu oproti řešením, jako je použití n-tice: $"{("StructuredCategory", myExpression)}"
.
Provedení převodu
Pokud je dán applicable_interpolated_string_handler_typeT
a interpolated_string_expressioni
, který má platný konstruktor Fc
a vyřešené metody Append...
Fa
, provede se převod pro i
následujícím způsobem:
- Všechny argumenty
Fc
, které se vyskytují lexicky předi
, jsou vyhodnoceny a uloženy do dočasných proměnných v lexikálním pořadí. Chcete-li zachovat lexikální řazení, pokudi
došlo jako součást většího výrazue
, všechny součástie
, ke kterým došlo předi
, budou vyhodnoceny také v lexikálním pořadí. -
Fc
se volá s délkou interpolovaných složek řetězcového literálu, počtem interpolačních otvorů, libovolnými dříve vyhodnocenými argumenty a argumentembool
out (pokudFc
byl vyřešen s jedním jako posledním parametrem). Výsledek se uloží do dočasné hodnotyib
.- Délka složek literálu se vypočítá po nahrazení libovolné open_brace_escape_sequence jediným
{
a libovolné close_brace_escape_sequence jediným}
.
- Délka složek literálu se vypočítá po nahrazení libovolné open_brace_escape_sequence jediným
- Pokud
Fc
skončil s argumentembool
typu 'out', je provedena kontrola tétobool
hodnoty. Pokud je hodnota true, budou volána metody vFa
. Jinak nebudou volána. - Pro každý
Fax
vFa
je volánoFax
naib
buď s aktuální literální komponentou, nebo s interpolačním výrazem, podle potřeby. PokudFax
vrátíbool
, výsledek je logicky AND s veškerými předchozími volánímiFax
.- Pokud je
Fax
volánímAppendLiteral
, literálová komponenta je „unescapeována“ nahrazením jakéhokoli open_brace_escape_sequence jediným{
a jakéhokoli close_brace_escape_sequence jediným}
.
- Pokud je
- Výsledek převodu je
ib
.
Znovu si všimněte, že argumenty předané Fc
a argumenty předané e
používají stejné dočasné úložiště. Převody mohou probíhat na tomto úložišti tak, aby splnily požadavky Fc
, ale například lambda výrazy nelze připojit k jinému typu delegáta mezi Fc
a e
.
otevřená otázka
Po tomto snížení se části interpolovaného řetězce, které následují po volání Append...
vracejícím hodnotu 'false', již nevyhodnocují. To může být velmi matoucí, zejména v případě, že mezera ve formátování má vedlejší účinky. Místo toho bychom mohli nejprve vyhodnotit všechny otvory formátu, pak opakovaně zavolat Append...
s výsledky a zastavit, pokud vrátí hodnotu false. Tím by se zajistilo, že se všechny výrazy vyhodnotí tak, jak se očekává, ale zavoláme jen tolik metod, kolik je potřeba. I když částečné vyhodnocení může být žádoucí pro některé pokročilejší případy, je možná neintuitivnější pro obecný případ.
Další alternativou, pokud chceme vždy vyhodnotit všechny otvory formátu, je odebrat Append...
verzi rozhraní API a provádět opakované Format
volání. Obslužná rutina může sledovat, jestli by měla argument zamítnout a ihned se vrátit pro tuto verzi.
Odpověď: Budeme provádět vyhodnocení otvorů za určitých podmínek.
Otevřená otázka
Potřebujeme likvidovat typy jednorázových obslužných rutin a zabalit volání try/finally, abychom zajistili, že se volá Dispose? Například interpolovaná obslužná rutina řetězce v BCL může mít uvnitř pronajaté pole, a pokud jedno z interpolačních míst vyvolá výjimku během vyhodnocení, může dojít k úniku pronajatého pole, pokud nebylo uvolněno.
odpověď: Ne. Obslužné rutiny lze přiřadit místním proměnným (například MyHandler handler = $"{MyCode()};
) a doba života těchto obslužných rutin je nejasná. Na rozdíl od enumerátorů foreach, kde je životnost zřejmá a není vytvořena žádná uživatelsky definovaná lokální proměnná pro enumerátor.
Dopad na nulovatelné odkazové typy
Abychom minimalizovali složitost implementace, máme několik omezení, jak provádíme analýzu nullovatelnosti u operátorů interpolace řetězců, které používáme jako argumenty metod nebo indexerů. Konkrétně nepropagujeme informace z konstruktoru zpět do původních slotů parametrů nebo argumentů z původního kontextu a nepoužíváme typy parametrů konstruktoru k indikaci obecného typového odhadu pro typové parametry v obsahující metodě. Příkladem toho, kde to může mít dopad, je:
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()
{
}
}
Další aspekty
Povolit, aby byly typy string
konvertovatelné na obslužné rutiny
Pro jednoduchost autora typu bychom mohli zvážit možnost implicitně převést výrazy typu string
na applicable_interpolated_string_handler_types. Jak je navrhováno dnes, autoři budou pravděpodobně muset přetížit tento typ obslužné rutiny i běžné typy string
, takže jejich uživatelé nemusí pochopit rozdíl. Může to být otravná a neviditelná režie, protože výraz string
lze považovat za interpolaci s předem vyplněnou délkou expression.Length
a 0 mezerami, které se mají vyplnit.
To by umožnilo novým rozhraním API vystavit pouze obslužnou rutinu, aniž by bylo nutné zveřejnit string
-accepting přetížení. Přesto se nevyhneme potřebě změn pro lepší převod výrazů, takže i když by to fungovalo, mohlo by se jednat o zbytečnou zátěž.
odpověď:
Myslíme si, že by to mohlo být matoucí, ale existuje jednoduché řešení pro vlastní typy obslužných rutin: přidat uživatelsky definovaný převod z řetězce.
Začlenění rozsahů pro řetězce bez využití haldy
ValueStringBuilder
, jak existuje dnes, má 2 konstruktory: jeden, který bere počet, a přiděluje haldu dychtivě a jeden, který přebírá Span<char>
. Tato Span<char>
je obvykle pevnou velikostí ve zdrojové základně kódu runtime, v průměru přibližně 250 prvků. Abychom tento typ skutečně nahradili, měli bychom zvážit rozšíření, které také zahrnuje rozpoznání GetInterpolatedString
metod, které přijímají Span<char>
, namísto pouze těch, které pracují pouze s počty. Vidíme však několik potenciálně trnitých případů, které je třeba tu vyřešit:
- Nechceme opakovaně používat stackalloc ve frekventované smyčce. Pokud bychom toto rozšíření této funkce udělali, budeme pravděpodobně chtít sdílet rozsah stackalloc mezi iteracemi smyčky. Víme, že je to bezpečné, protože
Span<T>
je odkazová struktura, která nemůže být uložena na haldě, a uživatelé by museli být poměrně vychytralí, aby dokázali extrahovat odkaz na tutoSpan
(například vytvořením metody, která přijímá takový obslužný modul, pak záměrně načteSpan
z obslužného modulu a vrátí jej volajícímu). Přidělování předem ale vytváří další otázky:- Měli bychom nadšeně používat stackalloc? Co když se smyčka nikdy nezadá nebo se ukončí, než bude potřebovat prostor?
- Pokud nepoužijeme stackalloc promptně, znamená to, že do každé smyčky zahrnujeme skrytou větev? Většina smyček se o to pravděpodobně nezajímá, ale může to mít vliv na některé těsné smyčky, které nechtějí platit náklady.
- Některé řetězce můžou být poměrně velké a odpovídající množství
stackalloc
závisí na řadě faktorů, včetně faktorů modulu runtime. Nechceme, aby kompilátor a specifikace jazyka C# tuto situaci předem určily, takže bychom chtěli vyřešit https://github.com/dotnet/runtime/issues/25423 a přidat rozhraní API pro volání kompilátoru v těchto případech. Přidává také další výhody a nevýhody k bodům z předchozí iterace, kde nechceme potenciálně přidělovat velká pole na haldě mnohokrát nebo před tím, než jedno z nich bude potřeba.
Odpověď:
Toto nespadá do rozsahu pro C# 10. Na tuto záležitost se můžeme podívat obecněji, když se podíváme na obecnější funkci params Span<T>
.
Verze rozhraní API bez vyzkoušení
Pro zjednodušení tato specifikace v současné době navrhuje rozpoznat metodu Append...
, a věci, které vždy uspějí (například InterpolatedStringHandler
), by vždy vrátily z metody hodnotu "true".
To bylo provedeno kvůli podpoře částečných scénářů formátování, kdy chce uživatel zastavit formátování, pokud dojde k chybě nebo pokud není potřeba, například případ protokolování, ale mohl by potenciálně zavést spoustu nepotřebných větví ve standardním interpolovaném použití řetězců. Můžeme zvážit dodatek, kdy používáme jenom FormatX
metody, pokud neexistuje žádná metoda Append...
, ale prezentuje otázky ohledně toho, co děláme, pokud existuje kombinace Append...
i FormatX
volání.
Odpověď:
Chceme verzi rozhraní API bez vyzkoušení. Návrh byl aktualizován tak, aby to odrážel.
Předání předchozích argumentů obslužné rutině
V návrhu v současné době existuje nešťastná absence symetrie: vyvolání metody rozšíření ve snížené formě vytváří jinou sémantiku než vyvolání metody rozšíření v normální podobě. To se liší od většiny ostatních míst v jazyce, kde snížená forma je jen cukr. Navrhujeme přidání atributu do architektury, kterou rozpoznáme při vazbě metody, která informuje kompilátor, že určité parametry by měly být předány konstruktoru v obslužné rutině. Využití vypadá takto:
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; }
}
}
Použití tohoto je tedy:
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);
Otázky, na které potřebujeme odpovědět:
- Líbí se nám tento vzor obecně?
- Chceme povolit, aby tyto argumenty pocházely zpoza parametru obslužné rutiny? Některé existující vzory v BCL, například
Utf8Formatter
, umisťují hodnotu, která se má formátovat před potřebnou věc k formátování. Abychom se co nejlépe přizpůsobili těmto vzorům, pravděpodobně bychom to chtěli povolit, ale musíme se rozhodnout, jestli je posouzení, které je mimo pořadí, v pořádku.
Odpověď:
Chceme to podporovat. Specifikace byla aktualizována tak, aby to odrážela. Argumenty budou nutné zadat v lexikálním pořadí v lokalitě volání, a pokud je potřebný argument metody create zadán po interpolovaném řetězcovém literálu, vytvoří se chyba.
await
použití v interpolačních otvorech
Vzhledem k tomu, že $"{await A()}"
je dnes platným výrazem, musíme racionalizovat interpolační díry pomocí await. Mohli bychom to vyřešit několika pravidly:
- Pokud interpolovaný řetězec použitý jako
string
,IFormattable
neboFormattableString
máawait
v interpolační dírě, vraťte se k formátovači starého stylu. - Pokud je interpolovaný řetězec předmětem implicit_string_handler_conversion a applicable_interpolated_string_handler_type je
ref struct
,await
není povoleno používat ve formátových otvorech.
V podstatě by toto desugarování mohlo použít ref strukturu v asynchronní metodě, pokud zaručujeme, že ref struct
nebude nutné ukládat do haldy, což by mělo být možné, pokud zakážeme await
v interpolačních otvorech.
Alternativně můžeme jednoduše vytvořit všechny typy obslužných rutin jako struktury nezaložené na odkazu, včetně obslužné rutiny frameworku pro interpolované řetězce. To by nám však zabránilo, abychom někdy mohli rozpoznat verzi Span
, která by vůbec nemusela přidělovat žádnou pomocnou paměť.
Odpověď:
Interpolované obslužné rutiny řetězců budeme považovat za stejné jako každý jiný typ: to znamená, že pokud je typ obslužné rutiny ref struct a aktuální kontext nepovoluje použití ref struct, je zde nelegální použít obslužnou rutinu. Specifikace týkající se snížení řetězcových literálů používaných jako řetězce je záměrně vágní, aby kompilátor mohl rozhodnout, jaká pravidla považuje za vhodná, ale pro vlastní typy obslužných rutin budou muset dodržovat stejná pravidla jako zbytek jazyka.
Obslužné rutiny jako parametry odkazu
Některé obslužné rutiny mohou být předány jako odkazové parametry (in
nebo ref
). Měli bychom povolit jednu nebo druhou? A pokud ano, jak bude obslužná rutina ref
vypadat?
ref $""
je matoucí, protože ve skutečnosti nepředáváte řetězec odkazem, předáváte obslužnou rutinu vytvořenou z odkazu pomocí odkazu a má podobné potenciální problémy s asynchronními metodami.
Odpověď:
Chceme to podporovat. Specifikace byla aktualizována tak, aby to odrážela. Pravidla by měla odrážet stejná pravidla, která platí pro rozšiřující metody u typů hodnot.
Interpolované řetězce pomocí binárních výrazů a převodů
Vzhledem k tomu, že tento návrh činí interpolované řetězce citlivými na kontext, chceme umožnit, aby kompilátor mohl binární výraz složený výhradně z interpolovaných řetězců, nebo interpolovaný řetězec, který je přetypován, zpracovat jako interpolovaný řetězcový literál pro účely rozlišení přetížení. Například použijte následující scénář:
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
To by bylo nejednoznačné a bylo by nutné přetypovat na Handler1
nebo Handler2
, aby bylo možné vyřešit. Při tomto přetypování bychom však potenciálně ztratili informace, které jsou v kontextu příjemce metody, což znamená, že přetypování selže, protože není nic, co by doplnilo informace o c
. K podobnému problému dochází při binárním spojování řetězců: uživatel by mohl chtít formátovat literál přes několik řádků, aby se zabránilo zalamování řádků, ale to by nemohl, protože by to už nebyl interpolovaný řetězcový literál, který lze převést na typ obslužné rutiny.
Při řešení těchto případů provedeme následující změny:
-
additive_expression, která je složena výhradně z interpolated_string_expressions a obsahuje pouze operátory
+
, se považuje za interpolated_string_literal pro účely převodů a rozlišení přetížení. Konečný interpolovaný řetězec se vytvoří logickým zřetězením všech jednotlivých interpolated_string_expression komponent zleva doprava. -
cast_expression nebo relational_expression s operátorem
as
, kterého operandem je interpolated_string_expressions, je považován za interpolated_string_expressions pro účely převodů a rozlišení přetížení.
otevřené otázky:
Chceme to udělat? Pro System.FormattableString
to neuděláme, ale to se dá rozdělit na jinou čáru, zatímco to může být závislé na kontextu, a proto se nedá rozdělit na jinou čáru. Neexistují žádné obavy z řešení přetížení u FormattableString
a IFormattable
.
odpověď:
Myslíme si, že se jedná o platný případ použití pro aditivní výrazy, ale že verze přetypování není v tuto chvíli dostatečně přitažlivá. V případě potřeby ho můžeme přidat později. Specifikace byla aktualizována tak, aby odrážela toto rozhodnutí.
Jiné případy použití
Příklady navrhovaných rozhraní API obslužných rutin používajících tento vzor najdete v https://github.com/dotnet/runtime/issues/50635.
C# feature specifications