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# コードを確認してみるのは勉強と助けになると思いました。