一般に、元のクラスに変化を加えること無くメソッドを追加したい場合、派生クラスを作成してそのクラスでメソッドを追加することになります。 拡張メソッド(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>
インターフェイスに対して追加される拡張メソッドです。
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が使用できるようになる
}
}
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クラスで定義されている静的メソッドです。 このため、次のように静的メソッドとして呼び出すこともできます。
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)); // 静的メソッドとして呼び出す
}
}
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属性を指定することによって拡張メソッドを実装することはできません。 次のようにコンパイルエラーとなります。
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クラスで多数宣言されています。
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());
}
}
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
を指定して静的メソッドを呼び出すことと変わらないため、実行時エラーとはなりません。
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));
}
}
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!" "" ""
非公開メンバの参照
見かけ上、拡張メソッドはインスタンスメソッドですが、実体はあくまで別のクラスで宣言された静的メソッドであるため、private
やprotected
など外部に公開されていないインスタンスメンバを拡張メソッド内から参照したり呼び出したりすることはできません。 通常のメソッドの場合と同様、拡張メソッド内でアクセスできるのは外部に公開されているメンバのみです。
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!