Amélioration des chaînes interpolées
Remarque
Cet article est une spécification de fonctionnalité. La spécification sert de document de conception pour la fonctionnalité. Il inclut les modifications de spécification proposées, ainsi que les informations nécessaires pendant la conception et le développement de la fonctionnalité. Ces articles sont publiés jusqu’à ce que les modifications de spécification proposées soient finalisées et incorporées dans la spécification ECMA actuelle.
Il peut y avoir des différences entre la spécification de la fonctionnalité et l’implémentation terminée. Ces différences sont consignées dans les notes pertinentes de la réunion de conception linguistique (LDM).
Vous pouvez en savoir plus sur le processus d’adoption des speclets de fonctionnalités dans la norme de langage C# dans l’article sur les spécifications .
Problème de champion : https://github.com/dotnet/csharplang/issues/4487
Résumé
Nous introduisons un nouveau modèle pour la création et l'utilisation d'expressions de chaîne interpolées, afin de permettre un formatage et une utilisation efficaces dans les scénarios généraux de string
, ainsi que dans des scénarios plus spécialisés, tels que les frameworks de journalisation, sans entraîner d'allocations inutiles lors du formatage de la chaîne dans le framework.
Motivation
Aujourd'hui, l'interpolation de chaînes de caractères se résume principalement à un appel à string.Format
. Cela, bien qu’à usage général, peut être inefficace pour plusieurs raisons :
- Il prend en compte tous les arguments struct, à moins que le runtime n'ait introduit une surcharge de
string.Format
qui prend exactement les bons types d'arguments dans le bon ordre.- Ce classement est la raison pour laquelle le runtime est hésitant à introduire des versions génériques de la méthode, car il conduirait à l’explosion combinatoire des instanciations génériques d’une méthode très commune.
- Dans la plupart des cas, il doit allouer un tableau pour les arguments.
- Il n’est pas possible d’éviter d’instancier l’instance si elle n’est pas nécessaire. Les frameworks de journalisation, par exemple, recommandent d’éviter l’interpolation de chaîne, car il entraîne la réalisation d’une chaîne qui peut ne pas être nécessaire, en fonction du niveau de journal actuel de l’application.
- Il ne peut jamais utiliser
Span
ou d’autres types de struct ref aujourd’hui, car les structs ref ne sont pas autorisés en tant que paramètres de type générique, ce qui signifie que si un utilisateur souhaite éviter de copier vers des emplacements intermédiaires, ils doivent mettre en forme manuellement des chaînes.
En interne, le runtime a un type appelé ValueStringBuilder
pour faciliter la résolution des 2 premiers scénarios. Ils passent un tampon alloué à la pile au constructeur, appellent AppendFormat
de manière répétée avec chaque partie, puis obtiennent une chaîne de caractères finale. Si la chaîne résultante dépasse les limites du tampon de la pile, ils peuvent alors passer à un tableau sur le tas. Cependant, il est dangereux d'exposer ce type directement, car une utilisation incorrecte pourrait conduire à une double disposition d'un tableau loué, ce qui entraînerait alors toutes sortes de comportements indéfinis dans le programme, car deux emplacements pensent qu'ils ont un accès exclusif au tableau loué. Cette proposition crée un moyen d’utiliser ce type en toute sécurité à partir du code C# natif en écrivant simplement un littéral de chaîne interpolé, en laissant le code écrit inchangé tout en améliorant chaque chaîne interpolée qu’un utilisateur écrit. Il étend également ce modèle pour permettre aux chaînes interpolées passées en tant qu’arguments à d’autres méthodes d’utiliser un modèle de gestionnaire, défini par le récepteur de la méthode, ce qui permettra aux éléments tels que les frameworks de journalisation d’éviter d’allouer des chaînes qui ne seront jamais nécessaires et de donner aux utilisateurs C# une syntaxe d’interpolation familière et pratique.
Conception détaillée
Le schéma du gestionnaire
Nous introduisons un nouveau modèle de gestionnaire qui peut représenter une chaîne interpolée passée en tant qu’argument à une méthode. L'anglais simple du modèle est le suivant :
Lorsqu’un interpolated_string_expression est passé en tant qu’argument à une méthode, nous examinons le type du paramètre. Si le type de paramètre a un constructeur qui peut être appelé avec 2 paramètres int, literalLength
et formattedCount
, prend éventuellement des paramètres supplémentaires spécifiés par un attribut sur le paramètre d’origine, a éventuellement un paramètre final booléen de sortie et le type du paramètre d’origine dispose de méthodes d’instance AppendLiteral
et AppendFormatted
qui peuvent être appelées pour chaque partie de la chaîne interpolée, alors nous abaissons l’interpolation en l’utilisant, plutôt que par un appel traditionnel à string.Format(formatStr, args)
. Un exemple plus concret est utile pour visualiser cela.
// 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);
Ici, étant donné que TraceLoggerParamsInterpolatedStringHandler
a un constructeur avec les paramètres corrects, nous disons que la chaîne interpolée a une conversion de gestionnaire implicite vers ce paramètre, et qu’elle est inférieure au modèle indiqué ci-dessus. Les spécifications nécessaires pour cela sont un peu compliquées et sont développées ci-dessous.
Le reste de cette proposition utilisera Append...
pour faire référence à l’une des AppendLiteral
ou AppendFormatted
dans les cas où les deux sont applicables.
Nouveaux attributs
Le compilateur reconnaît l'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()
{
}
}
}
Cet attribut est utilisé par le compilateur pour déterminer si un type est un type de gestionnaire de chaînes interpolé valide.
Le compilateur reconnaît également l'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; }
}
}
Cet attribut est utilisé sur les paramètres pour informer le compilateur comment réduire un modèle de gestionnaire de chaînes interpolé utilisé dans une position de paramètre.
Conversion de gestionnaire de chaînes interpolées
Le type T
est dit être un type de gestionnaire de chaîne interpolée applicable s'il est attribué à System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute
.
Il existe une conversion iinterpolated_string_handler_conversion vers T
à partir d'une interpolated_string_expression ou d'une dditive_expression entièrement d'expressions de chaînes interpolées et utilisant uniquement des opérateurs +
.
Par souci de simplicité dans le reste de cette spécification, interpolated_string_expression fait référence à une interpolated_string_expressionsimple et à une additive_expression composée entièrement de _interpolated_string_expression_s et en utilisant uniquement des opérateurs +
.
Notez que cette conversion existe toujours, indépendamment du fait qu'il y aura ou non des erreurs ultérieures lorsque l'on tentera effectivement d'abaisser l'interpolation à l'aide du motif gestionnaire. Cela permet de s’assurer qu’il existe des erreurs prévisibles et utiles et que le comportement d’exécution ne change pas en fonction du contenu d’une chaîne interpolée.
Ajustements des membres de fonction applicables
Nous adaptons la formulation de l'algorithme membre fonctionnel applicable (§12.6.4.2) comme suit (un nouveau sous-point est ajouté à chaque section, en gras) :
Un membre de fonction est considéré comme un membre de fonction applicable par rapport à une liste d’arguments A
lorsque toutes les valeurs suivantes sont vraies :
- Chaque argument de
A
correspond à un paramètre dans la déclaration de membre de fonction, comme décrit dans les paramètres correspondants (§12.6.2.2), et tout paramètre auquel aucun argument ne correspond est un paramètre facultatif. - Pour chaque argument de
A
, le mode de passage de paramètre de l’argument (c’est-à-dire, valeur,ref
ouout
) est identique au mode de passage de paramètre du paramètre correspondant et- pour un paramètre de valeur ou un tableau de paramètres, une conversion implicite (§10.2) existe de l’argument au type du paramètre correspondant, ou
- pour un paramètre
ref
dont le type est un type struct, il existe une interpolated_string_handler_conversion vers le type du paramètre correspondant, ou - pour un paramètre
ref
ouout
, le type de l’argument est identique au type du paramètre correspondant. Après tout, un paramètreref
ouout
est un alias pour l’argument passé.
Pour un membre de fonction qui inclut un tableau de paramètres, si le membre de la fonction est applicable par les règles ci-dessus, il est dit qu’il s’applique sous sa forme normale. Si un membre de fonction qui inclut un tableau de paramètres n’est pas applicable sous sa forme normale, le membre de la fonction peut être applicable dans son formulaire développé:
- Le formulaire développé est construit en remplaçant le tableau de paramètres dans la déclaration de membre de fonction par zéro ou plusieurs paramètres de valeur du type d’élément du tableau de paramètres, de sorte que le nombre d’arguments dans la liste d’arguments
A
correspond au nombre total de paramètres. SiA
a moins d’arguments que le nombre de paramètres fixes dans la déclaration de membre de fonction, la forme développée du membre de fonction ne peut pas être construite et n’est donc pas applicable. - Sinon, le formulaire développé s’applique si pour chaque argument dans
A
le mode de passage de paramètre de l’argument est identique au mode de passage de paramètre du paramètre correspondant et- pour un paramètre de valeur fixe ou un paramètre de valeur créé par l’extension, une conversion implicite (§10.2) existe du type de l’argument au type du paramètre correspondant, ou
- pour un paramètre
ref
dont le type est un type struct, il existe une interpolated_string_handler_conversion vers le type du paramètre correspondant, ou - pour un paramètre
ref
ouout
, le type de l’argument est identique au type du paramètre correspondant.
Remarque importante : cela signifie que s’il existe 2 surcharges équivalentes, qui diffèrent uniquement par le type de la applicable_interpolated_string_handler_type, ces surcharges sont considérées comme ambiguës. De plus, comme nous ne voyons pas les casts explicites, il est possible qu'il y ait un scénario non résolu où les deux surcharges applicables utilisent InterpolatedStringHandlerArguments
et sont totalement impossibles à appeler sans exécuter manuellement le schéma d'abaissement du gestionnaire. Nous pourrions éventuellement apporter des modifications à l'algorithme du membre de fonction le mieux adapté pour résoudre ce problème si nous le décidons, mais ce scénario est peu susceptible de se produire et n'est pas une priorité à traiter.
Meilleure conversion à partir des ajustements d’expression
Nous modifions la conversion préférable de la section de l’expression (§12.6.4.5) comme suit :
Étant donné une conversion implicite C1
qui convertit d’une expression E
à un type T1
, et une conversion implicite C2
qui convertit d’une expression E
à un type T2
, C1
est une meilleure conversion que C2
si :
-
E
est une interpolated_string_expression,C1
est une conversion de implicit_string_handler_conversion,T1
est un applicable_interpolated_string_handler_type applicable, etC2
n'est pas une implicit_string_handler_conversion, ou -
E
ne correspond pas exactement àT2
et au moins l'une des conditions suivantes est remplie :
Cela signifie qu’il existe des règles de résolution de surcharge potentiellement non évidentes, selon que la chaîne interpolée en question est une expression constante ou non. Par exemple:
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
Ceci est introduit afin que les choses qui peuvent simplement être émises en tant que constantes le font et n’entraînent aucune surcharge, tandis que les choses qui ne peuvent pas être constantes utilisent le modèle de gestionnaire.
InterpolatedStringHandler et utilisation
Nous introduisons un nouveau type dans System.Runtime.CompilerServices
: DefaultInterpolatedStringHandler
. Il s’agit d’un struct ref avec la plupart des mêmes sémantiques que ValueStringBuilder
, destinées à une utilisation directe par le compilateur C#. Ce struct ressemblerait approximativement à ceci :
// 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);
}
}
Nous modifions légèrement les règles relatives à la signification d’un interpolated_string_expression (§12.8.3) :
Si le type d’une chaîne interpolée est string
et que le type System.Runtime.CompilerServices.DefaultInterpolatedStringHandler
existe, et que le contexte actuel prend en charge l’utilisation de ce type, la chaîneest réduite à l’aide du modèle de gestionnaire. La valeur finale string
est ensuite obtenue en appelant ToStringAndClear()
sur le type de gestionnaire.Sinon, si le type d’une chaîne interpolée est System.IFormattable
ou System.FormattableString
[le reste n’est pas modifié]
La règle « et le contexte actuel prennent en charge l’utilisation de ce type » est intentionnellement vague pour permettre au compilateur d’optimiser l’utilisation de ce modèle. Le type de gestionnaire est susceptible d’être un type de struct ref, et les types de struct ref ne sont normalement pas autorisés dans les méthodes asynchrones. Dans ce cas particulier, le compilateur est autorisé à utiliser le gestionnaire si aucun des trous d’interpolation ne contient d’expression await
, car nous pouvons déterminer statiquement que le type de gestionnaire est utilisé en toute sécurité sans analyse complexe supplémentaire, car le gestionnaire sera supprimé après l’évaluation de l’expression de chaîne interpolée.
Ouvrir question:
Voulons-nous plutôt simplement que le compilateur reconnaisse DefaultInterpolatedStringHandler
et qu'il ignore complètement l'appel string.Format
? Cela nous permettrait de masquer une méthode que nous ne voulons pas nécessairement mettre dans les visages des gens lorsqu’ils appellent manuellement string.Format
.
Réponse: Oui.
Ouvrir question:
Voulons-nous également avoir des gestionnaires pour System.IFormattable
et System.FormattableString
?
Réponse: Non.
Modèle de gestionnaire codegen
Dans cette section, la résolution d’appel de méthode fait référence aux étapes répertoriées dans §12.8.10.2.
Résolution du constructeur
Compte tenu d'un applicable_interpolated_string_handler_typeT
et d'une expression de interpolated_string_expressioni
, la résolution et la validation de l'invocation d'une méthode pour un constructeur valide sur T
s'effectuent comme suit :
- La recherche de membres pour les constructeurs d'instance est effectuée sur
T
. Le groupe de méthodes résultant est appeléM
. - La liste d’arguments
A
est construite comme suit :- Les deux premiers arguments sont des constantes entières, représentant la longueur littérale de
i
, et le nombre de composants d’interpolation dansi
, respectivement. - Si
i
est utilisé en tant qu’argument pour certainspi
de paramètre dans la méthodeM1
, et que le paramètrepi
est attribué avecSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute
, puis pour chaque nomArgx
dans le tableauArguments
de cet attribut, le compilateur le correspond à un paramètrepx
qui a le même nom. La chaîne vide est associée au récepteur deM1
.- Si un
Argx
n’est pas en mesure d’être mis en correspondance avec un paramètre deM1
, ou si unArgx
demande le destinataire deM1
et queM1
est une méthode statique, une erreur est générée et aucune autre étape n’est effectuée. - Sinon, le type de chaque
px
résolu est ajouté à la liste d’arguments, dans l’ordre spécifié par le tableauArguments
. Chaquepx
est passé avec la même sémantiqueref
que celle spécifiée dansM1
.
- Si un
- L’argument final est un
bool
, passé en tant que paramètreout
.
- Les deux premiers arguments sont des constantes entières, représentant la longueur littérale de
- La résolution d’appel de méthode traditionnelle est effectuée avec le groupe de méthodes
M
et la liste d’argumentsA
. Aux fins de la validation finale de l'invocation de la méthode, le contexte deM
est traité comme un member_access à travers le typeT
.- Si un meilleur constructeur unique
F
a été trouvé, le résultat de la résolution de la surcharge estF
. - Si aucun constructeur applicable n’a été trouvé, l’étape 3 est retentée, en supprimant le paramètre
bool
final deA
. Si cette nouvelle tentative ne trouve pas non plus de membres applicables, une erreur est générée et aucune autre procédure n’est effectuée. - Si aucune méthode unique n’a été trouvée, le résultat de la résolution de surcharge est ambigu, une erreur est générée et aucune autre étape n’est effectuée.
- Si un meilleur constructeur unique
- La validation finale de
F
est effectuée.- Si un élément de
A
s’est produit lexicalement aprèsi
, une erreur est générée et aucune autre étape n’est effectuée. - Si
A
demande le récepteur deF
et queF
est un indexeur utilisé comme initializer_target dans un member_initializer, une erreur est signalée et aucune autre mesure n'est prise.
- Si un élément de
Note : la résolution ici n'utilise pas intentionnellement les expressions réelles passées comme autres arguments pour les éléments Argx
. Nous considérons uniquement les types après la conversion. Cela garantit que nous n’avons pas de problèmes de double conversion ou de cas inattendus où une expression lambda est liée à un type délégué lorsqu’elle est passée à M1
et liée à un type délégué différent lorsqu’elle est passée à M
.
Remarque : Nous signalons une erreur pour les indexeurs utilisés en tant qu'initialiseurs de membre en raison de l’ordre d’évaluation des initialiseurs de membre imbriqués. Considérez cet extrait de code :
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 { }
}
}
Les arguments de __c1.C2[]
sont évalués avant le récepteur de l'indexeur. Bien que nous puissions trouver un abaissement qui fonctionne pour ce scénario (soit en créant un temp pour __c1.C2
et en le partageant entre les appels de l’indexeur, soit seulement en l’utilisant pour le premier appel d’indexeur et le partage de l’argument entre les deux appels), nous pensons que tout abaissement serait déroutant pour ce que nous croyons est un scénario pathologique. Par conséquent, nous interdisons totalement ce scénario.
Question ouverte:
Si nous utilisons un constructeur au lieu de Create
, cela améliorerait la génération de code d'exécution, au détriment de restreindre légèrement le modèle.
Réponse : Nous nous limiterons aux constructeurs pour l'instant. Nous pouvons reconsidérer l’ajout d’une méthode générale Create
ultérieurement si le besoin se présente.
Append...
Résolution des surcharges de méthodes
Étant donné un applicable_interpolated_string_handler_typeT
et une interpolated_string_expressioni
, la résolution de surcharge pour un ensemble de méthodes Append...
valides sur T
est effectuée comme suit :
- S'il existe des composants interpolated_regular_string_character dans
i
:- Une recherche de membre sur
T
avec le nomAppendLiteral
est effectuée. Le groupe de méthodes résultant est appeléMl
. - La liste d’arguments
Al
est construite avec un paramètre valeur de typestring
. - La résolution d’appel de méthode traditionnelle est effectuée avec le groupe de méthodes
Ml
et la liste d’argumentsAl
. Pour les besoins de la validation finale de l’appel de méthode, le contexte deMl
est traité comme un member_access par le biais d’une instance deT
.- Si une seule meilleure méthode
Fi
est trouvée et qu'aucune erreur n'a été produite, le résultat de la résolution de l'invocation de la méthode estFi
. - Sinon, une erreur est signalée.
- Si une seule meilleure méthode
- Une recherche de membre sur
- Pour chaque composant d'interpolation
ix
dei
:- Une recherche de membre est effectuée sur
T
avec le nomAppendFormatted
. Le groupe de méthodes résultant est appeléMf
. - La liste d’arguments
Af
est construite :- Le premier paramètre est le
expression
deix
, transmis par valeur. - Si
ix
contient directement un composant constant_expression, un paramètre de valeur entière est ajouté, avec le nomalignment
spécifié. - Si
ix
est directement suivi d’un interpolation_format, un paramètre de valeur de chaîne est ajouté, avec le nomformat
spécifié.
- Le premier paramètre est le
- La résolution d’appel de méthode traditionnelle est effectuée avec le groupe de méthodes
Mf
et la liste d’argumentsAf
. Pour les besoins de la validation finale de l’appel de méthode, le contexte deMf
est traité comme un member_access par le biais d’une instance deT
.- Si une meilleure méthode
Fi
est trouvée, le résultat de la résolution de l'invocation de la méthode estFi
. - Sinon, une erreur est signalée.
- Si une meilleure méthode
- Une recherche de membre est effectuée sur
- Enfin, pour chaque
Fi
découverte aux étapes 1 et 2, la validation finale est effectuée :- Si une méthode
Fi
ne renvoie pasbool
par valeur ouvoid
, une erreur est signalée. - Si toutes les
Fi
ne retournent pas le même type, une erreur est signalée.
- Si une méthode
Notez que ces règles n’autorisent pas les méthodes d’extension pour les appels Append...
. Nous pourrions envisager d’activer cela si nous choisissons, mais cela est analogue au modèle d’énumérateur, où nous permettons GetEnumerator
être une méthode d’extension, mais pas Current
ou MoveNext()
.
Ces règles autorisent les paramètres par défaut pour les appels Append...
, qui fonctionnent avec des éléments tels que CallerLineNumber
ou CallerArgumentExpression
(lorsqu’ils sont pris en charge par la langue).
Nous avons des règles de recherche de surcharge distinctes pour les éléments de base et les trous d’interpolation, car certains gestionnaires souhaitent comprendre la différence entre les composants interpolés et les composants qui faisaient partie de la chaîne de base.
Question ouverte
Certains scénarios, comme la journalisation structurée, souhaitent être en mesure de fournir des noms pour les éléments d’interpolation. Par exemple, aujourd'hui, un appel au journal peut ressembler à Log("{name} bought {itemCount} items", name, items.Count);
. Les noms à l’intérieur de la {}
fournissent des informations de structure importantes pour les enregistreurs qui aident à garantir que la sortie est cohérente et uniforme. Certains cas peuvent être en mesure de réutiliser le composant :format
d’un trou d’interpolation pour cela, mais de nombreux enregistreurs d’événements comprennent déjà les spécificateurs de format et ont déjà un comportement existant pour la mise en forme de sortie en fonction de ces informations. Existe-t-il une syntaxe que nous pouvons utiliser pour activer la saisie de ces spécificateurs nommés ?
Dans certains cas, il est possible de s'en sortir avec CallerArgumentExpression
, à condition que le support soit disponible en C# 10. Toutefois, pour les cas qui appellent une méthode/propriété, cela peut ne pas suffire.
Réponse :
Bien qu’il existe quelques parties intéressantes pour les chaînes modèles que nous pourrions explorer dans une fonctionnalité de langage orthogonal, nous ne pensons pas qu’une syntaxe spécifique ici présente beaucoup d’avantages par rapport aux solutions telles que l’utilisation d’un tuple : $"{("StructuredCategory", myExpression)}"
.
Exécution de la conversion
Étant donné un type applicable_interpolated_string_handler_typeT
et une expression interpolated_string_expressioni
dont le constructeur Fc
est valide et dont les Append...
méthodes Fa
sont résolues, l'abaissement pour i
est effectué comme suit :
- Tous les arguments à
Fc
qui se produisent lexicalement avanti
sont évalués et stockés dans des variables temporaires dans l’ordre lexical. Afin de conserver l’ordre lexical, sii
s’est produit dans le cadre d’une expression plus grandee
, tous les composants dee
qui se sont produits avanti
seront également évalués, à nouveau dans l’ordre lexical. Fc
est appelée avec la longueur des composants littéraux de chaîne interpolés, le nombre de trous d'interpolation , les arguments préalablement évalués et un argumentbool
out (siFc
a été résolu avec un tel argument comme dernier paramètre). Le résultat est stocké dans une valeur temporaireib
.- La longueur des composants littérals est calculée après avoir remplacé n’importe quelle open_brace_escape_sequence par un seul
{
, et toute close_brace_escape_sequence par un seul}
.
- La longueur des composants littérals est calculée après avoir remplacé n’importe quelle open_brace_escape_sequence par un seul
- Si
Fc
se termine avec un argument sortantbool
, une vérification de cette valeurbool
est effectuée. Si la valeur est true, les méthodes deFa
sont appelées. Sinon, ils ne seront pas appelés. - Pour chaque
Fax
dansFa
,Fax
est appelé surib
avec le composant littéral actuel ou l'expression d'interpolation, selon le cas. SiFax
renvoie unbool
, le résultat est logiquement lié à tous les appelsFax
précédents.- Si
Fax
est un appel àAppendLiteral
, le composant littéral est désencapsulé en remplaçant toute séquence open_brace_escape_sequence par un seul{
, et toute séquence close_brace_escape_sequence par un seul}
.
- Si
- Le résultat de la conversion est
ib
.
Là encore, notez que les arguments passés à Fc
et ceux passés à e
sont la même variable temporaire. Des conversions peuvent se produire sur cette variable pour se conformer à une forme requise par Fc
, mais, par exemple, les expressions lambda ne peuvent pas être liées à un type de délégué différent entre Fc
et e
.
Question ouverte
Cet abaissement signifie que les parties de la chaîne interpolée qui suivent un appel à Append...
avec un faux retour ne sont pas évaluées. Cela peut être très déroutant, en particulier si le problème de format a des effets secondaires. Nous pourrions plutôt évaluer d'abord tous les trous de format, puis appeler Append...
de façon répétée avec les résultats, en s'arrêtant s'il renvoie un faux. Cela assurerait que toutes les expressions soient évaluées comme prévu, tout en appelant le moins de méthodes possible. Bien que l’évaluation partielle puisse être souhaitable pour certains cas plus avancés, il est peut-être non intuitif pour le cas général.
Une alternative, si nous voulons toujours évaluer tous les trous de format, consiste à supprimer la version Append...
de l'API et à effectuer simplement plusieurs appels Format
. Le gestionnaire peut savoir s'il doit simplement abandonner l'argument et retourner immédiatement pour cette version.
Réponse: nous aurons une évaluation conditionnelle des trous.
Question ouverte
Devons-nous nous débarrasser des types de gestionnaires jetables, et envelopper les appels avec try/finally pour s'assurer que Dispose est appelé ? Par exemple, le gestionnaire de chaîne interpolée dans la bcl peut avoir un tableau loué à l'intérieur, et si l'un des trous d'interpolation lève une exception pendant l'évaluation, ce tableau loué pourrait être divulgué s'il n'était pas éliminé.
Réponse: Non. Les gestionnaires peuvent être affectés à des locaux (tels que MyHandler handler = $"{MyCode()};
), et la durée de vie de ces gestionnaires n’est pas claire. Contrairement aux énumérateurs foreach, où la durée de vie est évidente et qu’aucun local défini par l’utilisateur n’est créé pour l’énumérateur.
Impact sur les types de référence nullables
Pour réduire la complexité de l’implémentation, nous avons quelques limitations sur la façon dont nous effectuons une analyse nullable sur les constructeurs de gestionnaires de chaînes interpolés utilisés comme arguments pour une méthode ou un indexeur. En particulier, nous ne transmettons pas les informations du constructeur vers les emplacements initiaux des paramètres ou des arguments du contexte initial, et nous n’utilisons pas les types des paramètres du constructeur pour guider l’inférence des types génériques pour les paramètres de type dans la méthode conteneur. Voici un exemple de l’endroit où cela peut avoir un impact :
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()
{
}
}
Autres considérations
Permettre aux types string
d'être également convertibles en gestionnaires
Pour la simplicité de l’auteur de type, nous pourrions envisager de permettre aux expressions de type string
d’être implicitement convertibles en applicable_interpolated_string_handler_types. Tel que proposé aujourd'hui, les auteurs devront probablement surcharger à la fois ce type de gestionnaire et les types string
ordinaires, afin que leurs utilisateurs n'aient pas à comprendre la différence. Il peut s'agir d'une surcharge gênante et non évidente, étant donné qu'une expression string
peut être considérée comme une interpolation avec expression.Length
longueur pré-remplie et 0 trou à remplir.
Cela permettrait aux nouvelles API de n'exposer qu'un gestionnaire, sans avoir à exposer également une surcharge string
-acceptante. Cependant, cela ne contournera pas le besoin de modifications pour une meilleure conversion à partir de l'expression ; ainsi, bien que cela fonctionnerait, cela pourrait constituer une charge inutile.
Réponse:
Nous pensons que cela pourrait se révéler déroutant et qu'il existe une solution de contournement facile pour les types de gestionnaires personnalisés : ajoutez une conversion utilisateur définie depuis une chaîne.
Incorporation des étendues pour les chaînes sans tas
ValueStringBuilder
tel qu'il existe aujourd'hui a 2 constructeurs : un qui prend un compte, et alloue sur le tas avec empressement, et un qui prend un Span<char>
. Cette Span<char>
est généralement une taille fixe dans la base de code du runtime, environ 250 éléments en moyenne. Pour vraiment remplacer ce type, nous devrions envisager une extension de ce type où nous reconnaissons également les méthodes GetInterpolatedString
qui prennent un Span<char>
, au lieu de la seule version de comptage. Toutefois, nous voyons quelques cas épineux potentiels à résoudre ici :
- Nous ne voulons pas empiler de façon répétée dans une boucle chaude. Si nous devions effectuer cette extension de la fonctionnalité, nous souhaiterions probablement partager la zone stackalloc'd entre les itérations de boucle. Nous savons que cela est sûr, car
Span<T>
est un "ref struct" qui ne peut pas être stocké sur le tas, et les utilisateurs doivent être assez rusés pour réussir à extraire une référence à ceSpan
(par exemple, la création d'une méthode qui accepte un tel gestionnaire, puis récupère volontairement leSpan
du gestionnaire et le retourne à l'appelant). Toutefois, l’allocation à l’avance génère d’autres questions :- Devrions-nous empiler avec empressement ? Que se passe-t-il si la boucle n'est jamais entrée, ou si elle se termine avant d'avoir besoin d'espace ?
- Si nous n'anticipons pas le stackalloc, cela signifie-t-il que nous introduisons une branche cachée dans chaque boucle ? La plupart des boucles ne s'en soucieront probablement pas, mais cela pourrait affecter certaines boucles étroites qui ne veulent pas payer le coût.
- Certaines chaînes peuvent être très volumineuses, et la quantité appropriée de
stackalloc
dépend d'un certain nombre de facteurs, y compris de facteurs liés au temps d'exécution. Nous ne voulons pas vraiment que le compilateur C# et la spécification doivent déterminer cela à l’avance. Nous voulons donc résoudre https://github.com/dotnet/runtime/issues/25423 et ajouter une API pour que le compilateur appelle dans ces cas. Cela ajoute également des avantages et des inconvénients aux points de la boucle précédente, où nous ne voulons pas potentiellement allouer de grands tableaux sur le tas plusieurs fois ou avant que l'on en ait besoin.
Réponse:
Cela n’est pas prévu pour C# 10. Nous pourrons nous pencher sur cette question de manière générale lorsque nous examinerons la fonctionnalité plus générale de params Span<T>
.
Version non d'essai de l’API
Par souci de simplicité, cette spécification propose simplement actuellement de reconnaître une méthode Append...
, et les éléments qui réussissent toujours (comme InterpolatedStringHandler
) renvoient toujours la valeur vraie à partir de la méthode.
Cela a été effectué pour prendre en charge les scénarios de mise en forme partielle dans lesquels l’utilisateur souhaite arrêter la mise en forme si une erreur se produit ou s’il n’est pas nécessaire, comme le cas de journalisation, mais peut éventuellement introduire un tas de branches inutiles dans l’utilisation standard des chaînes interpolées. Nous pourrions envisager un addenda où nous utilisons seulement les méthodes FormatX
si aucune méthode Append...
n'est présente, mais cela soulève des questions quant à ce que nous faisons s'il existe un mélange d'appels Append...
et FormatX
.
Réponse:
Nous voulons la version non try de l’API. La proposition a été mise à jour pour refléter cela.
Transmission des arguments précédents au gestionnaire
Il existe actuellement un manque de symétrie dans la proposition : l’appel d’une méthode d’extension sous forme réduite produit une sémantique différente de l’appel de la méthode d’extension sous forme normale. Ceci est différent de la plupart des autres emplacements dans la langue, où la forme réduite est juste un sucre. Nous proposons d’ajouter un attribut à l’infrastructure que nous reconnaîtrons lors de la liaison d’une méthode, qui informe le compilateur que certains paramètres doivent être passés au constructeur sur le gestionnaire. L’utilisation ressemble à ceci :
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; }
}
}
L'utilisation de ceci est alors la suivante :
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);
Les questions auxquelles nous devons répondre :
- Est-ce que nous aimons ce modèle en général ?
- Voulons-nous autoriser ces arguments à provenir après le paramètre du gestionnaire ? Certains motifs existants dans la BCL, tels que
Utf8Formatter
, placent la valeur à formater avant l'élément à formater. Pour s’adapter au mieux à ces modèles, nous voulons probablement autoriser cela, mais nous devons décider si cette évaluation hors ordre est correcte.
Réponse:
Nous voulons le soutenir. La spécification a été mise à jour pour refléter cela. Les arguments doivent être spécifiés dans l’ordre lexical sur le site d’appel et, si un argument nécessaire à la méthode de création est spécifié après le littéral de chaîne interpolé, une erreur est générée.
await
Utilisation dans les trous d'interpolation
Étant donné que $"{await A()}"
est une expression valide aujourd’hui, nous devons rationaliser les trous d’interpolation avec await. Nous pourrions résoudre ce problème avec quelques règles :
- Si une chaîne interpolée utilisée comme
string
,IFormattable
, ouFormattableString
a unawait
dans un trou d'interpolation, revenez à l'ancien formatage. - Si une chaîne interpolée fait l'objet d'une conversion implicite_string_handler_conversion et que applicable_interpolated_string_handler_type est un
ref struct
,await
n'est pas autorisé à être utilisé dans les trous de format.
Fondamentalement, cette désolidarisation pourrait utiliser une structure ref dans une méthode asynchrone tant que nous garantissons que le ref struct
n'aura pas besoin d'être sauvegardé sur le tas, ce qui devrait être possible si nous interdisons les await
dans les trous d'interpolation.
Sinon, nous pourrions simplement rendre tous les types de gestionnaires non-ref structs, y compris le gestionnaire d’infrastructure pour les chaînes interpolées. Cela nous empêcherait cependant de reconnaître un jour une version de Span
qui n'aurait pas besoin d'allouer d'espace de stockage (scratch space).
Réponse :
Nous traiterons les gestionnaires de chaînes interpolés identiques à tout autre type : cela signifie que si le type de gestionnaire est un struct ref et que le contexte actuel n’autorise pas l’utilisation des structs ref, il est illégal d’utiliser le gestionnaire ici. La spécification sur la réduction des littéraux de chaîne utilisés comme chaînes est intentionnellement vague pour permettre au compilateur de décider des règles qu'il juge appropriées, mais pour les types de gestionnaires personnalisés, ils devront suivre les mêmes règles que le reste du langage.
Gestionnaires en tant que paramètres ref
Certains gestionnaires peuvent vouloir être passés en tant que paramètres ref (in
ou ref
). Devrions-nous autoriser l’une ou l’autre ? Et si c’est le cas, à quoi ressemblera un gestionnaire ref
? ref $""
L'utilisation de paramètres ref est confuse, car vous ne passez pas la chaîne par ref, vous passez le gestionnaire qui est créé à partir du ref par ref, et présente des problèmes potentiels similaires avec les méthodes asynchrones.
Réponse:
Nous voulons le soutenir. La spécification a été mise à jour pour refléter cela. Les règles doivent refléter les mêmes règles que celles qui s’appliquent aux méthodes d’extension sur les types valeur.
Chaînes interpolées par le biais d’expressions binaires et de conversions
Étant donné que cette proposition rend les chaînes interpolées sensibles au contexte, nous aimerions permettre au compilateur de traiter une expression binaire composée entièrement de chaînes interpolées, ou une chaîne interpolée soumise à une conversion, comme un littéral de chaîne interpolée pour les besoins de la résolution de surcharge. Par exemple, prenez le scénario suivant :
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
Il s'agit d'une ambiguïté qui nécessite une conversion en Handler1
ou Handler2
pour la résoudre. Cependant, en effectuant ce cast, nous pourrions potentiellement jeter l'information qu'il y a un contexte du récepteur de la méthode, ce qui signifie que le cast échouerait parce qu'il n'y a rien pour remplir l'information de c
. Un problème similaire se pose avec la concaténation binaire de chaînes de caractères : l'utilisateur pourrait vouloir formater le littéral sur plusieurs lignes pour éviter les retours à la ligne, mais il ne le pourrait pas parce qu'il ne s'agirait plus d'un littéral de chaîne interpolé convertible en type gestionnaire.
Pour résoudre ces cas, nous effectuons les modifications suivantes :
- Une additive_expression composée entièrement d'interpolated_string_expressions et n'utilisant que les opérateurs
+
est considérée comme un interpolated_string_literal aux fins des conversions et de la résolution des surcharges. La chaîne interpolée finale est créée en concaténant logiquement tous les composants individuels de interpolated_string_expression, de gauche à droite. - Une cast_expression ou une relational_expression avec l'opérateur
as
dont l'opérande est une interpolated_string_expressions est considérée comme une interpolated_string_expressions aux fins des conversions et de la résolution des surcharges.
Questions ouvertes:
Voulons-nous le faire ? Nous ne faisons pas cela pour System.FormattableString
, par exemple, mais cela peut être décomposé sur une autre ligne, alors que cela peut être dépendant du contexte et ne peut donc pas être décomposé en une autre ligne. Il n'y a pas non plus de problème de résolution de surcharge avec FormattableString
et IFormattable
.
Réponse :
Nous pensons qu'il s'agit d'un cas d'utilisation valide pour les expressions additives, mais que la version cast n'est pas suffisamment convaincante pour le moment. Nous pouvons l’ajouter ultérieurement si nécessaire. La spécification a été mise à jour pour refléter cette décision.
Autres cas d’usage
Consultez https://github.com/dotnet/runtime/issues/50635 pour obtenir des exemples d’API de gestionnaire proposées à l’aide de ce modèle.
C# feature specifications