次の方法で共有


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

拡張メンバーを使用すると、新しい派生型を作成したり、再コンパイルしたり、元の型を変更したりせずに、既存の型にメソッドを "追加" できます。

C# 14 以降では、拡張メソッドの定義に使用する構文が 2 つあります。 C# 14 では、 extension コンテナーが追加されます。ここでは、型または型のインスタンスに対して複数の拡張メンバーを定義します。 C# 14 より前のバージョンでは、静的メソッドの最初のパラメーターに this 修飾子を追加して、メソッドがパラメーター型のインスタンスのメンバーとして表示されることを示します。

拡張メソッドは静的メソッドですが、拡張型のインスタンス メソッドであるかのように呼び出されます。 C#、F#、および Visual Basic で記述されたクライアント コードの場合、拡張メソッドの呼び出しと、型で定義されているメソッドの呼び出しに明らかな違いはありません。 どちらの形式の拡張メソッドも、同じ IL (中間言語) にコンパイルされます。 拡張メンバーのコンシューマーは、拡張メソッドの定義に使用された構文を知る必要はありません。

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

OrderBy の例

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

int[] numbers = [10, 45, 15, 39, 21, 26];
IOrderedEnumerable<int> result = numbers.OrderBy(g => g);
foreach (int i in result)
{
    Console.Write(i + " ");
}
//Output: 10 15 21 26 39 45

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

拡張メンバーを宣言する

C# 14 以降では、 拡張ブロックを宣言できます。 拡張ブロックは、型またはその型のインスタンスの拡張メンバーを含む、入れ子になっていない非ジェネリックの静的クラスのブロックです。 次のコード例では、 string 型の拡張ブロックを定義します。 拡張ブロックには、文字列内の単語をカウントするメソッドという 1 つのメンバーが含まれています。

namespace CustomExtensionMembers;

public static class MyExtensions
{
    extension(string str)
    {
        public int WordCount() =>
            str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

C# 14 より前のバージョンでは、最初のパラメーターに this 修飾子を追加して拡張メソッドを宣言しました。

namespace CustomExtensionMethods;

public static class MyExtensions
{
    public static int WordCount(this string str) =>
        str.Split([' ', '.', '?'], StringSplitOptions.RemoveEmptyEntries).Length;
}

どちらの形式の拡張も、入れ子になっていない非ジェネリック静的クラス内で定義する必要があります。

また、インスタンス メンバーにアクセスするための構文を使用して、アプリケーションから呼び出すことができます。

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

拡張メンバーは既存の型に新しい機能を追加しますが、拡張メンバーはカプセル化の原則に違反しません。 拡張型のすべてのメンバーのアクセス宣言は、拡張メンバーに適用されます。

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

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

上記の C# コードは、拡張メンバーの拡張ブロックと this 構文の両方に適用されます。 前述のコード:

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

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

一般に、拡張メンバーは実装するよりもはるかに頻繁に呼び出します。 拡張メンバーは拡張クラスのメンバーとして宣言されているかのように呼び出されるため、クライアント コードから使用するために特別な知識は必要ありません。 特定の型の拡張メンバーを有効にするには、メソッドが定義されている名前空間の using ディレクティブを追加するだけです。 たとえば、標準のクエリ演算子を使用するには、次の using ディレクティブをコードに追加します。

using System.Linq;

コンパイル時に拡張メンバーをバインドする

拡張メンバーを使用してクラスまたはインターフェイスを拡張できますが、クラスで定義されている動作をオーバーライドすることはできません。 インターフェイスまたはクラス メンバーと同じ名前とシグネチャを持つ拡張メンバーは呼び出されません。 コンパイル時に、拡張メンバーは常に、型自体で定義されているインスタンス (または静的) メンバーよりも優先順位が低くなります。 つまり、型に Process(int i) という名前のメソッドがあり、同じシグネチャを持つ拡張メソッドがある場合、コンパイラは常にメンバー メソッドにバインドします。 コンパイラは、メンバー呼び出しを検出すると、最初に型のメンバー内の一致を検索します。 一致するものが見つからない場合は、型に対して定義されている拡張メンバーを検索します。 見つけた最初の拡張メンバーにバインドされます。 次の例は、型のインスタンス メンバーにバインドするか、拡張メンバーにバインドするかを決定する際に C# コンパイラが従う規則を示しています。 静的クラス Extensions には、 IMyInterfaceを実装するすべての型に対して定義された拡張メンバーが含まれています。

public interface IMyInterface
{
    void MethodB();
}

// Define extension methods for IMyInterface.

// 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)");
}

同等の拡張機能は、C# 14 拡張メンバー構文を使用して宣言できます。

public static class Extension
{
    extension(IMyInterface myInterface)
    {
        public void MethodA(int i) =>
            Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, int i)");

        public void MethodA(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 void MethodB() =>
            Console.WriteLine("Extension.MethodB(this IMyInterface myInterface)");
    }
}

クラス AB、および C はすべてインターフェイスを実装します。

// Define three classes that implement IMyInterface, and then use them to test
// the extension methods.
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)");
    }
}

MethodB拡張メソッドは、その名前とシグネチャがクラスによって既に実装されているメソッドと完全に一致するため、呼び出されません。 一致するシグネチャを持つインスタンス メソッドが見つからない場合、コンパイラは、一致する拡張メソッドが存在する場合は、それにバインドします。

// 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.Arrayを実装するSystem.Collections.Generic.List<T>System.Collections.Generic.IEnumerable<T>などの任意のコレクションから機能を呼び出せるようにする利点があります。 Int32 の配列を使用する例については、 この記事の前半で説明します。

Layer-Specific 機能

Onion アーキテクチャまたはその他の階層化されたアプリケーション設計を使用する場合、アプリケーションの境界を越えて通信するために使用できる一連のドメイン エンティティまたはデータ転送オブジェクトを使用するのが一般的です。 通常、これらのオブジェクトには機能が含まれることはありません。また、アプリケーションのすべてのレイヤーに適用される最小限の機能のみが含まれます。 拡張メソッドを使用して、各アプリケーション レイヤーに固有の機能を追加できます。

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

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

C# 14 以降では、新しい拡張ブロック構文を使用して、同等の FullName プロパティを宣言できます。

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

定義済み型の拡張

再利用可能な機能を作成する必要があるときに新しいオブジェクトを作成するのではなく、多くの場合、.NET や CLR 型などの既存の型を拡張できます。 たとえば、拡張メソッドを使用しない場合は、 Engine または Query クラスを作成して、コード内の複数の場所から呼び出される可能性がある SQL Server でクエリを実行する処理を実行できます。 ただし、代わりに拡張メソッドを使用して System.Data.SqlClient.SqlConnection クラスを拡張し、SQL Server に接続している任意の場所からそのクエリを実行できます。 その他の例として、 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 IntExtensions
{
    extension(int number)
    {
        public void Increment()
            => number++;
    }

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

受信側の値によるパラメーター モードと ref によるパラメーター モードを区別するには、異なる拡張ブロックが必要です。

受信側に ref を適用する場合の違いは、次の例で確認できます。

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
    }
}

上記のサンプルは、C# 14 の拡張ブロックを使用して作成することもできます。

public static class AccountExtensions
{
    extension(ref Account account)
    {
        // ref keyword can also appear before the this keyword
        public void Deposit(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
        }
    }
}

これらの拡張メソッドには、次のようにアクセスできます。

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という名前の 1 つの名前空間に拡張メソッドを含む複数の静的クラスがある場合、それらのすべてが using Extensions; ディレクティブによってスコープに取り込まれます。

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

こちらも参照ください