Compartir a través de


Cadenas interpoladas mejoradas

Nota:

Este artículo es una especificación de características. La especificación actúa como documento de diseño de la característica. Incluye cambios de especificación propuestos, junto con la información necesaria durante el diseño y el desarrollo de la característica. Estos artículos se publican hasta que se finalizan los cambios de especificación propuestos y se incorporan en la especificación ECMA actual.

Puede haber algunas discrepancias entre la especificación de características y la implementación completada. Esas diferencias se recogen en las notas de la reunión de diseño de idioma (LDM) pertinentes.

Puede obtener más información sobre el proceso de adopción de especificaciones de características en el estándar del lenguaje C# en el artículo sobre las especificaciones.

Problema planteado por el experto: https://github.com/dotnet/csharplang/issues/4487

Resumen

Presentamos un nuevo patrón para crear y usar expresiones de cadena interpoladas para permitir un formato eficaz y usar en escenarios generales string y escenarios más especializados, como marcos de registro, sin incurrir en asignaciones innecesarias de aplicar formato a la cadena en el marco.

Motivación

En la actualidad, la interpolación de cadenas se reduce principalmente a una llamada a string.Format. Esto, aunque de uso general, puede ser ineficaz por varias razones:

  1. Se muestran los argumentos de estructura, a menos que el runtime haya introducido una sobrecarga de string.Format que toma exactamente los tipos correctos de argumentos en el orden correcto.
    • Este orden es el motivo por el que el tiempo de ejecución es indecente para introducir versiones genéricas del método, ya que provocaría una explosión combinatoria de instancias genéricas de un método muy común.
  2. Tiene que asignar una matriz para los argumentos en la mayoría de los casos.
  3. No hay ninguna oportunidad de evitar la creación de instancias de la instancia si no es necesario. Por ejemplo, los marcos de registro recomendarán evitar la interpolación de cadenas porque hará que se realice una cadena que puede no ser necesaria, en función del nivel de registro actual de la aplicación.
  4. Nunca puede usar Span u otros tipos de estructura ref hoy en día, ya que las estructuras ref no se permiten como parámetros de tipo genérico, lo que significa que si un usuario quiere evitar copiar en ubicaciones intermedias que tienen que dar formato manual a cadenas.

Internamente, el tiempo de ejecución tiene un tipo llamado ValueStringBuilder para ayudar a tratar los primeros 2 de estos escenarios. Pasan un búfer stackalloc'd al generador, llaman AppendFormat repetidamente con cada parte y, a continuación, obtienen una cadena final. Si la cadena resultante va más allá de los límites del búfer de pila, pueden pasar a una matriz en el montón. Sin embargo, este tipo es peligroso exponer directamente, ya que el uso incorrecto podría dar lugar a que una matriz alquilada se elimine doblemente, lo que provocará todo tipo de comportamiento indefinido en el programa, ya que dos ubicaciones creen que tienen acceso único a la matriz alquilada. Esta propuesta crea una manera de usar este tipo de forma segura desde código nativo de C# escribiendo un literal de cadena interpolada, dejando el código escrito sin cambios al mismo tiempo que mejora todas las cadenas interpoladas que escribe un usuario. También amplía este patrón para permitir que las cadenas interpoladas pasen como argumentos a otros métodos para usar un patrón de controlador, definido por el receptor del método, que permitirá que cosas como marcos de registro eviten asignar cadenas que nunca serán necesarias y dar a los usuarios de C# una sintaxis de interpolación cómoda y familiar.

Diseño detallado

Patrón de controlador

Presentamos un nuevo patrón de controlador que puede representar una cadena interpolada que se pasa como argumento a un método. El inglés sencillo del patrón es el siguiente:

Cuando se pasa un interpolated_string_expression como argumento a un método, examinamos el tipo del parámetro . Si el tipo de parámetro tiene un constructor que se puede invocar con 2 parámetros int yliteralLength, opcionalmente, formattedCount toma parámetros adicionales especificados por un atributo en el parámetro original, opcionalmente tiene un parámetro final booleano out y el tipo del parámetro original tiene instancias AppendLiteral y AppendFormatted métodos que se pueden invocar para cada parte de la cadena interpolada, a continuación, reducemos la interpolación con eso, en lugar de en una llamada tradicional a string.Format(formatStr, args). Un ejemplo más concreto es útil para imaginar esto:

// 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);

Aquí, dado TraceLoggerParamsInterpolatedStringHandler que tiene un constructor con los parámetros correctos, decimos que la cadena interpolada tiene una conversión implícita del controlador a ese parámetro y se reduce al patrón mostrado anteriormente. La especificación necesaria para esto es un poco complicada y se expande a continuación.

El resto de esta propuesta usará Append... para referirse a cualquiera de o AppendLiteralAppendFormatted en los casos en que ambos sean aplicables.

Nuevos atributos

El compilador reconoce :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()
        {
        }
    }
}

El compilador usa este atributo para determinar si un tipo es un tipo de controlador de cadenas interpolado válido.

El compilador también reconoce :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; }
    }
}

Este atributo se usa en parámetros para informar al compilador de cómo reducir un patrón de controlador de cadenas interpolado usado en una posición de parámetro.

Conversión de controlador de cadenas interpoladas

Se dice que el tipo T es un applicable_interpolated_string_handler_type si tiene el atributo System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute. Existe un interpolated_string_handler_conversion implícito a T desde un interpolated_string_expression, o un additive_expression compuesto completamente por _interpolated_string_expression_s y usando solo + operadores.

Por motivos de simplicidad en el resto de este speclet, interpolated_string_expression hace referencia tanto a un interpolated_string_expression simple como a un additive_expression compuesto por completo de _interpolated_string_expression_s y usando solo + operadores.

Tenga en cuenta que esta conversión siempre existe, independientemente de si habrá errores posteriores al intentar reducir realmente la interpolación mediante el patrón de controlador. Esto se hace para ayudar a garantizar que hay errores predecibles y útiles y que el comportamiento en tiempo de ejecución no cambia en función del contenido de una cadena interpolada.

Ajustes de miembro de función aplicables

Ajustamos el texto del algoritmo de miembro de función aplicable (§12.6.4.2) como se indica a continuación (se agrega una nueva sub viñeta a cada sección, en negrita):

Se dice que un miembro de función es un miembro de función aplicable con respecto a una lista de argumentos A cuando se cumplen todas las condiciones siguientes:

  • Cada argumento de A corresponde a un parámetro de la declaración de miembro de función tal como se describe en Parámetros correspondientes (§12.6.2.2) y cualquier parámetro al que no se corresponda ningún argumento es un parámetro opcional.
  • Para cada argumento de A, el modo de paso de parámetros del argumento (es decir, valor, refo out) es idéntico al modo de paso de parámetros del parámetro correspondiente y
    • para un parámetro de valor o una matriz de parámetros, existe una conversión implícita (§10.2) del argumento al tipo del parámetro correspondiente o
    • para un ref parámetro cuyo tipo es un tipo de estructura, existe un interpolated_string_handler_conversion implícito desde el argumento hasta el tipo del parámetro correspondiente o
    • para un ref parámetro o out , el tipo del argumento es idéntico al tipo del parámetro correspondiente. Después de todo, un ref parámetro o out es un alias para el argumento pasado.

Para un miembro de función que incluye una matriz de parámetros, si el miembro de función es aplicable según las reglas anteriores, se dice que es aplicable en su forma normal. Si un miembro de función que incluye una matriz de parámetros no es aplicable en su forma normal, el miembro de función puede ser aplicable en su forma expandida:

  • La forma expandida se construye sustituyendo la matriz de parámetros en la declaración del miembro de la función por cero o más parámetros de valor del tipo de elemento de la matriz de parámetros de forma que el número de argumentos de la lista de argumentos A coincida con el número total de parámetros. Si A tiene menos argumentos que el número de parámetros fijos en la declaración del miembro funcional, la forma expandida del miembro funcional no puede construirse y, por tanto, no es aplicable.
  • De lo contrario, el formulario expandido es aplicable si para cada argumento del A modo de paso de parámetros del argumento es idéntico al modo de paso de parámetros del parámetro correspondiente y
    • para un parámetro de valor fijo o un parámetro de valor creado por la expansión, existe una conversión implícita (§10.2) del tipo del argumento al tipo del parámetro correspondiente o
    • para un ref parámetro cuyo tipo es un tipo de estructura, existe un interpolated_string_handler_conversion implícito desde el argumento hasta el tipo del parámetro correspondiente o
    • para un ref parámetro o out , el tipo del argumento es idéntico al tipo del parámetro correspondiente.

Nota importante: esto significa que si hay 2 sobrecargas por lo demás equivalentes, que solo difieren según el tipo de applicable_interpolated_string_handler_type, estas sobrecargas se considerarán ambiguas. Además, dado que no se ven a través de conversiones explícitas, es posible que se produzca un escenario irresoluble en el que las sobrecargas aplicables usen InterpolatedStringHandlerArguments y sean totalmente incallables sin realizar manualmente el patrón de reducción del controlador. Podríamos realizar cambios en el algoritmo miembro de función mejor para resolverlo si lo eligemos, pero es poco probable que se produzca este escenario y no sea una prioridad abordar.

Mejor conversión de ajustes de expresiones

Cambiamos la mejor conversión de la sección expresión (§12.6.4.5) a la siguiente:

Dada una conversión implícita C1 que convierte de una expresión E a un tipo T1y una conversión implícita C2 que convierte de una expresión E a un tipo T2, C1 es una conversión mejor que C2 si:

  1. E es un interpolated_string_expression, no constante C1 es un implicit_string_handler_conversion, T1 es un applicable_interpolated_string_handler_type y C2 no es un implicit_string_handler_conversion, o bien,
  2. E no coincide exactamente con T2 y se cumple al menos una de las siguientes condiciones:

Esto significa que hay algunas reglas de resolución de sobrecarga potencialmente no obvias, en función de si la cadena interpolada en cuestión es una expresión constante o no. Por ejemplo:

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

Esto se introduce para que las cosas que simplemente se puedan emitir como constantes lo hacen y no incurren en ninguna sobrecarga, mientras que las cosas que no pueden ser constantes usan el patrón de controlador.

InterpolatedStringHandler y Usage

Presentamos un nuevo tipo en System.Runtime.CompilerServices: DefaultInterpolatedStringHandler. Se trata de una estructura de referencia con muchas de las mismas semánticas que ValueStringBuilder, diseñadas para su uso directo por parte del compilador de C#. Esta estructura tendría un aspecto aproximado al siguiente:

// 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);
    }
}

Realizamos un pequeño cambio en las reglas para el significado de un interpolated_string_expression (§12.8.3):

Si el tipo de una cadena interpolada es string y el tipo System.Runtime.CompilerServices.DefaultInterpolatedStringHandler existe, y el contexto actual admite el uso de ese tipo, la cadenase reduce mediante el patrón de controlador. A continuación, se obtiene el valor final string mediante una llamada ToStringAndClear() al tipo de controlador.De lo contrario, si el tipo de una cadena interpolada es System.IFormattable o System.FormattableString [el resto no cambia]

La regla "y el contexto actual admiten el uso de ese tipo" es intencionadamente impreciso para dar al compilador margen para optimizar el uso de este patrón. Es probable que el tipo de controlador sea un tipo de estructura ref y los tipos de estructura ref no se permiten normalmente en métodos asincrónicos. En este caso concreto, el compilador podría usar el controlador si ninguno de los agujeros de interpolación contienen una await expresión, ya que podemos determinar estáticamente que el tipo de controlador se usa de forma segura sin análisis complicado adicional porque el controlador se quitará después de evaluar la expresión de cadena interpolada.

Abrir pregunta:

¿Queremos hacer que el compilador sepa DefaultInterpolatedStringHandler y omita completamente la string.Format llamada? Nos permitiría ocultar un método que no queremos colocar necesariamente en las caras de las personas cuando llaman manualmente a string.Format.

Respuesta: Sí.

Abrir pregunta:

¿Queremos tener controladores para System.IFormattable y System.FormattableString también?

Respuesta: No.

Código de patrón de controlador

En esta sección, la resolución de invocación de método hace referencia a los pasos enumerados en §12.8.10.2.

Resolución de constructores

Dado un tipo_de_controlador_de_cadenas_interpoladas_aplicableT y una expresión_de_cadena_interpoladai, la resolución y validación de invocación de métodos para un constructor válido en T se realiza de la siguiente manera:

  1. La búsqueda de miembros para constructores de instancia se realiza en T. El grupo de métodos resultante se denomina M.
  2. La lista A de argumentos se construye de la siguiente manera:
    1. Los dos primeros argumentos son constantes enteras, que representan la longitud literal de iy el número de componentes de interpolación en i, respectivamente.
    2. Si i se usa como argumento para algún parámetro pi en el método M1, y el parámetro pi se atribuye a System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute, para cada nombre Argx de la Arguments matriz de ese atributo, el compilador lo coincide con un parámetro px que tiene el mismo nombre. La cadena vacía coincide con el receptor de M1.
      • Si alguno Argx no puede coincidir con un parámetro de M1o solicita Argx al receptor de M1 y M1 es un método estático, se genera un error y no se realizan pasos adicionales.
      • De lo contrario, el tipo de cada resuelto px se agrega a la lista de argumentos, en el orden especificado por la Arguments matriz. Cada px se pasa con la misma ref semántica que se especifica en M1.
    3. El argumento final es , boolque se pasa como parámetro out .
  3. La resolución de invocación de método tradicional se realiza con el grupo M de métodos y la lista Ade argumentos . Para los fines de la validación final de la invocación de métodos, el contexto de M se trata como un member_access mediante el tipo T.
    • Si se encontró un constructor F único, el resultado de la resolución de sobrecarga es F.
    • Si no se encontraron constructores aplicables, se reintenta el paso 3, quitando el parámetro final bool de A. Si este reintento tampoco encuentra miembros aplicables, se produce un error y no se realizan pasos adicionales.
    • Si no se encontró ningún método único, el resultado de la resolución de sobrecarga es ambiguo, se produce un error y no se realizan más pasos.
  4. Se realiza la validación final en F .
    • Si se produjo algún elemento de A léxicamente después ide , se produce un error y no se realizan más pasos.
    • Si alguna A solicita al receptor de F, y F es un indexador que se usa como initializer_target en un member_initializer, se informa de un error, y no se realizan pasos adicionales.

Nota: la resolución aquí no usa intencionadamente las expresiones reales pasadas como otros argumentos para Argx los elementos. Solo se consideran los tipos posteriores a la conversión. Esto garantiza que no tenemos problemas de conversión doble o casos inesperados en los que una expresión lambda está enlazada a un tipo delegado cuando se pasa a M1 un tipo de delegado y se enlaza a otro tipo de delegado cuando se pasa a M.

Nota: Notificamos un error para los indexadores que usan como inicializadores de miembro debido al orden de evaluación de inicializadores de miembros anidados. Tenga en cuenta este fragmento de código:


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 { }
    }
}

Los argumentos que se van a __c1.C2[] evaluar antes del receptor del indexador. Aunque podríamos encontrar una reducción que funcione para este escenario (ya sea creando una temporal para __c1.C2 y compartiándola en ambas invocaciones de indexador, o usando solo para la primera invocación del indexador y compartiendo el argumento entre ambas invocaciones), creemos que cualquier reducción sería confusa para lo que creemos que es un escenario patológico. Por lo tanto, prohibimos completamente el escenario.

Abrir pregunta:

Si usamos un constructor en lugar de Create, mejoraríamos el código en tiempo de ejecución, a costa de restringir el patrón un poco.

Respuesta: Por ahora, restringiremos a los constructores. Podemos volver a consultar la adición de un método general Create más adelante si surge el escenario.

Append... resolución de sobrecarga del método

Dada una applicable_interpolated_string_handler_typeT y una interpolated_string_expressioni, la resolución de sobrecarga de un conjunto de métodos válidos Append... en T se realiza de la siguiente manera.

  1. Si hay algún componente de carácter_de_cadena_regular_interpolado en i:
    1. Se realiza la búsqueda de T miembros con el nombre AppendLiteral . El grupo de métodos resultante se denomina Ml.
    2. La lista Al de argumentos se construye con un parámetro de valor de tipo string.
    3. La resolución de invocación de método tradicional se realiza con el grupo Ml de métodos y la lista Alde argumentos . Para los fines de la validación final de invocación de método, el contexto de Ml se trata como un member_access mediante una instancia de T.
      • Si se encuentra un método Fi único y no se produjo ningún error, el resultado de la resolución de invocación de método es Fi.
      • De lo contrario, se notifica un error.
  2. Para cada componente de interpolaciónix de i:
    1. Se realiza la búsqueda de T miembros con el nombre AppendFormatted . El grupo de métodos resultante se denomina Mf.
    2. La lista Af de argumentos se construye:
      1. El primer parámetro es el expression de ix, pasado por valor.
      2. Si ix contiene directamente un componente de constant_expression , se agrega un parámetro de valor entero, con el nombre alignment especificado.
      3. Si ix va seguido directamente de un interpolation_format, se agrega un parámetro de valor de cadena, con el nombre format especificado.
    3. La resolución de invocación de método tradicional se realiza con el grupo Mf de métodos y la lista Afde argumentos . Para los fines de la validación final de invocación de método, el contexto de Mf se trata como un member_access mediante una instancia de T.
      • Si se encuentra un método Fi único, el resultado de la resolución de invocación de método es Fi.
      • De lo contrario, se notifica un error.
  3. Por último, para cada Fi detectado en los pasos 1 y 2, se realiza la validación final:
    • Si alguno Fi no devuelve bool por valor o void, se notifica un error.
    • Si todos Fi no devuelven el mismo tipo, se notifica un error.

Tenga en cuenta que estas reglas no permiten métodos de extensión para las Append... llamadas. Podríamos considerar la posibilidad de habilitarlo si se elige, pero esto es análogo al patrón de enumerador, donde se permite GetEnumerator ser un método de extensión, pero no Current o MoveNext().

Estas reglas permiten parámetros predeterminados para las Append... llamadas, que funcionarán con cosas como CallerLineNumber o CallerArgumentExpression (cuando sean compatibles con el idioma).

Tenemos reglas de búsqueda de sobrecarga independientes para elementos base frente a agujeros de interpolación, ya que algunos controladores querrán comprender la diferencia entre los componentes que se interpolaron y los componentes que formaron parte de la cadena base.

Abrir pregunta

Algunos escenarios, como el registro estructurado, quieren poder proporcionar nombres para los elementos de interpolación. Por ejemplo, hoy en día una llamada de registro podría ser similar Log("{name} bought {itemCount} items", name, items.Count);a . Los nombres dentro de proporcionan {} información de estructura importante para los registradores que ayudan a garantizar que la salida sea coherente y uniforme. Es posible que algunos casos puedan reutilizar el componente de un agujero de interpolación para esto, pero muchos registradores ya comprenden especificadores de formato y tienen un comportamiento existente para el :format formato de salida en función de esta información. ¿Hay alguna sintaxis que se puede usar para habilitar la colocación de estos especificadores con nombre?

Es posible que algunos casos puedan salir con CallerArgumentExpression, siempre que el soporte técnico llegue a C# 10. Pero en los casos que invocan un método o propiedad, puede que no sea suficiente.

Respuesta:

Aunque hay algunas partes interesantes para las cadenas con plantilla que podríamos explorar en una característica de lenguaje ortogonal, no creemos que una sintaxis específica aquí tenga muchas ventajas sobre las soluciones como el uso de una tupla: $"{("StructuredCategory", myExpression)}".

Realización de la conversión

Dado un applicable_interpolated_string_handler_type y una T que tienen un constructor válido y métodos resueltos, la reducción se realiza de la siguiente manera:

  1. Cualquier argumento a Fc que se produzca léxicamente antes i de evaluarse y almacenarse en variables temporales en orden léxico. Para conservar la ordenación léxica, si i se produjo como parte de una expresión emayor, también se evaluarán los componentes de e que se produjeron antes i , de nuevo en orden léxico.
  2. Fc se llama a con la longitud de los componentes literales de cadena interpolados, el número de orificios de interpolación , los argumentos evaluados previamente y un bool argumento out (si Fc se resolvió con uno como último parámetro). El resultado se almacena en un valor ibtemporal .
    1. La longitud de los componentes literales se calcula después de reemplazar cualquier open_brace_escape_sequence por un único {y cualquier close_brace_escape_sequence por un único }.
  3. Si Fc finalizó con un bool argumento out, se genera una comprobación de ese bool valor. Si es true, se llamará a los métodos de Fa . De lo contrario, no se llamará.
  4. Para cada Fax en Fa, Fax se llama a on ib con el componente literal actual o la expresión de interpolación , según corresponda. Si Fax devuelve un bool, el resultado se realiza de forma lógica y con todas las llamadas anteriores Fax .
    1. Si Fax es una llamada a AppendLiteral, el componente literal no se captura reemplazando cualquier open_brace_escape_sequence por un solo {y cualquier close_brace_escape_sequence por un solo }.
  5. El resultado de la conversión es ib.

De nuevo, tenga en cuenta que los argumentos pasados a Fc y que se pasan a e son la misma temp. Las conversiones pueden producirse sobre la temp para convertir en un formulario que Fc requiera, pero por ejemplo, las lambdas no se pueden enlazar a un tipo delegado diferente entre Fc y e.

Abrir pregunta

Esta reducción significa que las partes posteriores de la cadena interpolada después de una llamada de devolución Append... falsa no se evalúan. Esto podría resultar muy confuso, especialmente si el agujero de formato tiene efectos secundarios. En su lugar, podríamos evaluar todos los agujeros de formato primero y luego llamar Append... repetidamente con los resultados, deteniendo si devuelve false. Esto garantizaría que todas las expresiones se evalúen como podría esperar, pero llamamos a tan pocos métodos como sea necesario. Aunque la evaluación parcial podría ser deseable para algunos casos más avanzados, es posible que no sea intuitivo para el caso general.

Otra alternativa, si queremos evaluar siempre todos los agujeros de formato, es quitar la Append... versión de la API y simplemente realizar llamadas repetidas Format . El controlador puede realizar un seguimiento de si solo debe quitar el argumento y devolver inmediatamente esta versión.

Respuesta: Tendremos una evaluación condicional de los agujeros.

Abrir pregunta

¿Necesitamos eliminar tipos de controlador descartables y encapsular las llamadas con try/finally para asegurarse de que se llama a Dispose? Por ejemplo, el controlador de cadenas interpoladas en la bcl podría tener una matriz alquilada dentro de ella y si uno de los agujeros de interpolación produce una excepción durante la evaluación, esa matriz alquilada podría filtrarse si no se elimina.

Respuesta: No. Los controladores se pueden asignar a variables locales (como MyHandler handler = $"{MyCode()};), y la duración de dichos controladores no está clara. A diferencia de los enumeradores foreach, donde la duración es obvia y no se crea ningún local definido por el usuario para el enumerador.

Impacto en los tipos de referencia que aceptan valores NULL

Para minimizar la complejidad de la implementación, tenemos algunas limitaciones sobre cómo se realiza un análisis que acepta valores NULL en constructores de controlador de cadenas interpoladas usados como argumentos para un método o indexador. En concreto, no se devuelve información del constructor a las ranuras originales de parámetros o argumentos del contexto original y no se usan tipos de parámetros de constructor para informar a la inferencia de tipos genéricos para los parámetros de tipo en el método contenedor. Un ejemplo de dónde puede tener un impacto es:

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()
    {
    }
}

Otras consideraciones

Permitir que string los tipos también se puedan convertir en controladores

Para simplificar la creación de tipos, podríamos considerar la posibilidad de permitir que las expresiones de tipo string se puedan convertir implícitamente en applicable_interpolated_string_handler_types. Como se propone hoy, es probable que los autores deban sobrecargar tanto en ese tipo de controlador como en los tipos normales string , por lo que sus usuarios no tienen que comprender la diferencia. Esto puede ser una sobrecarga molesta y no obvia, ya que una string expresión se puede ver como una interpolación con expression.Length longitud rellenada previamente y 0 agujeros que se van a rellenar.

Esto permitiría que las nuevas API solo expongan un controlador, sin tener que exponer una stringsobrecarga de aceptación. Sin embargo, no se solucionará la necesidad de cambios para mejorar la conversión de la expresión, por lo que, aunque funcionaría, podría ser una sobrecarga innecesaria.

Respuesta:

Creemos que esto podría resultar confuso y hay una solución fácil para los tipos de controlador personalizados: agregar una conversión definida por el usuario de la cadena.

Incorporación de intervalos para cadenas sin montón

ValueStringBuilder como existe actualmente tiene 2 constructores: uno que toma un recuento y asigna en el montón diligentemente, y uno que toma un Span<char>. Normalmente Span<char> es un tamaño fijo en el código base en tiempo de ejecución, alrededor de 250 elementos en promedio. Para reemplazar realmente ese tipo, debemos tener en cuenta una extensión en la que también reconocemos GetInterpolatedString los métodos que toman , Span<char>en lugar de simplemente la versión de recuento. Sin embargo, vemos algunos posibles casos espinosos para resolver aquí:

  • No queremos apilar repetidamente en un bucle activo. Si fueramos a realizar esta extensión a la característica, es probable que deseemos compartir el stackalloc'd intervalo entre iteraciones de bucle. Sabemos que esto es seguro, ya Span<T> que es una estructura ref que no se puede almacenar en el montón, y los usuarios tendrían que ser bastante desviados para administrar para extraer una referencia a eso Span (por ejemplo, crear un método que acepte dicho controlador y recuperar deliberadamente el Span del controlador y devolverlo al autor de la llamada). Sin embargo, la asignación por adelantado genera otras preguntas:
    • ¿Deberíamos apilar con entusiasmo? ¿Qué ocurre si el bucle nunca se introduce o sale antes de que necesite el espacio?
    • Si no apilamos diligentemente, ¿significa que se introduce una rama oculta en cada bucle? Es probable que la mayoría de los bucles no le interesen, pero podría afectar a algunos bucles estrechos que no quieran pagar el costo.
  • Algunas cadenas pueden ser bastante grandes y la cantidad adecuada depende stackalloc de varios factores, incluidos los factores en tiempo de ejecución. Realmente no queremos que el compilador y la especificación de C# tengan que determinar esto con antelación, por lo que nos gustaría resolver https://github.com/dotnet/runtime/issues/25423 y agregar una API para que el compilador llame en estos casos. También agrega más ventajas y desventajas a los puntos del bucle anterior, donde no queremos asignar matrices grandes en el montón muchas veces o antes de que se necesite una.

Respuesta:

Esto está fuera del ámbito de C# 10. Podemos examinar esto en general cuando nos fijamos en la característica más general params Span<T> .

Versión que no es de prueba de la API

Por motivos de simplicidad, esta especificación solo propone actualmente reconocer un Append... método y las cosas que siempre se realizan correctamente (como InterpolatedStringHandler) siempre devolverán true desde el método . Esto se hizo para admitir escenarios de formato parcial en los que el usuario quiere detener el formato si se produce un error o si no es necesario, como el caso de registro, pero podría introducir un montón de ramas innecesarias en el uso de cadenas interpoladas estándar. Podríamos considerar un anexo en el que usamos solo FormatX métodos si no hay ningún Append... método presente, pero presenta preguntas sobre lo que hacemos si hay una combinación de llamadas Append... y FormatX .

Respuesta:

Queremos la versión que no sea try de la API. La propuesta se ha actualizado para reflejar esto.

Pasar argumentos anteriores al controlador

Actualmente existe una falta desafortunada de simetría en la propuesta: invocar un método de extensión en forma reducida produce una semántica diferente que invocar el método de extensión en forma normal. Esto es diferente de la mayoría de las otras ubicaciones del idioma, donde la forma reducida es solo un azúcar. Se propone agregar un atributo al marco que reconoceremos al enlazar un método, que informa al compilador de que se deben pasar determinados parámetros al constructor en el controlador. El uso tiene este aspecto:

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; }
    }
}

El uso de esto es:

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);

Las preguntas que necesitamos responder:

  1. ¿Nos gusta este patrón en general?
  2. ¿Queremos permitir que estos argumentos provengan de después del parámetro de controlador? Algunos patrones existentes en la BCL, como Utf8Formatter, colocan el valor en el formato antes de lo necesario para dar formato a . Para adaptarnos mejor a estos patrones, es probable que deseemos permitir esto, pero es necesario decidir si esta evaluación fuera de orden es correcta.

Respuesta:

Queremos apoyar esto. La especificación se ha actualizado para reflejar esto. Los argumentos deben especificarse en orden léxico en el sitio de llamada y, si se especifica un argumento necesario para el método create después del literal de cadena interpolada, se produce un error.

await uso en agujeros de interpolación

Dado que $"{await A()}" hoy es una expresión válida, es necesario racionalizar los agujeros de interpolación con await. Podríamos resolver esto con algunas reglas:

  1. Si una cadena interpolada usada como string, IFormattableo FormattableString tiene un await elemento en un agujero de interpolación, vuelva al formateador de estilo antiguo.
  2. Si una cadena interpolada está sujeta a una implicit_string_handler_conversion y el applicable_interpolated_string_handler_type es un ref struct, no se puede utilizar el await en los espacios de formato.

Fundamentalmente, este desugado podría usar una estructura ref en un método asincrónico siempre que garanticemos que no será necesario guardar el ref struct montón, lo que debería ser posible si se prohíben awaitlos agujeros de interpolación.

Como alternativa, podríamos hacer que todos los tipos de controlador no sean structs ref, incluido el controlador de marco para cadenas interpoladas. Sin embargo, esto nos impediría reconocer algún día una Span versión que no necesita asignar ningún espacio temporal en absoluto.

Respuesta:

Trataremos los controladores de cadenas interpolados igual que cualquier otro tipo: esto significa que si el tipo de controlador es una estructura ref y el contexto actual no permite el uso de estructuras ref, es ilegal usar el controlador aquí. La especificación en torno a la reducción de literales de cadena que se usan como cadenas es intencionadamente imprecisa para permitir que el compilador decida qué reglas considera adecuadas, pero para los tipos de controlador personalizados, tendrá que seguir las mismas reglas que el resto del lenguaje.

Controladores como parámetros ref

Es posible que algunos controladores quieran pasarse como parámetros ref (o inref). ¿Deberíamos permitirlo? Y si es así, ¿qué aspecto tendrá un ref controlador? ref $"" es confuso, ya que realmente no pasa la cadena por ref, está pasando el controlador que se crea a partir del ref por ref y tiene problemas potenciales similares con los métodos asincrónicos.

Respuesta:

Queremos apoyar esto. La especificación se ha actualizado para reflejar esto. Las reglas deben reflejar las mismas reglas que se aplican a los métodos de extensión en los tipos de valor.

Cadenas interpoladas a través de expresiones binarias y conversiones

Dado que esta propuesta distingue el contexto de las cadenas interpoladas, nos gustaría permitir que el compilador tratara una expresión binaria compuesta completamente de cadenas interpoladas o una cadena interpolada sujeta a una conversión, como un literal de cadena interpolada con fines de resolución de sobrecarga. Por ejemplo, tome el siguiente escenario:

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

Esto sería ambiguo, lo que requeriría una conversión en Handler1 o Handler2 para resolverla. Sin embargo, al convertir esa conversión, podríamos tirar la información de que hay contexto del receptor del método, lo que significa que la conversión produciría un error porque no hay nada que rellenar la información de c. Se produce un problema similar con la concatenación binaria de cadenas: el usuario podría querer dar formato al literal en varias líneas para evitar el ajuste de líneas, pero no sería capaz de hacerlo porque ya no sería un literal de cadena interpolado convertible al tipo de controlador.

Para resolver estos casos, realizamos los siguientes cambios:

  • Una additive_expression compuesta por completo de interpolated_string_expressions y usando solo + operadores se considera un interpolated_string_literal a efectos de conversiones y resolución de sobrecargas. La cadena interpolada final se crea mediante la concatinación lógica de todos los componentes de interpolated_string_expression individuales, de izquierda a derecha.
  • Un cast_expression o un relational_expression con un operador as cuyo operando es un interpolated_string_expressions se considera un interpolated_string_expressions con fines de conversiones y resolución de sobrecargas.

Preguntas abiertas:

¿Queremos hacer esto? No lo hacemos para System.FormattableString, por ejemplo, pero eso se puede dividir en una línea diferente, mientras que esto puede ser dependiente del contexto y, por lo tanto, no se puede dividir en una línea diferente. Tampoco hay problemas de resolución de sobrecargas con FormattableString y IFormattable.

Respuesta:

Creemos que se trata de un caso de uso válido para expresiones aditivas, pero que la versión de conversión no es lo suficientemente atractiva en este momento. Podemos agregarlo más adelante si es necesario. La especificación se ha actualizado para reflejar esta decisión.

Otros casos de uso

Consulte https://github.com/dotnet/runtime/issues/50635 ejemplos de API de controlador propuestas mediante este patrón.