Nota
O acceso a esta páxina require autorización. Pode tentar iniciar sesión ou modificar os directorios.
O acceso a esta páxina require autorización. Pode tentar modificar os directorios.
En este tutorial, aprenderá a:
- Implemente el patrón de controlador de interpolación de cadenas.
- Interactúe con el receptor en una operación de interpolación de cadenas.
- Agregue argumentos al controlador de interpolación de cadenas.
- Comprenda las nuevas características de biblioteca para la interpolación de cadenas.
Prerrequisitos
Configure la máquina para ejecutar .NET. El compilador de C# está disponible a través de Visual Studio o el SDK de .NET.
En este tutorial se da por supuesto que está familiarizado con C# y .NET, incluidos Visual Studio o Visual Studio Code y C# DevKit.
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, el sistema procesa marcadores de posición similares 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. Considere preguntas como: ¿Se usa? ¿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 aplicar esa restricción, en lugar de forzar que todo el código de 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 ninguna de las expresiones utilizadas en los marcadores de posición, incluidas las trazas de pila.
Un controlador de cadenas interpolado puede determinar si se usa la cadena con formato y solo realizar el trabajo necesario si es necesario.
Implementación inicial
Comience con una clase básica Logger que admita diferentes 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, el registrador no genera ninguna salida. La API pública del registrador acepta una cadena con formato completo como mensaje. El autor de la llamada realiza todo el trabajo para crear la cadena.
Implementación del patrón del controlador
En este paso, creará un controlador de cadenas interpolado que vuelve 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,literalLengthyformattedCount. (Se permiten más parámetros). - Un método
AppendLiteralpúblico con la signaturapublic void AppendLiteral(string s). - Un método
AppendFormattedpúbico genérico con la signaturapublic void AppendFormatted<T>(T t).
Internamente, el generador crea la cadena con formato y proporciona un elemento para que un cliente pueda recuperarla. 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");
}
public override string ToString() => builder.ToString();
}
Nota:
Cuando la expresión de cadena interpolada es una constante en tiempo de compilación (es decir, no tiene marcadores de posición), el compilador usa el tipo string de destino en lugar de invocar un controlador de cadena interpolado personalizado. Este comportamiento significa que las cadenas interpoladas constantes omiten completamente los controladores personalizados.
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.ToString());
}
No es necesario quitar el método original LogMessage . Cuando el argumento es una expresión de cadena interpolada, el compilador prefiere un método con un parámetro de controlador interpolado sobre un método con un string parámetro .
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
AppendLiteralyAppendFormattedpara cada sección de la cadena literal y para cada marcador de posición. - El compilador invoca el método
LogMessagemediante elCoreInterpolatedStringHandlercomo 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.
ref struct los tipos tienen limitaciones, ya que deben almacenarse en la pila. Por ejemplo, no funcionan si un hueco de cadena interpolada contiene una await expresión porque el compilador debe almacenar el controlador en la implementación generada por el compilador IAsyncStateMachine.
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. Esa asignación proporciona al controlador la información necesaria para determinar si se debe evaluar la cadena interpolada.
Comience con 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, actualice la LogMessage declaración para que el compilador pase los parámetros adicionales al constructor del controlador. Controle este paso mediante el uso del System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute argumento del controlador:
public void LogMessage(LogLevel level, [InterpolatedStringHandlerArgument("", "level")] LogInterpolatedStringHandler builder)
{
if (EnabledLevel < level) return;
Console.WriteLine(builder.ToString());
}
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.
Nota:
Si la InterpolatedStringHandlerArgumentAttribute lista de argumentos del constructor está vacía, el comportamiento es el mismo que si el atributo se omitió por completo.
Puede ejecutar esta versión mediante 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.
Verás que los métodos AppendLiteral y AppendFormat son llamados, pero no realizan ningún trabajo. 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, cambie también el tipo de valor devuelto de los otros AppendFormatted métodos 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");
}
public void AppendFormatted<T>(T t, int alignment, string format) where T : IFormattable
{
Console.WriteLine($"\tAppendFormatted (IFormattable version) called: {t} with alignment {alignment} and format {{{format}}} is of type {typeof(T)},");
var formatString =$"{alignment}:{format}";
builder.Append(string.Format($"{{0,{formatString}}}", t));
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 invoca ninguno de los Append métodos.
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 {index++}");
numberOfIncrements++;
}
Console.WriteLine($"Value of index {index}, value of numberOfIncrements: {numberOfIncrements}");
Puede ver que la index variable se incrementa cada iteración del bucle. Dado que los marcadores de posición solo se evalúan para los niveles Critical, Error y Warning, y no para Information y Trace, el valor final de index no coincide con la expectativa.
Critical
Critical: Increment index 0
Error
Error: Increment index 1
Warning
Warning: Increment index 2
Information
Trace
Value of index 3, value of numberOfIncrements: 5
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.