12

C#で次のサンプルコードがあるとしましょう:

class BaseClass
  {
    public virtual void HelloWorld()
    {
      Console.WriteLine("Hello Tarik");
    }
  }

  class DerivedClass : BaseClass
  {
    public override void HelloWorld()
    {
      base.HelloWorld();
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      DerivedClass derived = new DerivedClass();
      derived.HelloWorld();
    }
  }

次のコードをildasmedしたとき:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       15 (0xf)
  .maxstack  1
  .locals init ([0] class EnumReflection.DerivedClass derived)
  IL_0000:  nop
  IL_0001:  newobj     instance void EnumReflection.DerivedClass::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  callvirt   instance void EnumReflection.BaseClass::HelloWorld()
  IL_000d:  nop
  IL_000e:  ret
} // end of method Program::Main

ただし、csc.exeは変換されましたderived.HelloWorld();-> callvirt instance void EnumReflection.BaseClass::HelloWorld()。何故ですか?メソッドのどこにもBaseClassについては触れませんでしたMain

また、それが呼び出している場合は、メソッドを直接呼び出しているように見えるので、代わりにBaseClass::HelloWorld()期待します。callcallvirtBaseClass::HelloWorld()

4

3 に答える 3

20

BaseClassはメソッドを定義するクラスであるため、呼び出しはBaseClass::HelloWorldに送られます。C#で仮想ディスパッチが機能する方法は、メソッドが基本クラスで呼び出されることです。仮想ディスパッチシステムは、メソッドの最も派生したオーバーライドが呼び出されるようにする責任があります。

Eric Lippertのこの回答は非常に有益です:https ://stackoverflow.com/a/5308369/385844

このトピックに関する彼のブログシリーズも同様です:http://blogs.msdn.com/b/ericlippert/archive/tags/virtual+dispatch/

なぜこれがこのように実装されているのか分かりますか?派生クラスのToStringメソッドを直接呼び出した場合はどうなりますか?このように、私にはこれを一見あまり感じませんでした...

コンパイラはオブジェクトの実行時型を追跡せず、それらの参照のコンパイル時型のみを追跡するため、このように実装されます。投稿したコードを使用すると、呼び出しがメソッドのDerivedClass実装に送られることが簡単にわかります。しかし、derived変数が次のように初期化されたとします。

Derived derived = GetDerived();

GetDerived()のインスタンスを返す可能性がありますStillMoreDerivedStillMoreDerived(または継承チェーン間Derivedおよび継承チェーン内の任意のクラス)がメソッドをオーバーライドする場合、メソッドの実装StillMoreDerivedを呼び出すことは正しくありません。Derived

静的分析を通じて変数が保持できる可能性のあるすべての値を見つけることは、停止性問題を解決することです。.NETアセンブリでは、アセンブリが完全なプログラムではない可能性があるため、問題はさらに悪化します。したがって、コンパイラがderived、より派生したオブジェクトへの参照(またはnull参照)を保持していないことを合理的に証明できるケースの数は少なくなります。

命令callではなく発行できるように、このロジックを追加するのにどれくらいの費用がかかりますか?callvirt間違いなく、コストは得られる小さな利益よりもはるかに高くなります。

于 2012-04-18T23:08:39.853 に答える
9

これについて考える方法は、仮想メソッドが実行時にメソッドを配置できる「スロット」を定義することです。callvirt命令を発行するとき、「実行時に、このスロットに何があるかを確認して呼び出します」と言っています。

スロットは、仮想メソッドをオーバーライドするタイプではなく、仮想メソッドを宣言したタイプに関するメソッド情報によって識別されます。

派生メソッドにcallvirtを発行することは完全に合法です。ランタイムは、派生メソッドが基本メソッドと同じスロットであり、結果がまったく同じであることを認識します。しかし、それを行う理由はありません。そのスロットを宣言するタイプを識別することによってスロットを識別する場合、より明確になります。

于 2012-04-19T04:34:42.557 に答える
1

DerivedClassこれは、として宣言した場合でも発生することに注意してくださいsealed

C#は、callvirt演算子を使用して任意のインスタンスメソッド(virtualまたはそうでない)を呼び出し、オブジェクト参照のnullチェックを自動的に取得します-NullReferenceExceptionメソッドが呼び出された時点でを発生させます。それ以外の場合、NullReferenceExceptionメソッド内のクラスのインスタンスメンバーを最初に実際に使用したときにのみ、が発生します。これは驚くべきことです。インスタンスメンバーが使用されていない場合、メソッドは例外を発生させることなく実際に正常に完了する可能性があります。

ILは直接実行されないことも覚えておく必要があります。これは、最初にJITコンパイラーによってネイティブ命令にコンパイルされます。これは、プロセスをデバッグしているかどうかに応じて、いくつかの最適化を実行します。x86 JIT for CLR 2.0は非仮想メソッドをインライン化したが、仮想メソッドと呼ばれていることがわかりました。これもインライン化されていConsole.WriteLineます。

于 2013-04-09T22:38:20.090 に答える