++C++; // 未確認飛行 C

Google
Web ufcpp.net

拡張メソッド

目次

キーワード

概要

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

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

と書いていたものを、

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

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

int x = "1".Parse();

拡張メソッド

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";
    }
}

また、呼びだしているところと同じ名前空間内に拡張メソッドがある場合、 同じ名前空間内の方が優先されます。

using System;

namespace ConsoleApplication1
{
    using NamespaceA;

    class Program   
    {
        static void Main()
        {
            1.WriteToConsole();
            // ↑
            // ConsoleApplication1.Extensions.WriteToConsole の方が優先。
            // この場合、using NamespaceA しててもエラーにはならない。
        }
    }

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

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

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

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

(まあ、インターフェースが実装を持っている(ように見える)というのもちょっと気持ち悪い話ではあるんですが。)

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

using System;
using System.Collections;

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

class Program
{
  static void Main(string[] args)
  {
    IEnumerable 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つでどの静的メソッドが呼ばれるのかが切り替わって、 なおのことどこに定義があるのかわかりにくくなっています。

インターフェースに実装が追加されているように見える

インターフェースというものは、 メソッドの外部仕様のみを定めるもので、 本来、実装(メソッドの中身)を持つべきものではありません。

それが、拡張メソッドを使うことで、あたかもインターフェースに実装が追加されているかのように見えます。 あくまで見た目上そう見えるというだけではあるんですが、 少々気持ちの悪いものではあります。

拡張メソッドの意義

前節の通り、実を言うと、拡張メソッドは両手ばなしによろこべる機能ではなかったりします。 むしろ、拡張メソッドを「クラスに後からメソッドを追加するもの」だと考えるとあまりメリットはないように思います。

拡張メソッドの意義は、「(本来は前置き記法である)静的メソッドを後置き記法で書ける」ということな気がします。

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

図1: パイプライン処理

PowerShell で書くと以下のような感じの処理。

8, 9, 10, 11, 12, 13 | where { $_ -gt 10 } | foreach { $_ * $_ }

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

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 は人名に由来する単語らしくて、他に意味はない。)

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

Transtation into English

[お問い合わせ](q)