Condividi tramite


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:

  1. 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.
  2. Deve allocare una matrice per gli argomenti nella maggior parte dei casi.
  3. 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.
  4. 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, refo out) è 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 o out, il tipo dell'argomento è identico al tipo del parametro corrispondente. In fin dei conti, un parametro ref o out è 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. Se A 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 o out, 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:

  1. E è un interpolated_string_expressionnon costante, C1 è un implicit_string_handler_conversion, T1 è un applicable_interpolated_string_handler_typee C2 non è un implicit_string_handler_conversiono
  2. E non corrisponde esattamente a T2 e si verifica almeno una delle seguenti condizioni:
    • E corrisponde esattamente T1 (§12.6.4.5)
    • T1 è una destinazione di conversione migliore rispetto a T2 (§12.6.4.6)

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:

  1. La ricerca dei membri per i costruttori di istanza viene eseguita su T. Il gruppo di metodi risultante viene chiamato M.
  2. L'elenco di argomenti A viene costruito come segue:
    1. I primi due argomenti sono costanti intere, che rappresentano rispettivamente la lunghezza letterale di ie il numero di componenti di interpolazione in di i.
    2. Se i viene usato come argomento per alcuni parametri pi nel metodo M1e il parametro pi viene attribuito con System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, per ogni nome Argx nella matrice Arguments di tale attributo il compilatore corrisponde a un parametro px con lo stesso nome. La stringa vuota viene associata al ricevitore di M1.
      • Se una Argx non è in grado di corrispondere a un parametro di M1o un Argx richiede il ricevitore di M1 e M1 è 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 matrice Arguments. Ogni px viene passato con la stessa semantica di ref specificata in M1.
    3. L'argomento finale è un bool, passato come un parametro out.
  3. La risoluzione delle chiamate al metodo tradizionale viene eseguita con il gruppo di metodi M e l'elenco di argomenti A. Ai fini della convalida finale della chiamata al metodo, il contesto di M viene considerato come un member_access tramite il tipo T.
    • 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 da A. 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.
  4. Viene eseguita la convalida finale in F.
    • Se un elemento di A si è verificato lessicalmente dopo i, viene generato un errore e non vengono eseguiti altri passaggi.
    • Se un A richiede il ricevitore di Fe F è un indicizzatore usato come initializer_target in un member_initializer, viene segnalato un errore e non vengono eseguiti altri passaggi.

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:

  1. Se sono presenti componenti interpolated_regular_string_character in i:
    1. Viene eseguita la ricerca di membri su T con il nome AppendLiteral. Il gruppo di metodi risultante viene chiamato Ml.
    2. L'elenco di argomenti Al viene costituito con un parametro di valore di tipo string.
    3. La risoluzione delle chiamate al metodo tradizionale viene eseguita con il gruppo di metodi Ml e l'elenco di argomenti Al. Ai fini della convalida finale della chiamata al metodo, il contesto di Ml viene considerato come un member_access tramite un'istanza di T.
      • 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.
  2. Per ogni componente di interpolazione ix di i:
    1. Viene eseguita la ricerca dei membri in T con il nome AppendFormatted. Il gruppo di metodi risultante viene chiamato Mf.
    2. L'elenco di argomenti Af viene costruito:
      1. Il primo parametro è il expression di ix, passato per valore.
      2. Se ix contiene direttamente un componente constant_expression, viene aggiunto un parametro di valore intero con il nome alignment specificato.
      3. Se ix è seguito direttamente da un interpolation_format, viene aggiunto un parametro di valore stringa, con il nome format specificato.
    3. La risoluzione delle chiamate al metodo tradizionale viene eseguita con il gruppo di metodi Mf e l'elenco di argomenti Af. Ai fini della convalida finale della chiamata al metodo, il contesto di Mf viene considerato come un member_access tramite un'istanza di T.
      • Se viene trovato un metodo ottimale unico Fi, il risultato della risoluzione delle invocazioni di metodo è Fi.
      • In caso contrario, viene segnalato un errore.
  3. Infine, per ogni Fi individuato nei passaggi 1 e 2, viene eseguita la convalida finale:
    • Se una Fi non restituisce bool come valore o void, viene segnalato un errore.
    • Se tutte le Fi non restituiscono lo stesso tipo, viene segnalato un errore.

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:

  1. Tutti gli argomenti a Fc che si presentano lessicalmente prima di i vengono valutati e archiviati in variabili temporanee in ordine lessicale. Per mantenere l'ordinamento lessicale, se i si è verificato come parte di un'espressione più grande e, tutti i componenti di e che si sono verificati prima di i verranno valutati anche in ordine lessicale.
  2. 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 uscita bool (se Fc è stato risolto con uno come ultimo parametro). Il risultato viene archiviato in un valore temporaneo ib.
    1. 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 }.
  3. Se Fc è terminato con un argomento bool out, viene generato un controllo sul valore bool. Se vero, verranno chiamati i metodi in Fa. In caso contrario, non verranno chiamati.
  4. Per ogni Fax in Fa, Fax viene chiamato su ib con, a seconda dei casi, il componente letterale corrente o l'espressione di interpolazione . Se Fax restituisce un bool, il risultato viene eseguito logicamente e con tutte le chiamate Fax precedenti.
    1. Se Fax è una chiamata a AppendLiteral, il componente letterale viene decodificato sostituendo qualsiasi open_brace_escape_sequence con un singolo {, e qualsiasi close_brace_escape_sequence con un singolo }.
  5. 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 tale Span (ad esempio, creando un metodo che accetta un tale handler e recuperando poi intenzionalmente il Span 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:

  1. Ci piace questo modello in generale?
  2. 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:

  1. Se una stringa interpolata usata come string, IFormattableo FormattableString ha un await in un campo di interpolazione, passare al formattatore in stile precedente.
  2. Se una stringa interpolata è soggetta a un implicit_string_handler_conversion e applicable_interpolated_string_handler_type è un ref struct, non è consentito utilizzare await 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 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 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.