コンストラクタ
初期化の必要性
前回までで、プレイヤーを作り、名前を付ける事まで出来ました。 ただし、これまでのクラスの形では何かと問題があります。 例えば、プレイヤーを作ったけど名前を付け忘れた!なんて事も良くあることです。
class Player { var Name; function setName( name ) { Name = name; } function showName() { return Name; } } var player1 = new Player(); System.inform( player1.showName() ); //↑setNameで値を指定せずに表示している!
本来であれば、メンバー関数setNameを呼び出してプレイヤーに名前を付けるところですが、 うっかり忘れ、いきなり名前を表示してしまっています。当然、何も表示されませんでしたね。 これでは、クラスを使う時に全てのメンバー設定をするために、各関数を呼ばなければなりません。
var player1 = new Player(); var player2 = new Player(); var player3 = new Player(); player1.setName("主人公"); player2.setName("仲間1"); player3.setName("仲間2"); player1.showName(); player2.showName(); player3.showName();
今回は名前だけですが、攻撃力や性別、装備品など、プレイヤーには色々な設定すべき項目がありますね。 それを、オブジェクトを作った段階で、一つ一つ入れるのはとても手間で、 せっかくクラスというデータベースを使う意味が薄れます。
クラスを実際に使うには、メンバー変数などの初期情報が必要である、 という事がお分かりいただけたでしょうか。
コンストラクタ
メンバー変数の初期化はとても手間の掛かりますから、 TJSではメンバーの設定を行う初期化用の関数が用意されています。
クラスの初期化の為の関数は、普通のメンバー関数と変わりません。 ですから初期化用の関数もクラスの中に直接書き込みます。 ただ一つ違うのは、初期化関数の名前はクラスの名前と同じにする必要があるという点です。
例えば、myclassという名前のクラスの初期化関数の名前はmyclassにする必要があります。
class myclass { function myclass() { //初期化処理 } }
とは言っても、これをわざわざ呼び出さなければならないのは困りますよね。 初期化忘れを防ぐために用意したのに、そもそも初期化関数を呼び忘れては話にならないからです。 おまけに、作ったオブジェクトの数だけ、初期化関数を呼ぶ手間も変わりません。
ところがそんな心配は必要ありません。 この初期化のための関数は、実物が作られる時に自動的に呼び出してくれるのです。 自動的に呼ぶ、とはどういう事か、処理を見てみましょう。
class myclass { function myclass() { System.inform("初期化しました。"); } } var m = new myclass();
これを実行すると「初期化しました」と表示されましたね。 まだ実物を作っただけで、関数myclassは呼び出していないのにです。 これが初期化用の関数の役目なんです。
まとめると、あるクラスの名前と同じ名前のメンバー関数を作ると、 その関数は実物が作られる時に自動的に呼び出される初期化用の関数になります。 この初期化用の関数をコンストラクタと呼びます。 コンストラクタはオブジェクトを作った段階で、自動的に呼び出されます。
引数を受け取るコンストラクタ
コンストラクタは初期化専用の関数です。 自動的に呼び出されるという特徴以外は、普通の関数なのです。 よって、普通は受け取った引数をメンバー変数に代入する、という形を取ります。
でも、少し困った事になりますね。何かおかしいですね。 初期化関数は自動的に呼び出されると書きました。つまりわざわざ呼び出す必要は無いのです。 しかし、値を関数に渡すには、関数を呼び出す時に指定する必要がある。 では、初期化関数に値を渡すにはどうすれば良いのでしょう?
実は、答えはすでに出ています。実物を作る方法を見てください。
new myclass();
この形、どこかで見た事がありませんか?
myclass();
そう、関数の呼び出しと同じです。 つまり、知らず知らずのうちに、既にコンストラクタを呼び出す、という行為を行っていたのです。
//クラスmyclassのコンストラクタmyclass()を呼ぶ new myclass(); // newによって実物が作られる
よって、コンストラクタに値を渡すには、実物を作る時に次のように書けば良い事が分かります。
new myclass( 引数 );
では、Playerクラスを更に改良しましょう。
class Player { var Name; //コンストラクタ(初期化用関数) function Player( name ) { setName( name ); } function setName( name ) { Name = name; } function showName() { return Name; } } var player1 = new Player("主人公"); System.inform( player1.showName() );
初期化関数Playerは、実物が作られる時にメンバー関数setNameを呼び出しています。 setNameはメンバー変数Nameに値を代入する為の関数ですから、結果的に変数Nameに値が代入される、という動作になります。 (もちろん、直接「Name=name;」としても構いません。)
しかし、残念ながらまだ問題があります。 もし、実物を作る時に次のように指定してしまったら?
var player1 = new Player();
やはり、メンバー変数Nameには何も代入されずに空っぽです。 そこで、引数に何も指定しなくても名前が付けられるように、適当な値を予め入れておく必要がありそうです。 ここで使うのはデフォルト引数です。(覚えていますか?)
という事で、Playerクラス改良版は次のようになります。
class Player { var Name; //コンストラクタ(初期化用関数) function Player( name="名無し" ) { setName( name ); } function setName( name ) { Name = name; } function showName() { return Name; } } //主人公という名前のプレイヤー var player1 = new Player("主人公"); //名前の無いプレイヤー var player2 = new Player(); System.inform( player1.showName() ); System.inform( player2.showName() );
オブジェクトの無効化
オブジェクトを作成するとメモリを消費します。(メモリとは、パソコンについている物理的なメモリと同等です。) よって、必要なくなったオブジェクトは削除すべきです。 さもないと、オブジェクトが確保されているメモリが、ずっと残り続けるからです。 (アプリケーションがずっと常駐している事に似ていますね。)
ここは新しく、オブジェクトを削除するための命令を覚える必要がありそうなので、 リファレンスを参照してみましょう。
TJS2 では、オブジェクトが削除される際、オブジェクトの無効化とオブジェクトの削除、という2つの段階を踏みます
つまり、オブジェクトを無効化出来れば、削除されるという事らしいですね。 最後にこう書かれています。
オブジェクトは invalidate 演算子で無効化することができます
これで削除する準備は整いました。実際に使用してみましょう
class Test { function show() { return "Test実行中"; } } var obj = new Test(); System.inform( obj.show() ); // オブジェクトを無効化 invalidate obj; System.inform( obj.show() );
これを実行すると最初に「名無し」、次に「例外が発生しました。オブジェクトは無効化されています」というエラーが出ました。 「例外」とは何かはとりあえず横において、invalidateによってobjが無効化されているため、二度目の名前表示には失敗したのです。
そのため、オブジェクトが無効化されている可能性がある場合、 そのオブジェクトの無効化をチェックしてから操作するのが無難です。 チェックするには次のような単語を用います。
isvalid オブジェクト;
オブジェクトが無効化されていれば偽、有効なら真が返ります。 すなわち、演算子if(if文とは違います。覚えていますか?)を用いて、 二度目のobj.show()呼び出しを次のようにすれば、先ほどのエラーは解消されます。
System.inform( obj.show() ) if isvalid obj;
今度は名無しという表示だけでエラーはなくなりました。 if演算子は右側が真なら左側を実行する、という動作をしますから、 ここでは右側のオブジェクトが有効(真)なら、左側を実行という処理になっているのです。
isvalidは、対象オブジェクトを先に書いても…つまり左側に書いても同じです。
obj isvalid
本入門では他の演算子と混乱しないためにも、右側にオブジェクトを書く事を推奨します。
デストラクタ
初期化したら後始末をしなければなりません。 風呂敷を広げたら、作業終了時に風呂敷をたたむ事に似ています。 クラスも同じ事で、様々な処理を行わせた後、必ず後始末をします。 リファレンスによると次のように書かれています
オブジェクトが無効化されるとき、 finalize メソッドが呼ばれ、そのオブジェクトは無効であるというマークがつけられます
finalizeとはクラスの後始末用の関数で、そのオブジェクトが無効化される時に自動的に呼ばれます。 作成時に呼ばれるコンストラクタの後処理版といった感じですね。
class myclass { function finalize() { //後始末処理 } }
このように、関数名にfinalizeを使うと、それは後始末専用の関数になります。 これをデストラクタと呼びます。
デストラクタは、次の特徴があります。
- オブジェクトが無効化された時点で自動的に呼ばれる
- 引数を受け取る事は出来ない
- 値を返すことも出来ない(後始末なので返す必要もない)
C++
TJSのデストラクタはfinalizeという固有の関数名を使います。 C++のように、関数名の頭にチルダ( ~ )を付ける訳ではない事に注目してください。
後始末と言われても、具体的に何をすれば良いのかピンと来ないかもしれませんが、 これは追って説明していきますので、このような関数がある事を記憶に留めておいてください。
また、基本的にfinalizeは省略する事も出来ますが、 特に後始末処理が無くても、何もしない関数として書いておく事が望ましいです。
invalidateの必要性
無効化される時に後始末関数が呼ばれるという事が分かりました。 しかし、次のようなプログラムを書いた時、不可解な動作をする事を確認してください。
class Test { function Test() { System.inform("コンストラクタ実行"); } function finalize() { System.inform("デストラクタ実行"); } } // 以下はブロック内。 { var obj = new Test(); } System.inform("ブロックを抜けた");
最後の表示で「デストラクタ実行」と出ましたね? invalidate を使って無効化していないのに、デストラクタが実行された事になります。 デストラクタはオブジェクトが無効化された時に呼ばれるのではないのでしょうか? やはり、リファレンスのお世話になりましょう。
invalidate 演算子を用いなくても、オブジェクトは必要とされなくなった時点で削除されます。このとき、無効化されていなければ、その時点で無効化されます。
リファレンスによると、不要なオブジェクトは自動的に呼ばれるようです。 では、何故objが不要と判断されたのか?それはobjがブロック内{}で宣言されているからです。
ブロック内で変数を宣言するとローカル変数になったように、 オブジェクトもローカルオブジェクトになりますから、 ブロック{}を抜けた段階で、オブジェクトobjは使い物にならなくなったというわけですね。
なんだ、ではinvalidateを使って無効化する意味は無いじゃないか、と思われるでしょう。 ですが、リファレンスには次のようにも書かれています。
TJS2 ではいつオブジェクトが削除されるかの明確な規定が無く、削除や無効化は「いつでもおこりうる」ことになります。
これを要約するといつデストラクタが呼ばれるか分からないという事になります。 それを決定付けるのが、先ほどのメッセージ表示です。 「コンストラクタ実行」「ブロックを抜けた」「デストラクタ実行」という順番で表示されています。 本来なら「コンストラクタ実行」「デストラクタ実行」「ブロックを抜けた」という表示になるべきです。
このような不可解な動作を避けるためにも、不要なオブジェクトは極力、 手動で無効化する方が良いのです。 プログラムを以下のように改良して、正しいメッセージの順番になる事を確認してください
class Test { function Test() { System.inform("コンストラクタ実行"); } function finalize() { System.inform("デストラクタ実行"); } } // 以下はブロック内。 { var obj = new Test(); invalidate obj; } System.inform("ブロックを抜けた");