【C#】少し変わった拡張メソッドを作成する

C#では既存のクラスにメソッドを追加できる「拡張メソッド」という機能があります。

今回はこの拡張メソッドの少々変わった使い方の紹介です。

確認環境

今回の確認環境は以下の通りです。

  • .NET Core5(C# 9.0)
  • VisualStudio 2019
  • Windows 10

拡張メソッドの基本

まずは基本的な書き方の説明です。

以下のように「this」をつけてメソッドを宣言します。

public static class IntExtension
{
    // int 型に PlusOne というメソッドを追加する
    // 拡張したい型を先頭に持ってきて this をつける
    public static int PlusOne(this int value) => value + 1; // 今の値に+1した値を返す
    
    // 1. 追加したい型を第一引数に指定する
    // 2. 引数の宣言の先頭に this を指定する
}

そうすると以下のように使用できるようになります。

public static void Foo()
{
    int value = 1;
    int value2 = value.PlusOne(); // 上記で追加したメソッドが使用できる
    // value2 = 2
}

少し変わった使い方

さて、タイトルの通り拡張メソッドの少し変わった使い方の紹介です。

Object に拡張メソッドを定義する

Object 型に拡張メソッドを定義するとすべての型で拡張メソッドが呼び出せるようになります。

以下のようにリフレクションで対象オブジェクトの private フィールド値を強制的に書き換える処理は汎用性があるかもしれません。インテリセンスに毎回出てくるようになるので少し邪魔かも?

public static class ObjectExtension
{
    // 指定したオブジェクトのprivateフィールドの値を強制定期に変更する
    public static void SetField(this object self, string name, object value)
    {
        self.GetType()
            .GetField(name, BindingFlags.InvokeMethod | 
                            BindingFlags.NonPublic | 
                            BindingFlags.Instance)
            .SetValue(self, value);
    }
}

// 他人が作ったクラスのprivateフィールドを強制的に書き換えられる
public static Foo()
{
    int i = 1;
    i.SetField("id", 10); // この例では無意味だけどこんな感じで使える

    double d = 2.0;
    d.SetField("v", 3.0);
}

ジェネリックやタプルと組み合わせる

通常ジェネリックの拡張メソッドはジェネリックで指定します。

// 通常ジェネリックの方を指定するときは拡張メソッドもジェネリックにする = 'T'
public static bool Foo<T>(this List<T> self, T value) { ... }

// 上記のように宣言するとどのListの型でも使用できるようになる
public static void Foo()
{
    List<int> intList = new();
    bool ret = intList.Foo(100);

    List<double> doubleList = new();
    ret = doubleList.Foo(100.0);
}

ただし特定の型を指定することもできます。

// List<int>の時にしか使用できない拡張メソッドの宣言
public static bool Foo(this List<int> self, int value) { ... }

// 今度はdouble型では使用できなくなる
public static void Foo()
{
    List<int> intList = new();
    bool ret = intList.Foo(100);

    List<double> doubleList = new();
    ret = doubleList.Foo(100.0); // エラーになる

    // エラー CS1929 'List<double>' に 'Foo' の定義が含まれておらず、最も適している
    // 拡張メソッド オーバーロード
    // 'SampleExtension.Foo(List<int>, int)' には 'List<int>' 型のレシーバーが必要です

}

で、このジェネリックはタプルが指定できるので以下のように特殊な値の時にしか使用できない拡張メソッドが定義できます。

以下例ではタプルでintが2つの組み合わせの時にしか使用できない拡張メソッドの定義です。

public static class SampleExtension
{
    // タプルでintが2つの組み合わせの時にしか使用できない拡張メソッド
    public static bool Foo(this List<(int, int)> self, int value)
    {
        foreach (var (a, b) in self)
        {
            if (value == a || value == b)
            {
                return true; // どちらか一方に一致すればtrue
            }
        }
        return false;
    }
}

public static void Foo()
{
    List<(int, int)> intList = new()
    {
        ( 0,  0),
        (10, 20),
        (30, 40),
    };
    bool ret = intList.Foo(100); // 個の組み合わせのタプルの時だけ使える

    List<(int, double)> doubleList = new()
    {
        (0, 1.1),
        (2, 3.3),
        (4, 5.5),
    };
    ret = doubleList.Foo(100.0); // こっちはエラーになる
    
    // エラー CS1929 'List<(int, double)>' に 'Foo' の定義が含まれておらず、
    // 最も適している拡張メソッド オーバーロード 
    // 'SampleExtension.Foo(List<(int, int)>, int)' には 'List<(int, int)>' 型のレシーバーが必要です
}

このジェネリックにタプルを使用する手法は局所的なデータの組み合わせにいちいちクラスを定義しなくても組み合わせを表現できるため実装テクニックとして覚えていても損はないと思います。