【C#】拡張メソッドとは?ずばり静的クラス内に定義された、第一仮引数にthisパラメータが付いた静的メソッド!

C#

ども、村正です!

今回は拡張メソッドについて紹介していきたいと思います!

拡張メソッドとは?

拡張メソッド(Extend Method)とは、静的クラス内に定義された、特定のクラスの変数の値を処理する静的メソッドのことです。拡張メソッドを使うことで変更ができない(メソッドが追加できない)クラスに対して(疑似的に)メソッドを追加を追加することができます。

この「疑似的に」というのがポイントです。拡張メソッドはさもクラス内にメソッドが存在しているかのように呼び出すことができるため「クラスにメソッドが存在している(追加されている)!」という使用感があります。しかし実際にはクラス内にメソッドが存在しているわけではなく、あくまで外部クラス内にある静的メソッドを呼び出しているだけです。

ここらへんは言葉で説明するよりも、実際のコードを見たほうが圧倒的にわかりやすいと思います。

というわけでサンプルとしてUserクラス用に作成した拡張メソッドGetAddSuffixNameを以下に示します。拡張メソッドGetAddSuffixNameはさもUserクラスに存在してるかのように使用できていますが、実際にはまったく別の静的クラス(UserExtensions)に定義されていてそれを呼び出しているにすぎないということがわかると思います。

/// <summary>
/// ユーザーを表します
/// </summary>
public class User
{
    private int Id { get; set; }

    public string Name { get; set; }

    private string Password { get; set; }

    public User(int id, string name, string password)
    {
        Id = id;
        Name = name;
        Password = password;
    }

    /// <summary>
    /// ユーザー情報を表示します
    /// </summary>
    public void ShowUserInfo()
    {
        Console.WriteLine($"ID:{Id} / 名前:{Name} / パスワード:{Password}");
    }
}
/// <summary>
/// Userクラスの拡張メソッドを表します
/// </summary>
public static class UserExtensions
{
    /// <summary>
    /// 名前の後ろに「様」を付けた文字列を取得します
    /// </summary>
    /// <param name="user"></param>
    /// <returns></returns>
    public static string GetAddSuffixName(this User user)
    {
        return $"{user.Name}様";     // ○○様
    }
}
public class Program
{
    static void Main(string[] args)
    {
        User user = new User(1, "ひよこ", "hiyoko36");     // ユーザー情報の作成

        Console.WriteLine("[通常メソッド使用]");
        user.ShowUserInfo();                               // ユーザー情報の表示

        Console.WriteLine("-------------------------------------------");

        Console.WriteLine("[拡張メソッド使用]");
        string transUserName = user.GetAddSuffixName();    // ユーザー名に「様」を付けた文字列を取得
        Console.WriteLine(transUserName);
    }
}

[通常メソッド使用]
ID:1 / 名前:ひよこ / パスワード:hiyoko36
——————————————-
[拡張メソッド使用]
ひよこ様

拡張メソッドを使うメリット

拡張メソッドを使うメリットはいくつかあります。

  • クラスを変更することなく外部からメソッドを(疑似的に)追加することができる。
  • コードの可読性を高めることができる。

クラスには変更できない(可能なかぎり変更したくない)クラスがあります。たとえば.NET Frameworkが用意しているList<T>クラスです。このクラスはプログラム上でリストを使いたいときに使用するクラスなのですが、このクラスに対して直接メソッドを追加することは(通常は)できません。標準ライブラリとして用意されているクラスなので直接変更してはいけないのです。

このとき「List<T>型の変数の要素の値を一覧で表示する」という処理を作りたいとします。表示対象のリストが複数ある場合は、重複コードの発生を防ぐためにメソッドとして切り出す必要があり、通常は以下のようにList<T>型の変数を引数として受け取るメソッドとして用意し、それを呼び出すような実装になると思います。

public class Program
{
    static void Main(string[] args)
    {
        // リスト1
        List<int> numbers_1 = new List<int>();

        numbers_1.Add(10);
        numbers_1.Add(5);
        numbers_1.Add(15);

        ShowListInfo(numbers_1);

        // ただの改行
        Console.WriteLine();

        // リスト2
        List<int> numbers_2 = new List<int>();

        numbers_2.Add(234);
        numbers_2.Add(5362);
        numbers_2.Add(1124);
        numbers_2.Add(241);

        ShowListInfo(numbers_2);
    }

    /// <summary>
    /// リストの情報を表示します
    /// </summary>
    /// <param name="list"></param>
    static public void ShowListInfo(List<int> list)
    {
        Console.WriteLine("【リストの情報】");
        for (var i = 0; i < list.Count; i++)
        {
            Console.WriteLine($"[{i}] : {list[i]}");
        }
    }
}

【リストの情報】
[0] : 10
[1] : 5
[2] : 15

【リストの情報】
[0] : 234
[1] : 5362
[2] : 1124
[3] : 241

もちろんこれでもいいのですが「引数にList<T>型の変数を指定してShowListInfoメソッドを呼び出す」というのは直感的ではなく、ちょっとわかりにくいコードです。以下のように「List<T>クラス自体がもつShowListInfoメソッドを呼び出す」みたいな記述できるともっと直感的でわかりやすい記述になるでしょう。

// 直感的ではなくわかりにくい...
ShowListInfo(numbers_1);
ShowListInfo(numbers_2);

// 通常のメソッドのように呼び出せると直感的でわかりやすい!!
number_01.ShowListInfo();
number_02.ShowListInfo();

繰り返しになりますがList<T>クラスを変更することはできません。つまりList<T>クラス自体にShowListInfoメソッドを追加してそれを呼び出すなんてことはできません。

ではどうすればいいのか。そんなとき使うのが「拡張メソッド」です。拡張メソッドとしてShowListInfoメソッドをつくることで、さもList<T>クラスにShowListInfoメソッドが存在するかのように呼び出して使うことができます

以下は拡張メソッドとしてShowListInfoメソッドを実装した例です。メソッドの呼び出し部分が直感的でわかりやすくなったかと思います。

/// <summary>
/// List<T>クラス用の拡張メソッドを提供します
/// </summary>
public static class ListExtensions
{
    /// <summary>
    /// リストの情報を表示します
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="list"></param>
    public static void ShowListInfo<T>(this List<T> list)
    {
        Console.WriteLine("【リストの情報】");
        for (var i = 0; i < list.Count; i++)
        {
            Console.WriteLine($"[{i}] : {list[i]}");
        }
    }
}
public class Program
{
    static void Main(string[] args)
    {
        // リスト1
        List<int> numbers_1 = new List<int>();

        numbers_1.Add(10);
        numbers_1.Add(5);
        numbers_1.Add(15);
        
        // List<T>クラスがShowListInfoメソッドを持っているかのように呼び出せる!
        numbers_1.ShowListInfo();

        // ただの改行
        Console.WriteLine();

        // リスト2
        List<int> numbers_2 = new List<int>();

        numbers_2.Add(234);
        numbers_2.Add(5362);
        numbers_2.Add(1124);
        numbers_2.Add(241);

        // List<T>クラスがShowListInfoメソッドを持っているかのように呼び出せる!
        numbers_2.ShowListInfo();
    }
}

【リストの情報】
[0] : 10
[1] : 5
[2] : 15

【リストの情報】
[0] : 234
[1] : 5362
[2] : 1124
[3] : 241

また、可動性が高まるという面では、拡張メソッドを利用することでメソッドチェーンが使えるようになるというのもあります。カッコの中にカッコがあってその中にカッコがあって…みたいな記述ではなく、「.」による記述が使えるようになります。

// ぱっと見で分かりずらい
メソッド1(メソッド2(メソッド3(メソッド4(XXX))));

// 拡張メソッドを使うと「メソッドチェーン」が使える!
XXX.メソッド1().メソッド2().メソッド3().メソッド4();

拡張メソッドを使うことで、変更できないクラスに対して”疑似的に”メソッドを追加することができ、さもメソッドが存在するかのよう呼び出して使用できるようになるため、可読性が高まるというわけです。

拡張メソッドの作りかた

拡張メソッドの作りかたは少し特殊ですが、すぐに慣れますし簡単です。

のちほど実際に拡張メソッドを作りながら説明していきますが、ざっと流れを説明すると以下の通りです。

  1. 何に対する拡張メソッドなのかを考える。
  2. 静的クラスを作る。
  3. 2で作成した静的クラスの中に、静的メソッドとして拡張メソッドを作る。
    ※第一引数に注意!(thisをつける)
  4. 「インスタンス名.拡張メソッド名」で呼び出して実行。

それでは実際に拡張メソッドを作りながら、手順ついて詳しくみていきましょう。

今回は例として、浮動小数点の値を格納できるDoubleクラスに対して「引数の値で四則演算した値の合計を返す拡張メソッド」を実装してみたいと思います。使用イメージとしては以下の通りです。

// Double型の変数(一般的な利用シーンと合わせるために"double"と書いていますが、"double"は"Double"の”別名”でなので実質的にDouble型の変数です)
double number = 5.5;

// 拡張メソッド
double result = number.GetCalcExtensionMethod(0.5);   // 【計算内容】6.0 + 5.0 + 27.5 + 11 = 49.5

// 結果の表示
Console.WriteLine(result);                // 【表示】49.5

1. 何に対する拡張メソッドなのかを考える

まずは何に対する拡張メソッドなのかを考えます。この「何に」というのは例えば「なんのクラスに対する拡張メソッドなのか」ということです。

今回すでに答えが出てしまっていますが、『Doubleクラスに対して「引数の値で四則演算した値の合計を返す拡張メソッド」を作る』ので、「Doubleクラスに対する拡張メソッド」になります。

✓ 今回はDoubleクラスに対する拡張メソッドを作ります。

2. 静的クラスを作る

前述したように、拡張メソッドの正体は「静的クラス内に定義された静的メソッド」です。そのため、拡張メソッドを定義する(入れておく)静的クラスを作る必要があります。

一般的に拡張メソッドは、同じ性質(処理や対象など)を持つものを1つの静的クラスにまとめて定義します。まとめかたはプロジェクトによって変わりますが、一般的に「対象としているクラス(や列挙体など)ごとにまとめる」「機能ごとにまとめる」という2パターンになります。

今回は「対象としているクラスごとに拡張メソッドをまとめる」というパターンを使って、「Double型の拡張メソッドをDoubleExtensionsという静的クラス内にまとめて定義する」というかたちにしてみます。

/// <summary>
/// Doubleクラス用の拡張メソッドを提供します
/// </summary>
public static class DoubleExtensions(){
    
    /*
     * この中に拡張メソッドを定義していく...
     */

}

ちなみにクラス名は上記のように「○○Extensions」という名前にするのが一般的です。クラス名から拡張メソッドが定義されたクラスであることがわかりやすくなります。

✓ 「○○Extensions」静的クラスを作成します。
✓ この静的クラスに拡張メソッドを定義していきます。

3. 拡張メソッドを定義する。

拡張メソッドを定義するための静的クラスを作ったら、そのクラスのなかに拡張メソッドを定義します(作ります)。拡張メソッドの実態は「静的クラス内に定義された静的メソッド」であるため、静的メソッドを定義するようなかんじで作っています。

ただし記述がすこし特殊です。言葉で説明するのは難しいので、図で説明したいと思います。ポイントとしては第一仮引数でthisパラメータをつけることと、呼び出し元で指定した引数は第二仮引数以降に渡されるといった点です。

/// <summary>
/// Doubleクラス用の拡張メソッドを提供します
/// </summary>
public static class DoubleExtensions(){
    
    ///<summary>
    /// 四則演算した値の合計を取得します。
    ///</summary>
    public static double GetCalcExtensionMethod(this double number, double d){
        // TODO:仮実装
        double result = number + 3.14;
        return result;
    }
}

初めのうちは「ん~と…」と理解が難しいところかもしれませんが、1,2個作ってみればすぐに慣れますし、簡単に作れるようにります。

さて、GetCalcEntensionMethodメソッドの中身ができていないのでサクッと作ってしまいます。

/// <summary>
/// Doubleクラス用の拡張メソッドを提供します
/// </summary>
public static class DoubleExtensions
{
    /// <summary>
    /// 四則演算した値の合計を取得します。
    /// </summary>
    /// <param name="number"></param>
    /// <param name="d"></param>
    /// <returns></returns>
    public static double GetCalcExtensionMethod(this double number, double d)
    {
        double tmp = 0.0;
        
        tmp += number + d;
        tmp += number - d;
        tmp += number * d;
        tmp += number / d;

        double result = tmp;
        
        return result;
    }
}

このような手順をとおしてGetCalcExtensionMethod拡張ソッドを作ることができました。

✓ 静的クラス内に、静的メソッドとして拡張メソッドを定義します。
✓ 拡張メソッドは通常のメソッドとは違い、第一仮引数にthisパラメータをつけます。

(4. 呼び出しは通常のメソッドを呼び出すように書けばOK)

拡張メソッドの呼び出し(使用)は通常のメソッドを呼び出すのと同じです。

// GetCalcEntensionMethod拡張メソッドの呼び出し
double result = number.GetCalcExtensionMethod(0.5);

✓ 拡張メソッドの呼び出しは普通のインスタンスメソッドを呼び出すような要領で記述します。

最終的に以下のようなコードが出来上がります

/// <summary>
/// Doubleクラス用の拡張メソッドを提供します
/// </summary>
public static class DoubleExtensions
{
    /// <summary>
    /// 四則演算した値の合計を取得します。
    /// </summary>
    /// <param name="number"></param>
    /// <param name="d"></param>
    /// <returns></returns>
    public static double GetCalcExtensionMethod(this double number, double d)
    {
        double tmp = 0.0;
        
        tmp += number + d;
        tmp += number - d;
        tmp += number * d;
        tmp += number / d;

        double result = tmp;
        
        return result;
    }
}
public class Program
{
    static void Main(string[] args)
    {
        // Double型の変数(一般的な利用シーンと合わせるために"double"と書いていますが、"double"は"Double"の”別名”でなので実質的にDouble型の変数です)
        double number = 5.5;

        // 拡張メソッドを呼び出す
        double result = number.GetCalcExtensionMethod(0.5);   // 【計算内容】6.0 + 5.0 + 2.75 + 11 = 24.75

        // 結果の表示
        Console.WriteLine(result);                            // 【表示】24.75
    }
}

24.75

まとめ

今回は拡張メソッドについて紹介していきました。

  • 拡張メソッドを使うことで(疑似的に)クラスにメソッドを追加することができる。
  • 拡張メソッドの正体は「静的クラス内に定義された静的メソッド」。
  • 拡張メソッドの第一仮引数にはthisパラメータをつける。
  • 拡張メソッドの呼び出し時に指定した引数は、拡張メソッドの第二仮引数以降に渡される。

拡張メソッドを初めてみたときは「ん~なにこれ?thisってなに??」とよくわからないものと言う印象を受けると思います。しかし実際にはなにも難しいことはありませんし、使えるようになるとさらに読みやすいコードが書けるようになります。

ぜひ積極的に拡張メソッドを作ってみてくださいね。

それでは!

コメント