Menu

C#の拡張メソッド:構文、IEnumerable、LINQ、ルール

C#の拡張メソッドの仕組みを解説します。最初の引数にthisを付けた静的クラスの静的メソッド、インスタンスメソッドのような呼び出し、string、enum、IEnumerable<T>の拡張、拡張メソッドで作られたLINQ、インスタンスメソッドが優先される理由、nullのレシーバー、そして必要なusingディレクティブを扱います。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

拡張メソッドは、既存の型を変更したり、継承したり、ラップしたりせずに、その型にメソッドを追加します。通常の静的メソッドを書いて最初の引数に this を付けると、呼び出し側は、その型が最初からそのメソッドを持っていたかのようにドット構文で使えます。

出力:

Extension methods let yo...
12
short
Extension...

title.Truncate(24) は、まさに StringExtensions.Truncate(title, 24) にコンパイルされます。ドット構文は便宜上のものにすぎません。メソッドは静的なままで、StringExtensions の中にあり、string の公開メンバーしか使えません。

宣言のルール

  • メソッドは static で、static で非ジェネリックで他のクラスに入れ子になっていないクラスの中になければなりません。非静的またはジェネリックなクラスではCS1106 Extension method must be defined in a non-generic static class、入れ子のクラスではCS1109 Extension methods must be defined in a top level static class になります。
  • this は最初の引数にだけ付け、その引数の型が拡張される型です。それ以降の引数は通常どおりです。
  • クラスはジェネリックにできませんが、メソッド自体はジェネリックにできます(this IEnumerable<T> source)。
  • 見えるのは外部のコードから見えるものだけで、public(同じアセンブリ内ならinternal)のメンバーです。privateなフィールドはprivateのままです。

慣例として、クラスには拡張する対象にちなんだ名前を付け(StringExtensions、EnumerableExtensions)、MyApp.Extensions のような名前空間にまとめます。

インターフェースとIEnumerable<T>の拡張

インターフェースを拡張すると、それを実装するすべての型にメソッドが追加されます。したがって IEnumerable<T> を拡張すると、配列、リスト、セット、辞書のキーのコレクション、LINQクエリの結果に一度にメソッドが加わります。

出力:

9.5
50
65
f0 f3 f6

EveryNth はジェネリックです。T はレシーバーから推論されるので、ここでは文字列に対して動き、他の任意の要素型にも使えます。yield return を使っているので、組み込みのLINQ演算子と同じく遅延評価され、それらと連結できます。

LINQは拡張メソッドで作られている

Where、Select、OrderBy、Sum、First などのLINQは、静的クラス System.Linq.Enumerable に定義された IEnumerable<T> の拡張メソッドです。だから list.Where(...) をコンパイルするにはファイルの先頭に using System.Linq; が必要で、どのコレクション型も Where メソッドを実装していないのにLINQが使えるのです。

出力:

72, 88, 95
72, 88, 95

2行は同じコードです。拡張の構文は、内側から外側へ読む入れ子の呼び出しを、処理が起きる順に読める連鎖に変えます。これが拡張メソッドが存在する主な理由で、C# 3でLINQとともに追加されました。演算子についてはLINQのページで扱います。

名前空間のインポートが必要

拡張メソッドがスコープに入るのは、その静的クラスの名前空間が using でインポートされているときだけです。インポートがないと、メソッドが存在しないかのように呼び出しが失敗します。

error CS1061: 'string' does not contain a definition for 'Truncate' and no accessible extension method 'Truncate' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)

拡張の名前空間の using を追加します(C# 10以降なら、プロジェクトに1回だけ global using を書く方法もあります)。

出力:

EUR 49.90

裏を返すと便利なこともあります。どこでも使えるようにはしたくない拡張メソッドを、一部のファイルだけがインポートする名前空間に置けます。

インスタンスメソッドが優先される

型がすでに適用可能なインスタンスメソッドを持っていると、コンパイラはそれを使い、拡張は決して検討しません。同じシグネチャの拡張は、ドット構文を使うすべての呼び出し箇所で死んだコードになります。

出力:

Invoice (instance method): 120
Invoice (extension): 120 USD
extension: only reachable as a static call

引数1つの拡張はインスタンスメソッドに負けました。引数2つのほうは、文字列を受け取るインスタンスメソッドがないので使われました。つまり、ライブラリの更新で自分の拡張と同じ名前のインスタンスメソッドが追加されると、呼び出しが何も言わずにライブラリ側に切り替わります。衝突しにくい拡張の名前を選びます。

nullに対する呼び出し

呼び出しは実際には静的な呼び出しなので、null のレシーバーでも呼び出し箇所で例外は投げられません。メソッドは最初の引数として null を受け取り、それが何を意味するかを決めます。

出力:

True
AL
ArgumentNullException: fullName

missing.IsBlank() が true を返すのは便利ですが、null に対するメンバー呼び出しは例外になると考えている読み手を驚かせるかもしれません。null を受け入れるのは、名前からそれが明らかなメソッド(IsBlank、OrEmpty)だけにし、それ以外ではLINQと同じく ArgumentNullException を投げます。

enumの拡張

enumは独自のメソッドを持てないので、拡張の自然な対象です。

enum OrderStatus { Pending, Paid, Shipped, Delivered, Cancelled }

static class OrderStatusExtensions
{
    public static bool IsFinal(this OrderStatus s) =>
        s == OrderStatus.Delivered || s == OrderStatus.Cancelled;
}

// usage: if (order.Status.IsFinal()) { ... }

C# 14の拡張メンバー

C# 14までは、拡張にできるのはメソッドだけでした。C# 14では extension ブロックが追加され、1つのレシーバーに対するメンバーをまとめられ、拡張プロパティや静的メンバーも書けるようになりました。

// C# 14 and later
public static class StringExtensions
{
    extension(string s)
    {
        public bool IsBlank => string.IsNullOrWhiteSpace(s);   // extension property
        public string Truncate(int max) => s.Length <= max ? s : s[..max] + "...";
    }
}

従来の this 引数による拡張メソッドは引き続き有効で、既存のコードやライブラリのほとんどすべてがこちらを使っています。

拡張メソッドを書くべきとき

拡張メソッドが向いているのは、型を所有していないとき(string、DateTime、フレームワークのインターフェース)、インターフェースのすべての実装で同じヘルパーを使いたいとき、入れ子の呼び出しより連鎖のほうが読みやすいときです。クラスを所有しているなら、代わりに本物のインスタンスメソッドを追加します。privateな状態を使え、読み手が探す場所に現れるからです。また、object の拡張は避けます。プログラム内のあらゆる値の補完リストに自分のメソッドが出てしまいます。

よくある質問

C#の拡張メソッドとは何ですか?

別の型のインスタンスメソッドであるかのように呼び出せる静的メソッドです。静的クラスの中に書き、最初の引数の前に this を付けます:public static bool IsBlank(this string s) => string.IsNullOrWhiteSpace(s);。すると、string を変更したり継承したりせずに、どの文字列でも name.IsBlank() が使えます。

拡張メソッドが見つからないのはなぜですか?

拡張メソッドは、その名前空間がインポートされているときにしか見えません。静的クラスが MyApp.Extensions にあるなら、呼び出すファイルに using MyApp.Extensions; を追加します。そうしないと、コンパイラは型にその名前の定義もアクセス可能な拡張メソッドもないというCS1061を報告します。クラスが static で、ジェネリックでなく、入れ子でないことも確認します。

拡張メソッドでインスタンスメソッドをオーバーライドできますか?

できません。コンパイラはまず適用可能なインスタンスメソッドを探し、それがない場合にだけ拡張メソッドを検討します。インスタンスメソッドと同じ名前と引数を持つ拡張は、インスタンスの構文を使うすべての呼び出し箇所で何も言わずに無視されます。

C#でnullに対して拡張メソッドを呼び出せますか?

呼び出せます。呼び出しは最初の引数を null とする静的メソッドの呼び出しにコンパイルされるので、呼び出しそのものでは NullReferenceException は投げられません。どうするかはメソッドが決めます。(IsNullOrEmpty のようなヘルパーのように)nullを有効な入力として扱うか、LINQのメソッドのように ArgumentNullException を投げます。

C#に拡張プロパティはありますか?

C# 14より前にはありません。それまでは拡張にできるのはメソッドだけなので、プロパティにしたいものはメソッドとして書きます(GetFullName())。C# 14では拡張ブロックが追加され、メソッドに加えて拡張プロパティや静的メンバーも宣言できるようになりました。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める