Megosztás a következőn keresztül:


Továbbfejlesztett interpolált karakterláncok

Jegyzet

Ez a cikk egy funkcióspecifikáció. A specifikáció a funkció tervezési dokumentumaként szolgál. Tartalmazza a specifikáció javasolt módosításait, valamint a funkció tervezése és fejlesztése során szükséges információkat. Ezeket a cikkeket mindaddig közzéteszik, amíg a javasolt specifikációmódosításokat nem véglegesítik, és be nem építik a jelenlegi ECMA-specifikációba.

A szolgáltatás specifikációja és a befejezett implementáció között eltérések lehetnek. Ezeket a különbségeket a vonatkozó nyelvi tervezési értekezlet (LDM) megjegyzései rögzítik.

A funkcióspektusok C# nyelvi szabványba való bevezetésének folyamatáról a specifikációkcímű cikkben olvashat bővebben.

Bajnok kérdés: https://github.com/dotnet/csharplang/issues/4487

Összefoglalás

Bevezetünk egy új mintát az interpolált sztringkifejezések létrehozására és használatára, amely lehetővé teszi a hatékony formázást és használatot mind az általános string forgatókönyvekben, mind a speciálisabb forgatókönyvekben, például a naplózási keretrendszerekben, anélkül, hogy szükségtelen lefoglalásokat okoznánk a sztring keretrendszerbeli formázásából.

Motiváció

Ma a sztring interpoláció főként a string.Formathívására redukálódik. Ez, bár általános célú, számos okból lehet nem hatékony:

  1. Minden strukturált argumentumot beszűkít, kivéve, ha a futtatókörnyezet túlterhelést okoz a string.Format, amely pontosan a megfelelő típusú argumentumokat veszi figyelembe pontosan a megfelelő sorrendben.
    • Ez a rendezés az oka annak, hogy a futtatókörnyezet nem hajlandó bevezetni a módszer általános verzióit, mivel ez egy nagyon gyakori módszer általános példányainak kombinatorikus robbanásához vezetne.
  2. A legtöbb esetben ki kell osztania egy tömböt az argumentumokhoz.
  3. Nincs lehetőség arra, hogy elkerülje az objektum példányosítását, ha nincs rá szükség. A naplózási keretrendszerek például azt javasolják, hogy kerüljék a sztring interpolációját, mert az alkalmazás aktuális naplószintjének függvényében olyan sztringet fog létrehozni, amely nem feltétlenül szükséges.
  4. Jelenleg egyáltalán nem használhat Span-t vagy más ref struct típusokat, mert a ref struct-ok nem engedélyezettek általános típusparaméterként, ami azt jelenti, hogy ha a felhasználó el akarja kerülni a köztes helyekre való másolást, akkor muszáj manuálisan formázni a karakterláncokat.

A futtatókörnyezet belsőleg egy ValueStringBuilder nevű típussal rendelkezik, amely segít a forgatókönyvek első 2 megoldásában. Átadnak egy veremfoglalással (stackalloc) létrehozott puffert az építőnek, ismételten meghívják a AppendFormat metódust minden részével, majd lekérnek egy végső karakterláncot. Ha az eredményül kapott karakterlánc túllépi a verempuffer határait, akkor áttérhetnek a halmon lévő tömbre. Ez a típus azonban veszélyes közvetlenül, mivel a helytelen használat miatt egy bérelt tömböt duplán kell megsemmisíteni, ami aztán mindenféle meghatározatlan viselkedést okoz a programban, mivel két hely úgy gondolja, hogy csak a bérelt tömbhöz férnek hozzá. Ez a javaslat lehetővé teszi, hogy natív C# kódból biztonságosan használhassa ezt a típust egy interpolált szöveges konstans írásával, változatlanul hagyva az írott kódot, miközben javítja a felhasználó által írt összes interpolált sztringet. Ez a minta azt is kiterjeszti, hogy lehetővé tegye az argumentumként átadott interpolált sztringeket más metódusoknak, hogy a metódus fogadója által meghatározott kezelőmintát használjon, amely lehetővé teszi például a naplózási keretrendszerek számára, hogy elkerüljék a soha nem szükséges sztringek kiosztását, és a C#-felhasználók számára ismerős, kényelmes interpolációs szintaxist biztosít.

Részletes tervezés

A kezelői minta

Bevezetünk egy új kezelőmintát, amely egy interpolált sztringet jelöl, amelyet argumentumként adunk át egy metódusnak. A minta egyszerű angol nyelvű leírása a következő:

Amikor egy interpolated_string_expression argumentumként ad át egy metódusnak, megvizsgáljuk a paraméter típusát. Ha a paramétertípusnak van egy konstruktora, amely 2 int paraméterrel, literalLength és formattedCount, hívható meg, opcionálisan az eredeti paraméter egy attribútuma által megadott további paramétereket is felvehet, opcionálisan 'out bool' zárójeles paraméterrel rendelkezhet, és az eredeti paraméter típusa olyan AppendLiteral és AppendFormatted metódusokkal rendelkezik, amelyek meghívhatók az interpolált szöveg minden részére, akkor az interpolációt így oldjuk meg, ahelyett, hogy hagyományos módon a string.Format(formatStr, args)-et hívnánk meg. Egy konkrétabb példa hasznos a képi szemléltetéshez:

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

Mivel itt a TraceLoggerParamsInterpolatedStringHandler rendelkezik a megfelelő paraméterekkel ellátott konstruktorral, azt mondjuk, hogy az interpolált sztring implicit kezelői átalakítást kap ehhez a paraméterhez, és az így egyszerűsödik a fent látható mintára. Az ehhez szükséges specifikáció kissé bonyolult, és az alábbiakban ki van bontva.

A javaslat többi részében a Append... fog hivatkozni a AppendLiteral-re vagy a AppendFormatted-re olyan esetekben, amikor mindkettő alkalmazható.

Új attribútumok

A fordító felismeri a 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()
        {
        }
    }
}

A fordító ezt az attribútumot használja annak megállapítására, hogy egy típus érvényes interpolált sztringkezelő típus-e.

A fordító felismeri a System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttributeis:

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

Ez az attribútum paramétereken alapul, és tájékoztatja a fordítót arról, hogyan csökkentheti a paraméterhelyzetben használt interpolált sztringkezelő mintát.

Interpolált karakterlánc kezelő konverzió

A(z) T típusról azt mondják, hogy applicable_interpolated_string_handler_type, ha rendelkezik a System.Runtime.CompilerServices.InterpolatedStringHandlerAttributeattribútummal. Létezik egy implicit interpolated_string_handler_conversion egy T, vagy egy teljes egészében _interpolated_string_expression_s álló additive_expression, amely csak + operátorokat használ.

A specifikáció további részében az egyszerűség kedvéért a interpolated_string_expression kifejezés mind egy egyszerű interpolated_string_expression, mind pedig egy teljes egészében _interpolated_string_expression_ elemekből álló, csak operátorokat használó + utal.

Vegye figyelembe, hogy ez az átalakítás mindig létezik, függetlenül attól, hogy lesznek-e későbbi hibák, amikor ténylegesen megpróbálják csökkenteni az interpolációt a kezelőmintával. Ez annak érdekében történik, hogy kiszámítható és kezelhető hibák legyenek, és hogy a futásidejű viselkedés ne változzon az interpolált sztring tartalma alapján.

A függvénytagok alkalmazható módosításai

Az alkalmazandó függvénytag-algoritmus szövegét (§12.6.4.2) az alábbiak szerint módosítjuk (minden szakaszhoz hozzáadunk egy új aljelet, félkövér betűvel):

Egy függvénytag akkor tekinthető alkalmazható függvénytagnak egy argumentumlista A esetén, ha az alábbiak mindegyike igaz:

  • A A minden argumentuma megfelel a függvénytag-deklaráció egyik paraméterének a Megfelelő paraméterekben (12.6.2.2) leírtak szerint, és bármely paraméter, amelynek argumentuma nem felel meg, választható paraméter.
  • A Aminden argumentuma esetében az argumentum paraméterátadási módja (például érték, refvagy out) megegyezik a megfelelő paraméter paraméterátadási módjával, és
    • értékparaméter vagy paramétertömb esetén implicit átalakítás (§10.2) létezik az argumentumból a megfelelő paraméter típusára, vagy
    • olyan ref paraméter esetében, amelynek típusa egy struktúra típus, létezik egy implicit interpolated_string_handler_conversion az argumentum és a megfelelő paraméter típusa között, vagy
    • egy ref vagy out paraméter esetében az argumentum típusa megegyezik a megfelelő paraméter típusával. Végül is egy ref vagy out paraméter az átadott argumentum aliasa.

Ha a függvénytagra, amely paramétertömböt tartalmaz, a fenti szabályok szerint alkalmazható, akkor azt a normál formájábanalkalmazhatónak mondható. Ha egy paramétertömböt tartalmazó függvénytag nem alkalmazható a normál formájában, akkor a függvénytag alkalmazható lehet a bővített űrlapon:

  • A kibontott űrlap úgy jön létre, hogy a függvénytag-deklarációban szereplő paramétertömbet a paramétertömb elemtípusának nulla vagy több értékparaméterére cseréli, így az argumentumlistában szereplő argumentumok száma A megegyezik a paraméterek teljes számával. Ha A kevesebb argumentummal rendelkezik, mint a függvénytag-deklaráció rögzített paramétereinek száma, a függvénytag kibontott formája nem hozható létre, ezért nem alkalmazható.
  • Ellenkező esetben a kibontott űrlap akkor alkalmazható, ha A minden argumentum esetében az argumentum paraméterátadási módja megegyezik a megfelelő paraméter paraméterátadási módjával, és
    • rögzített értékparaméter vagy a bővítés által létrehozott értékparaméter esetében implicit átalakítás (10.2.. §) létezik az argumentum típusától a megfelelő paraméter típusához, vagy
    • olyan ref paraméter esetében, amelynek típusa egy struktúra típus, létezik egy implicit interpolated_string_handler_conversion az argumentum és a megfelelő paraméter típusa között, vagy
    • egy ref vagy out paraméter esetében az argumentum típusa megegyezik a megfelelő paraméter típusával.

Fontos megjegyzés: ez azt jelenti, hogy ha 2 egyébként egyenértékű túlterhelés van, amely csak a applicable_interpolated_string_handler_typetípusától különbözik, ezek a túlterhelések nem egyértelműnek minősülnek. Továbbá, mivel nem látunk explicit konverziókon keresztül, lehetséges, hogy olyan megoldhatatlan forgatókönyv merülhet fel, amelyben mindkét alkalmazható túlterhelés InterpolatedStringHandlerArguments-t használ, és teljesen nem hívható, hacsak nem hajtjuk végre kézzel a kezelési minta csökkentését. Lehetséges, hogy módosítjuk a jobb függvénytag-algoritmust, hogy ezt megoldjuk, ha úgy döntünk, de ez a forgatókönyv valószínűleg nem fordul elő, és nem prioritás a megoldáshoz.

Jobb átalakítás kifejezések módosításával

A kifejezés (§12.6.4.5) szakaszának jobb konvertálását a következőre módosítjuk:

Egy kifejezésből C1Etípussá konvertáló implicit konverzió T1 és egy kifejezésből C2 típussá Ekonvertáló implicit konverzió T2C1jobb konverzió, mint C2, ha:

  1. E nem állandó interpolated_string_expression, C1 egy implicit_string_handler_conversion, T1 egy applicable_interpolated_string_handler_type, és C2 nem egy implicit_string_handler_conversion, vagy
  2. E nem egyezik meg pontosan T2-gyel, és legalább az alábbiak egyike igaz:

Ez azt jelenti, hogy vannak bizonyos nem nyilvánvaló túlterhelés-feloldási szabályok attól függően, hogy a szóban forgó interpolált sztring állandó kifejezés-e vagy sem. Például:

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

Ezt azért vezetjük be, hogy a konstansként kibocsátható dolgok ne okozzanak többletterhelést, míg azok a dolgok, amelyek nem lehetnek konstansok, a kezelőmintát használják.

InterpolatedStringHandler és Használat

Új típust vezetünk be a System.Runtime.CompilerServices: DefaultInterpolatedStringHandler. Ez egy olyan refstruktúra, amely számos olyan szemantikával rendelkezik, mint ValueStringBuilder, amelyet a C#-fordító közvetlen használatra szánt. Ez a szerkezet nagyjából így nézne ki:

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

A interpolated_string_expression jelentésére vonatkozó szabályokat kis mértékben módosítjuk (12.8.3. §):

Ha az interpolált sztring típusa string, és a típus System.Runtime.CompilerServices.DefaultInterpolatedStringHandler létezik, és az aktuális környezet támogatja az ilyen típus használatát, akkor asztringet a kezelőmintával alakítják át. A végső string értéket a kezelő típusának ToStringAndClear() meghívásával kérdezzük le.Ellenkező esetben, ha az interpolált sztring típusa System.IFormattable vagy System.FormattableString [a többi nem változik]

A "és a jelenlegi környezet támogatja az ilyen típus használatát" szabály szándékosan homályos, hogy a fordítóprogram számára mozgástere legyen a minta használatának optimalizálásában. A kezelő típusa valószínűleg egy ref struct típus, és az aszinkron metódusok általában nem engedélyezik a ref struct típusokat. Ebben az esetben a fordító akkor használhatja a kezelőt, ha az interpolációs lyukak egyike sem tartalmaz await kifejezést, mivel statikusan megállapíthatjuk, hogy a kezelőtípus biztonságosan használható további bonyolult elemzés nélkül, mert a kezelő az interpolált sztringkifejezés kiértékelése után el lesz ejtve.

Kérdésmegnyitása:

Inkább csak azt szeretnénk, hogy a fordító tudjon a DefaultInterpolatedStringHandler-ról, és teljesen kihagyjuk a string.Format hívást? Ez lehetővé tenné számunkra, hogy elrejtsük azt a módszert, amelyet nem feltétlenül szeretnénk az emberek arcába tenni, amikor manuálisan hívják string.Format.

Válasz: Igen.

Kérdésmegnyitása:

Szeretnénk kezelőket System.IFormattable és System.FormattableString számára is?

Válasz: Nem.

Kezelőminta kodgenje

Ebben a szakaszban a metódushívás feloldása a §12.8.10.2megadott lépésekre hivatkozik.

Konstruktor feloldása

Egy applicable_interpolated_string_handler_typeT és egy interpolated_string_expressioniesetén a metódushívás feloldása és érvényesítése az T érvényes konstruktorához az alábbiak szerint történik:

  1. Példánykonstruktorok tagkeresése a T-on történik. Az eredményként kapott metóduscsoport neve M.
  2. Az argumentumlista A a következőképpen épül fel:
    1. Az első két argumentum egész számállandó, amely a iliterális hosszát, valamint a i összetevőinek számát jelöli.
    2. Ha a i a pimetódus M1 paraméterének argumentumaként van használva, és a pi paraméter System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttributevan attribútálva, akkor az attribútum Argx tömbjében található minden Arguments nevű elemhez a fordító összepárosítja egy azonos nevű px paraméterrel. Az üres karaktersorozat illeszkedik a M1fogadójához.
      • Ha egy Argx nem tud egyezni a M1paraméterével, vagy ha egy Argx a M1 fogadóját kéri, és M1 statikus módszer, hiba keletkezik, és nem történik további lépés.
      • Ellenkező esetben minden feloldott px típusa hozzáadódik az argumentumlistához a Arguments tömb által megadott sorrendben. Minden px ugyanazzal a ref szemantikával kerül átadásra, ahogy az a M1-ben meg van határozva.
    3. Az utolsó argumentum egy bool, amelyet out paraméterként ad át.
  3. A hagyományos metódushívás feloldása a metóduscsoport M és az argumentumlista A. A metódushívások végső érvényességének ellenőrzése céljából a M kontextusát a member_access a Ttípuson keresztül kezeljük.
    • Ha egyetlen legjobb konstruktor F található, a túlterhelés feloldásának eredménye F.
    • Ha nem találhatók megfelelő konstruktorok, a 3. lépés újrapróbálásra kerül, amely eltávolítja a végső bool paramétert a A-ből. Ha ez az újrapróbálkozás nem talál megfelelő tagokat, hibaüzenet jelenik meg, és nem történik további lépés.
    • Ha nem található egyetlen legjobb módszer, a túlterhelés feloldása nem egyértelmű, hiba keletkezik, és nem történik további lépés.
  4. A végső ellenőrzést a F-n hajtják végre.
    • Ha a A bármely eleme lexikálisan iután következett be, hiba keletkezik, és nem történik további lépés.
    • Ha bármelyik A kéri a Ffogadóját, és F egy indexelő, amelyet initializer_target-ként használnak egy member_initializer-ban, akkor a rendszer hibát jelez, és nem hajt végre további lépéseket.

Megjegyzés: az itt található megoldás szándékosan nem az átadott tényleges kifejezéseket más argumentumként használni Argx elemekhez. Csak az átalakítás utáni típusokat vesszük figyelembe. Ez biztosítja, hogy ne legyenek dupla konverziós problémák, vagy váratlan esetek, amikor egy lambda egy delegált típushoz van kötve, amikor átadják M1, és egy másik delegálttípushoz vannak kötve, amikor átadják M.

Megjegyzés: A beágyazott tag inicializálók kiértékelési sorrendje miatt hibát jelentünk a tag inicializálóként használt indexelők esetében. Fontolja meg ezt a kódrészletet:


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

Az __c1.C2[] argumentumait értékelik ki, mielőtt megkapná az indexelőt. Bár létrehozhatunk egy olyan alacsonyabb értéket, amely működik ebben a forgatókönyvben (vagy úgy, hogy létrehozunk egy ideiglenes __c1.C2, és megosztjuk azt mindkét indexelő meghívásában, vagy csak az első indexelő meghívásához használjuk, és megosztjuk az argumentumot mindkét meghívás között), úgy gondoljuk, hogy a csökkentések zavaróak lennének a patológiás forgatókönyv szempontjából. Ezért teljes egészében tiltjuk a forgatókönyvet.

Nyitott kérdés:

Ha Createhelyett konstruktort használunk, javíthatnánk a futásidejű kódgenerálást, azzal az áron, hogy kissé szűkítjük a mintát.

Válasz: Egyelőre konstruktorokra korlátozódunk. A forgatókönyv felmerülése esetén később újra áttekinthetjük egy általános Create metódus hozzáadását.

Append... metódus túlterhelésének feloldás

applicable_interpolated_string_handler_typeT és egy interpolated_string_expressioniesetén az Append... rendszeren található érvényes T metódusok túlterhelés-feloldása az alábbi módon történik:

  1. Ha vannak interpolated_regular_string_character összetevők a i:
    1. A T-n történik a AppendLiteral nevű tag keresése. Az eredményként kapott metóduscsoport neve Ml.
    2. Az argumentumlista Al egy stringtípusú értékparaméterrel jön létre.
    3. A hagyományos metódushívás feloldása a metóduscsoport Ml és az argumentumlista Al. A metódushívás végső érvényesítése érdekében a Ml kontextusát úgy kezeljük, mint egy member_access egy Tpéldányán keresztül.
      • Ha egyetlen legjobb módszer Fi található, és nem keletkeztek hibák, a metódushívás feloldásának eredménye Fi.
      • Ellenkező esetben hiba jelenik meg.
  2. Az minden ixi összetevője esetében:
    1. A T-n történik a AppendFormatted nevű tag keresése. Az eredményként kapott metóduscsoport neve Mf.
    2. Az argumentumlista Af a következő:
      1. Az első paraméter a expression-beli ix, amely érték szerint kerül átadásra.
      2. Ha ix közvetlenül tartalmaz egy constant_expression összetevőt, akkor a rendszer hozzáad egy egész szám értékparamétert, amelynek neve alignment meg van adva.
      3. Ha ix-t közvetlenül egy interpolation_formatkövet, akkor hozzáadnak egy karakterlánc értékű paramétert, a format megadott névvel.
    3. A hagyományos metódushívás feloldása a metóduscsoport Mf és az argumentumlista Af. A metódushívás végső érvényesítése érdekében a Mf kontextusát úgy kezeljük, mint egy member_access egy Tpéldányán keresztül.
      • Ha megtalálják az egyetlen legjobb metódust Fi, akkor a metódushívás feloldásának eredménye Fi.
      • Ellenkező esetben hiba jelenik meg.
  3. Végül az 1. és 2. lépésben felderített Fi esetében a végső ellenőrzés a következő:
    • Ha egy Fi nem ad vissza bool érték vagy voidszerint, hibaüzenet jelenik meg.
    • Ha az összes Fi nem ugyanazt a típust adja vissza, hibaüzenet jelenik meg.

Vegye figyelembe, hogy ezek a szabályok nem engedélyezik a bővítménymetelyeket a Append... hívásokhoz. Megfontolhatjuk annak engedélyezését, ha úgy döntünk, de ez hasonló az enumerátor mintához, ahol lehetővé tesszük, hogy a GetEnumerator egy kiterjesztési módszer legyen, de a Current vagy a MoveNext()nem.

Ezek a szabályok engedélyezik az alapértelmezett paraméterek használatát a Append... hívásoknál, amelyek akkor működhetnek, ha a nyelv támogatja őket, például a CallerLineNumber vagy CallerArgumentExpression esetében.

Külön túlterheléskeresési szabályok vonatkoznak az alapelemekre és az interpolációs lyukakra, mert egyes kezelők tudni szeretnék, hogy mi a különbség az interpolált összetevők és az alapsztring részét képező összetevők között.

Nyissa meg kérdést

Egyes forgatókönyvek, például a strukturált naplózás, meg szeretnék adni az interpolációs elemek nevét. Ma például egy naplózási hívás így nézhet ki: Log("{name} bought {itemCount} items", name, items.Count);. A {} belüli nevek fontos szerkezeti információkat nyújtanak a naplózók számára, amelyek segítenek a kimenet konzisztens és egységes kialakításában. Bizonyos esetekben előfordulhat, hogy egy interpolációs lyuk :format összetevőjét újra felhasználhatja, de sok naplózó már ismeri a formátumjelölőket, és már rendelkezik a kimeneti formázás ezen információkon alapuló viselkedésével. Van olyan szintaxis, amely lehetővé teszi ezeknek a névvel ellátott meghatározóknak a elhelyezését?

Bizonyos esetekben meg lehet úszni a CallerArgumentExpression-t, feltéve, hogy a támogatás megérkezik a C# 10-be. A metódust/tulajdonságot meghívó esetekben azonban ez nem feltétlenül elegendő.

Válasz:

Bár a sablonos karakterláncoknak vannak érdekes részei, amelyeket egy másik, független nyelvi funkcióban is ki lehetne fejezni, nem gondoljuk, hogy egy adott szintaxis sokkal előnyösebb lenne az olyan megoldásokhoz képest, mint például egy tuple használata: $"{("StructuredCategory", myExpression)}".

Az átalakítás végrehajtása

Adott egy applicable_interpolated_string_handler_typeT és egy interpolated_string_expressioni, amely érvényes konstruktorral Fc és Append... módszerekkel Fa rendelkezik, a i előkészítése a következőképpen történik:

  1. A Fc előtt lexikálisan előforduló i argumentumok kiértékelése és tárolása lexikális sorrendben ideiglenes változókba történik. A lexikális rendezés megőrzése érdekében, ha i egy nagyobb kifejezés erészeként történt, a ei előtt történt összetevőit is kiértékeljük, ismét lexikális sorrendben.
  2. Fc az interpolált sztringkonstans-összetevők hosszával, a interpoláció lyukak számával, a korábban kiértékelt argumentumokkal és a bool argumentummal (ha Fc az utolsó paraméterrel oldották fel). Az eredmény egy ideiglenes ibértékben lesz tárolva.
    1. A literálösszetevők hosszát úgy számítják ki, hogy bármely open_brace_escape_sequence helyére egyetlen {-t, és bármely close_brace_escape_sequence helyére egyetlen }-t cserélnek.
  3. Ha a Fc egy bool kimeneti argumentummal végződik, akkor egy ellenőrzés generálódik ezen a bool értéken. Ha igaz, a Fa metódusai lesznek meghívva. Ellenkező esetben a rendszer nem hívja meg őket.
  4. A Faxminden Fa esetében a Fax-t hívják meg a ib-n, a jelenlegi literális összetevővel vagy interpolációs kifejezéssel, ahogy megfelelő. Ha a Fax egy bool-et ad vissza, az eredményt logikai ÉS műveletként összekapcsoljuk az összes előző Fax hívással.
    1. Ha a Fax egy AppendLiteralhívás, akkor a literál összetevő kibontásra kerül oly módon, hogy a open_brace_escape_sequence egyetlen {, és a close_brace_escape_sequence egyetlen }-el helyettesítendő.
  5. Az átalakítás eredménye ib.

Vegye figyelembe, hogy a Fc-nak és a e-nek átadott argumentumok ugyanazok az ideiglenes változók. Az ideiglenesen is előfordulhatnak konvertálások, hogy Fc által igényelt formátumra alakuljanak át, de például a lambdák nem köthetőek egy másik delegált típushoz Fc és eközött.

Nyissa meg kérdést

Ez a csökkentés azt jelenti, hogy az interpolált sztringnek a Append... hívás, amely hamis értéket ad vissza, utáni részei nem lesznek kiértékelve. Ez nagyon zavaró lehet, különösen akkor, ha a formátumbeli hiba mellékhatásokat okoz. Ehelyett először kiértékelhetnénk az összes formázási lyukat, majd ismételten meghívhatnánk a Append...-t az eredményekkel, és megállnánk, ha hamis értéket ad vissza. Ez biztosítaná, hogy az összes kifejezés az elvárásoknak megfelelően kerüljön kiértékelésre, miközben csak a szükséges legkevesebb metódust hívjuk meg. Bár a részleges értékelés néhány fejlettebb esetben kívánatos lehet, az általános esetben talán nem intuitív.

Egy másik megoldás, ha folyamatosan értékelni szeretnénk a formátumhiányokat, az az lenne, hogy eltávolítjuk az API Append... verzióját, és csak ismételjük a Format hívásokat. A kezelő nyomon tudja követni, hogy csak az argumentumot kell-e elvetnie, és azonnal vissza kell-e térnie ehhez a verzióhoz.

Válasz: Feltételesen kiértékeljük a lyukakat.

Nyissa meg kérdést

Meg kell szabadulnunk az eldobható kezelőtípusoktól, és be kell csomagolnunk a hívásokat try/finally blokkokkal annak biztosítására, hogy a Dispose hívásra kerüljön? A bcl interpolált sztringkezelője például tartalmazhat egy bérelt tömböt, és ha az interpolációs lyukak egyike kivételt vált ki a kiértékelés során, akkor a bérelt tömb kiszivároghat, ha nem bontották le.

Válasz: Nem. a kezelők hozzárendelhetők a helyiekhez (például MyHandler handler = $"{MyCode()};), és az ilyen kezelők élettartama nem egyértelmű. A foreach enumerátoroktól eltérően, ahol az élettartam nyilvánvaló, és a rendszer nem hoz létre felhasználó által definiált helyi értékeket az enumerátorhoz.

A null értékű referenciatípusokra gyakorolt hatás

Az implementáció összetettségének minimalizálása érdekében korlátozottan végezhetünk null értékű elemzést egy metódus vagy indexelő argumentumaként használt interpolált sztringkezelő konstruktorokon. A konstruktortól nem továbbítunk információt vissza az eredeti környezetből származó paraméterek vagy argumentumok eredeti helyeire, és nem használjuk a konstruktorparaméter-típusokat a típusparaméterek általános típuskövetkeztetésének meghatározására az adott metódusban. Példa arra, hogy ennek milyen hatása lehet:

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

Egyéb szempontok

Tegyük lehetővé, hogy a string típusok kezelőkké alakuljanak.

A szövegszerkesztő egyszerűsége érdekében fontolóra vehetjük, hogy a string típusú kifejezések implicit módon konvertálhatók legyenek applicable_interpolated_string_handler_types. A ma javasolt módon a szerzőknek valószínűleg túl kell terhelniük mind a kezelőtípust, mind a normál string típusokat, így a felhasználóknak nem kell megérteniük a különbséget. Ez bosszantó és nem nyilvánvaló többletterhelés lehet, mivel egy string kifejezés úgy tekinthető, mint egy interpoláció, amelynek expression.Length előre kitöltött hosszúsága van, és nincs hiányzó hely, amit ki kellene tölteni.

Ez lehetővé tenné, hogy az új API-k csak egy kezelőt tegyenek elérhetővé anélkül, hogy elérhetővé kellene tenni egy string-t elfogadó túlterhelést. Azonban nem lehet megkerülni a szükséges módosításokat a kifejezés jobb átalakításához, így bár működhet, szükségtelen többletterhelést jelenthet.

Válasz:

Úgy gondoljuk, hogy ez összezavarhat, és van egy egyszerű megoldás az egyéni kezelőtípusokhoz: adjon hozzá egy felhasználó által definiált átalakítást string típusból.

A heap nélküli stringek szélességének beépítése

ValueStringBuilder ahogy ma létezik, 2 konstruktorral rendelkezik: az egyik, amely egy számot fogad, és lelkesen foglal helyet a halomra, és egy, amely egy Span<char>-et fogad. Ez a Span<char> általában rögzített méretű a futtatókörnyezeti kódbázisban, átlagosan körülbelül 250 elem. Ahhoz, hogy valóban lecseréljük ezt a típust, érdemes megfontolnunk ennek a bővítménynek a kiterjesztését, ahol GetInterpolatedString metódusokat is felismerünk, amelyek Span<char>vesznek igénybe, nem csak a számverziót. Azonban néhány lehetséges tüskés esetet látunk itt megoldandóként:

  • Nem szeretnénk többször stackallocot használni egy intenzív ciklusban. Ha ezt a kiterjesztést a funkcióra alkalmaznánk, valószínűleg szeretnénk megosztani a stackalloc által lefoglalt tartományt a hurok iterációi között. Tudjuk, hogy ez biztonságos, mivel a Span<T> egy ref struktúra, amely nem tárolható a halomtárban, és a felhasználóknak elég találékonynak kellene lenniük ahhoz, hogy hivatkozást nyerjenek ki arra a Span objektumra (például létrehoznak egy metódust, amely elfogad egy ilyen kezelőt, majd szándékosan lekérik a Span-t a kezelőtől, és visszaadják azt a hívónak). Az előre történő kiosztás azonban további kérdéseket vet fel:
    • Lelkesedéssel kellene használnunk a stackalloc-ot? Mi van, ha a hurok soha nem indul el, vagy kilép, mielőtt még szükség lenne a helyre?
    • Ha nem használunk szívesen stackalloc-ot, ez azt jelenti, hogy megvalósítunk egy rejtett ágat minden egyes hurokban? A legtöbb hurok valószínűleg nem fogja ezt érdekelni, de hatással lehet néhány szoros hurokra, amelyek nem akarják viselni a költségeket.
  • Egyes karakterláncok meglehetősen nagyok lehetnek, és az stackalloc megfelelő mennyisége számos tényezőtől függ, beleértve a futási időbeli tényezőket is. Nem szeretnénk, hogy a C#-fordítónak és a specifikációnak előre meg kell határoznia ezt, ezért szeretnénk feloldani a https://github.com/dotnet/runtime/issues/25423, és hozzáadni egy API-t a fordítóhoz, hogy meghívja ezeket az eseteket. Emellett további előnyöket és hátrányokat is hozzáad az előző ciklus pontjaihoz, ahol nem szeretnénk nagy tömböket lefoglalni a halomra többször vagy mielőtt szükség lenne rá.

Válasz:

Ez a C# 10 hatókörén kívül esik. Ezt általában akkor tekinthetjük meg, ha az általánosabb params Span<T> funkciót nézzük.

Az API nem kipróbált verziója

Az egyszerűség kedvéért ez a specifikáció jelenleg csak egy Append... metódus felismerését javasolja, és a mindig sikeres dolgok (például InterpolatedStringHandler) mindig igaz eredményt adnának a módszerből. Ez olyan részleges formázási forgatókönyvek támogatásához készült, amelyekben a felhasználó le szeretné állítani a formázást, ha hiba történik, vagy ha ez szükségtelen, például a naplózási eset, de előfordulhat, hogy a szabványos interpolált sztringhasználatban egy csomó szükségtelen ág jelenhet meg. Megfontolhatnánk egy kiegészítést, amelyben csak FormatX metódusokat használunk, ha nincs Append... metódus, de kérdéseket vet fel azzal kapcsolatban, hogy mit csinálunk, ha Append... és FormatX hívásokat is keverünk.

Válasz:

Az API nem kipróbált verzióját szeretnénk használni. A javaslatot ennek megfelelően frissítettük.

Az előző argumentumok átadása a kezelő részére

A javaslatban jelenleg sajnálatosan hiányzik a szimmetria: a csökkentett formátumú bővítménymetódus meghívása eltérő szemantikát eredményez, mint a bővítménymetódus normál formában történő meghívása. Ez eltér a nyelv legtöbb más helyétől, ahol a csökkentett forma csak egy cukor. Javasoljuk, hogy adjon hozzá egy attribútumot a keretrendszerhez, amelyet egy metódus kötésekor ismerünk fel, amely tájékoztatja a fordítót arról, hogy bizonyos paramétereket át kell adni a konstruktornak a kezelőn. A használat a következőképpen néz ki:

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

Ennek a használata a következő:

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

A megválaszolandó kérdések:

  1. Általában tetszik ez a minta?
  2. Akarjuk, hogy ezek az argumentumok a kezelőparaméter után jöjjenek? A BCL néhány meglévő mintája, mint például a Utf8Formatter, az értéket formázza, mielőtt a formázáshoz szükséges elemet elhelyezné. Ahhoz, hogy a legjobban illeszkedjen ezekhez a mintákhoz, valószínűleg engedélyezni szeretnénk ezt, de el kell döntenünk, hogy ez a sorrenden kívüli értékelés rendben van-e.

Válasz:

Ezt támogatni szeretnénk. A specifikáció frissült, hogy tükrözze ezt. Az argumentumokat lexikális sorrendben kell megadni a hívási helyen, és ha az interpolált sztringkonstans után meg van adva a létrehozási módszerhez szükséges argumentum, hiba keletkezik.

await használat interpolációs lyukakban

Mivel a $"{await A()}" ma érvényes kifejezés, racionalizálnunk kell az interpolációs lyukakat az await használatával. Ezt néhány szabálysal megoldhatjuk:

  1. Ha egy interpolált sztringet string-ként, IFormattable-ként vagy FormattableString-ként használnak, és van benne egy await az interpolációs lyukban, akkor térjen át a régi stílusú formázóra.
  2. Ha egy interpolált sztring alá van vetve egy implicit_string_handler_conversion-nek, és a applicable_interpolated_string_handler_type egy ref struct, akkor await nem használható a formázási lyukakban.

Alapvetően, ez a desugaring használhat egy ref szerkezetet egy aszinkron metódusban, amennyiben garantáljuk, hogy a ref struct nem szükséges a halomba menteni, ami lehetséges, ha megtiltjuk a await-eket az interpolációs lyukakban.

Ehelyett egyszerűen az összes kezelőtípust nem ref struktúratípusként definiálhatnánk, beleértve az interpolált sztringekhez tartozó keretrendszerkezelőt is. Ez azonban megakadályozza, hogy egy nap felismerjünk egy Span verziót, amely egyáltalán nem igényel üres területet.

Válasz:

Az interpolált sztringkezelőket ugyanúgy kezeljük, mint bármely más típust: ez azt jelenti, hogy ha a kezelő típusa ref struct, és a jelenlegi környezet nem teszi lehetővé a ref struct-ok használatát, akkor a kezelő típusának használata illegális. A sztringként használt sztringkonstansok csökkentésére vonatkozó specifikáció szándékosan homályos ahhoz, hogy a fordító eldönthesse, milyen szabályokat tart megfelelőnek, de egyéni kezelőtípusok esetén ugyanazokat a szabályokat kell követniük, mint a többi nyelvnek.

Kezelők ref-paraméterekként

Előfordulhat, hogy egyes kezelők ref paraméterként szeretnének átadni (in vagy ref). Megengedjük valamelyiket? És ha igen, hogyan fog kinézni egy ref kezelő? ref $"" zavaró, mert valójában nem a sztring hivatkozását adja át, hanem a hivatkozás által létrehozott kezelőt hivatkozásként adja át, ami hasonló potenciális problémákat okozhat az aszinkron metódusokkal.

Válasz:

Ezt támogatni szeretnénk. A specifikáció frissült, hogy tükrözze ezt. A szabályoknak ugyanazokat a szabályokat kell tükrözniük, amelyek az értéktípusok bővítménymetelyekre vonatkoznak.

Interpolált szövegek bináris kifejezéseken és konverziókon keresztül

Mivel ez a javaslat az interpolált sztringeket kontextusfüggővé teszi, szeretnénk lehetővé tenni a fordító számára, hogy egy teljes mértékben interpolált sztringekből álló bináris kifejezést, vagy egy típuskonverzióval rendelkező interpolált sztringet interpolált sztring literálként kezelje a túlterhelés feloldása érdekében. Vegyük például a következő forgatókönyvet:

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

Ez nem egyértelmű, ezért a feloldáshoz típuskonverziót kell alkalmazni Handler1-ra vagy Handler2-re. Azonban, ha ezt a típuskényszerítést elvégezzük, potenciálisan elveszítjük azt az információt, hogy van kontextus a metódus fogadójából, ami azt jelenti, hogy a típuskényszerítés sikertelen lesz, mert nincs, ami kitöltse a cinformációját. Hasonló probléma merül fel a bináris összefűzés esetén: a felhasználó úgy formázhatná a literált, hogy az több sorban legyen a sortörés elkerülése érdekében, de erre nem lenne képes, mert az már nem lenne interpolált sztring literál, amely átalakítható a kezelő típusára.

Az ilyen esetek megoldásához a következő módosításokat hajtjuk végre:

  • A teljes egészében interpolated_string_expressions álló és csak operátorokat használó + a konverziók és túlterhelések feloldása céljából interpolated_string_literal-nek tekintendő. Az utolsó interpolált karakterlánc úgy jön létre, hogy logikusan összefűzi az egyes interpolated_string_expression összetevőket balról jobbra.
  • Az olyan cast_expression vagy relational_expression operátorral rendelkező as, amelynek operandusa interpolated_string_expressionsinterpolated_string_expressions-nak számítanak az átalakítás és a túlterhelés feloldása céljából.

Nyitott kérdések:

Akarjuk ezt csinálni? Például nem tesszük meg ezt a System.FormattableStringesetében, hanem azt áttördelhetjük egy másik sorra, míg ez a környezetfüggőség miatt nem tördelhető át másik sorra. Nincsenek túlterhelés-feloldási aggályok a FormattableString és IFormattableesetében sem.

Válasz:

Úgy gondoljuk, hogy ez egy érvényes használati eset az additív kifejezésekhez, de az öntött verzió jelenleg nem elég meggyőző. Szükség esetén később is hozzáadhatjuk. A specifikáció frissült, hogy tükrözze ezt a döntést.

Egyéb használati esetek

Lásd a https://github.com/dotnet/runtime/issues/50635-ban található példákat a minta használatával javasolt kezelő API-kra.