Table of Contents
Table of Contents
最初に言いましたが、この講座は(1,1)から(2,2)へ向けてのジャンプをお手伝 いするものです。実は「継承」というのは (2,2) 以上の技術です。
ですから、継承の必要性がわかる人は、C++ なら Scott Mayers の Effective C++ を、Java なら Joshua Bloch の Effective Java を買って読んでくださ い<ピリオド>
ひどいな。でも私自身、上記の本を頼りに、自分が落とし穴にはまらないよう に歩く事はできるけど、学生さんを責任もって引率できるほどじゃない。だか ら、上記の本を読んで、概ね分かったような気になれる人や、こんなの当り前 じゃんと思える人には教えることはありません。それほど高い本じゃないです から、自分のお小遣いで買ってください。
とはいえ、それじゃ無責任なので、ちょっとお話ししましょ。
継承の説明の例ではよく鳥さんが出てきます。でも、鳥のデータベースやら、 鳥の生態系シミュレーションをつくるやら、鳥のフライトシミュレータをつく るのでも無い限りは「フンフン、それで?」でという気分です。
プログラミング言語についている各種の機能は、目的があって追加されたもの です。目的とは何らかの問題があったという事です。つまり、その機能がない とどういう問題が解決できない(解決しにくい)のか分からないと、その機能を 上手に使いこなせません。
何度も同じようなコードを書くのは面倒ですよね。何度も同じコードを書くの は面倒ではないですよね。これが答えです?。全く同じで良ければ「書く」必 要はなくてコピペすれば良いですから。更に一歩進めて自分で良く使う関数は my_util.c とかに貯めて、プロジェクトの度に使い回せばよいのです。
でも、ちょっだけ状況が違う場合、ちょっとだけ書き直さなければならないで すね。ちょっとだけ書き直した関数にちょっとだけ違う名前を付けて(foo(), foo1(), foo2() とか) my_util.c に貯めていると、そのうち、どれがどれだ か分からなくなりますね。それに、もし後になって foo2() にバグが見付かっ て修正したとしても、foo() や foo1() にはそのバグが残ったままです。気が 付いて全部直せば良いですが、それは面倒ですし、気が付かないままになって、 また同じデバッグをするはめになるでしょう。なんとかしたいですね。こんな 酷い状況は。
こんな状況から抜け出すには、一度書いたら、(限度はありますが)どんな状況 でも使えるコードを書くしかありません。
仮にに C++ や Java のようなオブジェクト指向機能を言語レベルでもってい るプログラミング言語が無かったら(使えなかったら)と仮定してみます。
XML の章の本のデータベースについてですが、折角リンクリストを読んだり書 いたりできるようになったのですから、ほかの用途にも使えるようにしたくな ります。あるいは、フィールドを増やしてもっとたくさんの情報を詳しく保持 できるようにしたいとします。
よく見ると、リンクリストとして意味を持っているのは先頭 m_next だけです。 このメンバーはポインタですから、指している先の構造を気にするのはコンパ イラだけです。実行時にはポインタが指している先が実際になんなのかは関係 がありません。じゃコンパイラをだませばいいという事になります。走ってい る時の事はプログラマが責任を持つと言うことで。
struct foo_t { void* m_next; char* m_str; }; struct bar_t { void* m_next; int m_int; }; void print_foo_list(foo_t* list_head) { for (foo_t* item = list_head; item != NULL; item = (foo_t*)item->m_next) { printf("%s\n", item->m_str); } } void print_bar_list(bar_t* list_head) { for (bar_t* item = list_head; item != NULL; item = (bar_t*)item->m_next) { printf("%d\n", item->m_int); } }
と言うことになります。これだけでは、あまりご利益がありません。毎度使う リンクリスト固有のデータについて何度も書かなくても済むようにしたいです。 毎回つかう様なコードはマクロ化する。M$のアプリケーションみたいですね。
#define FOREACH_LIST(T, head, item) for (T* item = head; item != NULL; item = (T*)item->m_next) void print_foo_list(foo_t* list_head) { FOREACH_LIST(foo_t, list_head, item) { printf("%s\n", item->m_str); } } void print_bar_list(bar_t* list_head) { FOREACH_LIST(bar_t, list_head, item) { printf("%d\n", item->m_int); } }
このやり型の問題は、常に m_next というメンバを固定の位置(普通先頭)に 置かなければならないことです。つまり、最初からリンクリストとして管理す るつもりで構造体を定義しなければならないという事です。言い替えると、リ ンクリストにするつもりの無かった構造体を後から適用できない事です。
また、マクロをつかうやりかたの問題は、m_next に何のポインタをいれても コンパイラが全くチェックできない事です。
C++ の重要な目標は型のチェックをコンパイラにしっかりさせることです。 C++ は Java 等と違って、一旦コンパイルが済んだら、だれも支えてくれませ ん。NULLポインタの参照しても、Java の様にヌルポインターエクセプション で実行環境が捕まえてくれるわけでは有りません。そのまま、OS にトラップ されてプログラムを強制終了させられてしまいます。強制終了は実用プログラ ミングの世界では致命傷です。
デバッグが済んだつもりのプログラムで客先でヌルポをくらうのは、間違った データ型の参照した時です。幾ら論理的に考えてもなかなか見付からないです。 本来ポインタが入っている所に別のデータ(例えば int や double)が入ってい たら何処に飛んでいくか分かりません。
これはキャストの多用が原因です。キャストすれば、ポインタは結局ポイン タに過ぎないいう現実が見えてしまいます。型 foo_t を指しているポインタ も型 bar_t を指しているポインタもコンパイル済の機械語コードの上では区 別は付かないです。
それを解決するのが C++ の template 機能で、それをフルに活用したのが、 STL (Standard Template Library)です。template や STL の説明はここでは 省かせてもらいます。
さて、一方、Java や C# 等はオブジェクト参照(つまりポインタ)は単なるポ インタだけでなく、それがどんな構造体を指しているか、等を含んでいます。 だから、実行時に変換不可能なキャストをしようとするとエクセプションを投 げます。
Java には Vector 等のコンテナ(入れ物)クラスが豊富に用意されています。 Java と STL で Vector を比較するとその差がはっきりと現れて来ます。Java の Vector のそれぞれに入れるのはオブジェクト参照か基本型です。かつ、一 つの Vector に色々な物を混ぜて入れる事ができます。Java の場合
Vector v = new Vector(); v.add(1); v.add(1.32); v.add("Hello World");
これをメリットととる事もできますが、こんな使いかたの方が少ないと思いま す。しかし、通常はナニナニの Vector として使いたいはずです。
class MyBookmark { // ブックマークのエントリとなるクラスを自分で定義したとする。 public InputStream open() { // ブックマークで指定された URL を開いてストリームを返す } }; Vector bookmarks = new Vector(); bookmarks.add(new MyBookmark("http://www.yahoo.co.jp")); bookmarks.add(new MyBookmark("http://ring.shizuoka.ac.jp")); bookmarks.add(new MyBookmark("ftp://ftp.freebsd.org/pub/FreeBSD")); bookmarks.add("http://www.shizuoka.ac.jp"); // 大丈夫か?
とやってしまっても、コンパイル時にはエラーが出ません。
// ブックマークを巡回して保存する for (int i = 0, n = v.size(); i < n; i++) { MyBookmark bm = (MyBookmark)v.get(i); InputStream stream = bm.open(); ... }
さて、ループの4周目にどんな事が起こるでしょうか?このようなバグが実行 時にまで見付からないです。
一方、C++ + STL で書くと、
class MyBookmark { ... public: inputstream open() { } } vector<MyBookmark> bookmarks; bookmarks.push_back(new MyBookmark("http://www.yahoo.co.jp")); bookmarks.push_back(new MyBookmark("http://ring.shizuoka.ac.jp")); bookmarks.push_back(new MyBookmark("ftp://ftp.freebsd.org/pub/FreeBSD")); bookmarks.push_back("http://www.shizuoka.ac.jp"); // × コンパイルエラー
template と STL が C++ に完全な形で実現されるまでは、C++ は Java に比 べると安全性という点で不利な点が多かったですが、templateとSTLの登場に よって、完全に逆転しました...しかし、時既に遅し、でした。Java は安全だ という幻想が広がってしまっていました。それに、template や STL は使いこ なす事自体が難しいです。template を自分なりに使え、STL を使いこなせる ようになったらレベル2です。自分で STL を拡張できるようになったらレベ ル3です。
継承の例を説明しようとしているうちに、template の説明になっていまいま した。何故?。コードの再利用の道具としての継承というのは、template に 比べるとぜんぜん役に立たないのです。template が無い時代には継承を使っ てコードの再利用をしようとしていました。でもぜんぜんエレガントにならな いのです。
週間コミック誌の「モーニング」に故青木氏の「ナニワ金融道」(通称ナニ金) という面白い漫画がありました。返済ができそうもない利用者を、返済を確実 にさせるために、あの手この手で追い込んでいくのを「カタにハメル」といっ ていました。
MFC とか Java/AWT, Swing などはそうです。お金を借りているわけではない (逆だ、お金を出して買ってやっているのに)メーカの言いなりにメソッドを書 かなければならない。まさに型(フレームワーク)にはめられています。つまり、 自力では、どこでどうやったら良いか分からないプログラマ(返済の目処が立 たない人)にフレームワークを押しつけている。
なんか、ものすごい悪い印象を与えるような書き方をしてしまった(MSさん、 Sunさん、ごめんなさい) 。別に彼らに悪意があるわけではなく、むしろ、こ のフレームワークにはまれば、余計な設計や余計なコーディングをしないで済 み、生産性が上がり、しかもバグも少なくなる事を意図してくれているんです。 だから、フレームワークにしたがって書きましょう。
継承がどのようになっているのか、意味論でいくら議論しても意味がありませ ん。実際にコンパイラはどういうコードを吐くのか、その構造を理解して、そ れがどのような振る舞いをするのかを想像しましょう。構造論という事です。 意味が先にあり、それに従っ(縛られた)行動がとられるというのは人間の話 です。機械には構造があり、構造にしたがった行動をします。構造的に不可能 な行動はとりません。意味を与えるのは人間ですが、機械は人間が与えた意味 など意に介しません。コンピュータプログラミングとは、人間の意図を計算機 の中に構造として構築すると言う作業なのです。
下らない話しはさておき、
C++ができた初期でには、C++コンパイラというのはなくて、C++をCに変換する トランスレータだったという事を聞いた事があるでしょうか?cfrontというコ マンドだったのです。つまり、C++のコードは動作上は Cと等価なのです。つ まり、C++のコードはCとしてかけるという事で、言い方を変えればC にでき ないことはC++にもできないのです。
class foo_t { protected: int m_int; public: void func(int i) { m_int += i; } virtual void vfunc1(int i) { m_int += i; } virtual void vfunc2(int i) { m_int += i * 2; } };
上記のコードは中間コードとしての C に大体以下のように変換されます。
struct __foo_t__ { void** vtbl_ptr; int m_int; }; foo_t__func(__foo_t__* this, int i) { this->m_int += i; } foo_t__vfunc1(__foo_t__* this, int i) { this->m_int += i; } foo_t__vfunc2(__foo_t__* this, int i) { this->m_int += i * 2; } void* foo_t_vtable[] = { foo_t__vfunc1; foo_t__vfunc2; };
引数が二つの関数に変換されるのです。そして1番目の引数にはfoo_t のイン スタンスへのポインタつまり、this が与えられるのです。
では呼び出し側はどうなるかといえば
foo_t* obj = new foo_t; | __foo_t__* obj = (__foo_t__*)malloc(sizeof(__foo_t__)); | obj->vtbl_ptr = foo_t_vtable; | obj->func(10); | foo_t__func(obj, 10); | obj->vfunc1(1); | (obj->vtbl_ptr[0])(obj, 1); | obj->vfunc2(2); | (obj->vtbl_ptr[1])(obj, 2);
virtual 宣言をすると、随分変なことになっています。
さて、サブクラスをつくるとどうなるでしょう?
class bar_t : public foo_t { public: void func(int i) { m_int += i + 1; } virtual void vfunc1(int i) { m_int += i + 1; } virtual void vfunc2(int i) { m_int += i * 2 + 1; } }; ------------------------------------------------ bar_t__func(bar_t* this, int i) { this->m_int += i + 1; } bar_t__vfunc1(bar_t* this, int i) { this->m_int += i + 1; } bar_t__vfunc2(bar_t* this, int i) { this->m_int += i * 2 + 1; } void* bar_t_vtable[] = { bar_t__vfunc1; bar_t__vfunc2; };
では、呼び出してみると
bar_t* bobj = new bar_t; | __bar_t__* bobj = (__bar_t__*)malloc(sizeof(__bar_t__)); | bobj->vtbl_ptr = bar_t_vtable; | foo_t* fobj = bobj; | __foo_t__* fobj = (__foo_t__*)bobj; | fobj->func(10); | foo_t__func(fobj, 10); | bobj->vfunc1(1); | (bobj->vtbl_ptr[0])(obj, 1); | fobj->vfunc2(2); | (fobj->vtbl_ptr[1])(obj, 2);
さて、最後の fobj->vfunc2(2) で実際に呼び出されるメソッドはどれでしょう? foo_t::vfunc2() だと思います?ブー。bar_t::vfunc2() です。だって、new bat_t をしたときに初期化された vtbl_ptr はどこをどこを指しています?
これが理解できたら、晴れてレベル2です。
JavaScript でもオブジェクト指向プログラミングはできます。継承ももちろ んできます。
JavaScript では関数は Function 型のインスタンスです。これを new オペレー タで呼び出すと、その関数をコンストラクタとしてオブジェクトが作られます。
function BaseClass() { this.slot1 = 1; } function DerivedClass() { BaseClass.call(this); this.slot2 = 2; } DerivedClass.prototype = new BaseClass(); var obj = new DerivedClass(); WScript.Echo(obj.slot1 + " " + obj.slot2); // "1 2" と出力される。
JavaScript には call() という変な組み込み関数があります。これを使うと 継承が実現できてしまいます。これの説明は難しいです。私もこんな関数があ るのは、この文書を書くために調査していて知りました。
例題として、ディレクトリをスキャンしてファイルの属性を収集するスクリプ トを書いてみようと思います。
ここは、授業で説明します。
継承は考えれば考える程役に立たなく思えてきました。でもそれは、本当に継承が有効ではないケースで、継承を無理に使っていたからです。
オブジェクト指向の教科書では鳥さんでよく説明します。基底クラスの鳥は飛べる。だからそのサブクラスである鷲は飛べる。でもペンギンさんは飛べない、これをどうするか(オーバーライドするわけですね)。こういうケースでは継承はうまく働いていいるように見えます。つまり、データを記述する為に継承を使うのはOKだということです。
何でもかんでもクラスにしてそこから継承するというやり方に拘ると、動きのある部品をクラスにして、動作の性質などに基づいて階層化したくなってしまいます。オブジェクトヲタクの悪い癖です。動きのあるもの、言い替えると能動的なもの、もう少し平易に言うとメソッドを呼ぶと動き出すようなもの、は継承するようなクラスとして扱うのは無理が有るのです。
それに対して、動かないもの、言い替えると受動的なもの、もう少し平易に言うとメソッドを呼んでも動くわけではなく返事をするだけのもの、は継承するのに向いています。
例えば、鳥クラスには「君は飛べるの?」というメソッドはあっても、「飛べ!」というメソッドを直接与えるべきではないという事です。そういう場合は「飛べ!」というメソッドを持った「飛べるもの」という(C++なら)純粋仮想カラスや(JavaやC#なら)インターフェイスを作って、実際に飛べる鳥クラスにくっつける方がスマートです。飛べるかどうかわからない奴に「飛べ!」と言って「I can fly!」とか叫んで事故を起こされることがありません。