初めに
C#の拡張メソッドの存在意義が文法だけでは分かりにくいと感じ、調査を行いました。
前提条件
- C#の文法はなんとなく把握している
- 拡張メソッドの存在は知っている
対処したい状況
端的に言うと「本来メソッドが追加できないようなクラス・インターフェイスなどにメソッドを追加したい」状況です
「特定のデータに関連するコードが重複してしまうので、解決したい」という状況はよくあると思います。通常、特定の重複部分をメソッドへの抽出して、新しいクラスを定義することで解決ができます。
しかし、コードの変更・追加ができない場合もあります。具体例をいくつか挙げます。
-
サードパーティー製のコード
素晴らしい機能を提供してくれるライブラリでも、自分の欲しい機能を持つメソッドだけが、不運にも提供されていないことがまれによくあります。 -
列挙型
そもそもメソッドが持てません -
インターフェイス
そもそも実装を追加することができません
なお、ここで想定しているのは、インターフェイスを実装するクラスの内部実装ではなく、インターフェイスに対する利用者側のコードの重複です。基本的には、インターフェイスの内部実装はクラスごとに異なります。
(※C#8.0からはインターフェイスもデフォルト実装を持てますが、新しめの機能であることもあり、例えばUnityでは、Unity2020.1以前では使えないなど、完全な普及にはもうしばらくかかりそうです。)
これらに対していつも同じような処理をしている場合には、オブジェクト指向的な解決が難しくなります。以下では、サードパーティー製のクラスThirdPartyClass
に、とある機能DoSomething()
を追加したくなったという設定で、その解決策を考えます。
考えられる解決策
コピペ
重複したコードを生み出す手間を省くことはできますが、重複自体をなくすことはできません。いろいろな場所で言われることですが、コピペはやめましょう。
ソースを書き換える
サードパーティー製のコードに独自で変更を入れるのは、不可能ではないとしても、かなり辛いことになります。そのライブラリがバージョンアップするたびに自分で書いたコードの保守も必要になるからです。また、string型に対する処理など、クラス自体の動作に変更を加えられない場合もあります。
メソッドの導入
void DoSomething(ThirdpartyClass x){
// 直接メソッドを追加できない何らかの処理を行う
}
以上のようなコードを重複が発生しているクラスに書くことで重複を削除できます。
この方法だと、コードの見通しが悪くなってしまいます。
なぜなら、この処理が書かれているのは、DoSomething()
で処理するデータとは別のデータに関するメソッドを集めたクラスであり、単に内部的に重複したサードパーティー製のクラスへの処理を書くには、論理的に自然な場所とは言い難いからです。
この問題は、別のクラスに静的メソッドを追加することで緩和できます。
public static class ThirdPartyHealper{
public static void DoSomething(ThirdpartyClass x){
// 直接メソッドを追加できない何らかの処理を行う
}
}
継承による拡張
public class MyClass : ThirdpartyClass{
public void DoSomething(){
// 直接メソッドを追加できない何らかの処理を行う
}
}
問題となっているクラスを継承し、欲しい機能をメソッドとして追加することで、オブジェクト指向的に自然に機能を拡張することができます。
この方法をとる場合、既存のコードのすべてのサードパーティー製のクラス名を探して継承したものに置き換えなければならず、多少の面倒を伴います。重複が発生している部分以外に、newしている部分全てで新しいクラスのインスタンスを生成しなければならないからです。
ラッパークラスの作成
問題となっているクラスをprivateフィールドとして持ち、欲しい機能を実装したラッパーを作成することで、処理が書かれている場所を論理的に正しくできます。
class ThirdPartyWrapper{
private ThirdPartyClass wrappingObject;
public ThirdPartyWrapper(ThirdPartyClass x){
wrappingObject = x;
}
// 加えてThirdPartyClassの持っているすべてのpublicなフィールド・メソッドに関する同名の委譲メソッドを記述
public void ThirdPartyClassMethod(){
wrappingObject.ThirdPartyClassMethod();
}
public void DoSomething(){
// 直接メソッドを追加できない何らかの処理を行う
}
}
この方法では、継承によって解決した場合に比べてnewしているすべての場所を探す必要はありません。しかし、元クラスで公開されているメソッドとフィールドすべてについて、委譲するメソッドとプロパティを生成しなければならず非常に面倒です。
また、既存のコードをラッパーを使うように変更する必要が生まれます。
拡張メソッドの文法
基本的な文法をコードで説明します。今回の記事の趣旨から外れるので詳しい説明は省きます。
// 拡張メソッドは静的クラスで宣言しなければならない
public static class ExtensionMethodClass{
// 静的メソッドの第一引数の型の前にthisをつけると拡張メソッドになる
public static void ExtensionMethod(this int number){
}
public static void UseExtensionMethod(){
// 次の2つの呼び出しは同じ意味になる
ExtensionMethod(5);
// あたかもインスタンスメソッドかのように呼び出せる
5.ExtensionMethod();
}
}
拡張メソッドによる解決
拡張メソッドは、あたかもインスタンスメソッドであるかのように静的メソッドを呼び出すことができる機能です。
したがって、すでに紹介した静的メソッドによる解決を適用した後、それを拡張メソッドに書き換えるだけ解決できます。
public static class ThirdPartyHealper{
public static void DoSomething(this ThirdpartyClass x){
// 直接メソッドを追加できない何らかの処理を行う
}
}
拡張メソッドによる解決の利点
既存のコードに影響がない
ラッパーによる解決や継承による解決は自分の今まで書いたコードに変更を加えてしまいます。
対して拡張メソッドは、既存のコードに変更を加えずとも、重複が発生している部分だけを改善すれば十分です。
拡張が簡単
ラッパーによる解決はラッパー自体の作成による手間が多く、書くのが面倒ですが、拡張メソッドの場合は本質的なロジック以外のコードは最小限に抑えられています。
メソッドチェーンが可能になる
これは拡張メソッドが静的メソッドによる解決よりも優れている点です。C#において拡張メソッドが使われている代表的な存在であるLINQでは、以下のようなメソッドチェーンが書けるように定義されており、処理の流れが分かりやすくなります。Sum()
やSelect()
は、「IEnumerable
インターフェースに対していつも同様の処理をしていることが多いのに、インターフェースにコードが書けない」という、まさに今回の記事の課題を解決するものになります。
intArray.Select(x => 2 * x + 5).Where(x => x < 0).Sum();
これを静的メソッドで行うと以下のようになり、かっこの対応が非常に読みにくいです。
Sum(Where(Select(intArray,x => 2 * x + 5), x => x < 0));
利用者は静的クラスのことを気にせずにコードを書ける
利用者からすると、静的メソッドによる解決をした場合、どうしてもThirdPartyHealper
などといった本来知らなくていいクラスにアクセスしなければなりません。しかし、拡張メソッドを使えば、(using
などの必要はあるにせよ、ロジックを書く段階では)拡張メソッドがどこで定義されているかわからなくとも特定の機能を使うことができます。
まとめ
拡張メソッドは、その名前の通り、「本来プログラマが変更することが難しいクラスに、あたかもインスタンスメソッドを追加したような拡張をする」機能だと言えます。
蛇足:可読性の向上
これは今回の趣旨である、重複するコードの排除からは外れるかもしれませんが、単純なfor文に対して拡張メソッドを適用することで、以下のようなコードも書くことができ、可読性の向上に貢献します。
5.Times(()=>{
// 5回繰り返したい処理を書く
});
1.Upto(3,(i)=>{
// ループを3回おこない、ループごとにiに1から3が代入される
});