Udostępnij za pośrednictwem


Ulepszone ciągi interpolowane

Notatka

Ten artykuł jest specyfikacją funkcji. Specyfikacja służy jako dokument projektowy dla funkcji. Zawiera proponowane zmiany specyfikacji wraz z informacjami wymaganymi podczas projektowania i opracowywania funkcji. Te artykuły są publikowane do momentu sfinalizowania proponowanych zmian specyfikacji i włączenia ich do obecnej specyfikacji ECMA.

Mogą wystąpić pewne rozbieżności między specyfikacją funkcji a ukończoną implementacją. Te różnice są zawarte w odpowiednich notatkach ze spotkania dotyczącego projektu języka (LDM).

Więcej informacji na temat procesu wdrażania specyfikacji funkcji można znaleźć w standardzie języka C# w artykule dotyczącym specyfikacji .

Problem z liderem: https://github.com/dotnet/csharplang/issues/4487

Streszczenie

Wprowadzamy nowy wzorzec tworzenia i używania wyrażeń ciągów interpolowanych w celu umożliwienia wydajnego formatowania i używania zarówno w scenariuszach ogólnych string, jak i bardziej wyspecjalizowanych scenariuszach, takich jak struktury rejestrowania, bez ponoszenia niepotrzebnych alokacji z formatowania ciągu w strukturze.

Motywacja

Obecnie interpolacja ciągów sprowadza się głównie do wywołania string.Format. To, choć ogólnego przeznaczenia, może być nieefektywne z wielu powodów:

  1. Opakowuje wszystkie argumenty struktury, chyba że w środowisku uruchomieniowym doszło do wprowadzenia przeciążenia string.Format, które przyjmuje dokładnie właściwe typy argumentów we właściwej kolejności.
    • Dlatego środowisko uruchomieniowe jest niezdecydowane, aby wprowadzić generyczne wersje metody, ponieważ doprowadziłoby to do kombinatorycznego wybuchu ogólnych instancji bardzo powszechnej metody.
  2. W większości przypadków musi przydzielić tablicę dla argumentów.
  3. Nie ma możliwości uniknięcia utworzenia instancji, jeśli nie jest to konieczne. Na przykład frameworki logowania będą zalecać unikanie interpolacji ciągów, ponieważ spowoduje to utworzenie ciągu, który może nie być potrzebny, w zależności od bieżącego poziomu logowania aplikacji.
  4. Nigdy nie może używać Span lub innych typów struktur ref, ponieważ struktury ref nie są dozwolone jako parametry typu ogólnego, co oznacza, że jeśli użytkownik chce uniknąć kopiowania do lokalizacji pośrednich, muszą ręcznie sformatować ciągi.

Wewnętrznie środowisko uruchomieniowe ma typ o nazwie ValueStringBuilder, aby pomóc w radzeniu sobie z pierwszymi 2 z tych scenariuszy. Przekazują bufor przydzielony na stosie do konstruktora, wielokrotnie wywołują AppendFormat z każdą częścią, a następnie uzyskują końcowy ciąg znaków. Jeśli wynikowy ciąg przekroczy granice buforu stosu, mogą przejść do tablicy na stercie. Jednak ten typ jest niebezpieczny, aby bezpośrednio go ujawnić, ponieważ nieprawidłowe użycie może prowadzić do podwójnego usunięcia wypożyczonej tablicy, co spowoduje wszelkiego rodzaju niezdefiniowane zachowania w programie, ponieważ dwie lokacje pamiętają, że mają wyłączny dostęp do wynajętej tablicy. Ta propozycja wprowadza sposób na bezpieczne użycie tego typu bezpośrednio z natywnego kodu języka C# poprzez zastosowanie literałów ciągów interpolowanych, pozostawiając sam kod bez zmian, jednakże poprawiając każdy ciąg interpolowany tworzony przez użytkownika. Funkcja ta również rozszerza ten wzorzec, aby umożliwić interpolację ciągów przekazywanych jako argumenty do innych metod, użycia wzorca obsługi zdefiniowanego przez odbiorcę metody, który pozwoli na uniknięcie przydzielania niepotrzebnych ciągów przez struktury rejestrowania, zapewniając użytkownikom języka C# znaną, wygodną składnię interpolacji.

Szczegółowy projekt

Wzorzec obsługi

Wprowadzamy nowy wzorzec obsługi, który może reprezentować ciąg interpolowany przekazany jako argument do metody. Prosty angielski wzorca jest następujący:

Gdy interpolated_string_expression jest przekazywany jako argument do metody, przyjrzymy się typowi parametru. Jeśli typ parametru ma konstruktor, który można wywołać z 2 parametrami int, literalLength i formattedCount, opcjonalnie przyjmuje dodatkowe parametry określone przez atrybut w oryginalnym parametrze, opcjonalnie ma końcowy parametr typu boolowskiego, a typ oryginalnego parametru ma metody wystąpienia AppendLiteral i AppendFormatted, które można wywołać dla każdej części ciągu interpolowanego, wówczas przetwarzamy interpolację, używając tego, zamiast tradycyjnego wywołania do string.Format(formatStr, args). Bardziej konkretny przykład pomaga w zobrazowaniu tego.

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

W tym miejscu, ponieważ TraceLoggerParamsInterpolatedStringHandler ma konstruktor z poprawnymi parametrami, mówimy, że ciąg interpolowany ma niejawną konwersję programu obsługi do tego parametru i obniża go do wzorca pokazanego powyżej. Specyfikacje potrzebne do tego są nieco skomplikowane i są rozwinięte poniżej.

Pozostała część niniejszego wniosku będzie używać Append... do odwoływania się do AppendLiteral lub AppendFormatted w przypadkach, gdy mają zastosowanie oba.

Nowe atrybuty

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

Ten atrybut jest używany przez kompilator w celu określenia, czy typ jest prawidłowym typem procedury obsługi ciągów interpolowanych.

Kompilator rozpoznaje również 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; }
    }
}

Ten atrybut jest używany w parametrach, aby poinformować kompilator, jak obniżyć wzorzec obsługi ciągów interpolowanych używany w pozycji parametru.

Konwersja programu obsługi ciągów interpolowanych

Mówi się, że typ T jest applicable_interpolated_string_handler_type, jeśli jest opatrzony atrybutem System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Istnieje niejawna interpolated_string_handler_conversion do T z interpolated_string_expressionlub additive_expression składa się wyłącznie z _interpolated_string_expression_s i używania tylko operatorów +.

Dla uproszczenia w pozostałej części tej specyfikacji interpolated_string_expression odnosi się zarówno do prostego interpolated_string_expression, jak i do additive_expression, który składa się wyłącznie z _interpolated_string_expression_s oraz wyłącznie operatorów +.

Należy pamiętać, że ta konwersja zawsze istnieje, niezależnie od tego, czy podczas próby obniżenia interpolacji przy użyciu wzorca procedury obsługi wystąpią błędy późniejsze. Ma to na celu zapewnienie, że występują przewidywalne i przydatne błędy, a zachowanie środowiska uruchomieniowego nie zmienia się na podstawie zawartości ciągu interpolowanego.

Odpowiednie korekty składowych funkcji

Dostosowujemy sformułowanie algorytmu odpowiedniego członka funkcji (§12.6.4.2) w następujący sposób (do każdej sekcji jest dodawany nowy, pogrubiony podpunkt):

Mówi się, że element funkcji jest odpowiednim elementem funkcji w odniesieniu do listy argumentów A, gdy spełnione są wszystkie następujące warunki:

  • Każdy argument w A odpowiada parametrowi w deklaracji składowej funkcji zgodnie z opisem w artykule Odpowiednie parametry (§12.6.2.2), a każdy parametr, do którego żaden argument nie odpowiada, jest opcjonalnym parametrem.
  • Dla każdego argumentu w A, tryb przekazywania argumentu (tj. wartość, reflub out) jest identyczny z trybem przekazywania odpowiedniego parametru i
    • dla parametru wartości lub tablicy parametrów niejawna konwersja (§10.2) istnieje z argumentu na typ odpowiedniego parametru lub
    • dla parametru ref, którego typem jest struktura, istnieje niejawna konwersja interpolowanego ciągu znaków z argumentu do typu odpowiadającego parametru lub
    • dla parametru ref lub out typ argumentu jest identyczny z typem odpowiedniego parametru. W końcu parametr ref lub out jest aliasem przekazanego argumentu.

W przypadku elementu członkowskiego funkcji, który zawiera tablicę parametrów, jeśli zgodnie z powyższymi regułami jest on stosowalny, mówi się, że jest stosowany w swojej postaci normalnej. Jeśli element członkowski funkcji zawierający tablicę parametrów nie ma zastosowania w swojej normalnej postaci, wówczas może być zastosowany w swojej rozszerzonej formie :

  • Formularz rozwinięty jest konstruowany przez zastąpienie tablicy parametrów w deklaracji składowej funkcji z zerową lub większą liczbą parametrów wartości typu elementów tablicy parametrów, tak aby liczba argumentów na liście argumentów A odpowiadała łącznej liczbie parametrów. Jeśli A ma mniej argumentów niż liczba stałych parametrów w deklaracji składowej funkcji, rozszerzona forma składowej funkcji nie może być skonstruowana i w związku z tym nie ma zastosowania.
  • W przeciwnym razie rozszerzony formularz ma zastosowanie, jeśli dla każdego argumentu w A tryb przekazywania parametru argumentu jest identyczny z trybem przekazywania parametrów odpowiedniego parametru i
    • dla parametru wartości stałej lub parametru wartości utworzonego przez rozszerzenie, niejawna konwersja (§10.2) istnieje z typu argumentu do typu odpowiedniego parametru lub
    • dla parametru ref, którego typem jest typ struktury, istnieje niejawny interpolated_string_handler_conversion z argumentu do typu odpowiedniego parametru lub
    • dla parametru ref lub out typ argumentu jest identyczny z typem odpowiedniego parametru.

Ważna uwaga: oznacza to, że jeśli istnieją 2 w przeciwnym razie równoważne przeciążenia, które różnią się tylko typem applicable_interpolated_string_handler_type, te przeciążenia zostaną uznane za niejednoznaczne. Ponadto, ponieważ nie rozpoznajemy jawnych rzutów, możliwe jest wystąpienie nierozwiązywalnego scenariusza, w którym oba odpowiednie przeciążenia używają InterpolatedStringHandlerArguments i są całkowicie niemogące być wywołane bez ręcznego stosowania wzorca obniżania poziomu obsługi. Możemy potencjalnie wprowadzić zmiany w lepszym algorytmie składowym funkcji, aby rozwiązać ten problem, jeśli tak wybierzemy, ale ten scenariusz jest mało prawdopodobny i nie jest priorytetem dla rozwiązania problemu.

Lepsza konwersja wynikająca z dostosowania wyrażeń

Zmieniamy lepszą konwersję z wyrażenia (§12.6.4.5) na następującą sekcję:

Biorąc pod uwagę niejawną konwersję C1, która przekształca wyrażenie E na typ T1, oraz niejawną konwersję C2, która przekształca wyrażenie E na typ T2, C1 jest lepszą konwersją niż C2, jeśli:

  1. E jest niestała interpolated_string_expression, C1 jest implicit_string_handler_conversion, T1 jest applicable_interpolated_string_handler_type, a C2 nie jest implicit_string_handler_conversion.
  2. E nie jest dokładnie zgodna z T2 i co najmniej jeden z następujących warunków jest spełniony:

Oznacza to, że istnieją potencjalnie nieoczywiste reguły rozpoznawania przeciążenia, w zależności od tego, czy ciąg interpolowany jest wyrażeniem stałym, czy nie. Na przykład:

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

Jest to wprowadzane w taki sposób, aby elementy, które mogą być emitowane jako stałe, faktycznie takimi były, i nie powodowały żadnych narzutów, podczas gdy te, które nie mogą być stałymi, używają wzorca obsługi.

InterpolatedStringHandler i jego zastosowanie

Wprowadzamy nowy typ w System.Runtime.CompilerServices: DefaultInterpolatedStringHandler. Jest to struktura ref z wieloma tymi samymi semantykami co ValueStringBuilder, przeznaczona do bezpośredniego użycia przez kompilator języka C#. Ta struktura będzie wyglądać mniej więcej tak:

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

Wprowadzamy niewielkie zmiany w przepisach dotyczących znaczenia interpolated_string_expression (§12.8.3):

Jeśli typ ciągu interpolowanego jest string, a typ System.Runtime.CompilerServices.DefaultInterpolatedStringHandler istnieje, a bieżący kontekst obsługuje używanie tego typu, ciągjest obniżany przy użyciu wzorca procedury obsługi. Końcowa wartość string jest następnie uzyskiwana przez wywołanie ToStringAndClear() dla typu procedury obsługi.w przeciwnym razie, jeśli typ ciągu interpolowanego jest System.IFormattable lub System.FormattableString [reszta pozostaje niezmieniona]

Reguła "i bieżący kontekst obsługuje używanie tego typu" jest celowo niejasna, aby dać kompilatorowi swobodę optymalizacji użycia tego wzorca. Typ procedury obsługi prawdopodobnie będzie typem struktury ref, a typy struktury ref są zwykle niedozwolone w metodach asynchronicznych. W tym konkretnym przypadku kompilator może używać programu obsługi, jeśli żaden z otworów interpolacji nie zawiera wyrażenia await, ponieważ statycznie ustalimy, że typ procedury obsługi jest bezpiecznie używany bez dodatkowej skomplikowanej analizy, ponieważ program obsługi zostanie usunięty po obliczeniu wyrażenia ciągu interpolowanego.

otwórz pytanie:

Czy zamiast tego chcemy po prostu poinformować kompilator o DefaultInterpolatedStringHandler i całkowicie pominąć wywołanie string.Format? Pozwoliłoby nam to ukryć metodę, której niekoniecznie chcemy, aby była zbyt widoczna, gdy użytkownicy ręcznie wywołują string.Format.

Odpowiedź: Tak.

otwórz pytanie:

Czy chcemy również mieć handlery dla System.IFormattable i System.FormattableString?

Odpowiedź: Nie.

Generowanie kodu wzorca obsługi

W tej sekcji rozwiązanie wywołania metody odnosi się do kroków wymienionych w §12.8.10.2.

Rozwiązywanie konstruktora

Biorąc pod uwagę applicable_interpolated_string_handler_typeT i interpolated_string_expressioni, rozpoznawanie wywołania metody oraz walidacja prawidłowego konstruktora są wykonywane dla T w następujący sposób:

  1. Wyszukiwanie składowe dla konstruktorów wystąpień jest wykonywane na T. Wynikowa grupa metod jest nazywana M.
  2. Lista argumentów A jest konstruowana w następujący sposób:
    1. Pierwsze dwa argumenty to stałe całkowite reprezentujące długość literału ioraz liczbę składników interpolacji odpowiednio w i.
    2. Jeśli i jest używany jako argument do niektórych parametrów pi w metodzie M1, a parametr pi jest przypisywany System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, dla każdej nazwy Argx w tablicy Arguments tego atrybutu kompilator dopasuje go do parametru px o tej samej nazwie. Pusty ciąg jest dopasowywany do odbiornika M1.
      • Jeśli którykolwiek z Argx nie może być dopasowany do parametru M1, lub jeśli Argx żąda odbiornika dla M1, a M1 jest metodą statyczną, zostanie wygenerowany błąd i nie zostaną podjęte żadne dalsze kroki.
      • W przeciwnym razie typ każdego rozpoznanego px jest dodawany do listy argumentów zgodnie z kolejnością określoną przez tablicę Arguments. Każdy px jest przekazywany z tą samą semantyką ref, jak określono w M1.
    3. Ostatnim argumentem jest bool, przekazywany jako parametr out.
  3. Tradycyjne rozpoznawanie wywołań metody jest wykonywane z grupą metod M i listą argumentów A. Na potrzeby ostatecznej weryfikacji wywołania metody kontekst M jest traktowany jako member_access za pośrednictwem typu T.
    • Jeśli znaleziono jeden najlepszy konstruktor F, wynik rozpoznawania przeciążenia jest F.
    • Jeśli nie znaleziono odpowiednich konstruktorów, krok 3 zostanie ponowiony, usunięcie ostatniego parametru bool z A. Jeśli ta ponowna próba również nie znajdzie żadnych odpowiednich członków, zostanie wygenerowany błąd i nie zostaną podjęte żadne dalsze kroki.
    • Jeśli nie znaleziono żadnej najlepszej metody, wynik rozpoznawania przeciążenia jest niejednoznaczny, generowany jest błąd i nie są podejmowane żadne dalsze kroki.
  4. Wykonywana jest ostateczna walidacja F.
    • Jeśli jakikolwiek element A wystąpił leksykalnie po i, zostanie wygenerowany błąd i nie zostaną podjęte żadne dalsze kroki.
    • Jeśli jakikolwiek A żąda odbiorcy F, a F jest indeksatorem używanym jako initializer_target w member_initializer, zgłaszany jest błąd i nie są wykonywane żadne dalsze kroki.

Uwaga: rozwiązanie w tym miejscu celowo nie używać rzeczywistych wyrażeń przekazywanych jako inne argumenty dla elementów Argx. Uwzględniamy tylko typy po konwersji. Dzięki temu nie występują problemy z podwójną konwersją ani nieoczekiwane przypadki, w których lambda jest powiązana z jednym typem delegata po przekazaniu do M1, a z innym typem delegata po przekazaniu do M.

Uwaga: zgłaszamy błąd dla indeksatorów używanych jako inicjatory składowych ze względu na kolejność oceny zagnieżdżonych inicjatorów składowych. Rozważmy ten fragment kodu:


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[] są przetwarzane zanim zostaną przekazane do odbiornika indeksatora. Chociaż możemy wymyślić obniżenie, które działa w tym scenariuszu (przez utworzenie tempa dla __c1.C2 i udostępnienie go w obu wywołaniach indeksatora, lub tylko użycie go dla pierwszego wywołania indeksatora i udostępnienie argumentu w obu wywołaniach), uważamy, że każde obniżenie byłoby mylące dla tego, co uważamy za scenariusz patologiczny. Dlatego całkowicie zabraniamy tego scenariusza.

otwarte pytanie:

Jeśli używamy konstruktora zamiast Create, poprawimy generowanie kodu środowiska uruchomieniowego, kosztem zawężenia wzorca nieco.

Answer: na razie ograniczymy do konstruktorów. Jeśli pojawi się taki scenariusz, możemy później ponownie rozważyć dodanie ogólnej metody Create.

rozpoznawanie przeciążenia metody Append...

Z uwzględnieniem applicable_interpolated_string_handler_typeT oraz interpolated_string_expressioni, rozstrzyganie przeciążeń dla zestawu prawidłowych metod Append... na T odbywa się w następujący sposób:

  1. Czy w isą jakieś składniki interpolated_regular_string_character:
    1. Wyszukiwanie członka o nazwie AppendLiteral na T jest wykonywane. Wynikowa grupa metod jest nazywana Ml.
    2. Lista argumentów Al jest tworzona z jednym parametrem wartości typu string.
    3. Tradycyjne rozpoznawanie wywołań metody jest wykonywane z grupą metod Ml i listą argumentów Al. Na potrzeby ostatecznej weryfikacji wywołania metody kontekst Ml jest traktowany jako member_access za pośrednictwem wystąpienia T.
      • Jeśli znaleziono jedną najlepszą metodę Fi i nie zostały wygenerowane żadne błędy, wynikiem rozwiązania wywołania metody jest Fi.
      • W przeciwnym razie zgłaszany jest błąd.
  2. Dla każdego składnika ix interpolacjii:
    1. Wyszukiwanie elementów z nazwą AppendFormatted na T jest przeprowadzane. Wynikowa grupa metod jest nazywana Mf.
    2. Lista argumentów Af jest konstruowana:
      1. Pierwszy parametr to expression z ix, przekazywany przez wartość.
      2. Jeśli ix bezpośrednio zawiera składnik constant_expression, zostanie dodany parametr wartości całkowitej z określoną nazwą alignment.
      3. Jeśli bezpośrednio po ix następuje interpolation_format, to zostanie dodany parametr wartości ciągu z określoną nazwą format.
    3. Tradycyjne rozpoznawanie wywołań metody jest wykonywane z grupą metod Mf i listą argumentów Af. Na potrzeby ostatecznej weryfikacji wywołania metody kontekst Mf jest traktowany jako member_access za pośrednictwem wystąpienia T.
      • Jeśli zostanie znaleziona jedna najlepsza metoda Fi, wynikiem rozwiązania wywołania metody jest Fi.
      • W przeciwnym razie zgłaszany jest błąd.
  3. Na koniec dla każdego Fi wykrytego w krokach 1 i 2 wykonywana jest ostateczna walidacja:
    • Jeśli jakikolwiek Fi nie zwraca bool według wartości lub void, zgłaszany jest błąd.
    • Jeśli wszystkie Fi nie zwracają tego samego typu, zgłaszany jest błąd.

Zauważ, że te reguły nie zezwalają na metody rozszerzające dla wywołań Append.... Możemy rozważyć włączenie tego, jeśli wybierzemy, ale jest to analogiczne do wzorca modułu wyliczającego, w którym zezwalamy GetEnumerator na metodę rozszerzenia, ale nie Current lub MoveNext().

Te reguły zezwalają na domyślne parametry wywołań Append..., które będą działać z takimi elementami jak CallerLineNumber lub CallerArgumentExpression (jeśli są obsługiwane przez język).

Mamy oddzielne reguły wyszukiwania przeciążenia dla elementów bazowych a otworów interpolacji, ponieważ niektóre programy obsługi będą mogły zrozumieć różnicę między składnikami, które zostały interpolowane, a składnikami, które były częścią ciągu podstawowego.

Otwórz Pytanie

Niektóre scenariusze, takie jak rejestrowanie strukturalne, chcą mieć możliwość podania nazw elementów interpolacji. Na przykład dzisiaj wywołanie rejestrowania może wyglądać następująco: Log("{name} bought {itemCount} items", name, items.Count);. Nazwy wewnątrz {} zawierają ważne informacje o strukturze rejestratorów, które pomagają zapewnić, że dane wyjściowe są spójne i jednolite. Niektóre sytuacje mogą być w stanie ponownie użyć składnika interpolacji :format do tego celu, ale wiele rejestratorów już rozumie specyfikatory formatu i ma istniejące zachowania dla formatowania wyników na podstawie tych informacji. Czy istnieje jakaś składnia, która umożliwia umieszczanie tych nazwanych specyfikatorów?

Niektóre przypadki mogą użyć CallerArgumentExpression, pod warunkiem, że wsparcie zostanie dodane w C# 10. Jednak w przypadku przypadków, w których wywoływana jest metoda/właściwość, może to nie być wystarczające.

odpowiedź:

Chociaż istnieje kilka interesujących aspektów szablonowych ciągów, które możemy zbadać w ortogonalnym rozszerzeniu języka, nie uważamy, że określona składnia przynosi tutaj wiele korzyści w porównaniu do rozwiązań takich jak użycie krotki: $"{("StructuredCategory", myExpression)}".

Wykonywanie konwersji

Biorąc pod uwagę applicable_interpolated_string_handler_typeT i interpolated_string_expressioni, które mają prawidłowy konstruktor Fc oraz rozwiązane metody Append...Fa, obniżanie dla i jest wykonywane w następujący sposób:

  1. Wszelkie argumenty Fc, które występują leksykalnie przed i są oceniane i przechowywane w zmiennych tymczasowych w kolejności leksykalnej. Aby zachować kolejność leksykalną, jeśli i wystąpiła w ramach większego wyrażenia e, wszystkie składniki e, które wystąpiły przed i, zostaną również ocenione ponownie w kolejności leksykalnej.
  2. Fc jest wywoływana z długością składników literału ciągu interpolowanego, liczba interpolacji otworów, wszystkie wcześniej ocenione argumenty i argument bool out (jeśli Fc został rozwiązany z jednym jako ostatni parametr). Wynik jest przechowywany w wartości tymczasowej ib.
    1. Długość składników literału jest obliczana po zastąpieniu dowolnego open_brace_escape_sequence pojedynczym {i dowolnym close_brace_escape_sequence pojedynczym }.
  3. Jeśli Fc kończy się argumentem wyjściowym bool, zostanie wygenerowana kontrola wartości bool. Jeśli to prawda, metody w Fa będą wywoływane. W przeciwnym razie nie będą one wywoływane.
  4. W przypadku każdej Fax w Fa, Fax jest wywoływana na ib z bieżącym składnikiem literału lub odpowiednim wyrażeniem interpolacji . Jeśli Fax zwraca bool, wynik jest logicznie i ze wszystkimi poprzednimi wywołaniami Fax.
    1. Jeśli Fax jest wywołaniem AppendLiteral, składnik literału jest odbezpieczany przez zastąpienie dowolnego open_brace_escape_sequence jednym {i dowolnego close_brace_escape_sequence jednym }.
  5. Wynikiem konwersji jest ib.

Ponownie należy pamiętać, że argumenty przekazane do Fc i argumenty przekazane do e są takie same w temp. Konwersje mogą wystąpić w górnej części temp, aby przekonwertować na formularz, który Fc wymaga, ale na przykład lambdy nie mogą być powiązane z innym typem delegata między Fc a e.

otwórz pytanie

Obniżenie oznacza, że kolejne części ciągu interpolowanego po wywołaniu Append..., które zwraca false, nie są przetwarzane. Może to być bardzo mylące, szczególnie jeśli dziura w formacie ma skutki uboczne. Zamiast tego możemy najpierw ocenić wszystkie otwory formatu, a następnie wielokrotnie wywoływać Append... z wynikami, zatrzymując się, jeśli zwraca wartość false. Zapewniłoby to, że wszystkie wyrażenia zostaną ocenione zgodnie z oczekiwaniami, ale wywołujemy jak najmniej metod, ile trzeba. Chociaż ocena częściowa może być pożądana w przypadku niektórych bardziej zaawansowanych przypadków, być może nie jest intuicyjna dla ogólnego przypadku.

Inną alternatywą, jeśli chcemy zawsze oceniać wszystkie luki formatowania, jest usunięcie wersji Append... interfejsu API i po prostu powtarzać wywołania Format. Program obsługi może śledzić, czy powinien po prostu ignorować argument i natychmiast wracać do tej wersji.

Answer: będziemy mieli warunkową ocenę otworów.

Otwórz pytanie

Czy musimy usunąć jednorazowe typy procedur obsługi i opakowować wywołania try/finally, aby upewnić się, że funkcja Dispose jest wywoływana? Na przykład program obsługi ciągów interpolowanych w bcl może mieć w nim wynajętą tablicę, a jeśli jeden z otworów interpolacji zgłasza wyjątek podczas oceny, to wynajęty tablica może zostać ujawniona, jeśli nie została usunięta.

Answer: Nie. Procedury obsługi można przypisać do zmiennych lokalnych (takich jak MyHandler handler = $"{MyCode()};), a okres istnienia takich procedur jest niejasny. W przeciwieństwie do enumeratorów foreach, gdzie czas życia jest oczywisty i żadna lokalna zmienna zdefiniowana przez użytkownika nie jest tworzona dla enumeratora.

Wpływ na dopuszczalne do wartości null typy referencyjne

Aby zminimalizować złożoność implementacji, mamy kilka ograniczeń dotyczących wykonywania analizy dotyczącej wartości null w konstruktorach obsługi ciągów interpolowanych używanych jako argumenty metody lub indeksatora. W szczególności nie przekazujemy informacji z konstruktora z powrotem do oryginalnych miejsc docelowych parametrów lub argumentów z pierwotnego kontekstu i nie używamy typów parametrów konstruktora na potrzeby dedukcji typów generycznych dla parametrów typu w zawierającej metodzie. Przykładem tego, gdzie może to mieć wpływ, jest:

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

Inne zagadnienia

Pozwól na konwersję typów string na uchwyty również.

Dla uproszczenia autora typu, moglibyśmy rozważyć umożliwienie, aby wyrażenia typu string były niejawnie konwertowalne na applicable_interpolated_string_handler_types. Zgodnie z dzisiejszą propozycją autorzy prawdopodobnie będą musieli przeciążać zarówno ten typ obsługi, jak i zwykłe typy string, aby użytkownicy nie musieli rozumieć różnicy. Może to być irytujące i nieoczywiste obciążenie, ponieważ wyrażenie string można traktować jako interpolację z expression.Length wstępnie ustaloną długością i bez otworów do wypełnienia.

Dzięki temu nowe interfejsy API mogą uwidaczniać tylko obsługę, bez konieczności uwidaczniania przeciążenia akceptującego string. Jednak nie obejdzie potrzeby zmian, aby lepiej konwertować wyrażenia, więc chociaż może działać, może to być niepotrzebne przeciążenie.

odpowiedź:

Uważamy, że może to być mylące i istnieje łatwe obejście dla niestandardowych typów obsługi: dodanie konwersji zdefiniowanej przez użytkownika na podstawie ciągu znaków.

Dołączanie zakresów dla ciągów bez stertowania

ValueStringBuilder, jak już istnieje, ma 2 konstruktory: jeden, który przyjmuje liczbę, i przydziela stertę chętnie, a jeden, który przyjmuje Span<char>. Span<char> zwykle ma stały rozmiar w kodzie bazowym środowiska uruchomieniowego, średnio około 250 elementów. Aby naprawdę zastąpić ten typ, powinniśmy rozważyć rozszerzenie, w którym także rozpoznajemy metody GetInterpolatedString przyjmujące Span<char>, a nie tylko wersję zliczającą. Jednak widzimy kilka potencjalnych trudnych przypadków do rozwiązania:

  • Nie chcemy wielokrotnie używać stackalloc w intensywnej pętli. Gdybyśmy wykonali to rozszerzenie funkcji, prawdopodobnie chcielibyśmy udostępniać przestrzeń stackalloc między iteracjami pętli. Wiemy, że jest to bezpieczne, ponieważ Span<T> jest strukturą ref, która nie może być przechowywana na stosie, a użytkownicy musieliby być dość przebiegli, aby udało im się wyodrębnić odwołanie do tego Span (na przykład tworząc metodę, która akceptuje taki uchwyt, a następnie celowo pobiera Span z uchwytu i zwraca go do obiektu wywołującego). Jednak przydzielanie z wyprzedzeniem powoduje inne pytania:
    • Czy powinniśmy często używać stackalloc? Co zrobić, jeśli pętla nigdy nie zostanie wprowadzona lub zostanie wyjść, zanim będzie potrzebowała miejsca?
    • Jeśli nie używamy stackalloc, czy oznacza to, że wprowadzamy ukrytą gałąź w każdej pętli? Większość pętli prawdopodobnie nie będzie się tym martwić, ale może to mieć wpływ na niektóre ciasne pętle, które chcą uniknąć kosztów.
  • Niektóre ciągi mogą być dość duże, a odpowiednia ilość stackalloc zależy od wielu czynników, w tym czynników środowiska uruchomieniowego. Nie chcemy, aby kompilator języka C# i specyfikacja musiały ustalić to wcześniej, dlatego chcemy rozwiązać problem https://github.com/dotnet/runtime/issues/25423 i dodać interfejs API kompilatora do wywołania w tych przypadkach. Dodaje również więcej zalet i wad do omawianych punktów z poprzedniej pętli, gdzie nie chcemy potencjalnie przydzielać dużych tablic na stercie wiele razy lub zanim będą one potrzebne.

Odpowiedź:

Jest to poza zakresem języka C# 10. Możemy spojrzeć na to ogólnie, gdy przyjrzymy się bardziej ogólnej funkcji params Span<T>.

Wersja API, która nie jest wersją próbną

Dla uproszczenia, specyfikacja ta obecnie proponuje rozpoznawanie jedynie metody Append..., a rzeczy, które zawsze kończą się powodzeniem (jak na przykład InterpolatedStringHandler), zawsze zwracałyby wartość true z tej metody. Zostało to zrobione w celu obsługi scenariuszy częściowego formatowania, w których użytkownik chce zatrzymać formatowanie, jeśli wystąpi błąd lub okaże się niepotrzebne, tak jak w przypadku rejestrowania, ale może potencjalnie wprowadzić wiele niepotrzebnych gałęzi w standardowym użyciu ciągów interpolowanych. Moglibyśmy rozważyć dodanie, w którym używamy tylko metod FormatX, jeśli nie ma metody Append..., ale zawiera pytania dotyczące tego, co robimy, jeśli istnieje kombinacja wywołań zarówno Append..., jak i FormatX.

odpowiedź:

Chcemy, aby wersja interfejsu API nie wypróbowana. Wniosek został zaktualizowany w celu odzwierciedlenia tego.

Przekazywanie poprzednich argumentów do obsługi

Obecnie istnieje niefortunny brak symetrii w propozycji: wywoływanie metody rozszerzenia w postaci zredukowanej powoduje różne semantyki niż wywoływanie metody rozszerzenia w postaci normalnej. Różni się to od większości innych lokalizacji w języku, gdzie zredukowana forma jest po prostu cukrem. Zalecamy dodanie atrybutu do frameworku, który rozpoznamy podczas wiązania metody, który informuje kompilator, że niektóre parametry powinny zostać przekazane do konstruktora na handlerze. Użycie wygląda następująco:

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

Użycie tego elementu jest następujące:

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

Pytania, na które musimy odpowiedzieć:

  1. Czy ogólnie lubimy ten wzór?
  2. Czy chcemy zezwolić na używanie tych argumentów po parametrze programu obsługi? Niektóre istniejące wzorce w BCL, takie jak Utf8Formatter, umieszczają wartość do sformatowania przed obiektem, do którego należy formatować. Aby najlepiej dopasować się do tych wzorców, prawdopodobnie chcemy na to zezwolić, ale musimy zdecydować, czy ocenianie w nieprawidłowej kolejności jest właściwe.

Odpowiedź:

Chcemy to wspierać. Specyfikacja została zaktualizowana, aby to odzwierciedlić. Argumenty będą musiały być określone w kolejności leksykalnej w miejscu wywołania, a jeśli wymagany argument metody create zostanie określony po literałach ciągu znaków interpolowanego, zostanie wygenerowany błąd.

await użycie w otworach interpolacji

Ponieważ $"{await A()}" jest dzisiaj prawidłowym wyrażeniem, musimy uporządkować miejsca interpolacji za pomocą 'await'. Możemy rozwiązać ten problem przy użyciu kilku reguł:

  1. Jeśli ciąg interpolowany używany jako string, IFormattablelub FormattableString ma await w otworze interpolacji, użyj formatowania w stylu starego typu.
  2. Jeśli ciąg interpolowany podlega implicit_string_handler_conversion, a applicable_interpolated_string_handler_type jest ref struct, await nie może być używany w otworach formatu.

Zasadniczo, proces rozwijania kodu może korzystać ze struktury ref w metodzie asynchronicznej tak długo, jak gwarantujemy, że ref struct nie będzie musiał być zapisany na stercie, co jest możliwe, jeśli zabronimy awaitw otworach interpolacji.

Alternatywnie możemy po prostu utworzyć wszystkie typy obsługi jako struktury niebędące typami ref, w tym obsługi frameworku dla ciągów interpolowanych. Uniemożliwiłoby to nam jednak pewnego dnia rozpoznanie wersji Span, której w ogóle nie potrzeba przydzielać żadnego miejsca na pliki tymczasowe.

Odpowiedź:

Traktujemy procedury obsługi ciągów interpolowanych tak samo jak w przypadku dowolnego innego typu: oznacza to, że jeśli typ procedury obsługi jest strukturą ref, a bieżący kontekst nie zezwala na użycie struktur ref, jest to niedozwolone do użycia procedury obsługi w tym miejscu. Specyfikacje dotyczące obniżania literałów ciągów używanych jako ciągi są celowo niejasne, aby umożliwić kompilatorowi podjęcie decyzji o tym, jakie reguły uzna za odpowiednie, ale w przypadku typów niestandardowych procedur obsługi będą musieli postępować zgodnie z tymi samymi regułami co reszta języka.

Programy obsługi jako parametry ref

Niektóre procedury obsługi mogą być przekazywane jako parametry ref (in lub ref). Czy powinniśmy zezwolić na to? A jeśli tak, jak będzie wyglądać program obsługi ref? ref $"" jest mylące, ponieważ w rzeczywistości nie przekazujesz ciągu przez odwołanie, przekazujesz uchwyt utworzony z odwołania przez ref, co może prowadzić do podobnych potencjalnych problemów z metodą asynchroniczną.

Odpowiedź:

Chcemy to wspierać. Specyfikacja została zaktualizowana, aby to odzwierciedlić. Reguły powinny odzwierciedlać te same reguły, które mają zastosowanie do metod rozszerzeń dla typów wartości.

Ciągi interpolowane za pomocą wyrażeń binarnych i konwersji

Ponieważ ta propozycja sprawia, że ciągi interpolowane stają się wrażliwe na kontekst, chcemy umożliwić kompilatorowi traktowanie wyrażenia binarnego w całości składającego się z ciągów interpolowanych lub ciągów interpolowanych poddanych rzutowaniu jako literał ciągu interpolowanego na potrzeby rozpoznawania przeciążenia. Na przykład weź pod uwagę następujący scenariusz:

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

Byłoby to niejednoznaczne, co wymagałoby rzutowania do Handler1 lub Handler2 w celu rozwiązania problemu. Jednak w tworzeniu tego rzutowania potencjalnie pozbylibyśmy się informacji, że istnieje kontekst z kontekstu odbiornika metody, co oznacza, że rzutowanie nie powiedzie się, ponieważ nie ma żadnych danych do wypełnienia informacji c. Podobny problem pojawia się przy binarnym łączeniu ciągów: użytkownik może chcieć sformatować literał w kilku wierszach, aby uniknąć zawijania wierszy, ale nie byłby w stanie, ponieważ nie byłby to już interpolowany literał ciągu znaków konwertowalny na typ obsługi.

Aby rozwiązać te problemy, wprowadzamy następujące zmiany:

  • additive_expression składający się wyłącznie z interpolated_string_expressions i używający wyłącznie operatorów + jest uważany za interpolated_string_literal na potrzeby konwersji i rozpoznawania przeciążeń. Końcowy ciąg interpolowany jest tworzony przez logiczne łączenie wszystkich poszczególnych składników interpolated_string_expression od lewej do prawej.
  • cast_expression lub relational_expression z operatorem as, którego operandem jest interpolated_string_expressions, jest uznawany za interpolated_string_expressions na potrzeby konwersji i rozpoznawania przeciążeń.

otwarte pytania:

Czy chcemy to zrobić? Nie robimy tego dla System.FormattableString, na przykład, ale można to podzielić na inny wiersz, podczas gdy może to być zależne od kontekstu i dlatego nie można go podzielić na inną linię. Nie ma również problemów z rozwiązaniami przeciążeń w przypadku FormattableString i IFormattable.

Odpowiedź:

Uważamy, że jest to prawidłowy przypadek użycia wyrażeń addytywnych, ale wersja z rzutem nie jest wystarczająco przekonująca w tej chwili. W razie potrzeby możemy dodać go później. Specyfikacja została zaktualizowana w celu odzwierciedlenia tej decyzji.

Inne przypadki użycia

Zobacz w https://github.com/dotnet/runtime/issues/50635 przykłady proponowanych interfejsów API obsługi korzystających z tego wzorca.