Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
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.Formatche 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
Spano 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
Acorrisponde 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,refoout) è 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
refoout, il tipo dell'argomento è identico al tipo del parametro corrispondente. In fin dei conti, un parametrorefooutè 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
Acorrisponda al numero totale di parametri. SeAha 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
Ala 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 è una struct, esiste una conversione implicita interpolated_string_handler_conversion dall'argomento al tipo del parametro corrispondente, oppure - per un parametro
refoout, 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 T1e 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_typeeC2non è un implicit_string_handler_conversiono -
Enon corrisponde esattamente aT2e 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ì.
Aprire domanda:
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
Aviene costruito come segue:- I primi due argomenti sono costanti intere, che rappresentano rispettivamente la lunghezza letterale di
ie il numero di componenti di interpolazione in dii. - Se
iviene usato come argomento per alcuni parametripinel metodoM1e il parametropiviene attribuito conSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, per ogni nomeArgxnella matriceArgumentsdi tale attributo il compilatore corrisponde a un parametropxcon lo stesso nome. La stringa vuota viene associata al ricevitore diM1.- Se una
Argxnon è in grado di corrispondere a un parametro diM1o unArgxrichiede il ricevitore diM1eM1è un metodo statico, viene generato un errore e non vengono eseguiti altri passaggi. - In caso contrario, il tipo di ogni
pxrisolto viene aggiunto all'elenco di argomenti, nell'ordine specificato dalla matriceArguments. Ognipxviene passato con la stessa semantica direfspecificata 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
Me l'elenco di argomentiA. Ai fini della convalida finale della chiamata al metodo, il contesto diMviene 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
boolfinale 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
Asi è verificato lessicalmente dopoi, viene generato un errore e non vengono eseguiti altri passaggi. - Se un
Arichiede il ricevitore diFeFè 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
Tcon il nomeAppendLiteral. Il gruppo di metodi risultante viene chiamatoMl. - L'elenco di argomenti
Alviene costituito con un parametro di valore di tipostring. - La risoluzione delle chiamate al metodo tradizionale viene eseguita con il gruppo di metodi
Mle l'elenco di argomentiAl. Ai fini della convalida finale della chiamata al metodo, il contesto diMlviene considerato come un member_access tramite un'istanza diT.- Se viene trovato un metodo single-best
Fie 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
ixdii:- Viene eseguita la ricerca di membri su
Tcon il nomeAppendFormatted. Il gruppo di metodi risultante viene chiamatoMf. - L'elenco di argomenti
Afviene costruito:- Il primo parametro è il
expressiondiix, passato per valore. - Se
ixcontiene direttamente un componente constant_expression, viene aggiunto un parametro di valore intero con il nomealignmentspecificato. - Se
ixè seguito direttamente da un interpolation_format, viene aggiunto un parametro di valore stringa, con il nomeformatspecificato.
- Il primo parametro è il
- La risoluzione delle chiamate al metodo tradizionale viene eseguita con il gruppo di metodi
Mfe l'elenco di argomentiAf. Ai fini della convalida finale della chiamata al metodo, il contesto diMfviene 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 di membri su
- Infine, per ogni
Fiindividuato nei passaggi 1 e 2, viene eseguita la convalida finale:- Se una
Finon restituisceboolcome valore ovoid, viene segnalato un errore. - Se tutte le
Finon 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
Fcche si presentano lessicalmente prima diivengono valutati e archiviati in variabili temporanee in ordine lessicale. Per mantenere l'ordinamento lessicale, seisi è verificato come parte di un'espressione più grandee, tutti i componenti dieche si sono verificati prima diiverranno valutati anche in ordine lessicale. -
Fcviene 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 argomentoboolout, viene generato un controllo sul valorebool. Se vero, verranno chiamati i metodi inFa. In caso contrario, non verranno chiamati. - Per ogni
FaxinFa,Faxviene chiamato suibcon, a seconda dei casi, il componente letterale corrente o l'espressione di interpolazione . SeFaxrestituisce unbool, il risultato viene eseguito logicamente e con tutte le chiamateFaxprecedenti.- 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 ilSpandall'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
stackallocdipende 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,IFormattableoFormattableStringha unawaitin 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 utilizzareawaitnei 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 awaitnegli 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
asil 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