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:
- 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.
- W większości przypadków musi przydzielić tablicę dla argumentów.
- 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.
- 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ść,ref
lubout
) 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
lubout
typ argumentu jest identyczny z typem odpowiedniego parametru. W końcu parametrref
lubout
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śliA
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
lubout
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:
-
E
jest niestała interpolated_string_expression,C1
jest implicit_string_handler_conversion,T1
jest applicable_interpolated_string_handler_type, aC2
nie jest implicit_string_handler_conversion. -
E
nie jest dokładnie zgodna zT2
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:
- Wyszukiwanie składowe dla konstruktorów wystąpień jest wykonywane na
T
. Wynikowa grupa metod jest nazywanaM
. - Lista argumentów
A
jest konstruowana w następujący sposób:- Pierwsze dwa argumenty to stałe całkowite reprezentujące długość literału
i
oraz liczbę składników interpolacji odpowiednio wi
. - Jeśli
i
jest używany jako argument do niektórych parametrówpi
w metodzieM1
, a parametrpi
jest przypisywanySystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute
, dla każdej nazwyArgx
w tablicyArguments
tego atrybutu kompilator dopasuje go do parametrupx
o tej samej nazwie. Pusty ciąg jest dopasowywany do odbiornikaM1
.- Jeśli którykolwiek z
Argx
nie może być dopasowany do parametruM1
, lub jeśliArgx
żąda odbiornika dlaM1
, aM1
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żdypx
jest przekazywany z tą samą semantykąref
, jak określono wM1
.
- Jeśli którykolwiek z
- Ostatnim argumentem jest
bool
, przekazywany jako parametrout
.
- Pierwsze dwa argumenty to stałe całkowite reprezentujące długość literału
- Tradycyjne rozpoznawanie wywołań metody jest wykonywane z grupą metod
M
i listą argumentówA
. Na potrzeby ostatecznej weryfikacji wywołania metody kontekstM
jest traktowany jako member_access za pośrednictwem typuT
.- Jeśli znaleziono jeden najlepszy konstruktor
F
, wynik rozpoznawania przeciążenia jestF
. - Jeśli nie znaleziono odpowiednich konstruktorów, krok 3 zostanie ponowiony, usunięcie ostatniego parametru
bool
zA
. 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.
- Jeśli znaleziono jeden najlepszy konstruktor
- Wykonywana jest ostateczna walidacja
F
.- Jeśli jakikolwiek element
A
wystąpił leksykalnie poi
, zostanie wygenerowany błąd i nie zostaną podjęte żadne dalsze kroki. - Jeśli jakikolwiek
A
żąda odbiorcyF
, aF
jest indeksatorem używanym jako initializer_target w member_initializer, zgłaszany jest błąd i nie są wykonywane żadne dalsze kroki.
- Jeśli jakikolwiek element
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:
- Czy w
i
są jakieś składniki interpolated_regular_string_character:- Wyszukiwanie członka o nazwie
AppendLiteral
naT
jest wykonywane. Wynikowa grupa metod jest nazywanaMl
. - Lista argumentów
Al
jest tworzona z jednym parametrem wartości typustring
. - Tradycyjne rozpoznawanie wywołań metody jest wykonywane z grupą metod
Ml
i listą argumentówAl
. Na potrzeby ostatecznej weryfikacji wywołania metody kontekstMl
jest traktowany jako member_access za pośrednictwem wystąpieniaT
.- Jeśli znaleziono jedną najlepszą metodę
Fi
i nie zostały wygenerowane żadne błędy, wynikiem rozwiązania wywołania metody jestFi
. - W przeciwnym razie zgłaszany jest błąd.
- Jeśli znaleziono jedną najlepszą metodę
- Wyszukiwanie członka o nazwie
- Dla każdego składnika
ix
interpolacjii
:- Wyszukiwanie elementów z nazwą
AppendFormatted
naT
jest przeprowadzane. Wynikowa grupa metod jest nazywanaMf
. - Lista argumentów
Af
jest konstruowana:- Pierwszy parametr to
expression
zix
, przekazywany przez wartość. - Jeśli
ix
bezpośrednio zawiera składnik constant_expression, zostanie dodany parametr wartości całkowitej z określoną nazwąalignment
. - Jeśli bezpośrednio po
ix
następuje interpolation_format, to zostanie dodany parametr wartości ciągu z określoną nazwąformat
.
- Pierwszy parametr to
- Tradycyjne rozpoznawanie wywołań metody jest wykonywane z grupą metod
Mf
i listą argumentówAf
. Na potrzeby ostatecznej weryfikacji wywołania metody kontekstMf
jest traktowany jako member_access za pośrednictwem wystąpieniaT
.- Jeśli zostanie znaleziona jedna najlepsza metoda
Fi
, wynikiem rozwiązania wywołania metody jestFi
. - W przeciwnym razie zgłaszany jest błąd.
- Jeśli zostanie znaleziona jedna najlepsza metoda
- Wyszukiwanie elementów z nazwą
- Na koniec dla każdego
Fi
wykrytego w krokach 1 i 2 wykonywana jest ostateczna walidacja:- Jeśli jakikolwiek
Fi
nie zwracabool
według wartości lubvoid
, zgłaszany jest błąd. - Jeśli wszystkie
Fi
nie zwracają tego samego typu, zgłaszany jest błąd.
- Jeśli jakikolwiek
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:
- Wszelkie argumenty
Fc
, które występują leksykalnie przedi
są oceniane i przechowywane w zmiennych tymczasowych w kolejności leksykalnej. Aby zachować kolejność leksykalną, jeślii
wystąpiła w ramach większego wyrażeniae
, wszystkie składnikie
, które wystąpiły przedi
, zostaną również ocenione ponownie w kolejności leksykalnej. -
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 argumentbool
out (jeśliFc
został rozwiązany z jednym jako ostatni parametr). Wynik jest przechowywany w wartości tymczasowejib
.- 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}
.
- Długość składników literału jest obliczana po zastąpieniu dowolnego open_brace_escape_sequence pojedynczym
- Jeśli
Fc
kończy się argumentem wyjściowymbool
, zostanie wygenerowana kontrola wartościbool
. Jeśli to prawda, metody wFa
będą wywoływane. W przeciwnym razie nie będą one wywoływane. - W przypadku każdej
Fax
wFa
,Fax
jest wywoływana naib
z bieżącym składnikiem literału lub odpowiednim wyrażeniem interpolacji . JeśliFax
zwracabool
, wynik jest logicznie i ze wszystkimi poprzednimi wywołaniamiFax
.- Jeśli
Fax
jest wywołaniemAppendLiteral
, składnik literału jest odbezpieczany przez zastąpienie dowolnego open_brace_escape_sequence jednym{
i dowolnego close_brace_escape_sequence jednym}
.
- Jeśli
- 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 tegoSpan
(na przykład tworząc metodę, która akceptuje taki uchwyt, a następnie celowo pobieraSpan
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ć:
- Czy ogólnie lubimy ten wzór?
- 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ł:
- Jeśli ciąg interpolowany używany jako
string
,IFormattable
lubFormattableString
maawait
w otworze interpolacji, użyj formatowania w stylu starego typu. - 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 await
w 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.
C# feature specifications