Esercitazione: Scrivere un gestore di interpolazione di stringhe personalizzato
In questa esercitazione si apprenderà come:
- Implementare il modello del gestore di interpolazione di stringhe
- Interagire con il ricevitore in un'operazione di interpolazione di stringhe.
- Aggiungere argomenti al gestore di interpolazione di stringhe
- Comprendere le nuove funzionalità della libreria per l'interpolazione di stringhe
Prerequisiti
È necessario configurare il computer per eseguire .NET. Il compilatore C# è disponibile con Visual Studio 2022 o .NET SDK.
Questa esercitazione presuppone che si abbia familiarità con C# e .NET, incluso Visual Studio o l'interfaccia della riga di comando di .NET.
È possibile scrivere un gestore di stringhe interpolato personalizzato . Un gestore di stringhe interpolato è un tipo che elabora l'espressione segnaposto in una stringa interpolata. Senza un gestore personalizzato, i segnaposto vengono elaborati in modo simile a String.Format. Ogni segnaposto viene formattato come testo e quindi i componenti vengono concatenati per formare la stringa risultante.
È possibile scrivere un gestore per qualsiasi scenario in cui si usano informazioni sulla stringa risultante. Verrà usato? Quali vincoli si trovano nel formato? Alcuni esempi includono:
- Potrebbe essere necessario che nessuna delle stringhe risultanti sia maggiore di un limite, ad esempio 80 caratteri. È possibile elaborare le stringhe interpolate per riempire un buffer a lunghezza fissa e interrompere l'elaborazione una volta raggiunta la lunghezza del buffer.
- Potresti avere un formato tabulare, e ogni segnaposto deve avere una lunghezza fissa. Un gestore personalizzato può imporre che, anziché forzare la conformità di tutto il codice client.
In questa esercitazione viene creato un gestore di interpolazione di stringhe per uno degli scenari di prestazioni principali: librerie di registrazione. A seconda del livello di log configurato, il lavoro per costruire un messaggio di log non è necessario. Se la registrazione è spenta, il lavoro per costruire una stringa da un'espressione di stringa interpolata non è necessario. Il messaggio non viene mai stampato, quindi è possibile ignorare qualsiasi concatenazione di stringhe. Inoltre, tutte le espressioni usate nei segnaposto, inclusa la generazione di tracce dello stack, non devono essere eseguite.
Un gestore di stringhe interpolato può determinare se verrà usata la stringa formattata ed eseguire solo il lavoro necessario, se necessario.
Implementazione iniziale
Si inizierà da una classe Logger
di base che supporta livelli diversi:
public enum LogLevel
{
Off,
Critical,
Error,
Warning,
Information,
Trace
}
public class Logger
{
public LogLevel EnabledLevel { get; init; } = LogLevel.Error;
public void LogMessage(LogLevel level, string msg)
{
if (EnabledLevel < level) return;
Console.WriteLine(msg);
}
}
Questo Logger
supporta sei livelli diversi. Quando un messaggio non passa il filtro a livello di log, non è presente alcun output. L'API pubblica per il logger accetta una stringa (completamente formattata) come messaggio. Tutte le operazioni per creare la stringa sono già state eseguite.
Implementare il modello del gestore
Questo passaggio consiste nel compilare un gestore di stringhe interpolato che ricrea il comportamento corrente. Un gestore di stringhe interpolato è un tipo che deve avere le caratteristiche seguenti:
- Il System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute applicato al tipo.
- Costruttore con due parametri
int
,literalLength
eformattedCount
. Sono consentiti altri parametri. - Un metodo pubblico
AppendLiteral
con la firma:public void AppendLiteral(string s)
. - Un metodo pubblico generico
AppendFormatted
con la firma:public void AppendFormatted<T>(T t)
.
Internamente, il generatore crea la stringa formattata e mette a disposizione un membro per il client per recuperare tale stringa. Il codice seguente illustra un tipo di LogInterpolatedStringHandler
che soddisfa questi requisiti:
[InterpolatedStringHandler]
public struct LogInterpolatedStringHandler
{
// Storage for the built-up string
StringBuilder builder;
public LogInterpolatedStringHandler(int literalLength, int formattedCount)
{
builder = new StringBuilder(literalLength);
Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
}
public void AppendLiteral(string s)
{
Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
builder.Append(s);
Console.WriteLine($"\tAppended the literal string");
}
public void AppendFormatted<T>(T t)
{
Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");
builder.Append(t?.ToString());
Console.WriteLine($"\tAppended the formatted object");
}
internal string GetFormattedText() => builder.ToString();
}
È ora possibile aggiungere un overload a LogMessage
nella classe Logger
per provare il nuovo gestore di stringhe interpolate:
public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
Non è necessario rimuovere il metodo LogMessage
originale, il compilatore preferisce un metodo con un parametro del gestore interpolato su un metodo con un parametro string
quando l'argomento è un'espressione stringa interpolata.
È possibile verificare che il nuovo gestore venga richiamato usando il codice seguente come programma principale:
var logger = new Logger() { EnabledLevel = LogLevel.Warning };
var time = DateTime.Now;
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time}. This won't be printed.");
logger.LogMessage(LogLevel.Warning, "Warning Level. This warning is a string, not an interpolated string expression.");
L'esecuzione dell'applicazione produce un output simile al testo seguente:
literal length: 65, formattedCount: 1
AppendLiteral called: {Error Level. CurrentTime: }
Appended the literal string
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
Appended the formatted object
AppendLiteral called: {. This is an error. It will be printed.}
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
AppendLiteral called: {Trace Level. CurrentTime: }
Appended the literal string
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
Appended the formatted object
AppendLiteral called: {. This won't be printed.}
Appended the literal string
Warning Level. This warning is a string, not an interpolated string expression.
Tracciando l'output, è possibile vedere in che modo il compilatore aggiunge codice per chiamare il gestore e compilare la stringa:
- Il compilatore aggiunge una chiamata per creare il gestore, passando la lunghezza totale del testo letterale nella stringa di formato e il numero di segnaposto.
- Il compilatore aggiunge chiamate a
AppendLiteral
eAppendFormatted
per ogni sezione della stringa letterale e per ogni segnaposto. - Il compilatore richiama il metodo
LogMessage
utilizzando ilCoreInterpolatedStringHandler
come argomento.
Si noti infine che l'ultimo avviso non richiama il gestore di stringhe interpolato. L'argomento è un string
, quindi la chiamata invoca l'altro overload con un parametro stringa.
Importante
Usare ref struct
per i gestori di stringhe interpolati solo se assolutamente necessario. L'uso di ref struct
avrà limitazioni perché devono essere archiviate nello stack. Ad esempio, non funzioneranno se un foro di stringa interpolato contiene un'espressione await
perché il compilatore dovrà archiviare il gestore nell'implementazione del IAsyncStateMachine
generata dal compilatore.
Aggiungere altre funzionalità al gestore
La versione precedente del gestore di stringhe interpolate implementa il modello. Per evitare di elaborare ogni espressione segnaposto, sono necessarie ulteriori informazioni nella funzione di gestione. In questa sezione si migliora il gestore in modo che funzioni meno quando la stringa costruita non viene scritta nel log. Si usa System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute per specificare una mappatura tra i parametri di un'API pubblica e i parametri del costruttore di un gestore. Che fornisce al gestore le informazioni necessarie per determinare se la stringa interpolata deve essere valutata.
Iniziamo con le modifiche apportate al gestore. Per prima cosa, aggiungere un campo per tenere traccia di se il gestore è abilitato. Aggiungere due parametri al costruttore: uno per specificare il livello di log per questo messaggio e l'altro un riferimento all'oggetto log:
private readonly bool enabled;
public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel logLevel)
{
enabled = logger.EnabledLevel >= logLevel;
builder = new StringBuilder(literalLength);
Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
}
Successivamente, usare il campo in modo che il gestore accoda solo valori letterali o oggetti formattati quando verrà usata la stringa finale:
public void AppendLiteral(string s)
{
Console.WriteLine($"\tAppendLiteral called: {{{s}}}");
if (!enabled) return;
builder.Append(s);
Console.WriteLine($"\tAppended the literal string");
}
public void AppendFormatted<T>(T t)
{
Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");
if (!enabled) return;
builder.Append(t?.ToString());
Console.WriteLine($"\tAppended the formatted object");
}
Successivamente, è necessario aggiornare la dichiarazione di LogMessage
in modo che il compilatore passi i parametri aggiuntivi al costruttore dell'handler. Viene gestito utilizzando il System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute nell'argomento del gestore:
public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
Questo attributo specifica l'elenco di argomenti per LogMessage
che vengono mappati sui parametri che seguono i parametri richiesti literalLength
e formattedCount
. La stringa vuota (""), specifica il ricevitore. Il compilatore sostituisce il valore dell'oggetto Logger
rappresentato da this
come argomento successivo nel costruttore del gestore. Il compilatore sostituisce il valore di level
per l'argomento seguente. È possibile fornire qualsiasi numero di argomenti per ogni gestore che scrivi. Gli argomenti aggiunti sono argomenti stringa.
È possibile eseguire questa versione usando lo stesso codice di test. Questa volta vengono visualizzati i risultati seguenti:
literal length: 65, formattedCount: 1
AppendLiteral called: {Error Level. CurrentTime: }
Appended the literal string
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
Appended the formatted object
AppendLiteral called: {. This is an error. It will be printed.}
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
AppendLiteral called: {Trace Level. CurrentTime: }
AppendFormatted called: {10/20/2021 12:19:10 PM} is of type System.DateTime
AppendLiteral called: {. This won't be printed.}
Warning Level. This warning is a string, not an interpolated string expression.
È possibile notare che vengono chiamati i metodi AppendLiteral
e AppendFormat
, ma non funzionano. Il gestore ha determinato che la stringa finale non è necessaria, quindi il gestore non lo compila. Ci sono ancora un paio di miglioramenti da apportare.
In primo luogo, è possibile aggiungere un overload di AppendFormatted
che vincola l'argomento a un tipo che implementa System.IFormattable. Questo overload consente ai chiamanti di aggiungere stringhe di formato nei campi segnaposto. Mentre si apporta questa modifica, si modificherà anche il tipo restituito degli altri metodi AppendFormatted
e AppendLiteral
, da void
a bool
(se uno di questi metodi ha tipi restituiti diversi, viene visualizzato un errore di compilazione). Questa modifica abilita il cortocircuito di . I metodi restituiscono false
per indicare che l'elaborazione dell'espressione stringa interpolata deve essere arrestata. La restituzione del valore true
indica che l'operazione deve continuare. In questo esempio viene usato per interrompere l'elaborazione quando la stringa risultante non è necessaria. Il corto circuito supporta azioni più granulari. È possibile interrompere l'elaborazione dell'espressione una volta raggiunta una determinata lunghezza, per supportare buffer a lunghezza fissa. In alternativa, alcune condizioni potrebbero indicare che gli elementi rimanenti non sono necessari.
public void AppendFormatted<T>(T t, string format) where T : IFormattable
{
Console.WriteLine($"\tAppendFormatted (IFormattable version) called: {t} with format {{{format}}} is of type {typeof(T)},");
builder.Append(t?.ToString(format, null));
Console.WriteLine($"\tAppended the formatted object");
}
Inoltre, è possibile specificare stringhe di formato nell'espressione stringa interpolata:
var time = DateTime.Now;
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time}. The time doesn't use formatting.");
logger.LogMessage(LogLevel.Error, $"Error Level. CurrentTime: {time:t}. This is an error. It will be printed.");
logger.LogMessage(LogLevel.Trace, $"Trace Level. CurrentTime: {time:t}. This won't be printed.");
Il :t
nel primo messaggio specifica il "formato orario breve" per l'ora attuale. Nell'esempio precedente è stato illustrato uno degli overload per il metodo AppendFormatted
che è possibile creare per il gestore. Non è necessario specificare un argomento generico per l'oggetto formattato. Potrebbero essere disponibili modi più efficienti per convertire i tipi creati in stringa. È possibile scrivere overload di AppendFormatted
che accetta tali tipi anziché un argomento generico. Il compilatore seleziona l'overload migliore. Il runtime usa questa tecnica per convertire System.Span<T> in stringa di output. È possibile aggiungere un parametro integer per specificare l'allineamento dell'output, con o senza un IFormattable. Il System.Runtime.CompilerServices.DefaultInterpolatedStringHandler fornito con .NET 6 contiene nove sovraccarichi di AppendFormatted per usi diversi. È possibile usarlo come riferimento durante la creazione di un gestore per i tuoi scopi.
Esegui l'esempio ora e noterai che per il messaggio di Trace
viene chiamato solo il primo AppendLiteral
:
literal length: 60, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted called: 10/20/2021 12:18:29 PM is of type System.DateTime
Appended the formatted object
AppendLiteral called: . The time doesn't use formatting.
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:18:29 PM. The time doesn't use formatting.
literal length: 65, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted (IFormattable version) called: 10/20/2021 12:18:29 PM with format {t} is of type System.DateTime,
Appended the formatted object
AppendLiteral called: . This is an error. It will be printed.
Appended the literal string
Error Level. CurrentTime: 12:18 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
AppendLiteral called: Trace Level. CurrentTime:
Warning Level. This warning is a string, not an interpolated string expression.
È possibile eseguire un ultimo aggiornamento al costruttore del gestore che migliora l'efficienza. Il gestore può aggiungere un parametro out bool
finale. L'impostazione del parametro su false
indica che il gestore non deve essere chiamato affatto per elaborare l'espressione stringa interpolata:
public LogInterpolatedStringHandler(int literalLength, int formattedCount, Logger logger, LogLevel level, out bool isEnabled)
{
isEnabled = logger.EnabledLevel >= level;
Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");
builder = isEnabled ? new StringBuilder(literalLength) : default!;
}
Questa modifica significa che è possibile rimuovere il campo enabled
. È quindi possibile modificare il tipo restituito di AppendLiteral
e AppendFormatted
in void
.
Ora, quando si esegue l'esempio, viene visualizzato l'output seguente:
literal length: 60, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted called: 10/20/2021 12:19:10 PM is of type System.DateTime
Appended the formatted object
AppendLiteral called: . The time doesn't use formatting.
Appended the literal string
Error Level. CurrentTime: 10/20/2021 12:19:10 PM. The time doesn't use formatting.
literal length: 65, formattedCount: 1
AppendLiteral called: Error Level. CurrentTime:
Appended the literal string
AppendFormatted (IFormattable version) called: 10/20/2021 12:19:10 PM with format {t} is of type System.DateTime,
Appended the formatted object
AppendLiteral called: . This is an error. It will be printed.
Appended the literal string
Error Level. CurrentTime: 12:19 PM. This is an error. It will be printed.
literal length: 50, formattedCount: 1
Warning Level. This warning is a string, not an interpolated string expression.
L'unico output quando si specifica LogLevel.Trace
è quello del costruttore. Il gestore ha indicato che non è abilitato, quindi nessuno dei metodi Append
è stato invocato.
Questo esempio illustra un punto importante per i gestori di stringhe interpolati, soprattutto quando vengono usate le librerie di registrazione. Eventuali effetti collaterali nei segnaposto potrebbero non verificarsi. Aggiungere il codice seguente al programma principale e vedere questo comportamento in azione:
int index = 0;
int numberOfIncrements = 0;
for (var level = LogLevel.Critical; level <= LogLevel.Trace; level++)
{
Console.WriteLine(level);
logger.LogMessage(level, $"{level}: Increment index a few times {index++}, {index++}, {index++}, {index++}, {index++}");
numberOfIncrements += 5;
}
Console.WriteLine($"Value of index {index}, value of numberOfIncrements: {numberOfIncrements}");
È possibile notare che la variabile index
viene incrementata cinque volte ogni iterazione del ciclo. Poiché i segnaposto sono valutati solo ai livelli Critical
, Error
e Warning
, e non ai livelli Information
e Trace
, il valore finale di index
non soddisfa le attese.
Critical
Critical: Increment index a few times 0, 1, 2, 3, 4
Error
Error: Increment index a few times 5, 6, 7, 8, 9
Warning
Warning: Increment index a few times 10, 11, 12, 13, 14
Information
Trace
Value of index 15, value of numberOfIncrements: 25
I gestori di stringhe interpolati offrono un maggiore controllo sulla modalità di conversione di un'espressione stringa interpolata in una stringa. Il team di runtime .NET ha usato questa funzionalità per migliorare le prestazioni in diverse aree. È possibile usare la stessa funzionalità nelle proprie librerie. Per esplorare ulteriormente, esaminare il System.Runtime.CompilerServices.DefaultInterpolatedStringHandler. Offre un'implementazione più completa rispetto a quella compilata qui. Vengono visualizzati molti altri overload possibili per i metodi di Append
.