Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
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:
- 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.
- Tiene que asignar una matriz para los argumentos en la mayoría de los casos.
- 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.
- 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 AppendLiteral
AppendFormatted
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,ref
oout
) 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 oout
, el tipo del argumento es idéntico al tipo del parámetro correspondiente. Después de todo, unref
parámetro oout
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. SiA
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 oout
, 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 T1
y 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:
-
E
es un interpolated_string_expression, no constanteC1
es un implicit_string_handler_conversion,T1
es un applicable_interpolated_string_handler_type yC2
no es un implicit_string_handler_conversion, o bien, -
E
no coincide exactamente conT2
y se cumple al menos una de las siguientes condiciones:-
E
coincide exactamente conT1
(Sección 12.6.4.5) -
T1
es un destino de conversión mejor queT2
(§12.6.4.6)
-
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:
- La búsqueda de miembros para constructores de instancia se realiza en
T
. El grupo de métodos resultante se denominaM
. - La lista
A
de argumentos se construye de la siguiente manera:- Los dos primeros argumentos son constantes enteras, que representan la longitud literal de
i
y el número de componentes de interpolación eni
, respectivamente. - Si
i
se usa como argumento para algún parámetropi
en el métodoM1
, y el parámetropi
se atribuye aSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute
, para cada nombreArgx
de laArguments
matriz de ese atributo, el compilador lo coincide con un parámetropx
que tiene el mismo nombre. La cadena vacía coincide con el receptor deM1
.- Si alguno
Argx
no puede coincidir con un parámetro deM1
o solicitaArgx
al receptor deM1
yM1
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 laArguments
matriz. Cadapx
se pasa con la mismaref
semántica que se especifica enM1
.
- Si alguno
- El argumento final es ,
bool
que se pasa como parámetroout
.
- Los dos primeros argumentos son constantes enteras, que representan la longitud literal de
- La resolución de invocación de método tradicional se realiza con el grupo
M
de métodos y la listaA
de argumentos . Para los fines de la validación final de la invocación de métodos, el contexto deM
se trata como un member_access mediante el tipoT
.- Si se encontró un constructor
F
único, el resultado de la resolución de sobrecarga esF
. - Si no se encontraron constructores aplicables, se reintenta el paso 3, quitando el parámetro final
bool
deA
. 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.
- Si se encontró un constructor
- Se realiza la validación final en
F
.- Si se produjo algún elemento de
A
léxicamente despuési
de , se produce un error y no se realizan más pasos. - Si alguna
A
solicita al receptor deF
, yF
es un indexador que se usa como initializer_target en un member_initializer, se informa de un error, y no se realizan pasos adicionales.
- Si se produjo algún elemento de
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.
- Si hay algún componente de carácter_de_cadena_regular_interpolado en
i
:- Se realiza la búsqueda de
T
miembros con el nombreAppendLiteral
. El grupo de métodos resultante se denominaMl
. - La lista
Al
de argumentos se construye con un parámetro de valor de tipostring
. - La resolución de invocación de método tradicional se realiza con el grupo
Ml
de métodos y la listaAl
de argumentos . Para los fines de la validación final de invocación de método, el contexto deMl
se trata como un member_access mediante una instancia deT
.- 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 esFi
. - De lo contrario, se notifica un error.
- Si se encuentra un método
- Se realiza la búsqueda de
- Para cada componente de interpolación
ix
dei
:- Se realiza la búsqueda de
T
miembros con el nombreAppendFormatted
. El grupo de métodos resultante se denominaMf
. - La lista
Af
de argumentos se construye:- El primer parámetro es el
expression
deix
, pasado por valor. - Si
ix
contiene directamente un componente de constant_expression , se agrega un parámetro de valor entero, con el nombrealignment
especificado. - Si
ix
va seguido directamente de un interpolation_format, se agrega un parámetro de valor de cadena, con el nombreformat
especificado.
- El primer parámetro es el
- La resolución de invocación de método tradicional se realiza con el grupo
Mf
de métodos y la listaAf
de argumentos . Para los fines de la validación final de invocación de método, el contexto deMf
se trata como un member_access mediante una instancia deT
.- Si se encuentra un método
Fi
único, el resultado de la resolución de invocación de método esFi
. - De lo contrario, se notifica un error.
- Si se encuentra un método
- Se realiza la búsqueda de
- Por último, para cada
Fi
detectado en los pasos 1 y 2, se realiza la validación final:- Si alguno
Fi
no devuelvebool
por valor ovoid
, se notifica un error. - Si todos
Fi
no devuelven el mismo tipo, se notifica un error.
- Si alguno
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:
- Cualquier argumento a
Fc
que se produzca léxicamente antesi
de evaluarse y almacenarse en variables temporales en orden léxico. Para conservar la ordenación léxica, sii
se produjo como parte de una expresióne
mayor, también se evaluarán los componentes dee
que se produjeron antesi
, de nuevo en orden léxico. -
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 unbool
argumento out (siFc
se resolvió con uno como último parámetro). El resultado se almacena en un valorib
temporal .- 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}
.
- La longitud de los componentes literales se calcula después de reemplazar cualquier open_brace_escape_sequence por un único
- Si
Fc
finalizó con unbool
argumento out, se genera una comprobación de esebool
valor. Si es true, se llamará a los métodos deFa
. De lo contrario, no se llamará. - Para cada
Fax
enFa
,Fax
se llama a onib
con el componente literal actual o la expresión de interpolación , según corresponda. SiFax
devuelve unbool
, el resultado se realiza de forma lógica y con todas las llamadas anterioresFax
.- Si
Fax
es una llamada aAppendLiteral
, el componente literal no se captura reemplazando cualquier open_brace_escape_sequence por un solo{
y cualquier close_brace_escape_sequence por un solo}
.
- Si
- 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 string
sobrecarga 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 esoSpan
(por ejemplo, crear un método que acepte dicho controlador y recuperar deliberadamente elSpan
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:
- ¿Nos gusta este patrón en general?
- ¿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:
- Si una cadena interpolada usada como
string
,IFormattable
oFormattableString
tiene unawait
elemento en un agujero de interpolación, vuelva al formateador de estilo antiguo. - 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 elawait
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 await
los 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 in
ref
). ¿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.
C# feature specifications