補間文字列の改善
手記
この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
チャンピオンの課題: https://github.com/dotnet/csharplang/issues/4487
概要
挿入文字列式を作成および使用するための新しいパターンを導入し、一般的な string
シナリオと、フレームワーク内の文字列の書式設定から不要な割り当てを発生させることなく、ログ フレームワークなどのより特殊なシナリオの両方で効率的な書式設定と使用を可能にします。
モチベーション
現在、文字列補間は主に string.Format
の呼び出しまで下がっています。 これは一般的な目的ですが、さまざまな理由で非効率的になる可能性があります。
- ランタイムによって、引数の型と順序が完全に一致する
string.Format
のオーバーロードが実装されている場合を除き、構造体の引数はすべてのボックス化されます。- この順序は、ランタイムがメソッドのジェネリック バージョンを導入することをためらう理由です。これは、非常に一般的なメソッドのジェネリック インスタンス化の組み合わせ爆発につながるためです。
- ほとんどの場合、引数に配列を割り当てる必要があります。
- インスタンスが不要な場合でも、インスタンス化を避けることはできません。 たとえば、ログフレームワークでは、アプリケーションの現在のログレベルに応じて、必要のない文字列が生成されるため、文字列補間を回避することをお勧めします。
- 現在、
Span
またはその他の ref 構造体型を使用することはできません。ref 構造体はジェネリック型パラメーターとして使用できないためです。つまり、ユーザーが中間の場所へのコピーを避けたい場合は、文字列を手動で書式設定する必要があります。
内部的には、ランタイムには、これらのシナリオの最初の 2 つの処理に役立つ ValueStringBuilder
という型があります。 stackalloc'd バッファーをビルダーに渡し、すべての部分で AppendFormat
を繰り返し呼び出し、最後の文字列を取得します。結果の文字列がスタック バッファーの境界を超える場合は、ヒープ上の配列に移動できます。 ただし、この型は直接公開するのは危険です。不適切な使用により、レンタルされた配列が二重に破棄される可能性があるため、2 つの場所がレンタルされた配列にのみアクセスできると思われるため、プログラム内のすべての種類の未定義の動作が発生します。 この提案では、挿入文字列リテラルを記述するだけで、ネイティブ C# コードからこの型を安全に使用する方法が作成されます。記述されたコードは変更されず、ユーザーが書き込む挿入文字列はすべて改善されます。 また、このパターンを拡張して、他のメソッドに引数として渡される補間文字列が、メソッドの受信側によって定義されたハンドラー パターンを使用できるようにします。これにより、ログ フレームワークなど、不要になる文字列の割り当てを回避したり、C# ユーザーに使い慣れた便利な補間構文を提供したりできます。
詳細な設計
ハンドラー パターン
メソッドに引数として渡される挿入文字列を表すことができる新しいハンドラー パターンを紹介します。 パターンの単純な英語は次のとおりです。
interpolated_string_expression がメソッドに引数として渡されると、パラメーターの型を調べてみます。 パラメーターの型に、2 つの int 型パラメーター (literalLength
と formattedCount
) を受け取るコンストラクターが定義されている場合、また、元のパラメーターに付与された属性によって追加の引数を取ることができる場合、さらに、必要に応じて out 型の bool 値パラメーターが末尾に配置されている場合、そして、元のパラメーターの型に、補間された文字列の各部分に対して呼び出せる AppendLiteral
および AppendFormatted
メソッドが定義されている場合、従来の string.Format(formatStr, args)
を使用せず、それを利用して補間処理を行います。 より具体的な例は、これを画像化するのに役立ちます。
// 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);
ここでは、TraceLoggerParamsInterpolatedStringHandler
には正しいパラメーターを持つコンストラクターがあるため、補間された文字列にはそのパラメーターへの暗黙的なハンドラー変換があり、上記のパターンに下がると言います。 これに必要な仕様は少し複雑で、以下に展開されています。
この提案の残りの部分では、Append...
を使用して、両方が該当する場合に AppendLiteral
または AppendFormatted
のいずれかを参照します。
新しい属性
コンパイラは 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()
{
}
}
}
この属性は、型が有効な補間文字列ハンドラー型であるかどうかを判断するためにコンパイラによって使用されます。
コンパイラは、次の 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; }
}
}
この属性は、パラメーター位置で使用される補間文字列ハンドラー パターンを減らす方法をコンパイラに通知するために、パラメーターで使用されます。
補間文字列ハンドラー変換
型 T
は System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute
で修飾されている場合、applicable_interpolated_string_handler_type とみなされます。
T
への暗黙的な interpolated_string_handler_conversion は、interpolated_string_expression から行うか、+
演算子のみで連結した _interpolated_string_expression_s のみで構成される additive_expression から行います。
このスペックレットの残りの部分でわかりやすくするために、interpolated_string_expression は単純な interpolated_string_expressionと、_interpolated_string_expression_sのみで構成され、+
演算子のみを使用する additive_expression の両方を指します。
この変換は、ハンドラー パターンを使用して補間を実際に下げようとしたときに後でエラーが発生するかどうかに関係なく、常に存在することに注意してください。 これは、予測可能で有用なエラーが発生し、挿入文字列の内容に基づいて実行時の動作が変わらないことを確認するために行われます。
適用可能な関数メンバーの調整
適用可能な関数メンバー アルゴリズム (§12.6.4.2) の文言を次のように調整します (各セクションに新しいサブ行頭文字が太字で追加されます)。
全ての以下の条件が真である場合、関数メンバーは A
引数リストに関して 適用可能な 関数メンバーであると言われます。
A
の各引数は、「対応するパラメーター (§12.6.2.2)」で説明されているように、関数メンバー宣言のパラメーターに対応し、引数が対応しないパラメーターは省略可能なパラメーターです。A
の各引数について、引数のパラメーター受け渡しモード (値、ref
、またはout
) は、対応するパラメーターのパラメーター受け渡しモードと同じです。- 値パラメーターまたはパラメーター配列に対しては、引数から対応するパラメーターの型への暗黙の変換 (§10.2) が存在しています。
-
ref
パラメーターの型が構造体型の場合、引数から対応するパラメーターの型への暗黙的な interpolated_string_handler_conversion が存在するか、または ref
またはout
パラメーターの場合、引数の型は対応するパラメーターの型と同じです。 結局のところ、ref
またはout
パラメーターは、渡された引数のエイリアスです。
パラメーター配列を含む関数メンバーの場合、関数メンバーが上記の規則によって適用できる場合、その 通常の形式で適用可能であると言われます。 パラメーター配列を含む関数メンバーが通常の形式で適用できない場合、関数メンバーは代わりに展開形式で適用できます。
- 展開されたフォームは、関数メンバー宣言のパラメーター配列をパラメーター配列の要素型の 0 個以上の値パラメーターに置き換えて、引数リスト
A
の引数の数がパラメーターの合計数と一致します。A
が関数メンバー宣言の固定パラメーターの数よりも少ない引数を持つ場合、関数メンバーの拡張形式を構築できないため、適用できません。 - それ以外の場合、それぞれの引数が
A
内で対応するパラメーターのパラメーター受け渡しモードと同一の場合、展開形式が適用されます。- 固定値パラメーターまたは拡張によって作成された値パラメーターの場合は、引数の型から対応するパラメーターの型への暗黙的な変換 (§10.2) が存在します。
-
ref
パラメーターの型が構造体型の場合、引数から対応するパラメーターの型への暗黙的な interpolated_string_handler_conversion が存在するか、または ref
またはout
パラメーターの場合、引数の型は対応するパラメーターの型と同じです。
重要な注意: これは、2 つの同等のオーバーロードがある場合、applicable_interpolated_string_handler_typeの型によってのみ異なる場合、これらのオーバーロードはあいまいと見なされることを意味します。 さらに、明示的なキャストが見られないため、適用可能な両方のオーバーロードが InterpolatedStringHandlerArguments
を使用し、ハンドラーの低下パターンを手動で実行せずに完全に呼び出されないという、解決できないシナリオが発生する可能性があります。 これを解決するために、より優れた関数メンバー アルゴリズムに変更を加える可能性がありますが、このシナリオは発生する可能性は低く、対処の優先順位ではありません。
式の調整からの変換の向上
より適切な変換を式 (§12.6.4.5) セクションから次のように変更します。
式 E
から型 T1
に変換する暗黙的な変換 C1
と、式 E
から型 T2
に変換する暗黙的な変換 C2
を考えると、C1
は C2
よりも優れた変換です。
E
は非定数 interpolated_string_expression、C1
は implicit_string_handler_conversion、T1
は applicable_interpolated_string_handler_type、およびC2
は implicit_string_handler_conversionではありません。E
はT2
と完全には一致せず、次のうち少なくとも 1 つが保持されます。E
T1
と完全に一致します ( §12.6.4.5)T1
は、T2
(§12.6.4.6) よりも優れた変換ターゲットです
これは、問題の補間文字列が定数式かどうかに応じて、明らかではない可能性のあるオーバーロード解決規則があることを意味します。 例えば:
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
これは、定数として出力できるものがそうし、オーバーヘッドを発生させないように、一方で定数にできないものはハンドラー パターンを使用するように導入されています。
InterpolatedStringHandler と使用法
System.Runtime.CompilerServices
:DefaultInterpolatedStringHandler
で新しいタイプを紹介します。 これは、C# コンパイラによる直接使用を目的とした、ValueStringBuilder
と同じセマンティクスの多くを持つ ref 構造体です。 この構造体は、ほぼ次のようになります。
// 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);
}
}
interpolated_string_expression (§12.8.3) の意味に関する規則を若干変更します。
挿入文字列の型が string
され、型 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler
が存在し、現在のコンテキストがその型の使用をサポートしている場合、文字列はハンドラー パターンを使用して低下します。最後の string
値は、ハンドラー型で ToStringAndClear()
を呼び出すことによって取得されます。それ以外の場合、挿入文字列の型がSystem.IFormattable
または System.FormattableString
場合 [残りは変更されません]
"および現在のコンテキストでは、その型の使用をサポートしています" ルールは、このパターンの使用を最適化する余裕をコンパイラに与えるために意図的にあいまいです。 ハンドラー型は ref 構造体型である可能性が高く、通常、ref 構造体型は非同期メソッドでは許可されません。 この特定のケースでは、補間された文字列式が評価された後にハンドラーが削除されるため、追加の複雑な分析なしでハンドラーの型が安全に使用されていることを静的に判断できるため、補間ホールに await
式が含まれていない場合、コンパイラはハンドラーを使用できます。
未解決の質問:
代わりに、コンパイラに DefaultInterpolatedStringHandler
について知させ、string.Format
呼び出しを完全にスキップしますか? 手動で string.Format
を呼び出すときに、人々に目立たせたくないメソッドを非表示にすることができます。
回答: はい。
未解決の質問:
System.IFormattable
と System.FormattableString
のハンドラーも必要ですか?
回答: いいえ。
ハンドラー パターンのコード生成
このセクションでは、メソッド呼び出しの解決は、§12.8.10.2に記載されている手順を参照します。
コンストラクターの解決
applicable_interpolated_string_handler_typeT
と interpolated_string_expressioni
を指定すると、T
の有効なコンストラクターのメソッド呼び出しの解決と検証は次のように実行されます。
- インスタンス コンストラクターのメンバー参照は、
T
で実行されます。 結果のメソッド グループは、M
呼び出されます。 - 引数リスト
A
は次のように構成されます。- 最初の 2 つの引数は整数定数で、
i
のリテラル長と、i
内の 補間 コンポーネントの数をそれぞれ表します。 i
がメソッドM1
のパラメーターpi
の引数として使用され、パラメーターpi
がSystem.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute
で属性付けされている場合、その属性のArguments
配列内のすべての名前Argx
に対して、コンパイラは同じ名前を持つパラメーターpx
に一致します。 空の文字列は、M1
の受信側と一致します。Argx
がM1
のパラメーターと一致できない場合、またはArgx
がM1
の受信側を要求し、M1
が静的メソッドである場合、エラーが生成され、それ以上の手順は実行されません。- それ以外の場合は、解決されたすべての
px
の型が、Arguments
配列で指定された順序で引数リストに追加されます。 各px
は、M1
で指定されているのと同じref
セマンティクスで渡されます。
- 最後の引数は、
out
パラメーターとして渡されるbool
です。
- 最初の 2 つの引数は整数定数で、
- 従来のメソッド呼び出し解決は、メソッド グループの
M
と引数リストA
で実行されます。 メソッド呼び出しの最終的な検証のために、M
のコンテキストは型T
を介して member_access として扱われます。- 単一の最適なコンストラクター
F
が見つかった場合、オーバーロード解決の結果はF
。 - 該当するコンストラクターが見つからなかった場合は、手順 3 が再試行され、最後の
bool
パラメーターがA
から削除されます。 この再試行で該当するメンバーも検出されない場合は、エラーが生成され、それ以上の手順は実行されません。 - 単一の最適なメソッドが見つからなかった場合、オーバーロード解決の結果はあいまいになり、エラーが生成され、それ以上の手順は実行されません。
- 単一の最適なコンストラクター
F
の最終的な検証が実行されます。A
の要素i
後に字句的に発生した場合、エラーが生成され、それ以上の手順は実行されません。A
がF
の受信側を要求し、F
が member_initializerの initializer_target として使用されているインデクサーである場合、エラーが報告され、それ以上の手順は実行されません。
注: ここでの解決では、意図的に Argx
要素に渡された他の引数の実際の式を使用しません。 変換後の型のみを考慮します。 これにより、二重変換の問題が発生したり、M1
に渡されたときにラムダが 1 つのデリゲート型にバインドされ、M
に渡されたときに別のデリゲート型にバインドされたりする予期しないケースがなくなります。
注: ネストされたメンバー初期化子の評価順序のため、メンバー初期化子として使用されるインデクサーについてはエラーが報告されます。 次のコード スニペットを考えてみましょう。
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 { }
}
}
__c1.C2[]
の引数は、インデクサーのレシーバーよりも先に評価されます。 このケースに対応する低レベル変換 (__c1.C2
の一時変数を作成して両方のインデクサー呼び出しで共有するか、または最初のインデクサー呼び出しでのみ使用して引数を両方の呼び出しで共有する) を考え出すことはできますが、これは極端なケースであり、どのような低レベル変換を採用しても混乱を招くと考えています。 そのため、このシナリオは完全に禁止されています。
オープン質問:
Create
の代わりにコンストラクターを使用する場合は、パターンを少し絞り込む代わりにランタイム codegen を改善します。
回答: ここではコンストラクターに制限します。 シナリオが発生した場合は、後で一般的な Create
メソッドを追加する方法を再検討できます。
Append...
メソッドのオーバーロードの解決
applicable_interpolated_string_handler_typeT
と interpolated_string_expressioni
を指定すると、T
で有効な一連の Append...
メソッドのオーバーロード解決が次のように実行されます。
i
に interpolated_regular_string_character コンポーネントが含まれている場合:T
に対してAppendLiteral
という名前でメンバー検索が行われます。 結果のメソッド グループは、Ml
呼び出されます。- 引数リスト
Al
は、string
型の 1 つの値パラメーターで構成されます。 - 従来のメソッド呼び出し解決は、メソッド グループの
Ml
と引数リストAl
で実行されます。 メソッド呼び出しの最終的な検証のために、Ml
のコンテキストは、T
のインスタンスを介して member_access として扱われます。- 単一の最適なメソッド
Fi
が見つかり、エラーが生成されなかった場合、メソッド呼び出し解決の結果はFi
。 - それ以外の場合は、エラーが報告されます。
- 単一の最適なメソッド
i
の各 補間ix
コンポーネントに対して:T
に対してAppendFormatted
という名前でメンバー検索が行われます。 結果のメソッド グループは、Mf
呼び出されます。- 引数リスト
Af
が作成されます。- 最初のパラメーターは、値渡しされる
ix
のexpression
です。 ix
が constant_expression コンポーネントを直接含んでいる場合、名前としてalignment
が指定された整数値パラメーターが追加されます。ix
に直接 interpolation_formatが続く場合は、名前が指定された文字列値パラメーターformat
追加されます。
- 最初のパラメーターは、値渡しされる
- 従来のメソッド呼び出し解決は、メソッド グループの
Mf
と引数リストAf
で実行されます。 メソッド呼び出しの最終的な検証のために、Mf
のコンテキストは、T
のインスタンスを介して member_access として扱われます。- 単一の最適なメソッド
Fi
が見つかった場合、メソッド呼び出し解決の結果はFi
。 - それ以外の場合は、エラーが報告されます。
- 単一の最適なメソッド
- 最後に、手順 1 と 2 で検出されたすべての
Fi
について、最終的な検証が実行されます。Fi
が値としてbool
を返さないか、void
を返さない場合、エラーが報告されます。- すべての
Fi
が同じ型を返さない場合は、エラーが報告されます。
これらの規則では、Append...
呼び出しの拡張メソッドは許可されないことに注意してください。 これを選択した場合は有効にすることを検討できますが、これは列挙子パターンに似ています。ここで、GetEnumerator
は拡張メソッドですが、Current
や MoveNext()
はできません。
これらの規則 、Append...
呼び出しの既定のパラメーターを許可します。これは、CallerLineNumber
や CallerArgumentExpression
(言語でサポートされている場合) で動作します。
一部のハンドラーは、補間されたコンポーネントと基本文字列の一部であったコンポーネントの違いを理解できるようにする必要があるため、基本要素と補間ホールに対して個別のオーバーロード参照ルールがあります。
未解決の質問
構造化ログなどの一部のシナリオでは、補間要素の名前を指定できるようにする必要があります。 たとえば、現在のログの呼び出しは Log("{name} bought {itemCount} items", name, items.Count);
のようになります。 {}
内の名前は、出力の一貫性と均一性を確保するのに役立つロガーの重要な構造情報を提供します。 補間ホールの :format
コンポーネントを再利用できる場合もありますが、多くのロガーは既に書式指定子を理解しており、この情報に基づいて出力書式を設定するための既存の動作を持っています。 これらの名前付き指定子の配置を有効にするために使用できる構文はありますか?
サポートが C# 10 に到達することを条件として、あるケースでは CallerArgumentExpression
を上手くやり遂げることができる場合があります。 ただし、メソッド/プロパティを呼び出す場合は、十分ではない可能性があります。
回答:
直交する別の言語機能で探求できるテンプレート文字列にはいくつかの興味深い点がありますが、タプルの使用などの解決策に比べて、この場での特定の構文には大きな利点がないと考えています。$"{("StructuredCategory", myExpression)}"
。
変換の実行
applicable_interpolated_string_handler_typeT
と interpolated_string_expressioni
に対して、有効なコンストラクター Fc
と Append...
メソッド Fa
が解決された場合、i
は次のように行われます。
i
の前に字句的に発生するFc
の引数はすべて評価され、構文的な順序で一時変数に格納されます。 字句の順序を維持するために、より大きな式e
の一部としてi
が発生した場合、i
前に発生したe
のコンポーネントも構文的な順序で再び評価されます。Fc
は、補間文字列リテラル コンポーネントの長さ、補間 ホールの数、以前に評価された引数、およびbool
out 引数を使用して呼び出されます (Fc
が最後のパラメーターとして 1 つで解決された場合)。 結果は、ib
一時値に格納されます。- リテラル コンポーネントの長さは、任意の open_brace_escape_sequence を単一の
{
に置き換えた後、および任意の close_brace_escape_sequence を単一の}
に置き換えた後に計算されます。
- リテラル コンポーネントの長さは、任意の open_brace_escape_sequence を単一の
Fc
がbool
out 引数で終了する場合、そのbool
値に対するチェックが生成されます。 true の場合、Fa
内のメソッドが呼び出されます。 それ以外の場合、それらは呼び出されません。Fa
内のすべてのFax
について、Fax
は、必要に応じて、現在のリテラル コンポーネントまたは 補間 式を使用してib
で呼び出されます。Fax
がbool
を返す場合、その結果はそれまでのすべてのFax
呼び出しと論理積をとります。Fax
がAppendLiteral
の呼び出しである場合、リテラル コンポーネントは、任意の open_brace_escape_sequence を 1 つの{
に置き換え、close_brace_escape_sequence を 1 つの}
に置き換えることでエスケープ解除されます。
- 変換の結果は
ib
。
ここでも、Fc
に渡される引数と、e
に渡される引数は同じテンポラリであることに注意してください。変換は、Fc
が必要とする形式に変換するためにテンポラリ上で行われることがありますが、たとえば、ラムダを Fc
と e
の間で別のデリゲート型にバインドすることはできません。
未解決の質問
この低下は、false を返す Append...
呼び出しの後に補間された文字列の後続の部分が評価されないことを意味します。 これは非常に混乱を招く可能性があります。特に、穴埋め部分の式が値を返すだけでなく、他の処理も実行する場合は顕著です。 代わりに、すべてのフォーマット ホールを最初に評価してから、結果で Append...
を繰り返し呼び出し、false が返された場合は停止することができます。 これにより、すべての式が期待どおりに評価されますが、必要な数のメソッドを呼び出します。 一部のより高度なケースでは部分評価が望ましいかもしれませんが、一般的なケースでは直感的ではありません。
もう 1 つの方法として、すべてのフォーマット ホールを常に評価する場合は、API の Append...
バージョンを削除し、Format
呼び出しを繰り返すだけです。 ハンドラーは、引数を削除するだけで、このバージョンに対してすぐに戻る必要があるかどうかを追跡できます。
回答: 穴の条件付き評価を行います。
未解決の質問
破棄可能なハンドラー型を破棄し、Dispose が呼び出されるように try/finally で呼び出しをラップする必要がありますか? たとえば、bcl 内の補間文字列ハンドラーには、その内部にレンタルされた配列があり、評価中に補間ホールの 1 つが例外をスローした場合、そのレンタルされた配列が破棄されなかった場合にリークされる可能性があります。
回答: いいえ。ハンドラーをローカル (MyHandler handler = $"{MyCode()};
など) に割り当てることができ、そのようなハンドラーの有効期間は不明です。 foreach 列挙子とは異なり、有効期間は明らかであり、列挙子のユーザー定義ローカルは作成されません。
null 許容参照型への影響
実装の複雑さを最小限に抑えるために、メソッドまたはインデクサーの引数として使用される挿入文字列ハンドラー コンストラクターに対して null 許容分析を実行する方法に関するいくつかの制限があります。 特に、コンストラクターから元のコンテキストからのパラメーターまたは引数の元のスロットに情報をフローさせません。また、コンストラクター パラメーター型を使用して、包含メソッド内の型パラメーターのジェネリック型推論を通知することはありません。 これが影響を与える可能性がある例を次に示します。
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()
{
}
}
その他の考慮事項
string
型をハンドラーにも変換できるようにする
型作成者をわかりやすくするために、string
型の式を applicable_interpolated_string_handler_typesに暗黙的に変換できるようにすることを検討できます。 今日提案されているように、作成者は、そのハンドラー型と通常の string
型の両方でオーバーロードする必要があるため、ユーザーは違いを理解する必要はありません。 これは煩わしく分かりにくいオーバーヘッドとなる可能性があります。なぜなら string
式は、expression.Length
が事前に埋められて、穴埋め部分が 0 個の補間とみなすことができるためです。
これにより、新しい API はハンドラーのみを公開でき、string
受け入れるオーバーロードも公開する必要はありません。 ただし、式からの変換を改善するための変更の必要性は回避されないため、機能する一方で不要なオーバーヘッドになる可能性があります。
回答:
これは混乱を招く可能性があると考え、カスタム ハンドラー型の簡単な回避策があります。文字列からユーザー定義の変換を追加します。
ヒープレスな文字列のための Span の組み込み
現在の ValueStringBuilder
には 2 つのコンストラクターがあります。1 つはカウントを受け取り、即座にヒープ上にメモリを割り当てるもの、もう 1 つは Span<char>
を受け取るものです。 通常、この Span<char>
はランタイム コードベースの固定サイズであり、平均で約 250 個の要素です。 この型を本当に置き換えるには、カウント バージョンだけでなく、Span<char>
を受け取る GetInterpolatedString
メソッドも認識する拡張を検討する必要があります。 ただし、ここで解決する可能性のある厄介なケースがいくつかあります。
- 負荷の高いループで繰り返しstackallocを行いたくありません。 この機能の拡張を行う場合は、ループイテレーション間で stackalloc のスパンを共有する必要があります。 これは安全です。
Span<T>
はヒープに格納できない参照構造体であり、これにより安全性が保障されています。ユーザーがそのSpan
への参照を抽出するためには、かなり狡猾な手段を使う必要があります(たとえば、このようなハンドラーを受け入れるメソッドを作成し、ハンドラーからSpan
を意図的に取得して呼び出し元に返すなど)。 ただし、事前に割り当てると、他の質問が発生します。- 私たちは積極的に stackalloc を使うべきですか? ループが入力されない場合、またはスペースが必要な前に終了した場合はどうでしょうか。
- スタックアロックを熱心に行わないと、すべてのループで隠れた分岐を導入することになるのでしょうか? ほとんどのループではこれを気にしない可能性がありますが、コストを支払いたくないタイトなループに影響を与える可能性があります。
- 一部の文字列は非常に大きい場合があり、
stackalloc
に適した量は、実行時要因を含むさまざまな要因に依存します。 C# コンパイラと仕様で事前にこれを決定する必要はないため、https://github.com/dotnet/runtime/issues/25423 を解決し、このような場合にコンパイラが呼び出す API を追加したいと考えています。 前のループでのポイントに対して、さらに多くの長所と短所が追加されます。ヒープに大きな配列を何度も割り当てたくありませんし、必要になる前に大きな配列を割り当てることも避けたいのです。
回答:
これは C# 10 の範囲外です。 より一般的な params Span<T>
機能を見ると、これを一般的に見ることができます。
API の非試行バージョン
わかりやすくするために、この仕様は現在、Append...
メソッドの認識のみを提案しており、常に成功するもの (InterpolatedStringHandler
など) は常にメソッドから true を返します。
これは、エラーが発生した場合や不要な場合 (ログ記録の場合など) にユーザーが書式設定を停止したいが、標準の挿入文字列の使用で不要な分岐が多数発生する可能性がある部分書式設定シナリオをサポートするために行われました。 Append...
メソッドが存在しない場合は FormatX
メソッドだけを使用する補遺を検討できますが、Append...
と FormatX
の両方の呼び出しが混在している場合の処理に関する質問が表示されます。
回答:
試用以外のバージョンの API が必要です。 これを反映するように提案が更新されました。
ハンドラーに前の引数を渡す
現在、提案には残念な対称性がありません。拡張メソッドを縮小形式で呼び出すと、通常の形式で拡張メソッドを呼び出すのとは異なるセマンティクスが生成されます。 これは言語の他のほとんどの場所とは異なり、還元された形は単なる砂糖です。 特定のパラメーターをハンドラーのコンストラクターに渡す必要があることをコンパイラに通知する、メソッドのバインド時に認識される属性をフレームワークに追加することを提案します。 使用法は次のようになります。
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; }
}
}
この使用方法は次のとおりです。
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);
回答する必要がある質問:
- このパターンは一般的に好きですか?
- これらの引数がハンドラー パラメーターの後から来ることを許可しますか? BCL の既存のパターン (
Utf8Formatter
など) では、フォーマット対象の値をフォーマット先の前に配置します。 これらのパターンに最適にするには、これを許可する必要があります。ただし、この順序外の評価が問題ないかどうかを判断する必要があります。
回答:
これをサポートしたいと考えています。 仕様は、これを反映するように更新されました。 引数は呼び出しサイトで字句順に指定する必要があり、挿入文字列リテラルの後に create メソッドに必要な引数を指定すると、エラーが生成されます。
穴埋め部分での await
の使用
$"{await A()}"
は現在有効な式であるため、await を使用して補間ホールを合理化する必要があります。 これをいくつかのルールで解決できます。
string
、IFormattable
、またはFormattableString
として使用される補間文字列に補間穴にawait
がある場合は、古いスタイルのフォーマッタにフォールバックします。- 補間文字列が implicit_string_handler_conversion の対象であり、applicable_interpolated_string_handler_type が
ref struct
である場合、穴埋め部分でawait
を使用することはできません。
基本的に、この糖衣構文の展開では、ref struct を非同期メソッド内で使用できます。ただし、その ref struct
をヒープに保存する必要がないことを担保する必要があります。これは、補間部分で await
を禁止すれば可能です。
または、挿入文字列のフレームワーク ハンドラーを含め、すべてのハンドラー型を ref 以外の構造体にすることもできます。 しかし、これは、スクラッチ領域をまったく割り当てる必要のない Span
バージョンをいつか認識することを妨げるでしょう。
回答:
補間された文字列ハンドラーは他の型と同じように扱います。つまり、ハンドラー型が ref 構造体であり、現在のコンテキストで ref 構造体の使用が許可されていない場合、ここでハンドラーを使用することは無効です。 文字列として使用される文字列リテラルの下げに関する仕様は、コンパイラが適切と見なすルールを決定できるようにするために意図的にあいまいですが、カスタム ハンドラー型の場合は、言語の残りの部分と同じ規則に従う必要があります。
ref パラメーターとしてのハンドラー
一部のハンドラーは、ref パラメーター (in
または ref
) として渡されたいと望むかもしれません。 どちらかを許可する必要がありますか? その場合、ref
ハンドラーは次のようになります。 ref $""
は混乱を招きます。実際には ref で文字列を渡していないので、ref by ref から作成されたハンドラーを渡しており、非同期メソッドで同様の潜在的な問題があります。
回答:
これをサポートしたいと考えています。 仕様は、これを反映するように更新されました。 ルールには、値型の拡張メソッドに適用されるのと同じ規則が反映されている必要があります。
二項演算と変換による文字列補間
この提案では補間文字列がコンテキストに依存するため、コンパイラが補間文字列で構成されるバイナリ式、またはキャストの対象となる補間文字列を、オーバーロード解決の目的で補間文字列リテラルとして扱えるようにしたいと考えています。 たとえば、次のシナリオを考えてみましょう。
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
これはあいまいになるため、Handler1
または Handler2
へのキャストが必要になります。 しかし、そのキャストを行う際に、メソッドレシーバーからコンテキストがあるという情報を捨てる可能性があります。つまり、キャストは失敗します。これは、c
の情報を入力する必要がないためです。 文字列のバイナリ連結でも同様の問題が発生します。ユーザーは、行の途中で折り返されるのを避けるために、複数行にわたってリテラルをフォーマットしたいと考えるかもしれませんが、それでは補間可能な文字列リテラルとしてハンドラー型に変換できなくなるため、実現できません。
これらのケースを解決するために、次の変更を行います。
+
演算子のみで連結した interpolated_string_expressions のみで構成される additive_expression は、変換とオーバーロード解決の観点から interpolated_string_literal とみなされます。 最終的な補間文字列は、個々のすべての interpolated_string_expression コンポーネントを左から右に論理的に連結することによって作成されます。as
演算子を使用する cast_expression または relational_expression で、そのオペランドが interpolated_string_expressions である場合、それは変換とオーバーロード解決の観点から interpolated_string_expressions とみなされます。
オープン質問:
この操作を実行しますか? たとえば、System.FormattableString
ではこれを行いませんが、別の行に分割できますが、コンテキストに依存し、別の行に分割することはできません。 また、FormattableString
と IFormattable
に関するオーバーロード解決の問題もありません。
回答:
加算式のこのような使用は有効な使用例だと考えますが、キャスト版は現時点では十分に説得力があるとは言えません。 必要に応じて後で追加できます。 この決定を反映するように仕様が更新されました。
その他のユース ケース
このパターンを使用して提案されたハンドラー API の例については、https://github.com/dotnet/runtime/issues/50635 を参照してください。
C# feature specifications