ブログ「サイバー少年」

ブログ「サイバー少年」へようこそ!
小学六年生ごろからプログラミングを趣味にしている高校生のブログです。
勉強したことについての記事などを書いています。フリーソフトも制作、公開しています。
(当ブログについて詳しくは「ブログ概要紹介」を参照)

サイバー少年が作ったフリーソフトは「サイバー少年の作品展示場」へ

[C#] 継承の仕組みと仮想メソッドテーブル

私、Jimmyさんという方(昔コメントを頂いたことがあります)がやっているブログを見ていまして、

型の疑問[Java]#スーパー型にサブ型を代入した時、スーバー型がどのようにサブ型のメンバを管理しているのか分からない - Newt Net(ひよっこプログラマー日記)
http://newtgecko.blog.fc2.com/blog-entry-213.html

という記事にコメントしたんですよ。

ただ、コメントを書いてあるあいだに自分でもよくわからなくなってきて、矛盾していることを書いたりしてしまったので、こちらでまとめようと思います。


テーマは継承についてです。

継承というのはどういう仕組みで実現されているのかを書いていきます。

また、仮想メソッドテーブルという物についても書きます。


C#前提で話していきますが、Javaにも通用すると思います。
↑最近こればっかり言っている気がする…。




最初に、基底クラスParentを定義したとします。
次に、Parentを継承するクラスChildを定義したとします。


まず知っておかなければならないのは、このときChildのインスタンスはParentのインスタンスを内包するということです。

Parentの持つメンバを同じようにChildに持たせているのか、なんて思ったこともありますが間違いです。

といっても、インスタンスを内包するということは実質的には同じメンバを持たせることと同じですけどね。

継承関係にあるクラスの定義とインスタンス
継承関係にあるクラスの定義とインスタンス


この図は、ParentクラスとChildクラスがあったとして、定義の段階とインスタンスの段階でのそれぞれのイメージです。

まず、定義の段階ではParentとChildは分離しています。
ただし、ChildにはParentを継承しているという情報が含まれています。

そして、new Child(); というようにインスタンス化すると、
Childクラスのインスタンスが作成されるとともにParentクラスのインスタンスが作成され、Childに内包されます。

これによってChildのインスタンスはParentにMethodB()を付け加えたものになります。

これが継承関係にあるクラスのインスタンス化時の仕組みです。




では、このようなコードを考えてみます。

Parent obj = new Child();

これはChildをParentに暗黙的にキャストしていて、

Parent obj = (Parent) (new Child());

となっています。

派生クラスを何らかの基底クラスにキャストすることをアップキャストといいますが、アップキャストでは何が行われているでしょうか。

簡単にいうと、何も行われていません。

ただし、型が変わったので、Childのメンバにはアクセスできないように規制します。

本当はChildのインスタンスであるけども、Parentのインスタンスとして扱うわけです。


何もしないというのは変ですが、もともと型というのはそういうものなのです。

コンピュータのデータは全て数値の羅列ですが、“扱い方”次第では文字列にみなすことも画像に見なすこともできます。

その“扱い方”を定義したのが型なのです。


データの型を変えるということは、データに対する扱い方を変えるということで、
型を変えたからといってデータにも変換処理を行う必要は本来はありません。

まあ、それだと実用的ではないので、現在は型を変えたらデータにも変換処理を加えるのが常識です。


話がそれましたが、アップキャストのイメージ図も作成しました。

アップキャストのイメージ図
アップキャストのイメージ図




余談のような章です。

Parent obj = new Child();

はエラーになりませんが、逆の

Child obj = new Parent();

はコンパイルエラーになってしまいます。

Child obj = (Child) (new Parent());

とするとコンパイルできます。

基底クラスを派生クラスにキャストすることをダウンキャストといいますが、
アップキャストは暗黙的にできるのにダウンキャストは暗黙的にできないのは何故でしょうか。

それは、アップキャストは必ず成功するのに対して、ダウンキャストは必ず成功するとは限らないため、それを意識してもらうためです。

ダウンキャストが成功する例は、

Parent parent = new Child();
Child child = (Child)parent;


としたとき、

ダウンキャストが失敗する例は、

Parent parent = new Parent();
Child child = (Child)parent;


としたときなどです。

要するに、派生クラスは基底クラスに対して上位互換性を有しているわけで、
派生クラスから基底クラスの要素を生み出す(取り出す)ことは可能ですが、
基底クラスから派生クラスの要素を生み出すことはできません。

そのため、
基底クラスの正体が派生クラスであれば、先ほど説明したアクセスの制限を解けばいいのですから、エラーにはなりませんが、

基底クラスの正体が派生クラスでなかったり、ダウンキャストする対象とは別の派生クラスだったりするとエラーになります。




さてさて、もしメソッドのオーバーライドがない言語であればこれまでの説明だけで良いのですが、

C#のようにメソッドのオーバーライドが可能な言語では仮想メソッドテーブルというものが使われたりします。


例えばParentにMethodAメソッドを定義して、Childにオーバーライドさせたとします。

このとき、Childのインスタンスは下図のようになります。

MethodAをオーバーライドしている
MethodAをオーバーライドしている


このChildのインスタンスからそのままMethodAを呼ぶと、Childに書いたほうのMethodAが呼び出されるのですが、

このChildのインスタンスをParent型にアップキャストしてMethodAを呼ぶと、Parentに書いたほうのMethodAが呼び出されてしまいます。

なぜならアップキャストすると、派生クラスにMethodAが定義されているのか不明になるからです。

あるかどうかわからない派生クラスのMethodAは呼べません。

一度、派生クラスを確認してみて、MethodAがあれば呼ぶという仕組みにしてもよかったんでしょうけど、動的型言語っぽいですね。

静的型言語っぽくしつつ、こういうことをしようとするためにメソッドのオーバーライドを使うのです。


オーバーライドを実現するためには仮想メソッドテーブルを使います。

(他の形で実現する処理系もあるみたいです。)

全てのクラスに仮想メソッドテーブルというデータテーブルを持たせるようにして、(仮想メソッドが1つ以上定義されたクラスのみの場合もあり)

仮想メソッドを1つ定義するたびに仮想メソッドテーブルにレコードを1つ追加します。

レコードはメソッドの定義名と実装への参照で構成されています。

メソッドの定義名というのは要するにメソッド名のこと(ただし、引数の型や数が異なるものは別物とする)で、

実装への参照というのは要するにC言語でいう関数ポインタ、C#でいうデリゲートです。


つまり、
MethodA … アドレスxxxxにあるメソッド
MethodB … アドレスxxxxにあるメソッド
MethodC … アドレスxxxxにあるメソッド
という表を作って持たせるのです。

“実装への参照”にはデフォルト値として自分のクラスで書いた実装への参照を設定します。

ただし、抽象メソッドの場合はデフォルト値はnull的なものです。


仮想メソッドを呼び出すときは、そのまま呼び出すのではなく、一度この仮想メソッドテーブルを確認して、そこに書いてある実装の参照先を呼び出すようにします。


そして、派生クラスが何らかの仮想メソッドをオーバーライドしたさいは、
基底クラスの仮想メソッドテーブルからオーバーライドしたメソッドを探し、
そこに設定してある実装への参照を自分の物に書き換えます。


これによって、呼び出すときは派生クラスのメソッドが参照されるわけです。

これもイメージ図を書きました。

仮想メソッドテーブルのイメージ図
仮想メソッドテーブルのイメージ図


インスタンス化する前は仮想メソッドテーブルの実装への参照は設定おらず、インスタンス化するときに設定します。


そのさい、画像左側のParentのインスタンスはオーバーライドされていないので、自分のクラスのものが設定されていますが、

画像右側のChildのインスタンスは、Childがオーバーライドをしているので、Childのものが設定されています。


ちなみに、この図では簡単にするために実装への参照の値を「Class.Member」としていますが、
実際はデリゲートや関数ポインタが入っていると思います。




おまけ

基底クラスのメソッドをオーバーライドした派生クラスが、オーバーライド対象となるメソッドを呼び出したくなることがあります。

基底クラスのメソッドに少し処理の追加をしたかっただけ、などの目的でオーバーライドした場合です。

その場合はC#のbase、Javaのsuperキーワードを使用すれば仮想メソッドテーブルを無視して、直接、基底クラスのメソッドを呼び出すことができます。


先ほど説明したように、仮想メソッドは普通は仮想メソッドテーブルを参照して実装の参照先を呼び出しますが、

baseキーワードを使用して基底クラスのメンバを呼び出すときは、
仮想メソッドであってもそうでなくても、仮想メソッドテーブルを無視して呼び出すものと思われます。

つまり、baseキーワードを使用してアクセスすると、オーバーライドが無効化されるということです。


ただし、baseキーワードで基底クラスにアクセスできるのは派生クラス内のコードのみです。

外部からオーバーライドを無視してメソッド呼び出し…というのはなぜだか分かりませんが不可能ですのでご注意ください。

tag:

コメント

コメントの投稿

トラックバック

トラックバック URL
http://cyberboy6.blog.fc2.com/tb.php/332-dcc69f7f
この記事にトラックバックする(FC2ブログユーザー)

当ブログをご利用(閲覧等)になる場合は必ず「当ブログの利用規定」をお守りください。