クラスの継承
個人情報クラス
次の話に移る前に新しいクラスを考えておきます。 クラスの特徴を生かせる定番のプログラムは住所録です。 ここでは、ある人の名前と住所を記録するだけの簡単なクラスを作ってみます。
class Person { var name; var addr; function Person( name="名無し" , addr="不明" ) { setName( name ); setAddr( addr ); } function finalize(){} function setName( name ) { this.name = name; } function setAddr( addr ) { this.addr = addr; } function show() { return "名前:"+name+"\n"+"住所:"+addr; } }
name | 名前を保存する |
---|---|
addr | 住所を保存する |
setName() | 名前を設定する関数 |
setAddr() | 住所を設定する関数 |
show() | 名前と住所を表示する関数 |
今回は、この個人情報クラスを完成に近づける事を目標とします。
自分自身を示す
先ほどのPersonクラスで注目して欲しいのは、関数setNameの処理です。 内部に次のように書かれていますね。
this.name = name;
thisという見慣れない文字が出てきました。 これも、システムで使われる予約語の一つで、 クラスのオブジェクト自身を指す単語です。例えば
var obj = Person();
とした場合、thisとは obj を指し示します。 しかし、オブジェクトobjは自分自身がどういう名前で作られているのか分かりませんので、 this という単語が割り当てられているのです。
ところで、もし関数setNameの処理を次のように書いたらどうなるでしょう。
function setName( name )
{
name = name;
}
setNameは引数nameで値を受け取り、nameというメンバー変数に代入します。 同じ名前ですが、当然メンバー変数であるnameと、引数であるnameは全くの別物です。 ここで行いたいのは、メンバー変数であるnameに、引数であるnameの値を代入する事です。 しかし、単にnameと書いたのではメンバー変数のnameか引数nameか分からない。
これを解決するのがthisなんです。
this.name …オブジェクト自身が持つメンバー変数nameの参照
this.addr …オブジェクト自身が持つメンバー変数addrの参照
このように、thisに続いてドット( . )を書き、後に変数や関数を指定すると、 その変数や関数は、クラスが持っているメンバーですよ!と指示する事が出来ます。 これで、引数の名前やメンバー変数の名前を自由に付ける事が出来るようになりました。
ちなみに this とだけ書いた場合は、オブジェクト自身そのものを指し示します。 何かの関数にオブジェクト自身を渡したい時などに役に立つでしょう。
クラスを修正する
最初に簡単な個人情報クラスを作りました。 たった一人の情報しか入れる事は出来ませんが、動くことは動きます。
ですが、電話番号も住所録に登録できれば便利ですよね。 ある場所ではこの個人情報を使いたい、ある場所では電話番号を追加したクラスを使いたい。 こんな時、同じクラスをもう一つ作り、そちらを追加/修正すればいい…と考えるのはダメです。
新しく作るクラスは、古いクラスに機能を追加しただけのクラスです。 逆に言えば、新しく追加する機能以外は、古いクラスと全く同じにする必要が出てきます。
//古いクラス class myclass { var name; function set(){} .... } //新しいクラス class newclass { var name; function set(){} function get(){}//追加したメンバー関数 .... }
最も困るのは、古いクラスに不具合が見つかって修正した場合です。 新しく作ったクラスも、同様の箇所を修正しなければなりませんよね。 コンストラクタを直し、メンバー関数、変数を追加して……とか。 クラスを複製して、なおかつ修正するという事は、とても大変な事だという事が分かります。
このように、あるクラスがあって、これに機能追加または修正したいとします。 更に修正したいクラス自体も必要である場合、 またはクラスの修正が不可能な場合、 修正したいクラスのメンバー(関数や変数)を全て引き継ぎ、新たなクラスを作るというやり方が便利です。
次回は、クラスの引継ぎについて説明します。
クラスを継承する
古いクラスのメンバーを引き継いでクラスを作る場合、次のように書きます。 (古いクラスという言い方は、あまり好ましくないのですが、説明の便宜上ですのでご了承ください)
class 新しいクラス extends 古いクラス { }
この時、新しいクラスは古いクラスのメンバー変数やメンバー関数を全て持っています。
では、最初に示したPersonクラスのメンバーを引き継いで、
新しくnewPersonというクラスを作ってみましょう。追加するのは電話番号を保存する変数telです。
※Personクラスの後に続けて書いてください。
//新しく作るクラス class newPerson extends Person { var tel; }
これで新しいクラスnewPersonは、古いクラスPersonのメンバーを全て持つ様になりました。 これをクラスの継承と呼び、newPersonはPersonを継承すると言います。
この時、古いクラスの事を基本クラス、またはスーパークラスと呼びます。 逆に新しいクラスのことを派生クラス、またはサブクラスと呼びます。
専門用語に混乱……パート2
やはり、これらの専門用語は使う人によって様々な呼ばれ方をします。 ですから、あまり気にしないで好きなように呼んで構いません。 プログラムを作るうえで、用語は全く必要ありません。
当入門では基本クラス、派生クラスという呼び方を使用する事にします。
C++
TJSの継承は常にpublicな継承になります。 セクションが使えないので、アクセス制御を行う事は出来ません。
逆に言えば、基本クラスの全ての機能に派生クラスやグローバルスコープからアクセスできる事を意味しますので、 プレイベートなメンバーにしたい変数や関数の扱いには注意が必要です。
当入門では、privateにしたい変数や関数名に _ アンダーラインを付けることを推奨します。
_method() や _number;
Personを継承したクラスnewPersonは、メンバー変数nameやaddr、メンバー関数のshowなど、全てのメンバーを持っています。 派生クラスとは、いわば古い設計図の加筆修正版であると言えます。
ところで、全てのクラスの外側で以下の文を追加して実行してみてください。
var man1 = new newPerson("山田","京都府"); System.inform( man1.show() );
名前と住所は表示されませんでしたよね。 継承に失敗しているのか、と言うとそうではありません。 なぜなら、名前こそ表示されませんが、ちゃんと「名前:」「住所:」といった文字は表示されているからです。 これは基本クラスの関数showが呼ばれているという証明です。
では、何故メンバー変数に、引数に指定した文字が代入されなかったのでしょうか。 答えは簡単、基本クラスのコンストラクタが呼ばれていないからです。
ここが今回のポイントです。 派生クラスの実物を作った場合、基本クラスのコンストラクタは自動実行されない! つまり、手動で呼び出してやる必要があるのです。
C++
C++では基本クラスのコンストラクタが呼ばれますが、 TJSでは呼ばれません。ここが一番違うところなので要注意です。
基本クラスのコンストラクタ
派生クラスの実物(オブジェクト)を作った場合、 基本クラスの初期化関数(コンストラクタ)は自動で呼ばれないという事が分かりました。 メンバ変数nameやaddrに初期の値を代入するのは、基本クラスのコンストラクタの役目です。 つまり、このままでは基本クラスの機能すら継承出来ていない事になります。
よって、派生クラスに新しい初期化関数を作り、そこで処理させる必要があります。 派生クラスのコンストラクタは省略できないのです。
ただし、基本クラスにコンストラクタが必要ない場合は省略しても構いません。 が、基本的にコンストラクタは(何も処理が無いとしても)書いておく事を推奨します
では、newPersonクラスのメンバーとして、 次の初期化関数を書き込んでみてください。
//派生クラスnewPersonの初期化関数 function newPerson( name , addr ) { setName( name ); setAddr( addr ); }
今度はうまく表示されました。 つまり、派生クラスのコンストラクタは自動で呼ばれるという事が証明できました。
しかし、この書き方は推奨されません。 なぜなら、派生クラスのコンストラクタnewPersonの処理内容が、 基本クラスのコンストラクタと引数まで同じです。 これはいわば、基本クラスの初期化関数をコピーしたに過ぎないのです。
派生クラスの基本は、あくまでも基本クラスの機能を引き継ぎ、 加筆修正した最小限の機能だけを持たせる事が重要です。 最初に説明したとおり、基本クラスの不具合を修正する際、 派生クラスにまで作業が及ぶ事を、少しでも抑えるためです。
よって、基本クラスのメンバー関数で解決できる事は、 その関数に行わせる事が理想的なのです。
完全なクラス継承
前回の問題は、簡単に解決出来ます。 コンストラクタと言っても、それ自体は単なる関数です。 よって、普通の関数と同じように呼び出す事が出来ます。
派生クラスは基本クラスのメンバーを全て持っていると書きました。 これはコンストラクタも例外ではなく、しっかり継承されているのです。 つまり、派生クラスのコンストラクタの中で、基本クラスの初期化関数(コンストラクタ)を呼び出せばよい事が分かります。
function newPerson( name , addr ) { //基本クラスのコンストラクタを呼ぶ Person( name , addr ); }
これで基本クラスの機能を完全に継承出来ました。 今後は、派生クラス独自の機能を追加すれば完成します。
実はこのコンストラクタには大きな落とし穴があります。 これは少し後で説明します。
では、派生クラスの機能を追加していきましょう。 派生クラスnewPersonは、新しいメンバー変数としてtelを持っています。 これは電話番号を保存する為の変数です。
では、変数telに値を保存する為の関数として、setTelを作ります。それが以下です。
//メンバー変数telに引数telを代入 function setTel( tel ) { this.tel = tel; }
最後に、派生クラスのコンストラクタもいじります。 電話番号も初期化の時に一緒に保存できた方が便利ですよね。 後から関数setTelを呼び忘れると、電話番号が空っぽになってしまうからです。
/* 派生クラスnewPersonのコンストラクタ (nameとaddrは基本クラスのコンストラクタが処理するので、 あえてデフォルト引数にはしていません) */ function newPerson( name , addr, tel="0123-456-789" ) { //基本クラスのコンストラクタを呼ぶ Person( name , addr ); //メンバー関数setTelを呼ぶ setTel( tel ); }
コンストラクタnewPersonの引数にtelを追加しています。 これを、関数setTelに引数として渡してやると、 変数telに引数として渡した値が代入される、という仕組みです。
初期化処理の順番に注意
通常、初期化処理は基本クラスから行わせます。つまり
基本クラス→派生クラス
の順番で初期化を行います。 簡単に言えば、派生クラスのコンストラクタの一番最初に、 基本クラスのコンストラクタを呼ばないとダメ、という事です。
基本クラスのメンバーを、派生クラスのコンストラクタで操作する場合、 その時点で基本クラスの各メンバーの初期化が終わっていないと困るから、というのが理由の一つです。
個人情報クラスの完成
電話番号を保存する為の変数telを作り、 値を代入出来るように改良したnewPersonクラスが出来ました。 ただし、外部に次のように書いても電話番号が表示されません。
var man1 = new newPerson("山田","京都府","0123-456-789"); System.inform( man1.show() );
なぜなら、メンバー関数showに、電話番号を指す変数telを表示する処理が無いからです。
//基本クラスのメンバー関数 function show() { return "名前:"+name+"\n"+ "住所:"+addr; }
もちろん、この関数を修正してはダメですね。 基本クラスはメンバー変数telを持っていませんから、telを参照した時点でエラーになります。 どうやら、派生クラスに新しく電話番号も表示する関数を作る必要がありそうですね。
しかし、この関数名をどうするか悩みますね。 関数showは基本クラスPersonから継承しているので名前が被ります。 よって、関数名を少し変えてあげる事にしましょう。
以下の関数をnewPersonのメンバーに追加します。
//電話番号も表示する関数 function newShow() { return "名前:"+name+"\n"+ "住所:"+addr+"\n"+ "番号:"+tel; }
関数名を変えたので、呼び出す関数も次のようにします。
var man1 = new newPerson("田中","北海道","0123-456-789"); System.inform( man1.newShow() );
無事電話番号も表示されました。 これでPersonを継承したクラスnewPersonが実装できました。 今後は電話番号表示板、非表示版の両方のクラスを扱う事が出来ますね。
オーバーライド
「名前」「住所」「電話番号」を表示できる個人情報クラスに仕上げました。 とりあえずは完成と言って良いでしょう。しかし、残念な事に問題があります。
基本クラスの関数showは電話番号を表示できないので、派生クラスでnewShowという関数を作りましたよね。 でも、この二つはある人物の名前や住所を表示するという共通の処理を行います。 場合によっては、派生クラスと併用して基本クラスを使う事もあるでしょう。
そうなると、ちょっと面倒な事になります。例えば次の使い方。
//古い住所録を使う var man1 = new Person("斉藤","大阪府"); System.inform( man1.show() ); //新しい住所録を使う var man2 = new newPerson("田中","北海道","0123-456-789"); System.inform( man2.newShow() );
この時、情報を表示する関数に注目。 showもnewShowも、個人情報を表示する関数として共通します。 それにもかかわらず、使うクラスに応じて呼び出す関数名を変える必要があります。 例えば、関数名を逆に使ってしまうと大問題です。
var man1 = new Person("斉藤","大阪府"); System.inform( man1.newshow() ); //こんな関数無いです var man2 = new newPerson("田中","北海道","0123-456-789"); System.inform( man2.show() ); //電話番号表示されない
これを避ける為に、TJSには有効な機能が備わっています。
派生クラスの関数で基本クラスの関数と同じ関数名を使った場合、 基本クラスの同名関数を隠す事が出来る。 これをメンバー関数のオーバーライドと呼びます。
class myclass { function set(){……} } class myclass2 extends myclass { //関数setをオーバーライド function set(){……} } var m = new myclass2(); m.set();
オブジェクトmが呼ぶ関数setは基本クラス(つまり、myclass)の関数setでは無く、 派生クラス(つまり、myclass2)の関数setです。 これがオーバーライドの効果で、 基本クラスの関数setを隠し、あたかも存在しないかのように振る舞い、 派生クラスのsetを呼び出すように働きます。
関数show()の修正
では、newPersonクラスをもう少し改良しましょう。 ここでは、基本クラスの関数showをオーバーライドして、 個人情報を表示する関数名を統一します。
newPersonのメンバー関数、newShowの名前をshowに変更してみてください。
//電話番号も表示する関数 function show() { return "名前:"+name+"\n"+ "住所:"+addr+"\n"+ "番号:"+tel; } //呼び出し側 var man1 = new newPerson("山田","京都府","0123-456-789"); System.inform( man1.show() ); var man2 = new newPerson("田中","北海道","9876-543-210"); System.inform( man2.show() );
これを実行すると、電話番号がちゃんと表示されます。 ここで呼んでいる関数showは、newPersonクラスの関数になっている事が確認出来ます。
では、最後の落とし穴について考えてください。 基本クラスの関数をオーバーライドしたらどうなるのでしょうか。 答えは次の項目で明らかになります。
最後の落とし穴
基本クラスのコンストラクタのところで示した、 大きな落とし穴を解決しましょう。
TJSの仕様で、どのような関数でもオーバーライド出来ます。 後始末関数(デストラクタ)でも、初期化関数(コンストラクタ)であったとしても。 これが最後のポイントであり、落とし穴なんです。
newPersonクラスの中で、Personという名前の関数を使ったとしましょう。 Personという関数は、newPersonクラスにとって、基本クラスのコンストラクタです。 つまり、コンストラクタをオーバーライドしてしまう事になります。
class Person { …略… function Person() { …略… } } class newPerson extends Person { …略… //基本クラスの初期化関数のオーバーライド function Person() { …略… } }
この例で、派生クラスnewPersonの初期化処理として、 次のように書いたらどうなるでしょうか。
function newPerson( name , addr ) { //基本クラスのコンストラクタを呼ぶ……? Person( name , addr ); }
基本クラスのコンストラクタであるPersonは、オーバーライドの効果によって隠されています。 よって、ここで呼ぶPersonは派生クラスのメンバー関数Personになってしまうのです。 基本クラスのコンストラクタと同名の関数が、派生クラスで定義出来ないのでは問題です。
しかし、オーバーライドは基本クラスの関数を隠すだけで、 上書きしてしまうわけではありません。 コンストラクタPersonと、普通の関数Personという、同名の関数を二つ持っている事になります。

オーバーライドした関数ではなく、基本クラスの関数を明確に呼び出すには、 次のように書く必要があります。
super.関数名();
superに続いてドット「.」を書き、関数を呼び出します。 superを付けると、基本クラスのメンバーである事を指定できます。
クラスにおいて、メンバー参照には優先順位があって、以下の順に探します
- オブジェクト自身のメンバーを探す
- 基本クラスのメンバーを探す
- クラス外部の変数や関数を探す
- 見つからないとエラー
すなわち、クラスで各メンバーを明示的に呼び出すには、以下のように記す必要があるという事です。
- global.メンバー……クラスの外側のメンバー(変数や関数)
- super.メンバー……基本クラスのメンバー
- this.メンバー……自分自身のメンバー
というわけで、newPersonのコンストラクタは、次のように書けば問題が解決出来る事になります。
//派生クラスnewPersonの初期化関数 function newPerson( name , addr , tel="0123-456-789" ) { //基本クラスのコンストラクタを呼ぶ super.Person( name , addr ); setTel( tel ); }
もう一点、説明を加えておくと、後始末処理(デストラクタ)関数も、newPersonクラスに追加する必要があります。 コンストラクタと同じく、何の処理をしていなくても、基本クラスのデストラクタを呼び出しておくのが普通です。 よって、以下の関数もメンバーに加えればよいのです。
//派生クラスnewPersonの後始末関数 function finalize( ) { //基本クラスのデストラクタを呼ぶ super.finalize(); }
これで個人情報クラスはとりあえず完成です。
- 個人情報をプログラム実行時に入力するには?
- 複数の個人情報を保存して、住所録に仕上げるには?
- 保存した個人情報を外部ファイルに出力するには?
この課題について今回は目を瞑り、 次回以降に持ち越すことにしましょう。
全コード
今回作った個人情報クラスのコードをまとめておきました
//簡易住所録プログラム class Person { var name,addr; function Person( name="名無し" , addr="不明" ) { setName( name ); setAddr( addr ); } function finalize(){} function setName( name ) { this.name = name; } function setAddr( addr ) { this.addr = addr; } function show() { return "名前:"+name+"\n"+ "住所:"+addr; } } //Personを継承した新しい住所録 class newPerson extends Person { var tel; //派生クラスnewPersonの初期化関数 function newPerson( name , addr , tel="0123-456-789" ) { //基本クラスのコンストラクタを呼ぶ super.Person( name , addr ); setTel( tel ); } function finalize() { super.finalize(); } function setTel( tel ) { this.tel = tel; } function show() { return super.show()+"番号:"+tel; } }