C# Advent Calendar 2021 を見てると、C# のパターンマッチングが便利らしい。
でも、パターンマッチングの中身が実際どんな判定(動き)してるのかよくわからないし、なんか怖い。わけわからん動きをするバグの狂気に陥りそう。
そんなわけでパターンマッチングのデコンパイルコードを確認してみました。そこそこ実際のうごきを把握できる(はずな)ので経験値をかせげると思います。
デコンパイルコードの確認には下図のとおり「sharplab」を利用しています。(あと IL は長くなるので記事では掲載外)
C# は 2021 年末現在 C# 10.0 ですが、パターンマッチングは C# 7.0 から 10.0 のなかでアップデートが進んでいます。最新の .NET 6 などでテストしてみるとよいと思います。
Visual Studio 2019 だと .NET 5 まで対応。C# 9.0 までのパターンマッチングで遊ぶことができます。デフォルトだと C# 10.0 の「プロパティパターンの拡張」は利用できないはずです。
パターンマッチングの比較
古式ゆかしい if 文と、パターンマッチングを利用した if 文です。二つの式は同じうごきをしますが、デコンパイルした C# のコードに違いはあるのかチェックしてみる。(クラスとメソッドはなくてもいいのですが、デコンパイルコードと比較しやすいように掲載)
using System; public class Test { public static void Main() { var a = (x:1, y:2); if (a.x == 1 && a.y == 2) { Console.WriteLine("A"); } if (a is { x: 1, y: 2 }) { Console.WriteLine("B"); } } }
下が、C# Decompile のコードです。完全に生成されたコードが一致していました。なんで、見た目が違うだけで同じコードだとわかりました。
public class Test { public static void Main() { ValueTuple<int, int> valueTuple = new ValueTuple<int, int>(1, 2); if (valueTuple.Item1 == 1 && valueTuple.Item2 == 2) { Console.WriteLine("A"); } if (valueTuple.Item1 == 1 && valueTuple.Item2 == 2) { Console.WriteLine("B"); } } }
パターンマッチングのなかで _
をつかって、なんでもいい値を表現することがあると思います。
using System; public class Test { public static void Main() { var p = (x: 10, y: 20); if (p.Item1 == 10) Console.WriteLine("A"); if (p is {x: 10, y: _}) Console.WriteLine("B"); if (p is {x: 10 }) Console.WriteLine("C"); } }
B
と C
を出力する式は、同じ意味だとなんとなくわかると思います。下のように生成コードも同じです。(逆にこの場合は書いていても人の読みやすさだけに効果があり、実行時の処理コストは無い)
public class Test { public static void Main() { ValueTuple<int, int> valueTuple = new ValueTuple<int, int>(10, 20); if (valueTuple.Item1 == 10) { Console.WriteLine("A"); } if (valueTuple.Item1 == 10) { Console.WriteLine("B"); } if (valueTuple.Item1 == 10) { Console.WriteLine("C"); } } }
null チェックの比較
知らないと絶対意味がわからないパターンマッチングの null
チェック。
using System; public class Test { public static void Main() { int? p = null; if (p != null) Console.WriteLine("A"); if (p is not null) Console.WriteLine("B"); if (p is {}) Console.WriteLine("C"); } }
完全に同じコードになります。安心した。わけのわからない追加コードはありません。
public class Test { public static void Main() { Nullable<int> num = null; if (num.HasValue) { Console.WriteLine("A"); } if (num.HasValue) { Console.WriteLine("B"); } if (num.HasValue) { Console.WriteLine("C"); } } }
型パターン
型パターンも知らなかったら、コードが読めなくなるやつだと思います。 従来書いてたコードとすこし異なっていて機械的なコードが生成されました。が、常識の範囲内のごく普通のコードです。
using System; public class Test { public static void Main() { object i = 1; if (i is int) { int i2 = (int)i; Console.WriteLine(i2); } if (i is int i3) { Console.WriteLine(i3); } } }
public class Test { public static void Main() { object obj = 1; // i2 のテストコード if (obj is int) { int value = (int)obj; Console.WriteLine(value); } // i3 のテストコード int value2 = default(int); int num; if (obj is int) { value2 = (int)obj; num = 1; } else { num = 0; } if (num != 0) { Console.WriteLine(value2); } } }
WPF なんかだと IValueConverter みたいに、データを object 型にして渡してくるやつと格闘する機会に恵まれると思います。そういうときに型パターンやパターンマッチングを利用すると、(ガード節の部分なんかで)マシになると思います。
switch 文のパターンマッチング1
これもチェックしておきます。
using System; public class Test { public static void Main() { var p = (x: 10, y: 20); var p1 = p switch { { x: 10 } => "A", { y: >= 20 } => "B", _ => "X" }; Console.WriteLine(p1); } }
public class Test { public static void Main() { ValueTuple<int, int> valueTuple = new ValueTuple<int, int>(10, 20); int item = valueTuple.Item1; string text; if (item != 10) { int item2 = valueTuple.Item2; text = ((item2 < 20) ? "X" : "B"); } else { text = "A"; } string value = text; Console.WriteLine(value); } }
もうひとつ似たようなパターンを見ておきます。ほとんど同じコードが生成されています。
using System; public class Test { public static void Main() { var p = (x: 10, y: 20); string p2 = ""; switch (p) { case var (x, _) when x == 10: p2 = "A"; break; case var (_, y) when y >= 20: p2 = "B"; break; default: p2 = "X"; break; } Console.WriteLine(p2); } }
public class Test { public static void Main() { ValueTuple<int, int> valueTuple = new ValueTuple<int, int>(10, 20); string text = ""; ValueTuple<int, int> valueTuple2 = valueTuple; ValueTuple<int, int> valueTuple3 = valueTuple2; int item = valueTuple3.Item1; if (item != 10) { int item2 = valueTuple3.Item2; text = ((item2 < 20) ? "X" : "B"); } else { text = "A"; } Console.WriteLine(text); } }
switch 文のパターンマッチング2
型をチェックするパターンマッチングの例です。先の例と変わらないかも。
public class Test { public static void Main() { int a = 1; Method(a); } public static string Method(object o) { string result = ""; switch (o) { case int i when i > 0: result = "A"; break; case double d when d > 10: result = "B"; break; default: result = "X"; break; } return result; } }
public class Test { public static void Main() { int num = 1; Method(num); } public static string Method(object o) { string text = ""; if (o is int) { int num = (int)o; if (num > 0) { return "A"; } } else if (o is double) { double num2 = (double)o; if (num2 > 10.0) { return "B"; } } return "X"; } }
ちなみに、when を and に変えたコード (C# 9.0) でも、生成コードは変わらなかったです。
using System; public class Test { public static void Main() { int a = 1; Method(a); } public static string Method(object o) { string result = ""; switch (o) { case int i and > 0: result = "A"; break; case double d and > 10: result = "B"; break; default: result = "X"; break; } return result; } }
public class Test { public static void Main() { int num = 1; Method(num); } public static string Method(object o) { string text = ""; if (o is int) { int num = (int)o; if (num > 0) { return "A"; } } else if (o is double) { double num2 = (double)o; if (num2 > 10.0) { return "B"; } } return "X"; } }
個人的な感想
デコンパイルしたコードを確認してわかったことは、パターンマッチングのコードから見たこともない恐ろしいコードが生成されることはありませんでした。パターンマッチングのコードは、覚えきれない何千もの恐怖の形状ではなかったので、僕は狂気に陥らずにすみそうです。
というよりも、パターンマッチングのコードは、今風にイメージチェンジしただけの既存コードでした。C# の非同期、async/await とコルーチンを整理したときほどの苦労はありません。何か今までのコードと違っていたり、添加されている新しいコードもないとわかって安心できました。
それから、パターンマッチングのコードが読めないときは、デコンパイルした C# コードを確認してみるのは勉強と助けになると思いました。