目次

キーワード

概要

拡張メソッドは、静的メソッドをインスタンスメソッドと同じ形式で呼び出せるようにできるものです。 すなわち、 今までなら、

int x = int.Parse("1");      

と書いていたものを、

static class Extensions
{
    public static int Parse(this string str)
    {
        return int.Parse(str);
    }
}

というような静的メソッドを用意することで、 以下のような構文で呼び出せるようになります。

int x = "1".Parse();
ポイント
  • 拡張メソッド: 静的メソッドをインスタンスメソッドと同じ書式で呼び出せるようにすることで、 あたかもクラスに新しいメソッドを追加したかのように見せかける仕組みです。

  • 単に、静的メソッドを後置き記法で呼び出せるようになっただけとも考えることができます。

  • 定義側: 第1引数の前に this を付けます。

  • 利用側: インスタンスメソッドと同じ書き方をします。

拡張メソッド

C# 2.0 までの常識で言うと、 既存のクラスの機能拡張(=メソッドの追加)をしたければ、 そのクラスを継承したりなどして、新しいクラスを作るしかありませんでした。

これに対して、C# 3.0 では、後述する方法で、 既存のクラスにメソッドを追加できます。 (正確には、インスタンスメソッドの“ようなもの”。インスタンスメソッドと同じ構文で呼べるだけ。) このような、後から追加するメソッドのことを拡張メソッド(extension method)と呼びます。

まず、拡張メソッドの定義の仕方ですが、 以下のように、 静的クラス」中に、 第一引数に this キーワードを修飾子として付けた static メソッドを書きます

static class StringExtensions
{
  public static string ToggleCase(this string s)
  中身省略
}

このようにして定義したメソッドは、 通常通り、静的メソッドとして呼び出すこともできますが、 あたかも string 型のインスタンスメソッドであるかのように呼び出せるようになります。

string s = "This Is a Test String.";
string s1 = StringExtensions.ToggleCase(s); // 通常の呼び出し方。
string s1 = s.ToggleCase();                 // 拡張メソッド呼び出し。

上述のような拡張メソッドの利用例のソース全てを以下に示します。

using System;

namespace ConsoleApplication1
{
  static class StringExtensions
  {
    /// <summary>
    /// 文字列の大文字と小文字を入れ替える。
    /// </summary>
    /// <param name="s">変換元</param>
    /// <returns>変換結果</returns>
    public static string ToggleCase(this string s)
    {
      System.Text.StringBuilder sb = new System.Text.StringBuilder();
      foreach(char c in s)
      {
        if(char.IsUpper(c))
          sb.Append(char.ToLower(c));
        else if(char.IsLower(c))
          sb.Append(char.ToUpper(c));
        else
          sb.Append(c);
      }
      return sb.ToString();
    }
  }

  class ExtensionMethodTest
  {
    static void Main(string[] args)
    {
      string s = "This Is a Test String.";
      Console.Write(s.ToggleCase());
    }
  }
}
tHIS iS A tEST sTRING.

using ディレクティブによる拡張メソッドのインポート

通常、静的メソッドは「クラス名.メソッド名」という記法で呼び出します。 ところが、拡張メソッドでは、「クラス名」の部分をさぼって書けるようになっています。

じゃあ、どうやって「どのメソッドが呼ばれるか」を決定しているかというと、 using ディレクティブ」で指定した名前空間中のにある拡張メソッドが参照されるようになっています。

そのため、同じ名前空間内に2つ以上同名の拡張メソッドを定義してはいけません。

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            Console.Write(1.Square()); // エラーになる
        }
    }

    static class Extensions1
    {
        public static int Square(this int x)
        {
            return x * x;
        }
    }

    static class Extensions2
    {
        public static int Square(this int x) // エラーの原因
        {
            return x * x;
        }
    }
}

同名の拡張メソッドが定義されている名前空間を同時に using するのもご法度です。

using System;

namespace ConsoleApplication1
{
    using NamespaceA;
    //using NamespaceB;
    // ↑
    // ここのコメントを外してもやっぱりエラー。
    // using NamespaceA をコメントアウトして、
    // 代りに using NamespaceB するなら OK(表示結果が変わる)。

    class Program   
    {
        static void Main()
        {
            1.WriteToConsole();
            // ↑
            // NamespaceA.Extensions.WriteToConsole が呼ばれる
        }
    }
}

namespace NamespaceA
{
    static class Extensions
    {
        public static void WriteToConsole(this int x)
        {
            Console.Write("A {0}", x);
        }
    }
}

namespace NamespaceB
{
    static class Extensions
    {
        public static void WriteToConsole(this int x)
        {
            Console.Write("B {0}", x);
        }
    }
}

優先順位

拡張メソッドのせいで、 同じ名前のメソッドがいくつか同時に定義されてしまう可能性があります。 その場合、どのメソッドが呼ばれるか優先順位が決まっています。

インスタンス メソッド優先

まず、拡張メソッドよりも通常のインスタンスメソッドの方が優先されます。

using System;

class Program
{
    static void Main()
    {
        Console.Write(1.ToString());
        // ↑
        // Extensions.ToString ではなく、
        // int.ToString が呼ばれる。
    }
}

static class Extensions
{
    public static string ToString(this int x)
    {
        return "dummy data";
    }
}

オーバーロード解決ルールより、インスタンス メソッド優先が強い

通常、オーバーロードが複数ある場合は一番引数の一致度が高いものが呼ばれます。 例えば、以下のコードの場合は、object引数のものよりstring引数のものがまず優先、stringに合わない場合だけobjectのものが呼ばれます。

using static System.Console;

class X
{
    public void F(object x) => WriteLine($"object {x}");
    public void F(string x) => WriteLine($"string {x}");
}

class Program
{
    static void Main(string[] args)
    {
        var x = new X();
        x.F("abc"); // string のが呼ばれる
        x.F(10);    // int のオーバーロードがないので object のが呼ばれる
    }
}
string abc
object 10

ここで、int引数の拡張メソッドを足してみましょう。 しかし、拡張メソッドよりもインスタンス メソッドの方が優先的に呼ばれます。 引数の一致度が高くても、拡張メソッドの方は呼ばれません。

using static System.Console;

class X
{
    public void F(object x) => WriteLine($"object {x}");
    public void F(string x) => WriteLine($"string {x}");
}

static class XExtensions
{
    public static void F(this X @this, int x) => WriteLine($"int {x}");

}

class Program
{
    static void Main(string[] args)
    {
        var x = new X();
        x.F("abc"); // string のが呼ばれる
        x.F(10);    // int な拡張が増えたものの、インスタンス メソッド優先で object のが呼ばれる
    }
}
string abc
object 10

名前空間の優先度

名前空間違いで複数の拡張メソッドを定義することもできます。 この場合、優先度付けは名前空間の仕様に準じます:

特に、拡張メソッドを拡張メソッドとして呼びたい場合、完全修飾名は使えません。 上記ページの優先度付けが唯一の呼び分け手段になります。 以下のように、使う場所に近いほど優先、直接的なものほど優先で呼べます。 同優先度のものが複数ある場合はコンパイル エラーになります。

using static System.Console;
using A;

using Lib = C.Lib;
static class Lib { public static void F(this int x) => WriteLine("global"); }

namespace MyApp
{
    using B;

    static class Lib { public static void F(this int x) => WriteLine("MyApp"); }

    class Program
    {
        static void Main()
        {
            // F 拡張メソッドは5つある
            // この場合 MyApp.Lib.F が使われる
            // 優先度 高 MyApp > B > global = C > A 低
            10.F();

            // ちゃんと呼び分けたければ拡張メソッドとして使うことをあきらめる
            // 完全修飾名を使って、普通の静的メソッドとして呼ぶ
            A.Lib.F(10);
            B.Lib.F(10);
            C.Lib.F(10);
            MyApp.Lib.F(10);
            global::Lib.F(10);
        }
    }
}

namespace A
{
    static class Lib { public static void F(this int x) => WriteLine("A"); }
}
namespace B
{
    static class Lib { public static void F(this int x) => WriteLine("B"); }
}
namespace C
{
    static class Lib { public static void F(this int x) => WriteLine("C"); }
}

インターフェースに拡張メソッドを追加

拡張メソッドでは、1つ、通常のインスタンスメソッドにはできないことができます。 それは、「インターフェース」に対して、 インスタンスメソッド風のメソッドを定義できると言うことです。

通常、「インターフェース」は、メソッドの外部仕様のみを定義でき、 実装は定義できません。 しかしながら、拡張メソッドを利用することで、 インスタンスメソッド定義っぽいことが実現できます。

using System;
using System.Collections;

static class Extensions
{
  public static IEnumerable<T> Duplicate<T>(this IEnumerable<T> list)
  {
    foreach (var x in list)
    {
      yield return x;
      yield return x;
    }
  }
}

class Program
{
  static void Main(string[] args)
  {
    IEnumerable<int> data = new int[]{ 1, 2, 3 };

    // ↓インターフェースに対してメソッドを追加できる
    data = data.Duplicate();

    foreach (var x in data)
      Console.Write("{0}\n", x);
  }
}

C# 3.0 では、IEnumerable インターフェースなどに、 拡張メソッドとして Where や Select などのメソッド(「標準クエリ演算子」)が定義されています。

拡張メソッドの問題点

ちなみに、インスタンス メソッドでも拡張メソッドでもどちらでもいい場合、拡張メソッドの濫用は避けた方がいいでしょう。 拡張メソッドの濫用には不便な点もありますし、 いくつか問題を起こす可能性があります。

実体はあくまで静的メソッド

拡張メソッドは、 呼び出し側だけ見ると、一見、クラスにメソッドが追加されたように思えますが、 その実態はあくまで静的メソッドです。 それも、元のクラス中ではなく、別の静的クラスの中で定義された静的メソッドです。

元のクラスからみれば当然「外部」なので、 拡張メソッドから private / protected メンバーにアクセスすることはできません。

定義場所がどこかわからなくなる

クラス本体と別の場所にメソッド定義があるため、 定義された場所を探すのに苦労する可能性があります。

しかも、using 文を使ってインポートするため、 using 文1つでどの静的メソッドが呼ばれるのかが切り替わって、 なおのことどこに定義があるのかわかりにくくなっています。

拡張メソッドの意義

前節の通り、実を言うと、拡張メソッドは両手ばなしによろこべる機能ではなかったりします。 むしろ、拡張メソッドを「クラスに後からメソッドを追加するもの」だと考えるとあまりメリットはないように思います。 可能ならば素直にクラスのインスタンス メソッドとして定義すべきです。 「クラスを作った人とは全くの別人がメソッドを足せる」という点は便利そうですが、クラスの場合はあまりそういう場面は多くありません。

一方で、インターフェイスの場合は需要があります。多くの場合、インターフェイスを作る人と、そのインターフェイスを使った処理を書く人は別です。 通常、この「インターフェイスを使った処理」は静的メソッドになりがちです。 そして、拡張メソッドの真骨頂は「(本来は前置き記法である)静的メソッドを後置き記法で書ける」という部分にあると思っています。

例えば、下図のような、データ列に対するパイプライン処理を考えてみます。

パイプライン処理
パイプライン処理

まず、条件付けや値の加工のために以下のような静的メソッドを用意します。

static class Extensions
{
    public static IEnumerable<int> Where(this IEnumerable<int> array, Func<int, bool> pred)
    {
        foreach (var x in array)
            if (pred(x))
                yield return x;
    }

    public static IEnumerable<int> Select(this IEnumerable<int> array, Func<int, int> filter)
    {
        foreach (var x in array)
            yield return filter(x);
    }
}

これを、静的メソッド呼び出しの構文で書くと以下のようになります。

var input = new[] { 8, 9, 10, 11, 12, 13 };

var output =
    Extensions.Select(
        Extensions.Where(
            input,
            x => x > 10),
        x => x * x);

やりたいパイプライン処理の順序と、語順が逆になります。 また、「Where とそれに対する条件式 x > 10」や 「Select とそれに対する加工式 x * x」の位置が離れてしまいます。

これに対して、拡張メソッド構文を使うと、以下のようになります。

var input = new[] { 8, 9, 10, 11, 12, 13 };

var output = input
    .Where(x => x > 10)
    .Select(x => x * x);

ただ語順が違うだけなんですが、 こちらの方がやりたいことの意図が即座に伝わります。 すなわち、パイプライン処理(フィルタリング処理)は、 後置きの語順が好ましい処理です。

というように、 語順的に後置きの方がしっくりくる場合に (というか、むしろその場合のみに)、 静的メソッドを拡張メソッド化することをお勧めします。

拡張メソッドのデリゲートへの代入

拡張メソッドは、インスタンスメソッドと同じ構文で静的メソッドを呼べるものなわけですが、 デリゲートへの代入時にも、インスタンスメソッドと同じ構文で書けたりします。 (ただし、少々制約あり。)

すなわち、以下のようなコードは合法です。

using System;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main()
        {
            Func<string> f = "test".Duplicate;
            // ↑
            // 実行結果的には
            // Func<string> f = () => Extensions.Duplicate("test");
            // と同じ。
            // コンパイル結果的には、こんな余計な匿名デリゲートはできないらしい。
            // 直接 f に Extensions.Duplicate("test") が代入されるようなイメージ。
        }
    }

    static class Extensions
    {
        public static string Duplicate(this string x)
        {
            return x + x;
        }
    }
}

こういうように、メソッドの引数を何らかの値で束縛して、新しいデリゲートを作ることをカリー化(currying)といいます。 また、上述のようなデリゲートの作り方をカリー化デリゲート(curried delegate)というそうです。 (curry は人名に由来する単語らしくて、他に意味はない。)

ただし、カリー化デリゲートが作れるのは参照型の変数のみです。 値型の場合にはエラーになります。

更新履歴

ブログ