「ラムダ式本体によるメソッドの記述」はちょっと微妙

C# 6.0 から追加された「Expression bodies on method-like members(ラムダ式本体によるメソッドの記述)」という機能ですが、これをメソッドに使うのは少し考えた方がいいという話です。この機能を利用すると以下のようにメソッドで中カッコ({})を使用せずラムダ式風(=>)に記述することができます。

// 以下のようにメソッドの中身をラムダ風に記述できる
public static void Foo() => Console.WriteLine("Hey!");

VisualStudio では以下のように単一行のメソッドのリファクタリングの候補として「メソッドに式本体を使用する」という項目が(さもオススメという風に)表示されるので適用したことがある人もいるかと思います。

f:id:Takachan:20220115191309p:plain

この提案によって「メソッドに式本体を使用する」を適用するとメソッドは以下の形式に変換されます。

// 適用前
public static void Foo()
{
    Console.WriteLine("Hey!");
}

// 適用後
public static void Foo() => Console.WriteLine("Hey!");

メソッドへの使用の是非について

この構文ですが、プロパティに対して以下のように記述できますが

pribate int _no = -1;
public int No => _no;

// 旧来の記述方法
// public int No { get { return _no; } }
// public int No { get => _no; }

と記述するのは従来の記述に比べ、文字数の削減効果が高いです。もともとプロパティ自体このように単純な実装になる事が多いため使用の是非を考慮することはありません。

しかし、通常のメソッドの場合はこの構文を適用する場合にはタイプ量が大幅に減るわけではありません(=> と { } でタイプ量が減りません(ただし、4行が1行になって圧縮効果はあります)

また、メソッド内に 2行以上の処理を記述したい場合、結局今まで通りのブロック式に戻してから追加の処理を記述することになりますが、そもそも 1行の処理と 2行の処理で書き方を切り替えるなんて実装は(よほどその記述方法に慣れてない限り)通常しません。

また、少し前に(流行ってるんだか流行ってないんだか分かりませんが)ラムダが関数型言語から流入した影響で、OOP 言語の C# を一部の関数言語ライクな構文で記述できるため、こういった機能が実装されたのかもしれませんが、そもそも C# は関数型言語ではないですし、こういった形式ですべての処理が書ける訳でもありません(当然このように書いてもメソッドに副作用がある事もあります)

以上のことからこの構文をメソッドに適用するのは控えた方がいいと思います。

組み合わせることで問題の起きるケース

このラムダ式本体によるメソッドの記述の構文と、他の機能を組み合わせることで実装効率やメンテナンス性に問題が発生する機能を2つほど紹介したいと思います。

Tupleを使用したコンストラクタ代入

C# 7.0 で実装されたタプル機能で組み合わせを作成して同時に代入できる以下の構文があります。

// タプルを使用した代入
int a = 10;
int b = 20;
(a ,b) = (b, a); // 一時変数を使用することなく入れ替えられる

これをコンストラクタで使用することができます。

public class Sample
{
    public int _num;
    public string _str;
    
    // 適用前
    public Sample(int num, string str)
    {
       _num = num;
       _str = str;
    }
    
    // 適用後
    public Sampke(int num, string str) => (_num, _str) = (num, str);
}

この構文ですが、これくらいシンプルな時は特に問題ないですが、同じ型の引数が4つ5つと増えたり、なると順番を間違えたりしやすいです。

また代入以外の処理が入り2行以上になる場合、結局通常の構文に戻す必要がありますが手で代入を書き直すのは割と面倒です。

public class Sample
{
    public int _num1;
    public int _num2;
    public int _num3;
    public string _str;
    
    // 増えると訳が分からなくなって間違えやすい(これも間違えていますが分かりますか?
    public Sample(int num1, int num2, int num3, string str) 
        => (_num1, _num3, num3, _str) = (num1, num2, num3, str);

    // コンストラクタに処理が増えた場合戻すのがしんどい
    public Sample(int num1, int num2, int num3, string str)
    {
        _num1 = num1;
        _num2 = num2;
        _num3 = num3;
        _str = str;
    }
}

一瞬 C++ 風のコンストラクターの初期化にも見えなくはないですがこの構文は C# では使用してはいけないと思います。

switch式でラムダ式本体を使う

switch 式という構文が C# 8.0~ 利用できますが、switch をパターンマッチングという構文とラムダ式の概念で以下のように記述できるようになりました。

public void Foo(EnumValue e)
{
    // 適用前
    int i = 0;
    switch(e)
    {
        case Hoge:
            i = 1;
            break;
        case Bar:
            i = 2;
            break;
        default:
            i = 0;
            break;
    }
    
    // 適用後
    int i = e switch
    {
        Hoge => 1,
        Bar => 2,
        _ => 0
    }
}

この構文ですが、switch の結果を変数に代入するケースで有用で積極的に使用してもよさそうです。ただしこれもラムダ式本体を組み合わせると途端にしんどそうな感じになります。

// 適用前
public int Foo(EnumValue e)
{
    switch(e)
    {
        case Hoge: return 1;
        case Bar: return 2;
        default: return 0l
    }
   
}

// 適用後
public int Foo(EnumValue e) => e switch
{
    Hoge => 1,
    Bar => 2,
    _ => 0
};

一時変数に受ける必要がなくなりますが、これ処理が追加になったときにブロック式に書き換えるのは結構しんどいです。そもそも1行と2行以上でスタイルを切り替える必要性がありません。

IDEの提案を無効化する

最後に、この提案自体してほしくない人向けに無効化する方法を紹介します。

ファイル内で無効化するには以下をファイルの先頭で宣言します。

// ファイル内で無効化する

// コードの先頭に以下を記述する
#pragma warning disable IDE0022

ちなみに以下のように「.editorconfig」に記述しても IDE 上にはなぜか表示されてしまいます。

// .editorconfig

[*.cs]

# IDE0022: メソッドにブロック本体を使用する
csharp_style_expression_bodied_methods = false

新しい機能だからって全て良いものだとは限らないですよね。よく意味を考えて構文などは使うか使わないか決めた方がいいと思います。

以上。