この完全なC#プログラムは、問題を示しています。
public abstract class Executor<T>
{
public abstract void Execute(T item);
}
class StringExecutor : Executor<string>
{
public void Execute(object item)
{
// why does this method call back into itself instead of binding
// to the more specific "string" overload.
this.Execute((string)item);
}
public override void Execute(string item) { }
}
class Program
{
static void Main(string[] args)
{
object item = "value";
new StringExecutor()
// stack overflow
.Execute(item);
}
}
StackOverlowExceptionが発生し、この呼び出しパターンをたどって、より具体的なオーバーロードに呼び出しを転送しようとしました。驚いたことに、呼び出しはより具体的なオーバーロードを選択していませんでしたが、それ自体にコールバックしていました。それは明らかに基本型がジェネリックであることと関係がありますが、なぜExecute(string)オーバーロードを選択しないのかわかりません。
誰かこれについて何か洞察がありますか?
上記のコードはパターンを示すために簡略化されています。実際の構造はもう少し複雑ですが、問題は同じです。
これは、C#仕様5.0、7.5.3オーバーロード解決で言及されているように見えます。
オーバーロード解決は、C#内の次の異なるコンテキストで呼び出す関数メンバーを選択します。
- Invocation-expression(§7.6.5.1)で指定されたメソッドの呼び出し。
- Object-creation-expression(§7.6.10.1)で指定されたインスタンスコンストラクターの呼び出し。
- Element-accessによるインデクサーアクセサーの呼び出し(§7.6.6)。
- 式で参照される定義済みまたはユーザー定義の演算子の呼び出し(7.3.3および7.3.4)。
上記のセクションで詳細に説明されているように、これらの各コンテキストは、独自の方法で候補関数メンバーのセットと引数のリストを定義します。たとえば、メソッド呼び出しの候補のセットオーバーライドとマークされたメソッド(§7.4)は含まれず、基本クラスのメソッドは、派生クラスのメソッドが該当する場合は候補になりません(§7.6 .5.1)。
7.4を見ると:
型TのK型パラメーターを持つ名前Nのメンバー検索は、次のように処理されます。
•最初に、Nという名前のアクセス可能なメンバーのセットが決定されます。
Tが型パラメーターの場合、セットは次のセットの和集合です。
Tのプライマリ制約またはセカンダリ制約(§10.1.5)として指定された各タイプでNという名前のアクセス可能なメンバーと、オブジェクト内のNという名前のアクセス可能なメンバーのセット。それ以外の場合、セットは、継承されたメンバーとオブジェクト内のNという名前のアクセス可能なメンバーを含む、T in Nという名前のすべてのアクセス可能な(§3.5)メンバーで構成されます。 Tが構成型の場合、メンバーのセットは、§10.3.2で説明されているように型引数を置き換えることによって取得されます。 オーバーライド修飾子を含むメンバーはセットから除外されます。
override
を削除すると、アイテムをキャストするときにコンパイラーがExecute(string)
オーバーロードを選択します。
Jon Skeetの article on overloading で述べたように、基本クラスから同じ名前のメソッドもオーバーライドするクラスのメソッドを呼び出すと、コンパイラーは常にクラスではなくメソッド内メソッドを取得します署名が「互換性がある」場合、タイプの「特定性」に関係なく、オーバーライドします。
Jonはさらに、これは継承の境界を越えたオーバーロードを回避するための優れた引数であることを指摘しています。
他の回答が指摘しているように、これは設計によるものです。
それほど複雑ではない例を考えてみましょう:
_class Animal
{
public virtual void Eat(Apple a) { ... }
}
class Giraffe : Animal
{
public void Eat(Food f) { ... }
public override void Eat(Apple a) { ... }
}
_
問題は、giraffe.Eat(Apple)
が仮想Giraffe.Eat(Food)
ではなくAnimal.Eat(Apple)
に解決される理由です。
これは2つのルールの結果です。
(1)オーバーロードを解決する場合、レシーバーのタイプは、引数のタイプよりも重要です。
これが事実である理由が明確であることを願っています。派生クラスを作成する人は基本クラスを作成する人よりも厳密に多くの知識を持っています。これは、派生クラスを作成する人が基本クラスを使用し、その逆は行われないためです。
Giraffe
を書いた人は、「私はGiraffe
が任意の食べ物を食べる方法があります」と言っています。キリン消化の内部に関する特別な知識。その情報は基本クラスの実装には存在しません。基本クラスの実装はリンゴの食べ方しか知りません。
したがって、オーバーロードの解決では、基本クラスのメソッドを選択するよりも、派生クラスの適切なメソッドを選択することを常に優先する必要があります。引数の型変換の精度に関係なく
(2)仮想メソッドをオーバーライドするかどうかを選択することは、クラスのパブリックサーフェス領域の一部ではありません。これはプライベートな実装の詳細です。したがって、メソッドがオーバーライドされているかどうかに応じて変化するオーバーロード解決を実行するときに、決定を行う必要はありません。
オーバーロードの解決で、「オーバーライドされたため、仮想Animal.Eat(Apple)
を選択するつもりです」と表示されてはなりません。
さて、あなたは「OK、私が電話をかけるとき、insideキリンだとしましょう」と言うかもしれません。コードinsideキリンはプライベートな実装の詳細についてすべての知識を持っていますよね?したがって、Animal.Eat(Apple)
に直面したときに、Giraffe.Eat(Food)
ではなくvirtual giraffe.Eat(Apple)
を呼び出すことを決定できます。リンゴを食べるキリンの必要性を理解する実装があることを知っているからです。
それは病気よりも悪い治療法です。 identicalコードがどこに依存してdifferent動作をするかという状況になりました走る!クラスの外でgiraffe.Eat(Apple)
を呼び出し、それがクラスの内部になるようにリファクタリングすると、突然観察可能な動作が変化することを想像できます。
または、あなたは言うかもしれませんが、私のキリンのロジックは実際には基本クラスに移動するのに十分一般的ですが、アニマルには移動しないので、Giraffe
コードを次のようにリファクタリングします。
_class Mammal : Animal
{
public void Eat(Food f) { ... }
public override void Eat(Apple a) { ... }
}
class Giraffe : Mammal
{
...
}
_
そして今、giraffe.Eat(Apple)
へのすべての呼び出しinsideGiraffe
が突然differentリファクタリング後のオーバーロード解決動作?それは非常に予想外です!
C#は成功の秘訣言語です。階層内でメソッドがオーバーライドされる場所を変更するような単純なリファクタリングが動作に微妙な変化を引き起こさないことを確認したいのです。
まとめ:
関連する問題に関する追加の考えはここにあります: https://ericlippert.com/2013/12/23/closer-is-better/ そしてここ https://blogs.msdn .Microsoft.com/ericlippert/2007/09/04/future-breaking-changes-part-three /