拡張メソッド (C# プログラミング ガイド)

拡張メソッドを使用すると、新規の派生型の作成、再コンパイル、または元の型の変更を行うことなく既存の型にメソッドを "追加" できます。 拡張メソッドは静的メソッドですが、拡張された型のインスタンス メソッドのように呼び出します。 C#、F#、Visual Basic で作成されたクライアント コードの場合は、拡張メソッドの呼び出しと、型で定義されているメソッドの呼び出しに明確な違いはありません。

最も一般的な拡張メソッドは、既存の System.Collections.IEnumerable 型および System.Collections.Generic.IEnumerable<T> 型にクエリ機能を追加する LINQ 標準クエリ演算子です。 この標準クエリ演算子を使用するには、まず using System.Linq ディレクティブを使用して、スコープに含めます。 IEnumerable<T> を実装するすべての型は、GroupByOrderByAverage などのインスタンス メソッドを持っていると考えられます。 List<T>Array などの IEnumerable<T> 型のインスタンスの後に "ドット" を入力すると、IntelliSense により、ステートメントの入力候補としてこれらの追加メソッドが表示されます。

OrderBy の例

整数の配列において、標準クエリ演算子の OrderBy メソッドを呼び出す方法を次の例に示します。 かっこ内の式はラムダ式です。 標準クエリ演算子の多くはパラメーターとしてラムダ式を受け取りますが、拡張メソッドでは、これは必須ではありません。 詳細については、「ラムダ式」を参照してください。

class ExtensionMethods2
{

    static void Main()
    {
        int[] ints = [10, 45, 15, 39, 21, 26];
        var result = ints.OrderBy(g => g);
        foreach (var i in result)
        {
            System.Console.Write(i + " ");
        }
    }
}
//Output: 10 15 21 26 39 45

拡張メソッドは、静的メソッドとして定義しますが、インスタンス メソッドの構文を使用して呼び出します。 最初のパラメーターでは、メソッドによって操作される型を指定します。 このパラメーターの前には this 修飾子を付けます。 using ディレクティブを使用して名前空間をソース コードに明示的にインポートした場合、拡張メソッドはそのスコープでのみ有効です。

System.String クラスに対して拡張メソッドを定義する例を次に示します。 入れ子になっていない、非ジェネリックの静的クラス内で定義されています。

namespace ExtensionMethods
{
    public static class MyExtensions
    {
        public static int WordCount(this string str)
        {
            return str.Split(new char[] { ' ', '.', '?' },
                             StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }
}

この WordCount ディレクティブを使用することで、using 拡張メソッドをスコープに取り込むことができます。

using ExtensionMethods;

また、この構文を使用することで、アプリケーションから呼び出すことができます。

string s = "Hello Extension Methods";
int i = s.WordCount();

コードでは、インスタンス メソッドの構文を使用して、拡張メソッドを呼び出します。 コンパイラによって生成される中間言語 (IL) により、コードは静的メソッドの呼び出しに変換されます。 カプセル化の原則には実質的に違反していません。 拡張メソッドでは、それが拡張している型のプライベート変数にはアクセスできません。

MyExtensions クラスと WordCount メソッドは両方とも static であり、他のすべての static メンバーと同様にアクセスできます。 次のように、WordCount メソッドは、他の static メソッドと同様に呼び出すことができます。

string s = "Hello Extension Methods";
int i = MyExtensions.WordCount(s);

前述の C# コードでは、次のことが行われます。

  • "Hello Extension Methods" の値の s という名前の string を宣言して割り当てます。
  • MyExtensions.WordCount指定された引数 s を呼び出します

詳細については、「カスタム拡張メソッドを実装して呼び出す方法」を参照してください。

一般に、独自に実装するより、拡張メソッドを呼び出す方がはるかに多くなると思われます。 拡張メソッドは、インスタンス メソッドの構文を使用して呼び出すので、特別な知識がなくてもクライアント コードからそれらを使用できます。 メソッドが定義されている名前空間に関する using ディレクティブを追加するだけで、特定の型の拡張メソッドを使用できるようになります。 たとえば、標準クエリ演算子を使用するには、次の using ディレクティブをコードに追加します。

using System.Linq;

場合によっては、System.Core.dll への参照も追加する必要があります。ほとんどの IEnumerable<T> 型で利用できる追加メソッドとして、標準クエリ演算子が IntelliSense に表示されるようになりました。

コンパイル時の拡張メソッドのバインディング

拡張メソッドを使用してクラスまたはインターフェイスを拡張することはできますが、これらをオーバーライドすることはできません。 インターフェイス メソッドまたはクラス メソッドと同じ名前およびシグネチャを持つ拡張メソッドは決して呼び出されません。 コンパイル時に、型自体で定義されているインスタンス メソッドよりも低い優先順位が拡張メソッドには必ず設定されます。 つまり、型に Process(int i) という名前のメソッドがあり、これと同じシグネチャの拡張メソッドがある場合、コンパイラは必ずインスタンス メソッドにバインドします。 コンパイラは、メソッド呼び出しを検出すると、最初に型のインスタンス メソッドから一致するものを探します。 一致するものが見つからない場合、型に対して定義されている拡張メソッドを検索し、見つかった最初の拡張メソッドにバインドします。

次の例は、C# のコンパイラがメソッド呼び出しを型のインスタンス メソッドにバインドするか、拡張メソッドにバインドするかを決定するときに従う規則を示しています。 Extensions 静的クラスには、IMyInterface を実装する型に対して定義された拡張メソッドが含まれています。 AB、および C の各クラスはすべてこのインターフェイスを実装しています。

MethodB 拡張メソッドは、その名前とシグネチャがクラスにより既に実装されているメソッドと完全に一致しているため、呼び出されることはありません。

コンパイラは、一致するシグネチャを持つインスタンス メソッドを検出できない場合、一致する拡張メソッドが存在する場合はそれにバインドします。

// Define an interface named IMyInterface.
namespace DefineIMyInterface
{
    public interface IMyInterface
    {
        // Any class that implements IMyInterface must define a method
        // that matches the following signature.
        void MethodB();
    }
}

// Define extension methods for IMyInterface.
namespace Extensions
{
    using System;
    using DefineIMyInterface;

    // The following extension methods can be accessed by instances of any
    // class that implements IMyInterface.
    public static class Extension
    {
        public static void MethodA(this IMyInterface myInterface, int i)
        {
            Console.WriteLine
                ("Extension.MethodA(this IMyInterface myInterface, int i)");
        }

        public static void MethodA(this IMyInterface myInterface, string s)
        {
            Console.WriteLine
                ("Extension.MethodA(this IMyInterface myInterface, string s)");
        }

        // This method is never called in ExtensionMethodsDemo1, because each
        // of the three classes A, B, and C implements a method named MethodB
        // that has a matching signature.
        public static void MethodB(this IMyInterface myInterface)
        {
            Console.WriteLine
                ("Extension.MethodB(this IMyInterface myInterface)");
        }
    }
}

// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
namespace ExtensionMethodsDemo1
{
    using System;
    using Extensions;
    using DefineIMyInterface;

    class A : IMyInterface
    {
        public void MethodB() { Console.WriteLine("A.MethodB()"); }
    }

    class B : IMyInterface
    {
        public void MethodB() { Console.WriteLine("B.MethodB()"); }
        public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }
    }

    class C : IMyInterface
    {
        public void MethodB() { Console.WriteLine("C.MethodB()"); }
        public void MethodA(object obj)
        {
            Console.WriteLine("C.MethodA(object obj)");
        }
    }

    class ExtMethodDemo
    {
        static void Main(string[] args)
        {
            // Declare an instance of class A, class B, and class C.
            A a = new A();
            B b = new B();
            C c = new C();

            // For a, b, and c, call the following methods:
            //      -- MethodA with an int argument
            //      -- MethodA with a string argument
            //      -- MethodB with no argument.

            // A contains no MethodA, so each call to MethodA resolves to
            // the extension method that has a matching signature.
            a.MethodA(1);           // Extension.MethodA(IMyInterface, int)
            a.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

            // A has a method that matches the signature of the following call
            // to MethodB.
            a.MethodB();            // A.MethodB()

            // B has methods that match the signatures of the following
            // method calls.
            b.MethodA(1);           // B.MethodA(int)
            b.MethodB();            // B.MethodB()

            // B has no matching method for the following call, but
            // class Extension does.
            b.MethodA("hello");     // Extension.MethodA(IMyInterface, string)

            // C contains an instance method that matches each of the following
            // method calls.
            c.MethodA(1);           // C.MethodA(object)
            c.MethodA("hello");     // C.MethodA(object)
            c.MethodB();            // C.MethodB()
        }
    }
}
/* Output:
    Extension.MethodA(this IMyInterface myInterface, int i)
    Extension.MethodA(this IMyInterface myInterface, string s)
    A.MethodB()
    B.MethodA(int i)
    B.MethodB()
    Extension.MethodA(this IMyInterface myInterface, string s)
    C.MethodA(object obj)
    C.MethodA(object obj)
    C.MethodB()
 */

一般的な使用パターン

コレクションの機能

これまでは、特定の型の System.Collections.Generic.IEnumerable<T> インターフェイスが実装され、その型のコレクションに対して動作する機能が含まれている、"コレクション クラス" を作成するのが一般的でした。 この種類のコレクション オブジェクトを作成しても問題はありませんが、System.Collections.Generic.IEnumerable<T> の拡張機能を使用して同じ機能を実現できます。 拡張機能には、その型の System.Collections.Generic.IEnumerable<T> が実装されている System.ArraySystem.Collections.Generic.List<T> などの任意のコレクションから機能を呼び出すことができるという利点があります。 Int32 の配列のこの使用例については、この記事で既に示されています。

レイヤー固有の機能

オニオン アーキテクチャまたは他のレイヤー化アプリケーション設計を使用する場合は、アプリケーションの境界を越えて通信するために使用できるドメイン エンティティまたはデータ転送オブジェクトのセットを使用するのが一般的です。 これらのオブジェクトには、通常、機能が含まれていないか、またはアプリケーションのすべてのレイヤーに適用される最小限の機能のみが含まれています。 他のレイヤーで必要のないメソッドや望ましくないメソッドを使用してオブジェクトを読み込むことなしに、拡張メソッドを使用して、各アプリケーション レイヤーに固有の機能を追加することができます。

public class DomainEntity
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

static class DomainEntityExtensions
{
    static string FullName(this DomainEntity value)
        => $"{value.FirstName} {value.LastName}";
}

定義済みの型の拡張

再利用可能な機能を作成する必要があるときに、新しいオブジェクトを作成するのではなく、多くの場合、.NET や CLR 型などの既存の型を拡張できます。 たとえば、拡張メソッドを使用しない場合は、コード内の複数の場所から呼び出すことができる Engine または Query クラスを作成して、SQL サーバーに対するクエリの実行を処理することが考えられます。 一方、代わりに拡張メソッドを使用して System.Data.SqlClient.SqlConnection クラスを拡張すると、SQL サーバーに接続している任意の場所からそのクエリを実行することができます。 他の例としては、System.String クラスへの共通機能の追加、System.IO.Stream オブジェクトのデータ処理機能の拡張、特定のエラー処理機能のための System.Exception オブジェクトなどがあります。 この種のユース ケースは、開発者の想像力と良識によってのみ制限されます。

struct 型は、メソッドに値で渡されるため、定義済みの型を拡張するのが難かしい場合があります。 これは、構造体への変更が構造体のコピーに対して行われることを意味します。 そのような変更は、拡張メソッドが終了した後では認識できません。 ref 拡張メソッドにする最初の引数に ref 修飾子を追加できます。 ref キーワードは、セマンティックの相違なしに、this キーワードの前後に表示できます。 ref 修飾子の追加は、最初の引数が参照によって渡されることを示します。 これにより、拡張されている構造体の状態を変更する拡張メソッドを記述できます (プライベート メンバーはアクセスできることに注意してください)。 構造体に制約された値の型またはジェネリック型 (詳細についてはstruct制約を参照) のみ ref 拡張メソッドの最初のパラメーターとして許可されます。 次の例は、ref 拡張メソッドを使用して、結果の再割り当てしたり ref キーワードを使用した関数を通して結果を渡したりする必要なく、組み込みの型を直接変更する方法を示しています。

public static class IntExtensions
{
    public static void Increment(this int number)
        => number++;

    // Take note of the extra ref keyword here
    public static void RefIncrement(this ref int number)
        => number++;
}

public static class IntProgram
{
    public static void Test()
    {
        int x = 1;

        // Takes x by value leading to the extension method
        // Increment modifying its own copy, leaving x unchanged
        x.Increment();
        Console.WriteLine($"x is now {x}"); // x is now 1

        // Takes x by reference leading to the extension method
        // RefIncrement changing the value of x directly
        x.RefIncrement();
        Console.WriteLine($"x is now {x}"); // x is now 2
    }
}

次の例は、ユーザー定義の構造体の型の ref 拡張メソッドを示しています。

public struct Account
{
    public uint id;
    public float balance;

    private int secret;
}

public static class AccountExtensions
{
    // ref keyword can also appear before the this keyword
    public static void Deposit(ref this Account account, float amount)
    {
        account.balance += amount;

        // The following line results in an error as an extension
        // method is not allowed to access private members
        // account.secret = 1; // CS0122
    }
}

public static class AccountProgram
{
    public static void Test()
    {
        Account account = new()
        {
            id = 1,
            balance = 100f
        };

        Console.WriteLine($"I have ${account.balance}"); // I have $100

        account.Deposit(50f);
        Console.WriteLine($"I have ${account.balance}"); // I have $150
    }
}

一般的なガイドライン

オブジェクトのコードを変更したり新しい型を派生させたりすることによって機能を追加することが妥当かつ可能である場合は、そのようにすることがやはり推奨されますが、.NET エコシステムの全体で、拡張メソッドが再利用可能な機能を作成するための重要なオプションになってきています。 元のソースを制御できない場合、派生オブジェクトが不適切または不可能な場合、または該当するスコープを超えて機能を公開してはならない場合は、拡張メソッドが優れた選択肢になります。

派生型について詳しくは、「継承」をご覧ください。

ソース コードを制御できない型を、拡張メソッドを使用して拡張すると、型の実装の変更により拡張メソッドが破損するというリスクを負うことになります。

所定の型の拡張メソッドを実装する場合、次の点に注意してください。

  • 拡張メソッドが型で定義されているメソッドと同じシグネチャを持つ場合、その拡張メソッドは呼び出されません。
  • 拡張メソッドは名前空間レベルでスコープ内に取り込まれます。 たとえば、Extensions という名前の単一の名前空間に、拡張メソッドを含む複数の静的クラスがある場合、using Extensions; ディレクティブによって、それらのすべての拡張メソッドがスコープ内に取り込まれます。

実装したクラス ライブラリでは、アセンブリのバージョン番号のインクリメントを避けるために、拡張メソッドは使用しないでください。 ソース コードを所有するライブラリに重要な機能を追加する場合は、アセンブリのバージョン管理について、.NET ガイドラインに従う必要があります。 詳細については、「アセンブリのバージョン管理」を参照してください。

関連項目