C#7には、ついに
パターンマッチングと呼ばれる待望の機能があり
ます 。 F#などの関数型言語に精通している場合、現在の形式ではこの機能に少しがっかりするかもしれませんが、今日でもさまざまなシナリオでコードを簡素化できます。
各新機能には、パフォーマンスが重要なアプリケーションで作業する開発者にとって危険が伴います。 新しいレベルの抽象化は優れていますが、それらを効果的に使用するには、内部で何が起こっているのかを知る必要があります。 今日は、パターンマッチングの内部を調べて、これがどのように実装されているかを理解します。
C#言語は、is式および
switchステートメントの caseブロック内で使用できるパターンの概念を導入しました。
テンプレートには3つのタイプがあります。
is-expressionsのパターンマッチング
public void IsExpressions(object o) {
is式は値が定数であるかどうかをチェックでき、型チェックはさらに
パターン変数を作成でき
ます 。
is式のパターンマッチングに関連する興味深い側面がいくつか見つかりました。
- ifステートメントに入力された変数は、外側のスコープまで上昇します。
- ifステートメントに入力された変数は、パターンが一致した場合にのみ完全に定義されます(明確に割り当てられます)。
- is-expressionsでのconst-patternマッチングの現在の実装はあまり効率的ではありません。
最初に、最初の2つのケースを確認します。
public void ScopeAndDefiniteAssigning(object o) { if (o is string s && s.Length != 0) { Console.WriteLine("o is not empty string"); }
最初の
ifステートメントは変数
s を導入し、変数はメソッド全体の内側に表示されます。 これは合理的ですが、同じブロック内の他のifステートメントが同じ名前を再利用しようとすると、ロジックが複雑になります。 この場合、衝突を避けるために別の名前を使用する
必要があります。
is式で導入された変数は、述語が
trueの場合にのみ完全に定義され
ます 。 これは、2番目の
ifステートメントの変数
nが右側のオペランドで定義されていないことを意味しますが、この変数は既に宣言されている
ので 、
int.TryParseメソッドで
out変数として使用できます。
上記の3番目の側面が最も重要です。 次のコードを検討してください。
public void BoxTwice(int n) { if (n is 42) Console.WriteLine("n is 42"); }
ほとんどの場合、is式は
object.Equals(constValue、variable)に変換され
ます(仕様で
、==演算子をプリミティブ型に使用するように指定されている場合でも):
public void BoxTwice(int n) { if (object.Equals(42, n)) { Console.WriteLine("n is 42"); } }
このコードにより2つのボクシングが発生し、クリティカルなアプリケーションパスで使用するとパフォーマンスに重大な影響を与える可能性があります。
oがnull表現になるとパッケージ化が発生し(
eの準最適コードを参照)、現在の動作もすぐに修正されることを期待しています(githubに対応する
チケットがあります)。
n-変数のタイプが
objectの場合、
oが42の 場合 、1つのメモリ割り当てが発生します(リテラル
42をパックするため)。ただし、スイッチに基づく同様のコードはメモリ割り当てを行いません。
is-式のvarパターン
varパターンは、1つの重要な違いがある型パターンの特殊なケースです。値が
nullであっても、パターンは任意の値に一致し
ます 。
public void IsVar(object o) { if (o is var x) Console.WriteLine($"x: {x}"); }
oはオブジェクトがtrueである場合、
oは nullで
はありませんが、
oはvar xは常に
trueです。 コンパイラーはこれを認識しており、リリース(*)モードではifコンストラクトが完全に削除され、単にコンソールメソッドが呼び出されたままになります。 残念ながら、コンパイラは次の場合にコードが到達不能であることを警告しません。
if(!(oはvar x))Console.WriteLine( "Unreachable") 。 これも修正されることを願っています。
(*)動作がリリースモードでのみ異なる理由は明らかではありません。 しかし、私はすべての問題に1つの性質があると思います:機能の最初の実装は最適ではありません。 しかし
、 Neil Gafter
によるこのコメントに基づいて、これは次のように変更されます。 ここでお探しの改善のほとんどは、新しいコードで「無料」になると思います。」
nullチェックがないため、このケースは非常に特殊で潜在的に危険です。 しかし、正確に何が起こっているかを知っている場合は、この一致オプションが役立つことがあります。 式内に一時変数を導入するために使用できます。
public void VarPattern(IEnumerable<string> s) { if (s.FirstOrDefault(o => o != null) is var v && int.TryParse(v, out var n)) { Console.WriteLine(n); } }
IS式とエルビス演算子
もう1つ、非常に便利なケースがあります。 型パターンは、値が
nullでない場合にのみ値に一致し
ます 。 この「フィルタリング」ロジックを
ヌル伝播演算子と共に使用して、コードをより読みやすくすることができます。
public void WithNullPropagation(IEnumerable<string> s) { if (s?.FirstOrDefault(str => str.Length > 10)?.Length is int length) { Console.WriteLine(length); }
同じテンプレートを値型と参照型の両方に使用できることに注意してください。
パターンマッチングスイッチブロック
C#7は
switchステートメントを拡張して、
caseブロックでパターンを使用します。
public static int Count<T>(this IEnumerable<T> e) { switch (e) { case ICollection<T> c: return c.Count; case IReadOnlyCollection<T> c: return c.Count;
この例は、switchステートメントの最初の変更セットを示しています。
- switchステートメントは、任意のタイプの変数を使用できます。
- ケース節はパターンを示す場合があります。
- 場合の文の順序は重要です。 前のケースがベースタイプと一致し、次のケースが派生タイプと一致する場合、コンパイラはエラーをスローします。
- すべてのcaseブロックには、暗黙的なnull (**)チェックが含まれています 。 前の例では、引数がnullでない場合にのみ機能するため、最後のcaseブロックは正しいです。
(**)最後の
ケースブロックは、C#7で追加された、「
廃棄 」パターンと呼ばれる別の機能を示しています。 名前
_は特別で、変数が不要であることをコンパイラーに伝えます。
case句の型テンプレートには変数名が必要です。使用しない場合は、
_を使用して無視できます。
次のスニペットは、
スイッチに基づくパターンマッチングの別の機能、つまり述語を使用する機能を示しています。
public static void FizzBuzz(object o) { switch (o) { case string s when s.Contains("Fizz") || s.Contains("Buzz"): Console.WriteLine(s); break; case int n when n % 5 == 0 && n % 3 == 0: Console.WriteLine("FizzBuzz"); break; case int n when n % 5 == 0: Console.WriteLine("Fizz"); break; case int n when n % 3 == 0: Console.WriteLine("Buzz"); break; case int n: Console.WriteLine(n); break; } }
スイッチには、同じタイプの複数の
ケースブロックを
含めることができます。 この場合、コンパイラは1つのブロックですべての型チェックを組み合わせて、冗長な計算を回避します。
public static void FizzBuzz(object o) {
ただし、次の2つの点に注意してください。
- コンパイラは、同じタイプの連続したcaseブロックのみを組み合わせます。異なるタイプのブロックを混在させると、コンパイラは最適性の低いコードを生成します。
switch (o) {
コンパイラは次のように変換します。
if (o is int n && n == 1) return 1; if (o is string s && s == "") return 2; if (o is int n2 && n2 == 2) return 3; return -1;
- コンパイラは、 ケースブロックの順序が正しくないという典型的な問題を防ぐために、可能なすべてのことを行います。
switch (o) { case int n: return 1;
しかし、コンパイラーは、ある述部が別の述部よりも強力であることを認識していないため、実際、次のブロックを達成できません。
switch (o) { case int n when n > 0: return 1;
パターン101マッチング
- C#7で導入されたパターンは、constパターン、typeパターン、varパターン、 discardパターンです。
- サンプルは、is-expressionsおよびcaseブロックで使用できます。
- 値型のis-expressionsでのconstパターンの実装は、パフォーマンスの点で完全にはほど遠いです。
- varパターンは任意の値と一致するため、注意が必要です。
- switchステートメントを使用すると、 when句に追加の述語を使用して型チェックを設定できます。