Sdílet prostřednictvím


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ů:

  1. 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.
  2. Ve většině případů musí přidělit pole pro argumenty.
  3. 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.
  4. 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 Aje režim předávání parametru argumentu (tj. hodnota, refnebo out) 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 nebo out parametr je typ argumentu shodný s typem odpovídajícího parametru. Koneckonců, ref nebo out 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ů. Pokud A 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 nebo out 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 C1lepší převod než C2, pak:

  1. E není konstantní interpolated_string_expression, C1 je implicit_string_handler_conversion, T1 je applicable_interpolated_string_handler_typea C2 není implicit_string_handler_conversionnebo
  2. E přesně neodpovídá T2 a alespoň jedno z následujícího platí:

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.

  1. Vyhledávání členů pro konstruktory instancí se provádí na T. Výsledná skupina metod se nazývá M.
  2. Seznam argumentů A je vytvořen následujícím způsobem:
    1. První dva argumenty jsou celočíselné konstanty představující délku literálu ia počet interpolace komponent v i.
    2. Pokud se i používá jako argument k nějakému parametru pi v metodě M1a parametr pi je přiřazen atributem System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, pak pro každý název Argx v poli Arguments tohoto atributu kompilátor přiřazuje jméno k parametru px, který má stejný název. Prázdný řetězec se shoduje s přijímačem M1.
      • Pokud některý Argx není možné spárovat s parametrem M1, nebo Argx požaduje příjemce M1 a M1 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 polem Arguments. Každý px se předává se stejnou sémantikou ref, jak je uvedeno v M1.
    3. Posledním argumentem je bool, předaný jako out parametr.
  3. 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 kontext M považován za member_access prostřednictvím typu T.
    • Pokud byl nalezen jeden nejlepší konstruktor F, výsledek vyhodnocení přetížení je F.
    • Pokud nebyly nalezeny žádné použitelné konstruktory, krok 3 se opakuje a odebere konečný parametr bool z A. 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.
  4. Konečné ověření pro F je provedeno.
    • Pokud se jakýkoli prvek A objeví lexikálně po i, dojde k chybě a žádné další kroky se neprovedou.
    • Pokud některý A požádá příjemce Fa F je indexer používaný jako initializer_target v member_initializer, zobrazí se chyba a neprovedou se žádné další kroky.

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 Createpouž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:

  1. Pokud jsou v inějaké komponenty interpolated_regular_string_character:
    1. Provádí se vyhledávání člena na T s názvem AppendLiteral. Výsledná skupina metod se nazývá Ml.
    2. Seznam argumentů Al je vytvořen s jedním parametrem hodnoty typu string.
    3. 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 kontext Ml považován za přístup člena přes instanci T.
      • Pokud je nalezena jedna nejlepší metoda Fi a nebyly vytvořeny žádné chyby, výsledek řešení vyvolání metody je Fi.
      • V opačném případě se zobrazí chyba.
  2. Pro každou součást interpolace ixi:
    1. Provede se vyhledávání členů na T s názvem AppendFormatted. Výsledná skupina metod se nazývá Mf.
    2. Seznam argumentů Af je vytvořen:
      1. Prvním parametrem je expressionix, předaný hodnotou.
      2. Když ix přímo obsahuje komponentu vyjádření konstanta, přidá se celočíselná hodnota parametru se zadaným názvem alignment.
      3. Pokud je ix přímo následován interpolation_format, přidá se parametr řetězcové hodnoty s názvem format.
    3. Ř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 kontext Mf považuje za member_access prostřednictvím instance T.
      • Pokud se najde jedna nejlepší metoda Fi, výsledek řešení vyvolání metody je Fi.
      • V opačném případě se zobrazí chyba.
  3. 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 nebo void, zobrazí se chyba.
    • Pokud všechny Fi nevrátí stejný typ, zobrazí se chyba.

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 CallerArgumentExpressionza 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:

  1. Všechny argumenty Fc, které se vyskytují lexicky před i, jsou vyhodnoceny a uloženy do dočasných proměnných v lexikálním pořadí. Chcete-li zachovat lexikální řazení, pokud i došlo jako součást většího výrazu e, všechny součásti e, ke kterým došlo před i, budou vyhodnoceny také v lexikálním pořadí.
  2. 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 argumentem bool out (pokud Fc byl vyřešen s jedním jako posledním parametrem). Výsledek se uloží do dočasné hodnoty ib.
    1. 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 }.
  3. Pokud Fc skončil s argumentem bool typu 'out', je provedena kontrola této bool hodnoty. Pokud je hodnota true, budou volána metody v Fa. Jinak nebudou volána.
  4. Pro každý Fax v Faje voláno Fax na ib buď s aktuální literální komponentou, nebo s interpolačním výrazem, podle potřeby. Pokud Fax vrátí bool, výsledek je logicky AND s veškerými předchozími voláními Fax.
    1. Pokud je Fax voláním AppendLiteral, 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 }.
  5. 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 tuto Span (například vytvořením metody, která přijímá takový obslužný modul, pak záměrně načte Span 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:

  1. Líbí se nám tento vzor obecně?
  2. 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:

  1. Pokud interpolovaný řetězec použitý jako string, IFormattablenebo FormattableStringawait v interpolační dírě, vraťte se k formátovači starého stylu.
  2. 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 awaitv 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.FormattableStringto 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.