オブジェクト指向プログラミングにおいて、拡張メソッドとは、元のオブジェクトがコンパイルされた後にオブジェクトに追加されるメソッドのことです。変更されるオブジェクトは、多くの場合、クラス、プロトタイプ、または型です。拡張メソッドは、一部のオブジェクト指向プログラミング言語の機能です。拡張メソッドの呼び出しと型定義で宣言されたメソッドの呼び出しには、構文上の違いはありません。[ 1 ]
しかし、すべての言語が拡張メソッドを同じように安全に実装しているわけではありません。例えば、C#、Java(Manifold、Lombok、Fluent経由)、Kotlinなどの言語は、拡張クラスを一切変更しません。なぜなら、そうするとクラス階層が崩れ、仮想メソッドのディスパッチに支障をきたす可能性があるからです。代わりに、これらの言語は拡張メソッドを厳密に静的に実装し、静的ディスパッチを使用して呼び出します。
拡張メソッドは、 C#、Manifold、Lombok、Fluentを使用したJava、Gosu、JavaScript、Oxygene、Ruby、Smalltalk、Kotlin、Dart、Visual Basic.NET、Xojoなど、多くの言語の機能です。Pythonのような動的言語では、クラス (組み込みクラスを除く) は特別な構文なしで拡張できるため ( geventなどのライブラリで使用されている「モンキーパッチ」と呼ばれる手法) 、拡張メソッドの概念は不要です。
VB.NETとOxygeneでは、「 」キーワードまたは属性の存在によって認識されますextension。Xojoでは、「Extends」キーワードはグローバルメソッドで使用されます。
C# では、静的クラスの静的メソッドとして実装され、最初の引数は拡張クラスのもので、その前に「 」キーワードが付きます。拡張メソッドはC++thisへの導入が提案されており、C# スタイルの拡張メソッドを参考にしています。[ 2 ]
Javaでは、拡張メソッドはプロジェクトのクラスパスに追加されるjarファイルであるManifoldを介して追加されます。C#と同様に、Javaの拡張メソッドは、最初の引数が拡張クラスと同じ型で、かつ@Extensionアノテーションが付与された@Extension@Thisクラス内で静的に宣言されます。あるいは、Fluentプラグインを使用すると、メソッドのシグネチャが一致していれば、アノテーションを使用せずに任意の静的メソッドを拡張メソッドとして呼び出すことができます。
Smalltalkでは、任意のコードが、メソッド作成メッセージ(などmethodsFor:)を拡張したいクラスに送信することで、いつでも任意のクラスにメソッドを追加できます。Smalltalkのメソッドカテゴリは、慣例として、拡張機能を提供するパッケージ名をアスタリスクで囲んで命名します。たとえば、Etoysアプリケーションコードがコアライブラリのクラスを拡張する場合、追加されたメソッドは*etoys*カテゴリに配置されます。
Rubyでは、Smalltalkと同様に、拡張のための特別な言語機能はありません。Rubyでは、キーワードを使用していつでもクラスを再開し、class新しいメソッドを追加できます。Rubyコミュニティでは、拡張メソッドを一種のモンキーパッチと表現することがよくあります。オブジェクトに安全かつローカルな拡張を追加するための新しい機能としてRefinementsがありますが、あまり使用されていないようです。
Rustでは拡張メソッドは存在しませんが、ユーザーが定義する型については、impl任意の場所にブロック付きの新しいメソッドやimpl Traitブロックを任意に追加できます。implユーザーが所有していない型、またはトレイトや少なくとも 1 つの型が現在のクレートで定義されていない場合 (「孤立ルール」と呼ばれる) にブロックを配置することは違法です。[ 3 ]これは、2 つのクレートが同じ型に対して同じトレイトを互換性のない方法で実装することを防ぐために使用されます。
Swiftでは、extensionキーワードはクラスのような構造を示し、既存のクラスにメソッド、コンストラクタ、フィールドを追加したり、既存のクラスに新しいインターフェース/プロトコルを実装したりすることができます。[ 4 ]
後述するように、他者が作成したコードを拡張できる拡張メソッドに加えて、拡張メソッドはそれ自体で有用なパターンも可能にします。拡張メソッドが導入された主な理由は、言語統合クエリ(LINQ)です。コンパイラが拡張メソッドをサポートすることで、LINQを新しいコードと同様に古いコードにも深く統合できるだけでなく、現時点では主要なMicrosoft .NET言語に固有のクエリ構文もサポートされます。
Systemを使用します。Console.WriteLine ( new [ ] { Math.PI , Math.E } .Where ( d = > d > 3 ) .Select ( d = > Math.Sin ( d / 2 ) ) . Sum ( ) ) ; //出力: // 1しかし、拡張メソッドを使用すれば、継承や仮想メソッド呼び出しのオーバーヘッドを必要とせずに、機能を一度実装するだけで再利用が可能になり、インターフェースの実装者に些細な機能や非常に複雑な機能を実装させる必要もなくなります。
特に有用なシナリオは、その機能が具体的な実装が存在しないインターフェース上で動作する場合、またはクラスライブラリの作成者によって有用な実装が提供されていない場合です。たとえば、開発者にプラグインアーキテクチャや同様の機能を提供するライブラリでは、このようなケースがよく見られます。
次のコードを考えてみましょう。これはクラスライブラリに含まれる唯一のコードだとします。それでも、ILoggerインターフェースを実装するすべての開発者は、using MyCoolLoggerステートメントを含めるだけで、一度実装する必要もなく、クラスライブラリが提供するILoggerの実装をサブクラス化する必要もなく、フォーマットされた文字列を書き込むことができるようになります。
名前空間Wikipedia.Util.Logging ;Systemを使用します。public interface ILogger { void Write ( string text ); }public static class LoggerExtensions { public static void Write ( this ILogger logger , string format , params object [] args ) { if ( logger != null ) { logger . Write ( String . Format ( format , args )); } } }使用方法 :
MyProject.Util.Loggingを使用します。MyLoggerImplementation logger = new (); logger . Write ( "{0}: {1}" , "kiddo sais" , "Mam mam mam mam ..." ); logger . Write ( "{0}: {1}" , "kiddo sais" , "Ma ma ma ma... " ); logger . Write ( "{0}: {1}" , "kiddo sais" , "Mama mama mama mama " ); logger . Write ( "{0}: {1}" , "kiddo sais" , "Mamma mamma mamma ... " ); logger . Write ( "{0}: {1}" , "kiddo sais" , "Elisabeth Lizzy Liz..." ); logger . Write ( "{0}: {1}" , "mamma sais" , "WHAT?!?!!!" ); logger.Write ( "{0}: {1}" , "kiddo sais" , " hi." ) ;拡張メソッドを使用することで、クラスライブラリのユーザーは、そのライブラリ由来の型を持つ引数、変数、その他のものを宣言する必要がなくなります。クラスライブラリで使用される型の構築と変換は、拡張メソッドとして実装できます。変換とファクトリを慎重に実装すれば、コンパイラが拡張メソッドをバインドできるようにするusingステートメントを変更するだけで、クラスライブラリの切り替えが容易になります。
拡張メソッドは、いわゆる流暢なインターフェースを実装する際に特に役立ちます。例えば、MicrosoftのEntity Framework構成APIは、可能な限り通常の英語に近いコードを記述することを可能にします。
拡張メソッドを使わなくても同様のことは可能だと主張する人もいるかもしれないが、実際には、拡張メソッドを使うことで、クラス階層に制約が少なくなり、意図したとおりに動作し、読みやすくなるため、より優れたエクスペリエンスが得られることがわかるだろう。
次の例では、Entity Framework を使用し、TodoList クラスをデータベース テーブル Lists に格納するように構成し、主キーと外部キーを定義します。このコードは、おおよそ「TodoList には TodoListID というキーがあり、エンティティ セット名は Lists で、それぞれに必須の TodoList を持つ TodoItem が多数含まれている」と理解してください。
名前空間Wikipedia.Examples ;public class TodoItemContext : DbContext { public DbSet < TodoItem > TodoItems { get ; set ; } public DbSet < TodoList > TodoLists { get ; set ; }protected override void OnModelCreating ( DbModelBuilder modelBuilder ) { base.OnModelCreating ( modelBuilder ) ; modelBuilder.Entity <TodoList> ( ) .HasKey ( e = > e.TodoListId ) .HasEntitySetName ( " Lists " ) . HasMany ( e = > e.Todos ) .WithRequired ( e = > e.TodoList ) ; } }例えば、IEnumerable を考えてみましょう。そのシンプルさに注目してください。基本的に LINQ の基盤となるメソッドは 1 つだけです。Microsoft .NET には、このインターフェイスの実装が多数存在します。これらの実装それぞれに、System.Linq 名前空間で定義されている一連のメソッドを実装して IEnumerable を操作するように要求するのは、非常に面倒な作業だったでしょう。そうなると、Microsoft 以外の開発者も IEnumerable を使用してこれらのメソッドを実装することを検討する必要が生じ、このインターフェイスが広く使われていることを考えると非効率的でした。このインターフェイスの 1 つのメソッドを実装するだけで、LINQ をほぼすぐに使用できるようになります。ほとんどの場合、IEnumerable の GetEnumerator メソッドは、プライベート コレクション、リスト、または配列の GetEnumerator 実装に委譲されます。
名前空間Wikipedia.Examples ;using System ; using System.Collections.Generic ;public class BankAccount : IEnumerable < decimal > { private List < Tuple < DateTime , decimal >> credits ; // すべて負の値と想定private List < Tuple < DateTime , decimal >> debits ; // すべて正の値と想定public IEnumerator < decimal > GetEnumerator () { IEnumerator < decimal > query = from dc in debits . Union ( credits ) orderby dc . Item1 // Date select dc . Item2 ; // Amount foreach ( decimal amount in query ) { yield return amount ; } } } // BankAccount のインスタンス ba と a が与えられ、現在のファイルに対して System.Linq を使用すると、// ba.Sum() で口座残高を取得したり、ba.Reverse() で最新のトランザクションを最初に表示したり、// ba.Average() でトランザクションごとの平均金額を取得したり、などを行うことができます。算術演算子を記述する必要はありません。とはいえ、拡張メソッドによって提供される機能の追加実装を追加することで、パフォーマンスを向上させたり、異なる実装のインターフェース実装に対処したりすることができます。たとえば、IEnumerable配列専用の実装 ( System.SZArrayHelper) をコンパイラに提供することで、配列型の参照に対する拡張メソッド呼び出しに対して自動的に選択されます。これは、引数がインターフェースthis T[] valueのインスタンスを操作する同じ名前の拡張メソッドIEnumerable( this IEnumerable value) よりも具体的 ( ) になるためです。
ジェネリッククラスでは、拡張メソッドを使用することで、共通の基底クラスから派生する必要なく、また型パラメータを特定の継承ブランチに制限することなく、ジェネリック型のすべてのインスタンスで使用可能な動作を実装できます。これは、この議論が成り立つ状況では、非ジェネリック基底クラスが共有機能を実装する必要があり、その場合、ジェネリックサブクラスは、使用される型が型引数のいずれかであるたびに、ボクシングやキャストを実行する必要があるため、有利です。
再利用と適切なオブジェクト指向設計を実現する他の手段よりも拡張メソッドを優先することに関して、拡張メソッドは、Visual Studio の IntelliSense などのコードエディタの自動補完機能を煩雑にする可能性があるため、開発者が選択的にインポートできるように、独自の名前空間に配置する必要があります。あるいは、メソッドが関連性の高い場合にのみ IntelliSense に表示されるように、十分に具体的な型で定義する必要があります。上記を踏まえると、開発者がメソッドを期待していても、using ステートメントがないために IntelliSense で見つからない場合、見つけるのが難しい可能性があることを考慮してください。これは、開発者がメソッドを、それを定義するクラスや、メソッドが存在する名前空間ではなく、メソッドが拡張する型とその型が存在する名前空間に関連付けている可能性があるためです。
プログラミングでは、既存のクラスに機能を追加する必要が生じる場合があります。例えば、新しいメソッドを追加する場合などです。通常、プログラマーは既存のクラスのソースコードを変更しますが、この方法では、変更を加えたすべてのバイナリを再コンパイルする必要があり、また、クラス自体を変更できる必要があります。しかし、サードパーティのアセンブリからクラスを使用する場合など、常に変更できるとは限りません。この問題は通常、次の3つの方法のいずれかで回避されますが、いずれも多少制限があり、直感的ではありません。
最初の方法は原則として簡単ですが、多くのクラスが特定のメンバーの継承を制限したり、完全に禁止したりしているため、制約があります。これには、sealedクラスや、C#のint、float、stringなどのさまざまなプリミティブデータ型が含まれます。もう1つの方法はこれらの制約を受けませんが、対象のクラスのメソッドを直接使用するのではなく、別のクラスへの参照が必要となるため、直感的ではないかもしれません。
例えば、文字列クラスに、文字を逆順にした文字列を返す新しいreverseメソッドを追加する必要がある場合を考えてみましょう。文字列クラスはsealed型であるため、このメソッドは通常、次のような方法で新しいユーティリティクラスに追加されます。
string x = "some string value" ; string y = Utility . Reverse ( x );しかし、ユーティリティメソッドやクラスのライブラリが増えるにつれて、特に初心者にとっては、ナビゲーションがますます難しくなる可能性があります。また、ほとんどの文字列メソッドとは異なり、文字列クラスのメンバーではなく、別のクラスにあるため、場所も直感的ではありません。したがって、より良い構文は次のようになります。
string x = " some string value" ; string y = x.Reverse ( );ほとんどの点で、VB.NET のソリューションは上記の C# のソリューションと似ています。ただし、VB.NET には、メンバーを参照渡しで拡張機能に渡すことができるという利点があります (C# では値渡しのみ)。これにより、次のことが可能になります。
Dim x As String = "some string value" x . Reverse ()Visual Basicではソースオブジェクトを参照渡しできるため、別の変数を作成することなく、ソースオブジェクトを直接変更することが可能です。また、既存のクラスメソッドと一貫した動作をするため、より直感的に操作できます。
しかし、C# 3.0 の拡張メソッドという新しい言語機能により、後者のコードが可能になります。このアプローチでは、以下のように静的クラスと静的メソッドが必要です。
名前空間Wikipedia.Examples ;Systemを使用します。public static class Utility { public static string Reverse ( this string input ) { char [] chars = input . ToCharArray (); Array . Reverse ( chars ); return new String ( chars ); } }定義において、最初の引数の前の修飾子「this」は、それが拡張メソッド(この場合は型「string」への拡張)であることを指定します。呼び出しにおいて、最初の引数は「渡される」のではなく、既に「呼び出し元」オブジェクト(ドットの前のオブジェクト)として認識されているため、そのまま渡されます。
拡張メソッドの呼び出しと静的ヘルパーメソッドの呼び出しの主な違いは、静的メソッドは前置記法で呼び出されるのに対し、拡張メソッドは中置記法で呼び出される点です。後者の記法を用いることで、ある操作の結果を別の操作に利用する場合、コードの可読性が向上します。
HelperClass.Operation2 ( HelperClass.Operation1 ( x , arg1 ) , arg2 )x . Operation1 ( arg1 ). Operation2 ( arg2 )C# 3.0 では、同じシグネチャを持つインスタンス メソッドと拡張メソッドがクラス内に存在できます。このような場合、インスタンス メソッドが拡張メソッドよりも優先されます。コンパイラもMicrosoft Visual Studio IDE も、名前の競合について警告を発しません。次の C# クラスを考えてみましょうGetAlphabet()。このクラスのインスタンスに対してメソッドが呼び出されます。
名前空間Wikipedia.Examples ;Systemを使用します。class AlphabetMaker { // このメソッドが実装されると、ExtensionMethods クラスの実装がシャドウされます。public void GetAlphabet () { Console . WriteLine ( "abc" ); } }static class ExtensionMethods { // 同じシグネチャを持つインスタンスメソッドが存在しない場合にのみ呼び出されます。public static void GetAlphabet ( this AlphabetMaker am ) { Console . WriteLine ( "ABC" ); } }拡張メソッドのみが存在する場合に、GetAlphabet()インスタンスに対して呼び出しを行った結果:AlphabetMaker
ABC
インスタンスメソッドと拡張メソッドの両方が存在する場合の結果:
ABC