一般に、元のクラスに変化を加えること無くメソッドを追加したい場合、派生クラスを作成してそのクラスでメソッドを追加することになります。 拡張メソッド(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");
  }
}
静的メソッド
Imports System

Class Sample
  ' StringからIntegerに変換する静的メソッド
  Shared Function ToInt32(ByVal val As String) As Integer
    Return Integer.Parse(val)
  End Function

  Shared Sub Main()
    Dim val As String = "3"

    ' 上記の静的メソッドを呼び出すには
    ' 次のようにする
    Dim x As Integer = ToInt32(val)
    Dim y As Integer = ToInt32("42")
  End Sub
End Class
拡張メソッド
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();
  }
}
拡張メソッド
Imports System

Module Extension
  ' StringからIntegerに変換する拡張メソッド
  <System.Runtime.CompilerServices.Extension> _
  Public Function ToInt32(ByVal val As String) As Integer
    Return Integer.Parse(val)
  End Function
End Module

Class Sample
  Shared Sub Main()
    Dim val As String = "3"

    ' 拡張メソッドはインスタンスメソッドの
    ' ように呼び出すことができる
    Dim x As Integer = val.ToInt32()
    Dim y As Integer = "42".ToInt32()
  End Sub
End Class

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

拡張メソッドの使用

拡張メソッドを使った代表的な例に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が使用できるようになる
  }
}
拡張メソッドとLINQ
Imports System
Imports System.Collections.Generic
Imports System.Linq ' System.Linq名前空間をインポートする

Class Sample
  Shared Sub Main()
    Dim list As New List(Of Integer) From {0, 1, 2, 3, 4, 5}

    Console.WriteLine(list.Average()) ' 拡張メソッドAverageが使用できるようになる
  End Sub
End Class
実行結果
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)); // 静的メソッドとして呼び出す
  }
}
LINQメソッドを静的メソッドとして呼び出す
Imports System
Imports System.Collections.Generic

Class Sample
  Shared Sub Main()
    Dim list As New List(Of Integer) From {0, 1, 2, 3, 4, 5}

    Console.WriteLine(System.Linq.Enumerable.Average(list)) ' 静的メソッドとして呼び出す
  End Sub
End Class

拡張メソッドの宣言

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

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();
  }
}
拡張メソッドの宣言
Imports System

' 拡張メソッドはモジュールで宣言する必要がある
Module Extension
  ' Extension属性を付与すると拡張メソッドとなる
  ' (この場合第一引数がString型なので、String型に対する拡張メソッドとなる)
  <System.Runtime.CompilerServices.Extension> _
  Public Function ToInt32(ByVal val As String) As Integer
    Return Integer.Parse(val)
  End Function
End Module

Class Sample
  Shared Sub Main()
    Dim val As String = "3"

    Dim x As Integer = val.ToInt32()
    Dim y As Integer = "42".ToInt32()
  End Sub
End Class

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

引数のある拡張メソッド

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

引数のある拡張メソッドの宣言
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));
  }
}
引数のある拡張メソッドの宣言
Imports System

Module Extension
  <System.Runtime.CompilerServices.Extension> _
  Public Function ToHexString(ByVal val As UInteger, ByVal upperCase As Boolean) As String
    If upperCase Then
      ' 大文字で16進形式の文字列にする
      Return val.ToString("X")
    Else
      ' 小文字で16進形式の文字列にする
      Return val.ToString("x")
    End If
  End Function
End Module

Class Sample
  Shared Sub Main()
    Dim val As UInteger = 3735928495

    Console.WriteLine(val.ToHexString(True))
    Console.WriteLine(val.ToHexString(False))
  End Sub
End Class
実行結果
DEADBEAF
deadbeaf

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

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

列挙体に拡張メソッドを追加する
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());
  }
}
列挙体に拡張メソッドを追加する
Imports System
Imports System.IO

Module Extension
  ' FileAttributesの値がReadOnlyフラグを持っているか調べる拡張メソッド
  <System.Runtime.CompilerServices.Extension> _
  Public Function IsReadOnly(ByVal attr As FileAttributes) As Boolean
    Return attr.HasFlag(FileAttributes.ReadOnly)
  End Function
End Module

Class Sample
  Shared Sub Main()
    Dim attr1 As FileAttributes = FileAttributes.System Or FileAttributes.ReadOnly
    Dim attr2 As FileAttributes = FileAttributes.Normal

    Console.WriteLine(attr1.IsReadOnly()) ' attr1がReadOnlyフラグを持つか調べる
    Console.WriteLine(attr2.IsReadOnly())
    Console.WriteLine(FileAttributes.ReadOnly.IsReadOnly())
  End Sub
End Class
実行結果
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());
  }
}
インターフェイスに拡張メソッドを追加する(LINQ)
Imports System
Imports System.Collections.Generic
Imports System.Linq

Class Sample
  Shared Sub Main()
    Dim list As IEnumerable(Of Integer) = New List(Of Integer) From {0, 1, 2, 3, 4, 5}

    ' IEnumerable<T>インターフェイスに追加される拡張メソッドAverageを呼び出す
    Console.WriteLine(list.Average())
  End Sub
End Class

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

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

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

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

名前空間のインポート

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

拡張メソッドが宣言されている名前空間をインポートする
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);
    }
  }
}
拡張メソッドが宣言されている名前空間をインポートする
Imports System

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

Namespace MyApplication
  Class Sample
    Shared Sub Main()
      Dim val As String = "3"

      Dim x As Integer = val.ToInt32()
      Dim y As Integer = "42".ToInt32()
    End Sub
  End Class
End Namespace

Namespace MyNamespace
  Module Extension
    ' 拡張メソッド
    <System.Runtime.CompilerServices.Extension> _
    Public Function ToInt32(ByVal val As String) As Integer
      Return Integer.Parse(val)
    End Function
  End Module
End Namespace

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));
  }
}
nullが代入されている変数からの拡張メソッド呼び出し
Imports System

Module Extension
  <System.Runtime.CompilerServices.Extension> _
  Public Function Quote(ByVal val As String) As String
    Return String.Concat("""", val, """")
  End Function
End Module

Class Sample
  Shared Sub Main()
    Dim x As String = "Hello, world!"
    Dim y As String = Nothing

    Console.WriteLine(x.Quote())
    Console.WriteLine(y.Quote())
    Console.WriteLine(CType(Nothing, String).Quote())

    ' 上記の拡張メソッド呼び出しは、以下の静的メソッド呼び出しと同じなので
    ' いずれもヌル参照とはならない
    'Console.WriteLine(Extension.Quote(x))
    'Console.WriteLine(Extension.Quote(y))
    'Console.WriteLine(Extension.Quote(CType(Nothing, String)))
  End Sub
End Class
実行結果
"Hello, world!"
""
""

非公開メンバの参照

見かけ上、拡張メソッドはインスタンスメソッドですが、実体はあくまで別のクラスで宣言された静的メソッドであるため、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());
  }
}
拡張メソッドからの非公開メンバの参照
Imports System

Class Account
  Public DisplayName As String
  Private _id As Integer

  Public Sub New(ByVal id As Integer, ByVal displayName As String)
    Me._id = id
    Me.DisplayName = DisplayName
  End Sub
End Class

Module Extension
  <System.Runtime.CompilerServices.Extension> _
  Public Function GetID(ByVal account As Account) As Integer
    ' 拡張メソッド内からクラスの非公開メンバにアクセスすることはできない
    Return account._id
    ' error BC30390: 'Account._id' は 'Private' であるため、このコンテキストではアクセスできません。
  End Function
End Module

Class Sample
  Shared Sub Main()
    Dim a As New Account(0, "Alice")

    Console.WriteLine(a.GetID())
  End Sub
End Class

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

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

拡張メソッドと同名のメソッドが存在する場合
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); // 静的メソッドとして呼び出すことはできる
  }
}
拡張メソッドと同名のメソッドが存在する場合
Imports System
Imports System.Collections.Generic

Module Extension
  '  Listクラスに存在するメソッドと同名の拡張メソッド
  <System.Runtime.CompilerServices.Extension> _
  Public Sub Add(Of T)(ByVal list As List(Of T), ByVal item As T)
    Console.WriteLine("extension method!")
  End Sub
End Module

Class Sample
  Shared Sub Main()
    Dim list As New List(Of Integer)()

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

    Console.WriteLine("added!")

    Extension.Add(list, 0) ' 静的メソッドとして呼び出すことはできる
  End Sub
End Class
実行結果
added!
extension method!