注:もしあなたが初心者で、インスタンス・メソッドや仮想メソッドの呼び出しについてよくわかっていないのであれば、この記事を読まないことを強くお勧めします。
インスタンス・メソッドや仮想メソッドの動作メカニズム、そしてメソッドやオブジェクトのメモリ・レイアウトについてすでにご存知の方は、この記事を読むと、これまでの知識がひっくり返るかもしれません。
この記事を読むための****方法は、自分自身でそれを通り抜けることです。もしあなたがそれを読んで混乱したなら、それは普通のことです。
文字列変数はオブジェクトを直接参照できると言いました!
派生型の変数は基底型のオブジェクトを直接参照できると言いました!
冗談じゃない、派生型が基底型のオブジェクトを指すなんてありえない、とおっしゃるでしょう!
その奇跡を目の当たりにしていただき、記事の最後にもうひとつ、さらに驚くべき例を挙げましょう。
まず、以下のコードを見てください:
class Program {
static void Main(string[] args) {
Derived d=(Derived)new Base();
d.Print();
Console.Read();
}
}
class Base {
public void Print() {
Console.Write("in base");
}
}
class Derived : Base {
public new void Print() {
Console.WriteLine("in derived");
}
}
BaseオブジェクトをDerivedオブジェクトに変換できないため、実行時に例外がスローされることは間違いありません。
しかし今、dがBaseオブジェクトを指し、BaseのPrintメソッドを呼び出せるようにしたいのですが、どうすればいいでしょうか?
これはFiledOffsetで可能ですが、まずManagerというクラスを定義する必要があります。このクラスには2つのインスタンス・フィールドがあり、1つはDerived用、もう1つはBase用です:
[StructLayout(LayoutKind.Explicit)]
class Manager {
[FieldOffset(0)]
public Base b = new Base();
[FieldOffset(0)]
public Derived derived;
}
ここで、bとderivedの両方に同じオフセットを指定することで、bとderivedの両方が同じオブジェクト、Baseオブジェクトを指すようになります。
derivedはBaseオブジェクトを指すようになったので、d.Printメソッドを呼び出した場合、呼び出されるのはBaseのPrintfメソッドなのか、それともderivedのPrintメソッドなのか、それとも例外がスローされるのでしょうか。以下のコードをご覧ください:
class Program {
static void Main(string[] args) {
Manager m = new Manager();
m.derived.Print();
Console.Read();
}
}
上記のコードを実行すると、どのような出力が得られますか?
答えは "In Derived "です。
なぜなら、derivedはBaseオブジェクトを指しており、その呼び出しは確かにDerivedのメソッドを指しているからです。その理由を理解するために、下のイメージを見てください:
ここでは、derivedがBaseオブジェクトを指しているにもかかわらず、CLRはPrintが非仮想メソッドであるため、派生変数がどのオブジェクトを指しているかを気にしません。ここでは、derived は Derived 型なので、CLR は Print in Derived を呼び出し、最後に In Derived を出力します。
二つ目の例:
次の例も素晴らしく、あなたの従来の考えを覆すでしょう。
上のprintメソッドを仮想メソッドに変更してみましょう。
[StructLayout(LayoutKind.Explicit)]
class Manager {
[FieldOffset(0)]
public Base b = new Base();
[FieldOffset(0)]
public Derived derived;
}
class Base {
public virtual void Print() {
Console.Write("in base");
}
}
class Derived : Base {
public override void Print() {
Console.WriteLine("in derived");
}
}
では、以下のテストコードを実行してください:
class Program {
static void Main(string[] args) {
Manager m = new Manager();
m.derived.Print();
Console.Read();
}
}
今回の結果はどうなるでしょうか?その答えを自分で考えてみることを強くお勧めします。
その結果が『イン・ベース』です!
信じられないでしょう?その理由をより明確に理解するために、下の写真を見てください:
ここでは、derivedがBaseオブジェクトを指していても、CLRがderived.Printのコード行を見たとき、Printはダミーメソッドなので、CLRはderivedが指すBaseオブジェクトを見ます。
概要
要約がないのは良くないですね。
基本的に、サブタイプは親タイプのオブジェクトを参照できません。しかし、この制限はFieldOffsetによって回避することができます。サブタイプの変数を通して親オブジェクトのメソッドを呼び出すことは非常に信じられないことですが、サブタイプの変数が親オブジェクトを指しているときに子メソッドを呼び出すことはさらに信じられないことです!
では、上記の本質は何でしょうか?CLR が非仮想メソッドを呼び出すとき、CLR は変数の型によってメソッドを呼び出しているので、変数が何を指しているかは気にしません。もしメソッドが仮想メソッドであれば、CLRはポリモーフィズムを実現するために、変数がどのオブジェクトを指しているかを確認し、オブジェクトの型オブジェクトポインタから対応する型オブジェクトを見つけ、その型オブジェクトのメソッドを呼び出す必要があります。