一般に、元のクラスに変化を加えること無くメソッドを追加したい場合、派生クラスを作成してそのクラスでメソッドを追加することになります。 拡張メソッド(extension methods)を使うと、元の型への変更をせず、かつ直接継承関係にない型からメソッドを追加することができます。 追加といっても実際に型に対してメソッドが追加されるのではなく、型の外部で宣言された静的メソッドを、あたかもインスタンスメソッドのように呼び出すことができるようにするのが拡張メソッドです。 拡張メソッドを宣言すると、見かけ上は型にメソッドが追加されたように見えます。

例えば次のような静的メソッドがあったとします。 これを拡張メソッドにすると、あたかもインスタンスメソッドであるかのように呼び出すことができます。

静的メソッド
using System;

class Sample {
  // stringからintに変換する静的メソッド
  static int ToInt32(string val)
  {
    return int.Parse(val);
  }

  static void Main()
  {
    string val = "3";

    // 上記の静的メソッドを呼び出すには
    // 次のようにする
    int x = ToInt32(val);
    int y = ToInt32("42");
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
拡張メソッド
using System;

static class Extension {
  // stringからintに変換する拡張メソッド
  public static int ToInt32(this string val)
  {
    return int.Parse(val);
  }
}

class Sample {
  static void Main()
  {
    string val = "3";

    // 拡張メソッドはインスタンスメソッドの
    // ように呼び出すことができる
    int x = val.ToInt32();
    int y = "42".ToInt32();
  }
}
Copyright© 2016 . Released under the WTFPL version 2.

このように、拡張メソッドを用いると既存の型に一切変更を加えずにメソッドを追加できます。 また、拡張メソッドとして宣言すると、静的メソッドをインスタンスメソッドのように呼び出すことができます。 拡張メソッドは、値型など継承することができない型に対してもメソッドを追加することができます。

§1 拡張メソッドの使用

拡張メソッドを使った代表的な例にLINQがあります。 System.Linq名前空間をインポートすることにより、LINQの拡張メソッドが使えるようになります。 例えば、次の例で使用しているAverageメソッドIEnumerable<T>インターフェイスに対して追加される拡張メソッドです。

拡張メソッドとLINQ
using System;
using System.Collections.Generic;
using System.Linq; // System.Linq名前空間をインポートする

class Sample {
  static void Main()
  {
    var list = new List<int>() {0, 1, 2, 3, 4, 5};

    Console.WriteLine(list.Average()); // 拡張メソッドAverageが使用できるようになる
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
2.5

このAverageメソッドは本来IEnumerable<T>List<T>クラスには存在しないものですが、List<T>クラスに追加されたメソッドのように使用することができます。

拡張メソッドはインスタンスメソッドのように呼び出せる点をのぞくと、実体は単なる静的メソッドです。 例えば上記のAverageメソッドも実際にはEnumerableクラスで定義されている静的メソッドです。 このため、次のように静的メソッドとして呼び出すこともできます。

LINQメソッドを静的メソッドとして呼び出す
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    var list = new List<int>() {0, 1, 2, 3, 4, 5};

    Console.WriteLine(System.Linq.Enumerable.Average(list)); // 静的メソッドとして呼び出す
  }
}
Copyright© 2016 . Released under the WTFPL version 2.

§2 拡張メソッドの宣言

クラスライブラリで提供されるもの以外にも、拡張メソッドを独自に宣言して追加することもできます。 拡張メソッドは、静的メソッドを宣言するときとさほど変わらない方法によって宣言することができます。

C#では、メソッドの第一引数にthisキーワードを前置すると、その引数の型に対する拡張メソッドとして公開することができます。 たとえばthis string valという引数を記述するとstring型に対する拡張メソッドとなります。 一方VBには拡張メソッドを宣言する専用のキーワードは用意されておらず、メソッドにExtension属性(System.Runtime.CompilerServices名前空間)を付与することで拡張メソッドを宣言します。

拡張メソッドは静的クラス(static class、VBではモジュール)で宣言する必要があります。 第一引数でメソッドを追加する型を定めるため、拡張メソッドは必然的に引数が一つ以上のメソッドとなります。

拡張メソッドの宣言
using System;

// 拡張メソッドは静的クラスで宣言する必要がある
static class Extension {
  // 第一引数にthisキーワードを付けると拡張メソッドとなる
  // (この場合第一引数がstring型なので、string型に対する拡張メソッドとなる)
  public static int ToInt32(this string val)
  {
    return int.Parse(val);
  }
}

class Sample {
  static void Main()
  {
    string val = "3";

    int x = val.ToInt32();
    int y = "42".ToInt32();
  }
}
Copyright© 2016 . Released under the WTFPL version 2.

thisキーワードを使って宣言した拡張メソッドには、コンパイル時に自動的にExtension属性が付与されます。 .NET Framework 4以前の場合、Extension属性はSystem.Core.dllで宣言されているため、System.Core.dllを参照に追加する必要があります。


なおC#では、VBのようにExtension属性を指定することによって拡張メソッドを実装することはできません。 次のようにコンパイルエラーとなります。

C#ではExtension属性を使った拡張メソッドの宣言はできない
using System;

static class Extension {
  // error CS1112: 'System.Runtime.CompilerServices.ExtensionAttribute' を使用しないでください。 代わりに 'this' キーワードを使用してください。
  [System.Runtime.CompilerServices.Extension]
  public static int ToInt32(string val)
  {
    return int.Parse(val);
  }
}
Copyright© 2016 . Released under the WTFPL version 2.

§2.1 引数のある拡張メソッド

拡張メソッドの場合もほかのメソッドと同様に引数を渡すことができます。 第一引数には拡張メソッドの呼び出しを行ったインスタンス、第二引数以降には拡張メソッドの呼び出しに指定された引数が渡されます。

引数のある拡張メソッドの宣言
using System;

static class Extension {
  public static string ToHexString(this uint val, bool upperCase)
  {
    if (upperCase)
      // 大文字で16進形式の文字列にする
      return val.ToString("X");
    else
      // 小文字で16進形式の文字列にする
      return val.ToString("x");
  }
}

class Sample {
  static void Main()
  {
    uint val = 3735928495;

    Console.WriteLine(val.ToHexString(true));
    Console.WriteLine(val.ToHexString(false));
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
DEADBEAF
deadbeaf

§2.2 インターフェイス・列挙体の拡張メソッド

拡張メソッドはクラス・構造体のほか、インターフェイスや列挙体に対しても追加することができます。 次の例では、列挙体に拡張メソッドを持たせるようにしています。

列挙体に拡張メソッドを追加する
using System;
using System.IO;

static class Extension {
  // FileAttributesの値がReadOnlyフラグを持っているか調べる拡張メソッド
  public static bool IsReadOnly(this FileAttributes attr)
  {
    return attr.HasFlag(FileAttributes.ReadOnly);
  }
}

class Sample {
  static void Main()
  {
    var attr1 = FileAttributes.System | FileAttributes.ReadOnly;
    var attr2 = FileAttributes.Normal;

    Console.WriteLine(attr1.IsReadOnly()); // attr1がReadOnlyフラグを持つか調べる
    Console.WriteLine(attr2.IsReadOnly());
    Console.WriteLine(FileAttributes.ReadOnly.IsReadOnly());
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
True
False
True

この例の場合、IsReadOnlyはメソッドではなくプロパティとする場合が多いですが、既存の型にプロパティを追加する拡張プロパティのような機能はなく、またそういったプロパティを宣言することもできないため、拡張メソッドとして宣言しています。


LINQはインターフェイスへ拡張メソッドを追加する最たる例です。 LINQでは、IEnumerable<T>インターフェイスに追加される拡張メソッドがEnumerableクラスで多数宣言されています。

インターフェイスに拡張メソッドを追加する(LINQ)
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  static void Main()
  {
    IEnumerable<int> list = new List<int>() {0, 1, 2, 3, 4, 5};

    // IEnumerable<T>インターフェイスに追加される拡張メソッドAverageを呼び出す
    Console.WriteLine(list.Average());
  }
}
Copyright© 2016 . Released under the WTFPL version 2.

LINQの拡張メソッドがどのように実装され、機能するかについてはLINQ:実装して理解するで解説しています。

§2.3 拡張メソッドを宣言するクラスの名前

拡張メソッドを宣言するクラスには任意の名前を使用することができます。 必ずしも拡張メソッドを追加する型と関連する名前となっている必要はありませんが、拡張メソッドを含むクラスであることをわかりやすくするためにXxxExtension(s)というクラス名(string型に対する拡張メソッドを追加するクラスならStringExtensionなど)にすることが多いようです。

§3 拡張メソッドを使用・宣言する際の注意点

§3.1 名前空間のインポート

異なる名前空間の型で宣言されている拡張メソッドを使用する場合は、その名前空間をインポートしておく必要があります。 クラス・構造体など他の型と同様、インポートされていない名前空間で宣言されている拡張メソッドを参照しようとしても、名前を解決できずコンパイルエラーとなります。

拡張メソッドが宣言されている名前空間をインポートする
using System;

using MyNamespace; // 拡張メソッドを使用するには宣言されている名前空間をインポートする

namespace MyApplication {
  class Sample {
    static void Main()
    {
      string val = "3";

      int x = val.ToInt32();
      int y = "42".ToInt32();
    }
  }
}

namespace MyNamespace {
  static class Extension {
    // 拡張メソッド
    public static int ToInt32(this string val)
    {
      return int.Parse(val);
    }
  }
}
Copyright© 2016 . Released under the WTFPL version 2.

§3.2 null変数からの呼び出し

拡張メソッドはnull/Nothingが代入されている変数からも呼び出すことができます。 見かけ上はヌル参照となるように見えますが、実際のところこれは引数にnull/Nothingを指定して静的メソッドを呼び出すことと変わらないため、実行時エラーとはなりません。

nullが代入されている変数からの拡張メソッド呼び出し
using System;

static class Extension {
  public static string Quote(this string val)
  {
    return string.Concat("\"", val, "\"");
  }
}

class Sample {
  static void Main()
  {
    string x = "Hello, world!";
    string y = null;

    Console.WriteLine(x.Quote());
    Console.WriteLine(y.Quote());
    Console.WriteLine(((string)null).Quote());

    // 上記の拡張メソッド呼び出しは、以下の静的メソッド呼び出しと同じなので
    // いずれもヌル参照とはならない
    //Console.WriteLine(Extension.Quote(x));
    //Console.WriteLine(Extension.Quote(y));
    //Console.WriteLine(Extension.Quote((string)null));
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
"Hello, world!"
""
""

§3.3 非公開メンバの参照

見かけ上、拡張メソッドはインスタンスメソッドですが、実体はあくまで別のクラスで宣言された静的メソッドであるため、privateprotectedなど外部に公開されていないインスタンスメンバを拡張メソッド内から参照したり呼び出したりすることはできません。 通常のメソッドの場合と同様、拡張メソッド内でアクセスできるのは外部に公開されているメンバのみです。

拡張メソッドからの非公開メンバの参照
using System;

class Account {
  public string DisplayName;
  private int _id;

  public Account(int id, string displayName)
  {
    this._id = id;
    this.DisplayName = displayName;
  }
}

static class Extension {
  public static int GetID(this Account account)
  {
    // 拡張メソッド内からクラスの非公開メンバにアクセスすることはできない
    return account._id;
    // error CS0122: `Account._id' はアクセスできない保護レベルになっています。
  }
}

class Sample {
  static void Main()
  {
    var a = new Account(0, "Alice");

    Console.WriteLine(a.GetID());
  }
}
Copyright© 2016 . Released under the WTFPL version 2.

§3.4 同一シグネチャのインスタンスメソッドの呼び出し

拡張メソッドと同名で同じシグネチャ(引数リスト)のインスタンスメソッドがすでに型に存在している場合は、型に存在するメソッドの呼び出しが優先されます。 同名のメソッドが存在する場合でも、拡張メソッドとしてではなく静的メソッドとして呼び出すことはできます。

拡張メソッドと同名のメソッドが存在する場合
using System;
using System.Collections.Generic;

static class Extension {
  // Listクラスに存在するメソッドと同名の拡張メソッド
  public static void Add<T>(this List<T> list, T item)
  {
    Console.WriteLine("extension method!");
  }
}

class Sample {
  static void Main()
  {
    var list = new List<int>();

    list.Add(0); // 拡張メソッドではなくListクラスのAddメソッドが呼び出される

    Console.WriteLine("added!");

    Extension.Add(list, 0); // 静的メソッドとして呼び出すことはできる
  }
}
Copyright© 2016 . Released under the WTFPL version 2.
実行結果
added!
extension method!