sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

C# パターンマッチングのデコンパイルコードを確認する

f:id:shikaku_sh:20210823173353p:plain:w300

C# Advent Calendar 2021 を見てると、C# のパターンマッチングが便利らしい。

でも、パターンマッチングの中身が実際どんな判定(動き)してるのかよくわからないし、なんか怖い。わけわからん動きをするバグの狂気に陥りそう。

そんなわけでパターンマッチングのデコンパイルコードを確認してみました。そこそこ実際のうごきを把握できる(はずな)ので経験値をかせげると思います。

デコンパイルコードの確認には下図のとおり「sharplab」を利用しています。(あと IL は長くなるので記事では掲載外)

f:id:shikaku_sh:20211222163335p:plain:w500
sharplab

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");
    }
}

BC を出力する式は、同じ意味だとなんとなくわかると思います。下のように生成コードも同じです。(逆にこの場合は書いていても人の読みやすさだけに効果があり、実行時の処理コストは無い)

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

参考