Interpolazione migliorata delle stringhe
Nota
Questo articolo è una specifica di funzionalità. La specifica funge da documento di progettazione per la funzionalità. Include le modifiche specifiche proposte, insieme alle informazioni necessarie durante la progettazione e lo sviluppo della funzionalità. Questi articoli vengono pubblicati fino a quando le modifiche specifiche proposte non vengono completate e incorporate nella specifica ECMA corrente.
Potrebbero verificarsi alcune discrepanze tra la specifica di funzionalità e l'implementazione completata. Tali differenze vengono catturate nelle note pertinenti della riunione di progettazione del linguaggio (LDM) .
Nell'articolo riguardante le specifiche , puoi trovare ulteriori informazioni sul processo di adozione delle speclette delle funzionalità nello standard del linguaggio C#.
Problema del campione: https://github.com/dotnet/csharplang/issues/4487
Sommario
Viene introdotto un nuovo modello per la creazione e l'uso di espressioni di stringa interpolate per consentire una formattazione efficiente e l'uso sia in scenari di string
generali che in scenari più specializzati, ad esempio framework di registrazione, senza incorrere in allocazioni non necessarie dalla formattazione della stringa nel framework.
Motivazione
Attualmente, l'interpolazione di stringhe si riduce principalmente a una chiamata a string.Format
. Questo, mentre per utilizzo generico, può risultare inefficiente per diversi motivi:
- Effettua il boxing di qualsiasi argomento della struttura, a meno che il runtime non introduca un overload di
string.Format
che accetta esattamente i tipi corretti di argomenti nell'ordine corretto.- Questo ordinamento è il motivo per cui il runtime è esitante a introdurre versioni generiche del metodo, in quanto porterebbe a un'esplosione combinatorica di istanze generiche di un metodo molto comune.
- Deve allocare una matrice per gli argomenti nella maggior parte dei casi.
- Non è possibile evitare di creare l'istanza se non è necessaria. I framework di registrazione, ad esempio, consigliano di evitare l'interpolazione di stringhe perché questo provoca la creazione di una stringa che potrebbe non essere necessaria, a seconda del livello di registrazione corrente dell'applicazione.
- Non può mai usare
Span
o altri tipi di struct ref, perché gli struct di riferimento non sono consentiti come parametri di tipo generico, ovvero se un utente vuole evitare di copiare in posizioni intermedie devono formattare manualmente le stringhe.
Internamente, il runtime ha un tipo denominato ValueStringBuilder
per gestire i primi 2 di questi scenari. Passano un buffer stackalloc al generatore, chiamano ripetutamente AppendFormat
con ogni parte e quindi ottengono una stringa finale. Se la stringa risultante supera i limiti del buffer dello stack, può quindi passare a una matrice nell'heap. Tuttavia, questo tipo è pericoloso da esporre direttamente, poiché l'utilizzo non corretto potrebbe portare un array noleggiato a essere eliminato due volte, il che causerà ogni tipo di comportamento indefinito nel programma, in cui due posizioni credono di avere accesso esclusivo all'array. Questa proposta crea un modo per utilizzare questo tipo in modo sicuro nel codice C# nativo semplicemente scrivendo un valore letterale di stringa interpolata, lasciando il codice scritto invariato e migliorando al contempo ogni stringa interpolata che un utente scrive. Estende anche questo modello per consentire alle stringhe interpolate passate come argomenti ad altri metodi di usare un modello di gestore, definito dal ricevitore del metodo, che consentirà a elementi come i framework di registrazione di evitare l'allocazione di stringhe che non saranno mai necessarie e fornire agli utenti C# familiari e pratici sintassi di interpolazione.
Progettazione dettagliata
Modello del gestore
Viene introdotto un nuovo modello di gestore che può rappresentare una stringa interpolata passata come argomento a un metodo. L'inglese semplice del modello è il seguente:
Quando un interpolated_string_expression viene passato come argomento a un metodo, viene esaminato il tipo del parametro . Se il tipo di parametro ha un costruttore che può essere richiamato con 2 parametri int, literalLength
e formattedCount
, accetta facoltativamente parametri aggiuntivi specificati da un attributo nel parametro originale, dispone opzionalmente di un parametro finale booleano out, e il tipo del parametro originale ha metodi di istanza AppendLiteral
e AppendFormatted
che possono essere richiamati per ogni parte della stringa interpolata, allora abbassiamo l'interpolazione usando ciò, anziché in una chiamata tradizionale a string.Format(formatStr, args)
. Un esempio più concreto è utile per illustrare quanto segue:
// 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);
In questo caso, poiché TraceLoggerParamsInterpolatedStringHandler
ha un costruttore con i parametri corretti, si dice che la stringa interpolata ha una conversione implicita del gestore in tale parametro e si riduce al modello illustrato in precedenza. Le specifiche necessarie per questo sono un po' complicate e sono ampliate qui sotto.
La parte restante di questa proposta userà Append...
per fare riferimento a uno dei AppendLiteral
o AppendFormatted
nei casi in cui entrambi sono applicabili.
Nuovi attributi
Il compilatore riconosce il 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()
{
}
}
}
Questo attributo viene usato dal compilatore per determinare se un tipo è un tipo di gestore di stringhe interpolato valido.
Il compilatore riconosce anche il 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; }
}
}
Questo attributo viene usato nei parametri per informare il compilatore su come abbassare un modello di gestore di stringhe interpolato usato in una posizione del parametro.
Conversione del gestore di stringhe interpolata
Il tipo T
viene detto essere un applicable_interpolated_string_handler_type se è attribuito con System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute
.
Esiste una conversione implicita di tipo interpolated_string_handler_conversion verso T
da un'espressione di tipo interpolated_string_expressiono un'espressione additiva di tipo additive_expression composta interamente da _interpolated_string_expression_s e usando solo operatori +
.
Per semplicità nel resto di questo speclet, interpolated_string_expression fa riferimento sia a un semplice interpolated_string_expression, sia a un additive_expression composto interamente da _interpolated_string_expression_s e usando solo operatori +
.
Si noti che questa conversione esiste sempre, indipendentemente dal fatto che ci saranno errori successivi quando si tenta effettivamente di abbassare l'interpolazione usando il modello del gestore. Questa operazione viene eseguita per garantire che siano presenti errori prevedibili e utili e che il comportamento di runtime non cambi in base al contenuto di una stringa interpolata.
Regolazioni dei membri della funzione applicabili
La formulazione dell'algoritmo del membro di funzione applicabile (§12.6.4.2) come indicato di seguito (viene aggiunto un nuovo sotto-punto a ciascuna sezione, in grassetto):
Un membro della funzione è detto essere un membro della funzione applicabile rispetto a un elenco di argomenti A
quando sono soddisfatte tutte le condizioni seguenti:
- Ogni argomento in
A
corrisponde a un parametro nella dichiarazione del membro della funzione come descritto in Parametri corrispondenti (§12.6.2.2) e qualsiasi parametro a cui nessun argomento corrisponde è un parametro facoltativo. - Per ciascun argomento in
A
, la modalità di passaggio del parametro dell'argomento (cioè, valore,ref
oout
) è identica alla modalità di passaggio del parametro corrispondente e- per un parametro value o una matrice di parametri, esiste una conversione implicita (§10.2) dall'argomento al tipo del parametro corrispondente oppure
-
per un parametro
ref
, il cui tipo è una struct, esiste una conversione implicita interpolated_string_handler_conversion dall'argomento al tipo del parametro corrispondente, oppure - per un parametro
ref
oout
, il tipo dell'argomento è identico al tipo del parametro corrispondente. In fin dei conti, un parametroref
oout
è un alias per l'argomento passato.
Per un membro della funzione che include una matrice di parametri, se il membro della funzione è applicabile dalle regole precedenti, si dice che sia applicabile nel formato normale . Se un membro della funzione che include una matrice di parametri non è applicabile nel formato normale, il membro della funzione può essere invece applicabile nel relativo modulo espanso:
- Il modulo espanso viene costruito sostituendo la matrice di parametri nella dichiarazione membro della funzione con zero o più parametri valore del tipo di elemento della matrice di parametri in modo che il numero di argomenti nell'elenco di argomenti
A
corrisponda al numero totale di parametri. SeA
ha meno argomenti rispetto al numero di parametri fissi nella dichiarazione del membro della funzione, la forma espansa del membro della funzione non può essere costruita e pertanto non è applicabile. - In caso contrario, il modulo espanso è applicabile se per ogni argomento in
A
la modalità di passaggio del parametro dell'argomento è identica alla modalità di passaggio del parametro corrispondente e- per un parametro di valore fisso o un parametro di valore creato dall'espansione, esiste una conversione implicita (§10.2) dal tipo dell'argomento al tipo del parametro corrispondente oppure
-
per un parametro
ref
il cui tipo è struct, esiste un interpolated_string_handler_conversion implicito dall'argomento al tipo corrispondente del parametro, o - per un parametro
ref
oout
, il tipo dell'argomento è identico al tipo del parametro corrispondente.
Nota importante: questo significa che se sono presenti 2 overload equivalenti, che differiscono solo per il tipo di applicable_interpolated_string_handler_type, questi overload verranno considerati ambigui. Inoltre, poiché non vengono visualizzati cast espliciti, è possibile che si verifichi uno scenario non risolvibile in cui entrambi gli overload applicabili usano InterpolatedStringHandlerArguments
e sono totalmente inutilizzabili senza eseguire manualmente il modello di riduzione del gestore. È possibile apportare modifiche all'algoritmo membro della funzione migliore per risolvere questo problema se si sceglie, ma questo scenario potrebbe non verificarsi e non è una priorità da affrontare.
Migliore conversione dalle modifiche delle espressioni
Modifichiamo la migliore conversione della sezione di espressione (§12.6.4.5) nel modo seguente:
Dato un C1
di conversione implicita che esegue la conversione da un'espressione E
a un tipo T1
e un C2
di conversione implicita che esegue la conversione da un'espressione E
a un tipo T2
, C1
è una conversione migliore rispetto a C2
se:
-
E
è un interpolated_string_expressionnon costante,C1
è un implicit_string_handler_conversion,T1
è un applicable_interpolated_string_handler_typeeC2
non è un implicit_string_handler_conversiono -
E
non corrisponde esattamente aT2
e si verifica almeno una delle seguenti condizioni:
Ciò significa che esistono alcune regole di risoluzione dell'overload potenzialmente non ovvie, a seconda che la stringa interpolata in questione sia un'espressione costante o meno. Per esempio:
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
Questa operazione viene introdotta in modo che gli elementi che possono essere semplicemente emessi come costanti lo siano e non comportino alcun sovraccarico, mentre gli elementi che non possono essere costanti utilizzano il pattern del gestore.
InterpolatedStringHandler e utilizzo
Introduciamo un nuovo tipo in System.Runtime.CompilerServices
: DefaultInterpolatedStringHandler
. Si tratta di uno struct di riferimento con molte delle stesse semantiche di ValueStringBuilder
, destinate all'uso diretto da parte del compilatore C#. Questo struct sarà simile al seguente:
// 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);
}
}
Si apporta una leggera modifica alle regole per il significato di un interpolated_string_expression (§12.8.3):
Se il tipo di una stringa interpolata è string
e il tipo System.Runtime.CompilerServices.DefaultInterpolatedStringHandler
esiste e il contesto corrente supporta l'uso di tale tipo, la stringaviene ridotta usando il modello del gestore. Il valore finale string
viene quindi ottenuto chiamando ToStringAndClear()
sul tipo di gestore.In caso contrario, se il tipo di una stringa interpolata è System.IFormattable
o System.FormattableString
[il resto è invariato]
La regola "e il contesto corrente supporta l'uso di tale tipo" è intenzionalmente vago per fornire al compilatore il modo di ottimizzare l'utilizzo di questo modello. Il tipo di gestore è probabilmente un tipo di struct ref e i tipi di struct ref non sono in genere consentiti nei metodi asincroni. Per questo caso specifico, il compilatore può usare il gestore se nessuno dei fori di interpolazione contiene un'espressione await
, in quanto è possibile determinare in modo statico che il tipo di gestore viene usato in modo sicuro senza ulteriori analisi complesse perché il gestore verrà eliminato dopo la valutazione dell'espressione di stringa interpolata.
Aprire domanda:
Si vuole fare invece in modo che il compilatore sappia DefaultInterpolatedStringHandler
e ignorare completamente la chiamata string.Format
? Ci permetterebbe di nascondere un metodo che non vogliamo necessariamente inserire nei volti delle persone quando chiamano manualmente string.Format
.
Risposta: Sì.
Domanda aperta:
Vogliamo avere anche gestori per System.IFormattable
e System.FormattableString
?
Risposta: No.
Codegen del pattern handler
In questa sezione, la risoluzione delle chiamate al metodo si riferisce ai passaggi elencati in §12.8.10.2.
Risoluzione del costruttore
Dato un applicable_interpolated_string_handler_typeT
e un interpolated_string_expressioni
, la risoluzione delle chiamate al metodo e la convalida per un costruttore valido in T
viene eseguita come segue:
- La ricerca dei membri per i costruttori di istanza viene eseguita su
T
. Il gruppo di metodi risultante viene chiamatoM
. - L'elenco di argomenti
A
viene costruito come segue:- I primi due argomenti sono costanti intere, che rappresentano rispettivamente la lunghezza letterale di
i
e il numero di componenti di interpolazione in dii
. - Se
i
viene usato come argomento per alcuni parametripi
nel metodoM1
e il parametropi
viene attribuito conSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute
, per ogni nomeArgx
nella matriceArguments
di tale attributo il compilatore corrisponde a un parametropx
con lo stesso nome. La stringa vuota viene associata al ricevitore diM1
.- Se una
Argx
non è in grado di corrispondere a un parametro diM1
o unArgx
richiede il ricevitore diM1
eM1
è un metodo statico, viene generato un errore e non vengono eseguiti altri passaggi. - In caso contrario, il tipo di ogni
px
risolto viene aggiunto all'elenco di argomenti, nell'ordine specificato dalla matriceArguments
. Ognipx
viene passato con la stessa semantica diref
specificata inM1
.
- Se una
- L'argomento finale è un
bool
, passato come un parametroout
.
- I primi due argomenti sono costanti intere, che rappresentano rispettivamente la lunghezza letterale di
- La risoluzione delle chiamate al metodo tradizionale viene eseguita con il gruppo di metodi
M
e l'elenco di argomentiA
. Ai fini della convalida finale della chiamata al metodo, il contesto diM
viene considerato come un member_access tramite il tipoT
.- Se è stato trovato un costruttore migliore
F
, il risultato della risoluzione dell'overload èF
. - Se non sono stati trovati costruttori applicabili, viene eseguito di nuovo il passaggio 3, rimuovendo il parametro
bool
finale daA
. Se questo nuovo tentativo non trova anche membri applicabili, viene generato un errore e non vengono eseguiti altri passaggi. - Se non è stato trovato alcun metodo single-best, il risultato della risoluzione dell'overload è ambiguo, viene generato un errore e non vengono eseguiti altri passaggi.
- Se è stato trovato un costruttore migliore
- Viene eseguita la convalida finale in
F
.- Se un elemento di
A
si è verificato lessicalmente dopoi
, viene generato un errore e non vengono eseguiti altri passaggi. - Se un
A
richiede il ricevitore diF
eF
è un indicizzatore usato come initializer_target in un member_initializer, viene segnalato un errore e non vengono eseguiti altri passaggi.
- Se un elemento di
Nota: la risoluzione qui non usa le espressioni effettive passate come altri argomenti per gli elementi Argx
. Consideriamo solo i tipi dopo la conversione. Ciò garantisce che non siano presenti problemi di conversione doppia o casi imprevisti in cui un'espressione lambda è associata a un tipo delegato quando viene passato a M1
e associato a un tipo delegato diverso quando viene passato a M
.
Nota: segnaliamo un errore per gli indicizzatori usati come inizializzatori di membri a causa del processo di valutazione degli inizializzatori di membri annidati. Si consideri questo frammento di codice:
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 { }
}
}
Gli argomenti da __c1.C2[]
vengono valutati prima di ricevitore dell'indicizzatore. Anche se è possibile arrivare a una soluzione che funziona per questo scenario (creando un elemento temporaneo per __c1.C2
e condividendolo tra entrambe le chiamate dell'indicizzatore, oppure usandolo solo per la prima chiamata e condividendo l'argomento tra entrambe le chiamate), crediamo che qualsiasi soluzione sarebbe confusa per quello che riteniamo sia uno scenario patologico. Pertanto, si vieta completamente lo scenario.
domanda aperta:
Se usiamo un costruttore invece di Create
, miglioreremmo il codegen di runtime, a scapito di restringere un po' il pattern.
Answer: Per il momento, ci limiteremo ai costruttori. Possiamo riesaminare l'aggiunta di un metodo generale Create
in un secondo tempo qualora si presenti lo scenario.
risoluzione dell'overload del metodo Append...
Data una applicable_interpolated_string_handler_typeT
e un interpolated_string_expressioni
, la risoluzione dell'overload per un set di metodi di Append...
validi su T
viene eseguita come segue:
- Se sono presenti componenti interpolated_regular_string_character in
i
:- Viene eseguita la ricerca di membri su
T
con il nomeAppendLiteral
. Il gruppo di metodi risultante viene chiamatoMl
. - L'elenco di argomenti
Al
viene costituito con un parametro di valore di tipostring
. - La risoluzione delle chiamate al metodo tradizionale viene eseguita con il gruppo di metodi
Ml
e l'elenco di argomentiAl
. Ai fini della convalida finale della chiamata al metodo, il contesto diMl
viene considerato come un member_access tramite un'istanza diT
.- Se viene trovato un metodo single-best
Fi
e non sono stati generati errori, il risultato della risoluzione delle chiamate al metodo èFi
. - In caso contrario, viene segnalato un errore.
- Se viene trovato un metodo single-best
- Viene eseguita la ricerca di membri su
- Per ogni componente di interpolazione
ix
dii
:- Viene eseguita la ricerca dei membri in
T
con il nomeAppendFormatted
. Il gruppo di metodi risultante viene chiamatoMf
. - L'elenco di argomenti
Af
viene costruito:- Il primo parametro è il
expression
diix
, passato per valore. - Se
ix
contiene direttamente un componente constant_expression, viene aggiunto un parametro di valore intero con il nomealignment
specificato. - Se
ix
è seguito direttamente da un interpolation_format, viene aggiunto un parametro di valore stringa, con il nomeformat
specificato.
- Il primo parametro è il
- La risoluzione delle chiamate al metodo tradizionale viene eseguita con il gruppo di metodi
Mf
e l'elenco di argomentiAf
. Ai fini della convalida finale della chiamata al metodo, il contesto diMf
viene considerato come un member_access tramite un'istanza diT
.- Se viene trovato un metodo ottimale unico
Fi
, il risultato della risoluzione delle invocazioni di metodo èFi
. - In caso contrario, viene segnalato un errore.
- Se viene trovato un metodo ottimale unico
- Viene eseguita la ricerca dei membri in
- Infine, per ogni
Fi
individuato nei passaggi 1 e 2, viene eseguita la convalida finale:- Se una
Fi
non restituiscebool
come valore ovoid
, viene segnalato un errore. - Se tutte le
Fi
non restituiscono lo stesso tipo, viene segnalato un errore.
- Se una
Si noti che queste regole non consentono metodi di estensione per le chiamate Append...
. È possibile considerare l'abilitazione di questo tipo se si sceglie, ma questo è analogo al modello di enumeratore, in cui è possibile consentire GetEnumerator
di essere un metodo di estensione, ma non Current
o MoveNext()
.
Queste regole consentono parametri predefiniti per le chiamate Append...
, che funzioneranno con elementi come CallerLineNumber
o CallerArgumentExpression
(se supportati dal linguaggio).
Sono disponibili regole di ricerca di overload separate per gli elementi di base e i fori di interpolazione perché alcuni gestori dovranno essere in grado di comprendere la differenza tra i componenti interpolati e i componenti che fanno parte della stringa di base.
domanda aperta
Alcuni scenari, ad esempio la registrazione strutturata, vogliono essere in grado di fornire nomi per gli elementi di interpolazione. Ad esempio, oggi una chiamata di log potrebbe essere come Log("{name} bought {itemCount} items", name, items.Count);
. I nomi all'interno del {}
forniscono informazioni importanti sulla struttura per i logger che consentono di garantire che l'output sia coerente e uniforme. Alcuni casi potrebbero essere in grado di riutilizzare il componente :format
di un foro di interpolazione per questo, ma molti logger comprendono già gli identificatori di formato e hanno un comportamento esistente per la formattazione dell'output in base a queste informazioni. Esiste una sintassi che è possibile usare per abilitare l'inserimento di questi identificatori denominati?
Alcune situazioni possono cavarsela con CallerArgumentExpression
, a condizione che il supporto arrivi in C# 10. Tuttavia, per i casi che richiamano un metodo o una proprietà, ciò potrebbe non essere sufficiente.
Risposta:
Sebbene ci siano alcune parti interessanti per le stringhe basate su modelli che è possibile esplorare in una funzionalità del linguaggio ortogonale, non si ritiene che una sintassi specifica in questo caso abbia molto vantaggio rispetto a soluzioni come l'uso di una tupla: $"{("StructuredCategory", myExpression)}"
.
Esecuzione della conversione
Dato un applicable_interpolated_string_handler_typeT
e un interpolated_string_expressioni
che abbia un costruttore valido Fc
e metodi Append...
Fa
risolti, la riduzione per i
viene eseguita come segue:
- Tutti gli argomenti a
Fc
che si presentano lessicalmente prima dii
vengono valutati e archiviati in variabili temporanee in ordine lessicale. Per mantenere l'ordinamento lessicale, sei
si è verificato come parte di un'espressione più grandee
, tutti i componenti die
che si sono verificati prima dii
verranno valutati anche in ordine lessicale. -
Fc
viene chiamato con la lunghezza dei componenti letterali di stringa interpolata, il numero di buchi di interpolazione , qualsiasi argomento valutato in precedenza e un argomento di uscitabool
(seFc
è stato risolto con uno come ultimo parametro). Il risultato viene archiviato in un valore temporaneoib
.- La lunghezza dei componenti letterali viene calcolata dopo aver sostituito qualsiasi open_brace_escape_sequence con un singolo
{
e qualsiasi close_brace_escape_sequence con un singolo}
.
- La lunghezza dei componenti letterali viene calcolata dopo aver sostituito qualsiasi open_brace_escape_sequence con un singolo
- Se
Fc
è terminato con un argomentobool
out, viene generato un controllo sul valorebool
. Se vero, verranno chiamati i metodi inFa
. In caso contrario, non verranno chiamati. - Per ogni
Fax
inFa
,Fax
viene chiamato suib
con, a seconda dei casi, il componente letterale corrente o l'espressione di interpolazione . SeFax
restituisce unbool
, il risultato viene eseguito logicamente e con tutte le chiamateFax
precedenti.- Se
Fax
è una chiamata aAppendLiteral
, il componente letterale viene decodificato sostituendo qualsiasi open_brace_escape_sequence con un singolo{
, e qualsiasi close_brace_escape_sequence con un singolo}
.
- Se
- Il risultato della conversione è
ib
.
Anche in questo caso, si noti che gli argomenti passati a Fc
e gli argomenti passati a e
sono la stessa variabile temporanea. Le conversioni possono avvenire sulla variabile temporanea per convertirla in un formato richiesto da Fc
, ma ad esempio le espressioni lambda non possono essere associate a un tipo di delegate diverso tra Fc
e e
.
Domanda Aperta
Questa riduzione significa che le parti successive della stringa interpolata dopo una chiamata Append...
che restituisce false non vengono esaminate. Questo potrebbe potenzialmente essere molto confuso, in particolare se il problema di formattazione ha effetti collaterali. È invece possibile valutare prima tutti i fori di formato, quindi chiamare ripetutamente Append...
con i risultati, interrompendo se restituisce false. In questo modo si garantisce che tutte le espressioni vengano valutate come ci si aspetterebbe, ma vengono chiamati solo i metodi strettamente necessari. Anche se la valutazione parziale potrebbe essere utile per alcuni casi più avanzati, è forse non intuitiva per il caso generale.
Un'altra alternativa, se si vuole valutare sempre tutti i buchi di formato, consiste nel rimuovere la versione Append...
dell'API e solo ripetere Format
chiamate. Il gestore può tenere traccia se deve semplicemente eliminare l'argomento e restituire immediatamente per questa versione.
Answer: Effettueremo la valutazione condizionale dei fori.
Domanda Aperta
È necessario eliminare i tipi di gestori eliminabili ed eseguire il wrapping delle chiamate con try/finally per assicurarsi che venga chiamato Dispose? Ad esempio, il gestore di stringhe interpolato nell'elenco bcl potrebbe avere una matrice noleggiata al suo interno e se uno dei fori di interpolazione genera un'eccezione durante la valutazione, tale matrice noleggiata potrebbe essere persa se non è stata eliminata.
Risposta: No. I gestori possono essere assegnati a variabili locali (come MyHandler handler = $"{MyCode()};
), e la durata di tali gestori non è ben definita. A differenza degli enumeratori foreach, in cui la durata è ovvia e non viene creato alcun elemento locale definito dall'utente per l'enumeratore.
Impatto sul tipo di riferimento nullabile
Per ridurre al minimo la complessità dell'implementazione, esistono alcune limitazioni su come viene eseguita un'analisi nullable sui costruttori di gestori di stringhe interpolati usati come argomenti per un metodo o un indicizzatore. In particolare, non facciamo fluire informazioni dal costruttore fino agli slot originali di parametri o argomenti dal contesto originale e non usiamo i tipi di parametri del costruttore per guidare l'inferenza del tipo generico per i parametri di tipo nel metodo contenitore. Un esempio di dove questo può avere un impatto è:
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()
{
}
}
Altre considerazioni
Consentire anche ai tipi di string
di essere convertibili in gestori
Per semplicità dell'autore del tipo, è possibile considerare la possibilità di consentire alle espressioni di tipo string
di essere convertibile in modo implicito in applicable_interpolated_string_handler_types. Come proposto oggi, gli autori dovranno probabilmente sovraccaricare sia il tipo di gestore che i tipi regolari string
, affinché gli utenti non debbano comprendere la differenza. Questo può essere un sovraccarico fastidioso e non evidente, poiché un'espressione string
può essere vista come un'interpolazione con una lunghezza expression.Length
predefinita e 0 spazi da riempire.
In questo modo, le nuove API possono esporre solo un gestore, senza dover esporre anche un overload che accetta string
. Tuttavia, non eliminerà la necessità di modifiche per una migliore conversione dall'espressione in questione, quindi, anche se funzionerebbe, potrebbe non essere uno sforzo necessario.
Risposta:
Si ritiene che ciò possa generare confusione ed esiste una soluzione alternativa semplice per i tipi di gestori personalizzati: aggiungere una conversione definita dall'utente da stringa.
Incorporamento di intervalli per stringhe senza heap
ValueStringBuilder
come esiste oggi ha 2 costruttori: uno che accetta un conteggio e alloca nell'heap in modo anticipato e uno che accetta un Span<char>
. Questo Span<char>
è in genere una dimensione fissa nella codebase di runtime, circa 250 elementi in media. Per sostituire realmente tale tipo, è consigliabile considerare un'estensione a questa posizione in cui vengono riconosciuti anche i metodi GetInterpolatedString
che accettano un Span<char>
, anziché solo la versione del conteggio. Tuttavia, vediamo alcuni casi potenzialmente spinosi da risolvere qui.
- Non si vuole eseguire ripetutamente lo stackalloc in un ciclo critico. Se dovessimo fare questa estensione della funzionalità, probabilmente vorremmo condividere lo spazio stackalloc tra le iterazioni del ciclo. Sappiamo che questo è sicuro, poiché
Span<T>
è uno struct di riferimento che non può essere archiviato nell'heap, e gli utenti dovrebbero essere piuttosto ingegnosi per riuscire a estrarre un riferimento a taleSpan
(ad esempio, creando un metodo che accetta un tale handler e recuperando poi intenzionalmente ilSpan
dall'handler per restituirlo al chiamante). Tuttavia, l'allocazione anticipata produce altre domande:- Dovremmo usare stackalloc con entusiasmo? Cosa accade se il ciclo non viene mai eseguito oppure viene terminato prima che lo spazio sia necessario?
- Se non eseguiamo lo stackalloc in modo proattivo, significa che introduciamo un ramo nascosto in ogni ciclo? La maggior parte dei cicli probabilmente non ne tiene conto, ma potrebbe influire su alcuni cicli stretti che non vogliono sostenere il costo.
- Alcune stringhe possono essere piuttosto grandi e la quantità appropriata di
stackalloc
dipende da diversi fattori, inclusi i fattori di runtime. Non è necessario che il compilatore e la specifica C# determinino questo in anticipo, quindi si vuole risolvere https://github.com/dotnet/runtime/issues/25423 e aggiungere un'API che il compilatore chiami in questi casi. Aggiunge anche altri vantaggi e svantaggi ai punti del ciclo precedente, in cui non si vogliono allocare matrici di grandi dimensioni nell'heap più volte o prima che ne sia necessaria una.
Risposta:
Questo non rientra nell'ambito di C# 10. È possibile esaminare questo aspetto in generale quando si esamina la funzionalità di params Span<T>
più generale.
Versione non di prova dell'API
Per semplicità, questa specifica propone attualmente di riconoscere un metodo Append...
e gli elementi che hanno sempre esito positivo (ad esempio InterpolatedStringHandler
) restituiscono sempre il valore true quando eseguono il metodo.
Questa operazione è stata eseguita per supportare scenari di formattazione parziale in cui l'utente vuole interrompere la formattazione se si verifica un errore o se non è necessario, ad esempio il caso di registrazione, ma potrebbe potenzialmente introdurre una serie di rami non necessari nell'utilizzo standard delle stringhe interpolate. È possibile considerare un addendum in cui si usano solo metodi FormatX
se non è presente alcun metodo Append...
, ma presenta domande su cosa facciamo se esiste una combinazione di chiamate sia Append...
che FormatX
.
Risposta:
Vogliamo la versione non di prova dell'API. La proposta è stata aggiornata per riflettere questa situazione.
Passaggio di argomenti precedenti al gestore
Attualmente esiste una mancanza di simmetria nella proposta: richiamare un metodo di estensione in forma ridotta produce semantiche diverse rispetto a richiamare il metodo di estensione in forma normale. Questo è diverso dalla maggior parte delle altre posizioni nella lingua, dove la forma ridotta è solo uno zucchero. Si propone di aggiungere un attributo al framework che verrà riconosciuto durante l'associazione di un metodo, che informa il compilatore che determinati parametri devono essere passati al costruttore nel gestore. L'utilizzo è simile al seguente:
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; }
}
}
L'utilizzo di questo è quindi:
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);
Le domande a cui è necessario rispondere:
- Ci piace questo modello in generale?
- Si vuole consentire a questi argomenti di provenire dopo il parametro del gestore? Alcuni modelli esistenti nella BCL, ad esempio
Utf8Formatter
, mettono il valore da formattare prima dell'elemento necessario per formattare. Per adattarsi meglio a questi modelli, è probabile che si voglia consentire questo, ma è necessario decidere se questa valutazione non ordinata sia accettabile.
Risposta
Vogliamo sostenere questo. La specifica è stata aggiornata in modo da riflettere questa situazione. Gli argomenti devono essere specificati in ordine lessicale nel sito di chiamata e, se un argomento necessario per il metodo create viene specificato dopo il letterale di stringa interpolata, viene generato un errore.
Utilizzo di await
nei fori di interpolazione
Poiché $"{await A()}"
è oggi un'espressione valida, è necessario razionalizzare i fori di interpolazione con await. È possibile risolvere questo problema con alcune regole:
- Se una stringa interpolata usata come
string
,IFormattable
oFormattableString
ha unawait
in un campo di interpolazione, passare al formattatore in stile precedente. - Se una stringa interpolata è soggetta a un implicit_string_handler_conversion e applicable_interpolated_string_handler_type è un
ref struct
, non è consentito utilizzareawait
nei fori di formato.
Fondamentalmente, questo desugaring potrebbe usare un ref struct in un metodo asincrono, purché si garantisca che il ref struct
non dovrà essere salvato nell'heap, il che dovrebbe essere possibile se si vietano await
negli spazi di interpolazione.
In alternativa, è possibile semplicemente creare tutti i tipi di gestori non ref struct, incluso il gestore del framework per le stringhe interpolate. Questo, tuttavia, ci impedirebbe di riconoscere un giorno una versione Span
che non ha bisogno di allocare alcuno spazio temporaneo.
Risposta:
I gestori di stringhe interpolati verranno trattati come qualsiasi altro tipo: ciò significa che se il tipo di gestore è uno struct ref e il contesto corrente non consente l'utilizzo di struct di riferimento, è illegale usare qui il gestore. La specifica relativa all'abbassamento dei valori letterali stringa usati come stringhe è intenzionalmente vaga per consentire al compilatore di decidere quali regole ritiene appropriate, ma per i tipi di gestori personalizzati dovranno seguire le stesse regole del resto del linguaggio.
Gestori come parametri di riferimento
Alcuni gestori potrebbero voler essere passati come parametri ref (in
o ref
). Dovremmo permettere uno dei due? Se è così, che aspetto avrà un gestore ref
?
ref $""
genera confusione, poiché in realtà non si passa la stringa come riferimento; si passa come riferimento il gestore creato dal riferimento e questo presenta potenziali problemi simili con i metodi asincroni.
Risposta:
Vogliamo sostenere questo. La specifica è stata aggiornata in modo da riflettere questa situazione. Le regole devono riflettere le stesse regole che si applicano ai metodi di estensione sui tipi valore.
Stringhe interpolate tramite espressioni binarie e conversioni
Poiché questa proposta rende le stringhe interpolate sensibili al contesto, si vuole consentire al compilatore di trattare un'espressione binaria composta interamente da stringhe interpolate, o una stringa interpolata sottoposta a un cast, come letterale di stringa interpolata ai fini della risoluzione dell'overload. Si prenda ad esempio lo scenario seguente:
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
Ciò sarebbe ambiguo, richiedendo un cast a Handler1
o Handler2
per risolvere il problema. Tuttavia, nel fare quel cast, potremmo potenzialmente scartare le informazioni che provengono dal contesto del ricevitore del metodo, il che significa che il cast fallirebbe perché non c'è nulla che possa riempire le informazioni di c
. Un problema simile si verifica con la concatenazione binaria di stringhe: l'utente potrebbe voler formattare il valore letterale tra più righe per evitare il wrapping delle righe, ma non sarebbe più in grado di perché non sarebbe più un valore letterale stringa interpolato convertibile nel tipo di gestore.
Per risolvere questi casi, vengono apportate le modifiche seguenti:
- Un additive_expression composto interamente da interpolated_string_expressions e che utilizza solo gli operatori
+
viene considerato un interpolated_string_literal ai fini delle conversioni e della risoluzione dell'overload. La stringa interpolata finale viene creata concatinando logicamente tutti i singoli componenti interpolated_string_expression, da sinistra a destra. - Un cast_expression o un relational_expression con operatore
as
il cui operando è un interpolated_string_expressions viene considerato un interpolated_string_expressions ai fini delle conversioni e della risoluzione dell'overload.
Aprire domande:
Vogliamo farlo? Questa operazione non viene eseguita per System.FormattableString
, ad esempio, ma può essere suddivisa in una riga diversa, mentre questo può essere dipendente dal contesto e pertanto non può essere suddiviso in una riga diversa. Non ci sono inoltre problemi di risoluzione dell'overload con FormattableString
e IFormattable
.
Risposta:
Riteniamo che si tratta di un caso d'uso valido per le espressioni additive, ma che la versione del cast non è abbastanza convincente in questo momento. Se necessario, è possibile aggiungerlo in un secondo momento. La specifica è stata aggiornata per riflettere questa decisione.
Altri casi d'uso
Vedere https://github.com/dotnet/runtime/issues/50635 per esempi di API del gestore proposte che usano questo modello.
C# feature specifications