Tutorial: Escritura de un controlador de interpolación de cadenas personalizado
En este tutorial, aprenderá a:
- Implementación del patrón de controlador de interpolación de cadenas
- Interactúe con el receptor en una operación de interpolación de cadenas.
- Adición de argumentos al controlador de interpolación de cadenas
- Descripción de las nuevas características de biblioteca para la interpolación de cadenas
Prerrequisitos
Debe configurar la máquina para ejecutar .NET. El compilador de C# está disponible con de Visual Studio 2022 o el SDK de .NET de .
En este tutorial se da por supuesto que está familiarizado con C# y .NET, incluido Visual Studio o la CLI de .NET.
Puede escribir un controlador personalizado de cadenas interpoladas . Un controlador de cadenas interpoladas es un tipo que procesa la expresión del marcador de posición en una cadena interpolada. Sin un controlador personalizado, los marcadores de posición se procesan de forma similar a String.Format. Cada marcador de posición tiene el formato de texto y, a continuación, los componentes se concatenan para formar la cadena resultante.
Puede escribir un controlador para cualquier escenario en el que use información sobre la cadena resultante. ¿Se usará? ¿Qué restricciones tienen el formato? Algunos ejemplos son:
- Es posible que no sea necesario que ninguna de las cadenas resultantes sea mayor que un límite, como 80 caracteres. Puede procesar las cadenas interpoladas para rellenar un búfer de longitud fija y detener el procesamiento una vez alcanzada la longitud del búfer.
- Es posible que tenga un formato tabular y cada marcador de posición debe tener una longitud fija. Un controlador personalizado puede exigirlo, en lugar de obligar a que todo el código del cliente se ajuste.
En este tutorial, creará un controlador de interpolación de cadenas para uno de los escenarios principales de rendimiento: bibliotecas de registro. Según el nivel de registro configurado, el trabajo para construir un mensaje de registro no es necesario. Si el registro está desactivado, no es necesario el proceso de formar una cadena a partir de una expresión de cadena interpolada. El mensaje nunca se imprime, por lo que se puede omitir cualquier concatenación de cadenas. Además, no es necesario realizar las expresiones usadas en los marcadores de posición, incluida la generación de seguimientos de pila.
Un controlador de cadenas interpolado puede determinar si se usará la cadena con formato y solo realizar el trabajo necesario si es necesario.
Implementación inicial
Comencemos desde una clase de Logger
básica que admite distintos niveles:
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);
}
}
Este Logger
admite seis niveles diferentes. Cuando un mensaje no pasa el filtro de nivel de registro, no hay ninguna salida. La API pública del registrador acepta una cadena (con formato completo) como mensaje. Ya se ha realizado todo el trabajo para crear la cadena.
Implementación del patrón del controlador
Este paso consiste en crear un controlador de cadenas interpolado que vuelva a crear el comportamiento actual. Un controlador de cadenas interpolado es un tipo que debe tener las siguientes características:
- Tener aplicado System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute al tipo.
- Constructor que tiene dos parámetros
int
,literalLength
yformattedCount
. (Se permiten más parámetros). - Un método
AppendLiteral
público con la signaturapublic void AppendLiteral(string s)
. - Un método
AppendFormatted
púbico genérico con la signaturapublic void AppendFormatted<T>(T t)
.
Internamente, el compilador crea la cadena con formato y proporciona un miembro para que un cliente recupere esa cadena. El código siguiente muestra un tipo de LogInterpolatedStringHandler
que cumple estos requisitos:
[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();
}
Ahora puede agregar una sobrecarga a LogMessage
en la clase Logger
para probar el nuevo controlador de cadenas interpoladas:
public void LogMessage(LogLevel level, LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
No es necesario quitar el método LogMessage
original, el compilador prefiere un método con un parámetro de controlador interpolado sobre un método con un parámetro string
cuando el argumento es una expresión de cadena interpolada.
Puede comprobar que se invoca el nuevo controlador mediante el código siguiente como programa principal:
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.");
La ejecución de la aplicación genera una salida similar al texto siguiente:
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.
Al rastrear la salida, puede ver cómo el compilador añade código para llamar al controlador y construir la cadena:
- El compilador agrega una llamada para construir el controlador y pasa la longitud total del texto literal en la cadena de formato y el número de marcadores de posición.
- El compilador agrega llamadas a
AppendLiteral
yAppendFormatted
para cada sección de la cadena literal y para cada marcador de posición. - El compilador invoca el método
LogMessage
mediante elCoreInterpolatedStringHandler
como argumento.
Por último, observe que la última advertencia no invoca el controlador de cadenas interpoladas. El argumento es un valor string
, por lo que la llamada invoca a la otra sobrecarga con un parámetro de cadena.
Importante
Use ref struct
para controladores de cadenas interpolados solo si es absolutamente necesario. El uso de ref struct
tendrá limitaciones, dado que deben ser almacenados en la pila. Por ejemplo, no funcionarán si un agujero de cadena interpolado contiene una expresión de await
porque el compilador tendrá que almacenar el controlador en la implementación del IAsyncStateMachine
generado por el compilador.
Adición de más funcionalidades al controlador
La versión anterior del controlador de cadenas interpoladas implementa el patrón . Para evitar el procesamiento de cada expresión de marcador de posición, necesita más información en el controlador. En esta sección, mejora el controlador para que haga menos trabajo cuando la cadena construida no está escrita en el registro. Utilizas System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute para especificar un mapeo entre los parámetros de una API pública y los parámetros del constructor de un controlador. Esto proporciona al controlador la información necesaria para determinar si se debe evaluar la cadena interpolada.
Comencemos con los cambios en el controlador. En primer lugar, agregue un campo para realizar un seguimiento si el controlador está habilitado. Agregue dos parámetros al constructor: uno para especificar el nivel de registro de este mensaje y el otro una referencia al objeto de registro:
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}");
}
A continuación, use el campo para que el controlador solo anexe literales o objetos con formato cuando se use la cadena final:
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");
}
A continuación, debe actualizar la declaración LogMessage
para que el compilador pase los parámetros adicionales al constructor del controlador. Esto se controla mediante el System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute en el argumento del controlador:
public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.GetFormattedText());
}
Este atributo especifica la lista de argumentos para LogMessage
que se asignan a los parámetros que siguen los parámetros literalLength
y formattedCount
necesarios. La cadena vacía (""), especifica el receptor. El compilador sustituye el valor del objeto Logger
representado por this
para el siguiente argumento al constructor del controlador. El compilador sustituye el valor de level
por el argumento siguiente. Puede proporcionar cualquier número de argumentos para cualquier controlador que escriba. Los argumentos que agregue son argumentos de cadena.
Puede ejecutar esta versión con el mismo código de prueba. Esta vez, verá los siguientes resultados:
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.
Puede ver que se llama a los métodos AppendLiteral
y AppendFormat
, pero no están haciendo nada. El controlador determinó que la cadena final no es necesaria, por lo que el controlador no lo compila. Todavía hay un par de mejoras para realizar.
En primer lugar, puede agregar una sobrecarga de AppendFormatted
que restrinja el argumento a un tipo que implementa System.IFormattable. Esta sobrecarga permite a los autores de llamada agregar cadenas de formato a los marcadores de posición. Al realizar este cambio, también vamos a cambiar el tipo de valor devuelto de los otros métodos de AppendFormatted
y AppendLiteral
, de void
a bool
(si alguno de estos métodos tiene tipos de valor devuelto diferentes, obtendrá un error de compilación). Ese cambio permite el cortocircuito. Los métodos devuelven false
para indicar que se debe detener el procesamiento de la expresión de cadena interpolada. Al devolver true
se indica que se debe continuar. En este ejemplo, se usa para detener el procesamiento cuando no se necesita la cadena resultante. El cortocircuito admite acciones más específicas. Puede detener el procesamiento de la expresión una vez que alcanza una longitud determinada, para admitir búferes de longitud fija. O bien, alguna condición podría indicar que no se necesitan elementos restantes.
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");
}
Con esa adición, puede especificar cadenas de formato en la expresión de cadena interpolada:
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.");
El :t
del primer mensaje especifica el "formato de tiempo corto" para la hora actual. En el ejemplo anterior se mostraba una de las sobrecargas al método AppendFormatted
que puede crear para el controlador. No es necesario especificar un argumento genérico para el objeto al que se va a dar formato. Es posible que tenga formas más eficaces de convertir los tipos que usted crea a cadena de texto. Puede escribir sobrecargas de AppendFormatted
que toma esos tipos en lugar de un argumento genérico. El compilador elige la mejor sobrecarga. El entorno de ejecución utiliza esta técnica para convertir System.Span<T> en una cadena de caracteres. Puede agregar un parámetro entero para especificar la alineación de la salida, con o sin un IFormattable. El elemento System.Runtime.CompilerServices.DefaultInterpolatedStringHandler que se incluye con .NET 6 contiene nueve sobrecargas de AppendFormatted para distintos usos. Puede usarlo como referencia al crear un controlador para sus fines.
Ejecute el ejemplo ahora y verá que, para el mensaje Trace
, solo se llama al primer 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.
Puede realizar una actualización final del constructor del controlador que mejore la eficacia. El controlador puede agregar un parámetro final out bool
. Establecer ese parámetro en false
indica que no se debe llamar al controlador en absoluto para procesar la expresión de cadena interpolada:
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!;
}
Ese cambio significa que puede quitar el campo enabled
. A continuación, puede cambiar el tipo de valor devuelto de AppendLiteral
y AppendFormatted
a void
.
Ahora, al ejecutar el ejemplo, verá la siguiente salida:
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.
La única salida cuando se especificó LogLevel.Trace
es la salida del constructor. El controlador indicó que no está habilitado, por lo que no se invocó ninguno de los métodos Append
.
En este ejemplo se muestra un punto importante para los controladores de cadenas interpolados, especialmente cuando se usan bibliotecas de registro. Es posible que no se produzcan efectos secundarios en los marcadores de posición. Agregue el código siguiente al programa principal y vea este comportamiento en acción:
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}");
Puede ver que la variable index
se incrementa cinco veces cada iteración del bucle. Dado que los marcadores de posición solo se evalúan para los niveles Critical
, Error
y Warning
, no para Information
y Trace
, el valor final de index
no cumple la expectativa:
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
Los controladores de cadenas interpoladas proporcionan un mayor control sobre cómo se convierte una expresión de cadena interpolada en una cadena. El equipo en tiempo de ejecución de .NET usó esta característica para mejorar el rendimiento en varias áreas. Puede usar la misma funcionalidad en sus propias bibliotecas. Para explorar más a fondo, examine System.Runtime.CompilerServices.DefaultInterpolatedStringHandler, Proporciona una implementación más completa de la que ha creado aquí. Verá muchas más sobrecargas que son posibles para los métodos Append
.