web-dev-qa-db-ja.com

C#メソッドによる解決の奇妙さのオーバーライド

次のコードスニペットを検討してください。

_using System;

class Base
{
    public virtual void Foo(int x)
    {
        Console.WriteLine("Base.Foo(int)");
    }
}

class Derived : Base
{
    public override void Foo(int x)
    {
        Console.WriteLine("Derived.Foo(int)");
    }

    public void Foo(object o)
    {
        Console.WriteLine("Derived.Foo(object)");
    }
}

public class Program
{
    public static void Main()
    {
        Derived d = new Derived();
        int i = 10;
        d.Foo(i);
    }
}
_

そして、驚くべき出力は次のとおりです。

_Derived.Foo(object)
_

より具体的であるため、オーバーライドされたFoo(int x)メソッドを選択することを期待しています。ただし、C#コンパイラは、継承されていないFoo(object o)バージョンを選択します。これにより、ボクシング操作も発生します。

この動作の理由は何ですか?

25
Impworks

これがルールであり、あなたはそれを好きではないかもしれません...

引用元 Eric Lippert

より派生したクラスのいずれかのメソッドが適用可能な候補である場合、より派生していないメソッドのシグネチャマッチが優れていても、派生の少ないクラスのメソッドよりも自動的に優れています。

その理由は、メソッド(つまり、より優れたシグネチャの一致)が後のバージョンで追加され、それによって " brittle base class "エラーが発生する可能性があるためです。


:これはC#仕様のかなり複雑で詳細な部分であり、すべてジャンプします場所の上。ただし、発生している問題の主な部分は次のように書かれています

更新

そして、これが私がstackoverflowを好きな理由です。

メソッド呼び出しのランタイム処理に関するセクションを引用していました。 コンパイル時のオーバーロードの解決についての質問があるので、そうすべきです。

7.6.5.1メソッド呼び出し

...

候補メソッドのセットは、最も派生したタイプのメソッドのみを含むように縮小されます。セット内の各メソッドCFについて、CはメソッドFが宣言されているタイプ、ベースで宣言されているすべてのメソッドCのタイプはセットから削除されます。さらに、Cがオブジェクト以外のクラス型である場合、インターフェイス型で宣言されたすべてのメソッドがセットから削除されます。 (後者のルールは、メソッドグループが、オブジェクト以外の有効な基本クラスと空でない有効なインターフェイスセットを持つ型パラメーターのメンバールックアップの結果である場合にのみ影響します。)

エリックの投稿の回答 https://stackoverflow.com/a/52670391/1612975 を参照して、ここで行われていることの詳細と仕様の適切な部分を確認してください。

元の

C#言語仕様バージョン5.0

7.5.5関数メンバーの呼び出し

...

関数メンバー呼び出しの実行時処理は、次のステップで構成されます。ここで、Mは関数メンバーであり、Mがインスタンスメンバーの場合、Eはインスタンス式です。

...

Mが参照型で宣言されたインスタンス関数メンバーの場合:

  • Eが評価されます。この評価により例外が発生した場合、それ以上のステップは実行されません。
  • 引数リストは、§7.5.1の説明に従って評価されます。
  • Eの型が値型の場合、Eを型オブジェクトに変換するためにボックス化変換(§4.3.1)が実行され、Eは次の手順で型オブジェクトであると見なされます。この場合、MはSystem.Objectのメンバーのみになることができます。
  • Eの値が有効であることを確認します。 Eの値がnullの場合、System.NullReferenceExceptionがスローされ、それ以上のステップは実行されません。
  • 呼び出す関数メンバーの実装が決定されます:
    • Eのバインディング時の型がインターフェイスの場合、呼び出す関数メンバーは、E によって参照されるインスタンスのランタイム型によって提供されるMの実装です。この関数メンバーは、インターフェースマッピングルール(§13.4.4)を適用して、Eによって参照されるインスタンスのランタイム型によって提供されるMの実装を決定することによって決定されます。
    • それ以外の場合、Mが仮想関数メンバーの場合、呼び出す関数メンバーは、Eによって参照されるインスタンスのランタイム型によって提供されるMの実装です。この関数メンバーは、 E.によって参照されるインスタンスの実行時の型に関して、Mの最も派生した実装(§10.6.3)を決定する
    • それ以外の場合、Mは非仮想関数メンバーであり、呼び出す関数メンバーはM自体です。

興味深いのは仕様を読んだ後、メソッドを説明するインターフェースを使用すると、コンパイラーはオーバーロード署名を選択し、期待どおりに機能します。

  public interface ITest
  {
     void Foo(int x);
  }

ここに表示できるもの

インターフェースに関しては、過負荷動作がBrittle基本クラスから保護するために実装されていることを考えると意味があります。


その他のリソース

Eric Lippert、近い方が良い

本日お話ししたいC#のオーバーロード解決の側面は、特定の呼び出しサイトで潜在的なオーバーロードが別のオーバーロードよりも優れていると判断される基本的なルールです。近いほど常に遠いほど優れています。 C#で「近さ」を特徴付ける方法はいくつかあります。最も近いものから始めて、外に出ましょう。

  • 派生クラスで最初に宣言されたメソッドは、基本クラスで最初に宣言されたメソッドよりも近くなっています。
  • ネストされたクラスのメソッドは、包含クラスのメソッドよりも近くなります。
  • 受信タイプのメソッドは、どの拡張メソッドよりも近くなっています。
  • ネストされた名前空間のクラスにある拡張メソッドは、外部の名前空間のクラスにある拡張メソッドよりも近くなっています。
  • 現在の名前空間のクラスにある拡張メソッドは、usingディレクティブで指定された名前空間のクラスにある拡張メソッドよりも近くなっています。
  • ディレクティブがネストされた名前空間にあるusingディレクティブで言及されている名前空間のクラスにある拡張メソッドは、ディレクティブが外部の名前空間にあるusingディレクティブで言及されている名前空間のクラスにある拡張メソッドよりも近くなっています。
28
Michael Randall