C# 9.0 のパターン マッチングの変更
手記
この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
C# 9.0 のパターン マッチングに対する少数の機能強化を検討しています。C# 9.0 には自然な相乗効果があり、多くの一般的なプログラミングの問題に対処するのに適しています。
- https://github.com/dotnet/csharplang/issues/2925 型パターン
- 新しい組み合わせ子の優先順位を強調または明確にするために、かっこで囲まれたパターンを使用する https://github.com/dotnet/csharplang/issues/1350
- https://github.com/dotnet/csharplang/issues/1350 2つの異なるパターンの両方を一致させる必要がある結合パターン
and
。 - https://github.com/dotnet/csharplang/issues/1350 2 つの異なるパターンのいずれかが一致する必要がある離接的
or
パターン。 - https://github.com/dotnet/csharplang/issues/1350 指定されたパターンと一致する必要がない否定された
not
パターン。および - https://github.com/dotnet/csharplang/issues/812 入力値が特定の定数より小さい、以下であるなどを必要とする関係パターン。
括弧で囲まれたパターン
かっこで囲まれたパターンを使用すると、プログラマは任意のパターンの周りにかっこを配置できます。 これは C# 8.0 の既存のパターンではそれほど役に立ちませんが、新しいパターンコンバイネータでは、プログラマがオーバーライドする優先順位が導入されています。
primary_pattern
: parenthesized_pattern
| // all of the existing forms
;
parenthesized_pattern
: '(' pattern ')'
;
型パターン
パターンとして型を許可します。
primary_pattern
: type-pattern
| // all of the existing forms
;
type_pattern
: type
;
これにより、既存の is-type-expression が、パターンが type-pattern である is-pattern-expression に戻されますが、コンパイラによって生成される構文ツリーは変更されません。
微妙な実装の問題の 1 つは、この文法があいまいであるという点です。 a.b
などの文字列は、修飾名 (型コンテキスト内) または点線の式 (式コンテキスト内) として解析できます。 コンパイラは、e is Color.Red
などを処理するために、修飾名をドット式と同じように扱うことができます。 コンパイラのセマンティック分析は、このコンストラクトをサポートするためにバインドされた型パターンとして扱うために、(構文的な) 定数パターン (ドット式など) を型としてバインドできるようにさらに拡張されます。
この変更後は、次のように記述できます。
void M(object o1, object o2)
{
var t = (o1, o2);
if (t is (int, string)) {} // test if o1 is an int and o2 is a string
switch (o1) {
case int: break; // test if o1 is an int
case System.String: break; // test if o1 is a string
}
}
リレーショナル パターン
リレーショナル パターンを使用すると、プログラマは、定数値と比較して、入力値がリレーショナル制約を満たす必要があることを表します。
public static LifeStage LifeStageAtAge(int age) => age switch
{
< 0 => LifeStage.Prenatal,
< 2 => LifeStage.Infant,
< 4 => LifeStage.Toddler,
< 6 => LifeStage.EarlyChild,
< 12 => LifeStage.MiddleChild,
< 20 => LifeStage.Adolescent,
< 40 => LifeStage.EarlyAdult,
< 65 => LifeStage.MiddleAdult,
_ => LifeStage.LateAdult,
};
リレーショナル パターンは、式内の同じ型の 2 つのオペランドを持つこのような二項関係演算子をサポートするすべての組み込み型で、関係演算子の <
、<=
、>
、および >=
をサポートします。 具体的には、sbyte
、byte
、short
、ushort
、int
、uint
、long
、ulong
、char
、float
、double
、decimal
、nint
、および nuint
のためのこれらのリレーショナル パターンをすべてサポートします。
primary_pattern
: relational_pattern
;
relational_pattern
: '<' relational_expression
| '<=' relational_expression
| '>' relational_expression
| '>=' relational_expression
;
式は定数値に評価するために必要です。 その定数値が double.NaN
または float.NaN
場合はエラーです。 式が null 定数の場合はエラーです。
入力型に対して、入力を左オペランド、特定の定数を右オペランドとする適切な組み込み二項関係演算子が定義されている場合、その演算子の評価がリレーショナルパターンの意味となります。 それ以外の場合は、明示的な null 許容変換またはボックス化解除変換を使用して、入力を式の型に変換します。 このような変換が存在しない場合は、コンパイル時エラーです。 変換が失敗した場合、パターンは一致しないと見なされます。 変換が成功した場合、パターン マッチング操作の結果は、変換された入力 e OP v
式を評価した結果です。ここで、e
はリレーショナル演算子、OP
v
は定数式です。
パターン コンバイネーター
パターン コンバイネータ、and
を使用して 2 つの異なるパターンの両方を照合できます (これは、and
の繰り返し使用によって任意の数のパターンに拡張できます)、or
(ditto) を使用する 2 つの異なるパターンのいずれか、または not
を使用したパターンの 否定 のいずれかです。
組み合わせ子の一般的な用途の一つがイディオムです。
if (e is not null) ...
現在のイディオム e is object
よりも読みやすく、このパターンは、null 以外の値をチェックしていることを明確に表しています。
and
と or
の組み合わせ子は、値の範囲をテストするのに役立ちます
bool IsLetter(char c) => c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';
この例は、and
の解析優先度が or
よりも高い (つまり、より密接にバインドされる) ことを示しています。 プログラマは、のかっこでつながれたパターン を使用して、優先順位を明示的にすることができます。
bool IsLetter(char c) => c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');
すべてのパターンと同様に、これらのコンビネータは、入れ子になったパターン、is-pattern-expression、switch-expression、およびswitch文のケースラベルのパターンなど、パターンが想定される任意のコンテキストで使用できます。
pattern
: disjunctive_pattern
;
disjunctive_pattern
: disjunctive_pattern 'or' conjunctive_pattern
| conjunctive_pattern
;
conjunctive_pattern
: conjunctive_pattern 'and' negated_pattern
| negated_pattern
;
negated_pattern
: 'not' negated_pattern
| primary_pattern
;
primary_pattern
: // all of the patterns forms previously defined
;
6.2.5 文法のあいまいさに移動する
型パターンの導入により、トークン =>
の前にジェネリック型が表示される可能性があります。 したがって、§6.2.5 文法のあいまいさ に記載されている一連のトークンに =>
を追加して、型引数リストを開始する <
のあいまいさを解消できます。 https://github.com/dotnet/roslyn/issues/47614も参照してください。
提案された変更に関する未解決の問題
関係演算子の構文
and
、or
、および not
は何らかのコンテキスト キーワードですか? そうである場合、破壊的な変更はありますか (たとえば、declaration-pattern での指定子としての使用と比較して)。
関係演算子のセマンティクス (型など)
関係演算子を使用して式で比較できるすべてのプリミティブ型をサポートすることが期待されます。 単純な場合の意味は明確です
bool IsValidPercentage(int x) => x is >= 0 and <= 100;
しかし、入力がそのようなプリミティブ型でない場合、どのような型に変換しますか?
bool IsValidPercentage(object x) => x is >= 0 and <= 100;
入力型が既に同等のプリミティブである場合、それが比較の型であることを提案しました。 ただし、入力が同等のプリミティブでない場合、リレーショナルはリレーショナルの右側にある定数の型に対する暗黙的な型テストを含むものとして扱われます。 プログラマが複数の入力型をサポートする場合は、明示的に行う必要があります。
bool IsValidPercentage(object x) => x is
>= 0 and <= 100 or // integer tests
>= 0F and <= 100F or // float tests
>= 0D and <= 100D; // double tests
結果: リレーショナルには、リレーショナルの右側にある定数の型に対する暗黙的な型テストが含まれます。
and
の左から右へ流れる型情報
and
コンビネータを記述すると、最上位の型について左側で学んだ型情報が右側に流れる可能性があると示唆されています。 例えば
bool isSmallByte(object o) => o is byte and < 100;
ここで、2番目のパターンに対する入力タイプは、and
の左側のタイプ絞り込み要件により絞り込まれます。 次のように、すべてのパターンに対して型の縮小セマンティクスを定義します。 パターン P
の 狭い型 は、次のように定義されます。
P
が型パターンの場合、縮小型 はその型パターンの型です。P
が宣言パターンの場合、縮小型 は宣言パターンの型です。P
が明示的な型を与える再帰パターンである場合、絞り込まれた型 はその型です。P
が とITuple
の規則に従って照合された場合、の縮小型 は型System.Runtime.CompilerServices.ITuple
です。P
が定数パターンであり、定数が NULL 定数ではなく、式に入力型への定数式変換が存在しない場合、絞り込まれた型は定数の型です。P
がリレーショナル パターンであり、定数式が 定数式変換 を 入力型に対して持たない場合、縮小型 はその定数の型になります。P
がor
パターンの場合、もしそのような共通型が存在するならば、縮小型 は、サブパターンの 縮小型 の共通型となります。 この目的のために、共通型アルゴリズムでは ID、ボックス化、および暗黙的な参照変換のみが考慮され、一連のor
パターンのすべてのサブパターンが考慮されます (かっこで保護されたパターンは無視されます)。P
がand
パターンの場合、狭い型 は、右側のパターンの 狭い型 です。 また、左パターンの 狭型 は、右パターンの 入力タイプ である。- それ以外の場合、
P
の 狭い型 はP
の入力型です。
結果: 上記の縮小セマンティクスが実装されました。
変数定義と確定代入
or
パターンと not
パターンを追加すると、パターン変数と明確な代入に関する興味深い新しい問題が発生します。 変数は通常、最大で 1 回宣言できるため、or
パターンの一方で宣言されたパターン変数は、パターンが一致したときに確実に割り当てられないように見えます。 同様に、not
パターン内で宣言された変数は、パターンが一致したときに確実に割り当てられるとは想定されません。 これに対処する最も簡単な方法は、これらのコンテキストでのパターン変数の宣言を禁止することです。 ただし、これは制限が厳しすぎる可能性があります。 考慮すべきその他の方法があります。
考慮する必要があるシナリオの 1 つは、次の点です。
if (e is not int i) return;
M(i); // is i definitely assigned here?
これは現在機能しません。
これをサポートすることは、否定条件 if
ステートメントのサポートを追加するよりも(プログラマの観点から)簡単です。 このようなサポートを追加しても、プログラマはなぜ上記のスニペットが機能しないのか疑問に思うでしょう。 一方、
これに関連するのは、disjunctive-pattern における明確な割り当ての問題です。
if (e is 0 or int i)
{
M(i); // is i definitely assigned here?
}
入力が 0 でない場合にのみ、i
が確実に割り当てられることが予想されます。 しかし、入力がブロック内でゼロであるかどうかはわかりませんので、i
は確実に割り当てられません。 しかし、i
を異なる相互排他的パターンで宣言することを許可した場合はどうでしょうか。
if ((e1, e2) is (0, int i) or (int i, 0))
{
M(i);
}
ここで、i
変数はブロック内で確実に割り当てられ、ゼロ要素が見つかったときにタプルの他の要素から値を受け取ります。
また、ケース ブロックのすべてのケースで変数を定義 (乗算) することを許可することも推奨されています。
case (0, int x):
case (int x, 0):
Console.WriteLine(x);
この作業を行うには、このような複数の定義が許可される場所と、そのような変数が確実に割り当てられていると見なされる条件を慎重に定義する必要があります。
そのような作業を後で(私がアドバイスする)まで延期する場合は、C#9で言うことができます
not
またはor
の下では、パターン変数を宣言できません。
その後、後でリラックスすることの可能な価値に関する洞察を提供するいくつかの経験を開発する時間があります。
結果: パターン変数は、not
または or
パターンの下で宣言できません。
診断、包含、および網羅性
これらの新しいパターンフォームは、診断可能なプログラマエラーの多くの新しい機会を導入します。 診断するエラーの種類とその方法を決定する必要があります。 いくつかの例を次に示します。
case >= 0 and <= 100D:
このケースは一致しません (入力を int
と double
の両方にすることはできません)。 一致することのないケースを検出した場合には既にエラーが発生しますが、新しいシナリオでは、その文言 ("スイッチケースは以前のケースで既に処理されています" と "パターンはスイッチ式の以前の分岐ですでに処理されています") が誤解を与える可能性があります。 パターンが入力と一致しないという表現を変更する必要がある場合があります。
case 1 and 2:
同様に、値を 1
と 2
の両方にできないため、これはエラーになります。
case 1 or 2 or 3 or 1:
このケースは一致させることができますが、最後の or 1
はパターンに意味を追加しません。 私は、複合パターンの論理結合または論理分離が、パターン変数を定義しないか、一致した値のセットに影響を与えない場合には必ずエラーを発生させることを目指すべきであると提案します。
case < 2: break;
case 0 or 1 or 2 or 3 or 4 or 5: break;
ここでは、0 or 1 or
は 2 番目のケースに何も追加しません。これらの値は最初のケースによって処理されるためです。 これもエラーに値します。
byte b = ...;
int x = b switch { <100 => 0, 100 => 1, 101 => 2, >101 => 3 };
このようなスイッチ式は網羅的であると考えるべきです (考えられるすべての入力値を処理します)。
C# 8.0 では、型 byte
値に対して arm を持つ switch 式であっても、C# 8 では網羅的とは見なされません。 リレーショナル パターンの網羅性を適切に処理するには、このケースも処理する必要があります。 これは技術的には重大な変更ですが、ユーザーは気付かない可能性があります。
C# feature specifications