拡張メソッドとは既存のクラスにインスタンスメソッドを追加できる仕組みです。
拡張メソッドを定義するには以下のように書きます。
namespace HigLabo.Extensions
{
public static class StringExtensions
{
public static String Left(this String value, Int32 length)
{
return value.Substring(0, length);
}
public static String Right(this String value, Int32 length)
{
return value.Substring(value.Length - length, length);
}
}
}
まずstaticクラスを定義します。拡張メソッドはstaticクラスにしか作成できません。
名前の付け方ですがExtensionsというSuffixをつけるのが一般的です。
次にstaticメソッドを作成します。通常のstaticメソッドの定義とほぼ同じですが1つだけ異なる点があり第1引数にthisをつけます。
次に呼び出し側で名前空間をusingします。
これにより第1引数のクラス(この場合はStringクラス)にLeft、Rightというメソッドが追加され呼び出すことができるようになります。
using HigLabo.Extensions;
static void Main(string[] args)
{
String text = "これは拡張メソッドの例文です。";
String left = text.Left(5);
String right = text.Right(5);
}
leftには"これは拡張"という文字が入っています。rightには"例文です。"という文字が入っています。
拡張メソッドを使用することで自分がソースコードを持っていないクラス、例えば.NET Frameworkで定義されているクラスや他の人が作成したクラスにメソッドを追加することが可能です。
これによって既存のクラスで足りないメソッドを自分で追加できプログラムの作成が容易になります。
拡張メソッドは既存のクラスにインスタンスメソッドを追加できると解説しましたが実際には違います。
拡張メソッドはあくまでstaticメソッドであり、コンパイル時にコンパイラがメソッド呼び出しを解決してくれてコンパイル後には単なるstaticメソッドの呼び出しとなります。
例えば
String left = "これは拡張メソッドの例文です。".Left(5);
というメソッド呼び出しがある場合のコンパイル時のコンパイラの動作について簡単に解説してみます。
1. コンパイラはまずStringクラスにLeft(Int32 length)メソッドが定義されていないかどうか探します。
2. 定義されていない場合、自分自身の名前空間とusingしている名前空間の中でstaticクラスをリストアップします。
3. リストアップしたstaticクラスのメソッドの中で第1引数がStringでthisキーワードがついているLeftメソッドを探します。
4. そのメソッドの中で第2引数がInt32のメソッドを探してもし存在すればコードを次のように書き換えコンパイルします。
String left = HigLabo.Extensions.StringExtensions.Left("これは拡張メソッドの例文です。", 5);
以上のルールからインスタンスメソッドと拡張メソッドがある場合は必ずインスタンスメソッドが優先されます。ですので既存のインスタンスメソッドを書き換えることはできません。
一見便利な拡張メソッドですがメリットとデメリットがあります。まずは使ってもほとんど問題が発生しない場面についていくつか解説します。
インターフェースにメソッドを追加する
インターフェースには実装を追加することができませんが拡張メソッドを使用することでMix-Inのような形で実装を追加することが可能です。
.NET Frameworkでの例でいえばIEnumerable<T>クラスがあげられます。
Enumerableにはこのクラスの拡張メソッドが定義されていて、各種コレクションに対して走査、フィルタ、変換処理などを行うことが可能になっています。
Enumにメソッドを追加する
例えば以下のようなEnumを定義します。
public enum ProgressState
{
NotStart,
Executing,
Suspend,
Error,
Complete
}
次に以下のような拡張メソッドを定義します。
public static class ProgressStateExtensions
{
public static String GetText(this ProgressState state)
{
switch (state)
{
case ProgressState.NotStart: return "未実行";
case ProgressState.Executing: return "実行中";
case ProgressState.Suspend: return "延期";
case ProgressState.Error: return "エラー";
case ProgressState.Complete: return "完了";
}
return "";
}
}
そうするとEnumを以下のようにテキストに変換することが可能になります。
ProgressState s = ProgressState.Executing;
var text = s.GetText(); //実行中
Enumはもともとメソッドの定義ができませんが、拡張メソッドを使用することでEnumへのメソッドの追加ができるようになります。
DLLの依存関係を拡張メソッドで回避する
例えば以下のような場合を考えます。
Biz.dllにSalaryクラス
Entity.dllにPersonクラス
Appから両方のDLLを参照します。給料の計算をするために以下の形で呼び出したいとします。
Person p = new Person();
Decimal d = Salary.GetMonthSalary(p);
通常であればSalaryクラスにGetMonthSalaryメソッドを追加することになります。
しかしメソッドの引数にPersonクラスを使用しているのでBiz.dllがEntity.dllを参照しなければなりません。
Biz.dllが多くの場所で使用され多くの場合で必ずしもPersonクラスを必要としてない場合、DLL間の依存関係を避けたいということがありえます。
そのような場合は通常のインスタンスメソッドで定義をせずにEntity.dllにSalaryExtensionsクラスを定義して拡張メソッドとして定義をします。
namespace MyProduct.Entity
{
public static class SalaryExtensions
{
public static Decimal GetMonthSalary(this Salary salary, Person person)
{
Decimal d = 0;
//計算処理…
return d;
}
}
}
そうすることで通常のインスタンスメソッドでの呼び出しを可能にしつつ、DLL依存関係を避けることが可能になります。
名前の衝突
コンパイラの動作の章で説明したように拡張メソッドはusingした名前空間内に定義されているstaticクラスを探してメソッド呼び出しを解決します。
たくさんの名前空間をusingしていると全く同じ拡張メソッドが見つかる場合がありえます。
その場合はコンパイラはどちらのメソッドを優先すればよいかわからないためコンパイルエラーが発生します。
小規模なプログラムで拡張メソッドを自分で定義して使う場合にはこういった問題は発生する可能性は低いです。
しかし大規模なシステムで複数人が自由に拡張メソッドを定義していくと名前の衝突がおこる可能性が高くなります。
名前の衝突が起こった場合はどちらかの名前空間をusingしないか拡張メソッドを通常のstaticメソッドの呼び出しに置き換える必要があります。
//String left = text.Left(5); の呼び出し部分を以下に置き換える
String left = HigLabo.Extensions.StringExtensions.Left("これは拡張メソッドの例文です。", 5);
バージョンアップ時の挙動の変更
拡張しているクラスに新たにインスタンスメソッドが追加された場合には動作が変更されることになります。
この
場合コンパイルエラーが発生しないため変更に気づかないという欠点があります。
追加されたインスタンスメソッドと自分で定義した拡張メソッドの実装が全く同じならば問題になりません。
拡張メソッドの仕様はバージョンアップ時に知らない間に動作が変更されるリスクを含むことは頭に入れておいたほうがよいでしょう。
既存のAPIの汚染
大規模なシステムで複数の人が自由に拡張メソッドを定義していくとインテリセンスで表示したときに既存のAPI+拡張メソッドが定義されることになります。
作成した拡張メソッドの全てにわかりやすい名前がついていれば問題はありませんが、例えば判りづらい拡張メソッドが100個ほどStringクラスに追加される可能性がありえます。
それぞれのクラスに定義されたメソッドというのはその
クラスの設計者が最適と考えて定義したメソッドが定義されているわけです。
拡張メソッドが既存のAPIを判りづらいものにしてしまうリスクがあることは頭に入れておいたほうがよいでしょう。