拡張メソッドの解決
コンパイラーが拡張メソッドを見つける仕組みを理解します。
「拡張メソッドの解決」はCoddyKit上の無料C# Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはC# Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 C# Academyコースには全4レッスンが含まれています。
コンパイラーが拡張メソッドを見つける仕組み
value.Method() と記述したときに一致するインスタンスメソッドがない場合、コンパイラーは拡張メソッドを探します。ただし、using ディレクティブによって、その拡張メソッドを含む名前空間がスコープ内にある場合だけが対象です。
using ディレクティブで拡張メソッドをスコープに取り込む
拡張メソッドは、その名前空間をインポートした場合にのみ参照できます。適切な using がなければ、アセンブリにコンパイルされているにもかかわらず、メソッドが存在しないように見えます。
using System;
using MyHelpers; // brings StringExtensions into scope
namespace MyHelpers
{
public static class StringExtensions
{
public static string Reverse(this string s)
{
var arr = s.ToCharArray();
Array.Reverse(arr);
return new string(arr);
}
}
}
public class Program
{
public static void Main()
{
Console.WriteLine("hello".Reverse());
}
}インスタンスメソッドが最初に確認される
コンパイラーは段階的に呼び出しを解決します。まず、一致するインスタンスメソッド(継承されたものを含む)を探します。適合するものがない場合にのみ、拡張メソッドを探します。
using System;
using System.Collections.Generic;
public static class ListExt
{
// Ignored: List<int> already has Add(int)
public static void Add(this List<int> list, int a, int b)
=> list.Add(a + b);
}
public class Program
{
public static void Main()
{
var list = new List<int>();
list.Add(5); // the built-in instance Add wins for this signature
Console.WriteLine(list[0]);
}
}より近い名前空間が優先される
一致する拡張メソッドが 2 つある場合、名前空間の入れ子構造でより近い方が選ばれます。コンパイラーが解消できない曖昧さがあるとコンパイルエラーになり、呼び出しを明示的に区別する必要があります。
using System;
namespace Outer
{
public static class A { public static string Tag(this int n) => "A" + n; }
namespace Inner
{
public static class B { public static string Tag(this int n) => "B" + n; }
public class Program
{
public static void Main()
{
// B is in the nearer (Inner) namespace, so it is preferred
Console.WriteLine(7.Tag());
}
}
}
}静的呼び出しで区別する
拡張メソッドが競合する場合や明示的に呼び出したい場合は、完全なクラス名を使って静的メソッドを直接呼び出します。これにより、拡張メソッドの解決を完全に回避できます。
using System;
public static class FormatExt
{
public static string Bracket(this string s) => "[" + s + "]";
}
public class Program
{
public static void Main()
{
// Explicit static call, no ambiguity possible
Console.WriteLine(FormatExt.Bracket("x"));
}
}拡張メソッド間のオーバーロード解決
複数の拡張メソッドが同じ名前を持つ場合は、通常のオーバーロード解決により、通常のメソッドオーバーロードと同じようにパラメーター型に基づいて最適なものが選ばれます。
using System;
public static class PrintExt
{
public static string Describe(this int n) => "int:" + n;
public static string Describe(this string s) => "str:" + s;
}
public class Program
{
public static void Main()
{
Console.WriteLine(42.Describe());
Console.WriteLine("hi".Describe());
}
}解決時のジェネリック型推論
ジェネリック拡張メソッドでは、コンパイラーがレシーバーと引数から型引数を推論します。推論に失敗した場合、その拡張メソッドは候補になりません。
using System;
using System.Collections.Generic;
public static class SeqExt
{
public static T FirstItem<T>(this IEnumerable<T> source)
{
foreach (var item in source) return item;
throw new InvalidOperationException("empty");
}
}
public class Program
{
public static void Main()
{
var nums = new List<int> { 5, 6, 7 };
// T inferred as int from the List<int> receiver
Console.WriteLine(nums.FirstItem());
}
}global using と暗黙のインポート
最新の C# では、global using ディレクティブを宣言して、プロジェクト全体で名前空間をスコープ内に置くことができます。これにより、インポートを繰り返さなくても、共有の拡張メソッドライブラリをどこからでも利用できます。
using System;
// In a real project a separate file might say: global using MyHelpers;
namespace MyHelpers
{
public static class IntExt { public static int Twice(this int n) => n * 2; }
}
public class Program
{
public static void Main()
{
// Works here because MyHelpers is referenced explicitly
Console.WriteLine(MyHelpers.IntExt.Twice(8));
}
}レシーバーの型が重要になる場合
メソッドの解決は、レシーバーの実行時型ではなく静的型に基づきます。変数が派生クラスのインスタンスを保持していても、基底型に対して宣言された拡張メソッドが適用されます。ただし、より具体的な拡張メソッドがスコープ内にある場合は除きます。
using System;
public class Animal { }
public class Cat : Animal { }
public static class AnimalExt
{
public static string Kind(this Animal a) => "animal";
}
public class Program
{
public static void Main()
{
Animal a = new Cat();
// Chosen by static type Animal
Console.WriteLine(a.Kind());
}
}解決のメンタルモデル
解決の順序を次のように考えてください。(1) インスタンスメソッド、次に (2) インポートされた名前空間の拡張メソッド(近い名前空間から)、最後に (3) 候補となるメソッド間のオーバーロード解決です。この順序を知っていれば、ほとんどの意外な結果の理由を説明できます。
using System;
public static class Ext
{
public static string Wrap(this string s) => "<" + s + ">";
}
public class Program
{
public static void Main()
{
// No instance Wrap exists, so the extension is used
Console.WriteLine("node".Wrap());
}
}自分で試す
名前空間内に拡張メソッドを定義し、インポートして呼び出してみましょう。次に、同じメソッドを静的メソッドとして明示的に呼び出し、どちらの方法でも動作することを確認してください。
using System;
using Text.Helpers;
namespace Text.Helpers
{
public static class StringExtensions
{
public static int Vowels(this string s)
{
int count = 0;
foreach (var c in s.ToLower())
if ("aeiou".IndexOf(c) >= 0) count++;
return count;
}
}
}
public class Program
{
public static void Main()
{
Console.WriteLine("education".Vowels());
Console.WriteLine(Text.Helpers.StringExtensions.Vowels("education"));
}
}クイックチェック
拡張メソッドが参照可能になる条件を考えてみましょう。
まとめ
拡張メソッドの解決には明確な順序があります。
- 最初にインスタンスメソッドが試され、拡張メソッドは不足する機能を補う場合にのみ使われます。
- 拡張メソッドは、
usingまたはglobal usingによって名前空間がスコープ内にある場合にのみ参照できます。 - より近い名前空間が優先され、解消できない同順位はコンパイルエラーになります。
- 静的メソッドを明示的に呼び出して、いつでも曖昧さを解消できます。
- 解決にはレシーバーの静的型が使われます。
よくある質問
「拡張メソッドの解決」レッスンは無料ですか?
はい。「拡張メソッドの解決」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、C# Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 C# Academyコースには全4レッスンが含まれています。
「拡張メソッドの解決」で何を学びますか?
コンパイラーが拡張メソッドを見つける仕組みを理解します。 ブラウザで直接実行するハンズオンコードでC# Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
C# Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのC# Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「拡張メソッドの解決」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このC# Academyレッスンでコードを書いて実行できますか?
はい。すべてのC# Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- 拡張メソッドの定義
- インターフェイスとジェネリックの拡張
- 拡張メソッドの解決
- 適切な拡張の設計