【C#】拡張メソッド

序論

C#には拡張メソッドという機能があって、まぁ知ってれば大変便利なのですが、弊社謹製レガシーライブラリはなんともまぁ前時代的な書き方をしてあり大変煩わしいしです。そのうえ誰もメンテしないから使いにくくて仕方がない、私がメンテしようにもテストコードすら書かれていないのでめんどくせぇ。というヘイト全開で拡張メソッドの素晴らしさを書いていこうと思います。弊社に届け、この思い☆

拡張メソッド

C#における拡張メソッドは、あたかもそのクラスに元からあるかのように使用できるインスタンスメソッドを実装するという方法です。

たとえば、stringにはstring.IsNullOrEmptyというメソッドがあります。引数で渡された文字列が空白またはnullの場合にtrueを返してくれるというメソッドです。

string text = "";

Console.Write(string.IsNullOrEmpty(text));
// 結果 -> true

この書き方、なんか嫌じゃないですか?つまり、textというインスタンスがIsNullOrEmptyかどうかをいちいちstringのメソッド越しに聞くの嫌じゃないですか?子どもと話してて、お父さんお母さんに「何歳ですか?」って聞いてるみたいで嫌じゃないですか?子どもに聞いて子どもに答えてほしい。なのでそうしましょう。

public static class StringExtension
{
    public static bool IsNullOrEmpty(this string text)
    {
        return string.IsNullOrEmpty(text);
    }
}

上記のメソッドを用意することで、以下のように書けます。

string text = "";

Console.Write(text.IsNullOrEmpty());
// 結果 -> true

比べてみましょう。

string text = "";

Console.Write(string.IsNullOrEmpty(text));  // 拡張メソッドじゃないver
Console.Write(text.IsNullOrEmpty());        // 拡張メソッドver

あらまぁ見てくださいよこのコード量の違いを。素晴らしいですね。

あたかも、textというインスタンスのIsNullOrEmptyメソッドを呼び出しているようでしょう。

拡張メソッド記述のポイント

拡張メソッドのポイントは3つ。

  1. staticクラス内に記載すること
  2. staticメソッドにすること
  3. 第1引数の頭にthisキーワードをつけること

こうすることで、そのメソッドをインスタンスメソッドのように扱うことができます。

コードがスッキリする

例えば以下のようなコードがあるとしましょう。

string result = string.Join(",", Enumerable.Range(1, 20).Select(n => ((n % 3 == 0) || $"{n}".Contain("3")) ? "アホ" : $"{n}"));

// 結果 -> 1, 2, アホ, 4, 5, アホ, 7, 8, アホ, 10, 11, アホ, アホ, 14, アホ, 16, 17, アホ, 19, 20

上記コードは1から20のうち、3の倍数と3がつく数字の時は”アホ”を、それ以外はその数字を返すコレクションを", "で区切って連結するというものですが、このコードを読む時って

  1. まずstring.Joinがあることで何かが区切って連結される
  2. 何が区切られるかを読む

という順番で読むことになると思います。で、何が区切られるのかを読み始めたらめんどくさいコレクション操作をしていて嫌になりますね。

ですが、以下のような拡張メソッドが用意されていたらどうでしょう。

public static class GenericsExtensions
{
    public static string JoinBy<T>(this IEnumerable<T> source, string separater)
    {
        // nullチェックは省略
        return string.Join(separater, source);
    }
}

これを使うと、以下のように書けます。

string result = Enumerable.Range(1, 20).Select(n => ((n % 3 == 0) || $"{n}".Contain("3")) ? "アホ" : $"{n}").JoinBy(",");

// 結果 -> 1, 2, アホ, 4, 5, アホ, 7, 8, アホ, 10, 11, アホ, アホ, 14, アホ, 16, 17, アホ, 19, 20

これならどうでしょう。対象の数字を”アホ”へ変換した後に、", "で区切って連結させると自然に読めますよね。

この自然に読めるということが超重要なんです。ただでさえ頭を使うことが多いので、不要なストレスを省くようにすべきだと思います。(別の話になりますが、LINQは頭の中のロジック通りに書けて、書いてある通りに自然に読めるので本当にすごい)

拡張メソッドを上手く活用できれば、可読性を上げることもできます。

おまけですが、以下のようなアホ判定拡張メソッドを用意するともっとスッキリすることでしょう。

public static class IntExtensions
{
    public static bool IsMultipleOf(this int value, int @base)
    {
        return (value % @base == 0);
    }

    public static bool IsAho(this int value)
    {
        const int AhoTrigger = 3;
        return value.IsMultipleOf(AhoTrigger) || $"{value}".Contains($"{AhoTrigger}");
    }
}

// こう書ける
string result = Enumerable.Range(1, 20).Select(n => n.IsAho() ? "アホ" : $"{n}").JoinBy(", ");

// 結果 -> 1, 2, アホ, 4, 5, アホ, 7, 8, アホ, 10, 11, アホ, アホ, 14, アホ, 16, 17, アホ, 19, 20

終わりに

拡張メソッドは上記の通りとてもメリットが大きく、一度実装してしまえば使う側は何も考えずに使える素晴らしい機能です。再利用性や拡張性も高いです。

とりあえずよく使うstring.IsNullOrEmptystring.Formatなどを拡張メソッドにするだけでもかなり便利になります。

これでみんな幸せになります、めでたしめでたし…と言いたいところですが全然めでたくないのが弊社です。私は日々弊社謹製レガシーライブラリと戦っています。ちくせう。