基礎知識編の総まとめとして、簡易住所録を実用的なプログラムに仕上げて終わりたいと思います。
今回のプログラムは、前回までの小規模なものと比べても、 割と難しく感じられると思いますので、つまづいたらこれまでのページを読み返し、 必ず理解してから進めるようにしてください。
必要な知識は「クラス」「プロパティ」「データ入力」など、 これまで説明してきたほぼ全てです。 まさに、基礎知識編の締めくくりとしては最適なプログラムとなりそうです。
では、心して臨んでください。
最初に改良点を考えておきましょう。
メンバー関数とは、いわばリモコンのボタンのようなものなのです。 使う側が関数(ボタン)を押して、テレビ(オブジェクト)を操作する。
よって、メンバー関数は必要最小限だけ用意する事が望ましいと言えます。 よく分からないボタンだらけのリモコンは使いづらいでしょう?
手始めにメンバー変数を操作している関数を全てプロパティに変える事にします。
住所や電話番号を管理するクラスを作り、それらを一つ一つ使っていたのでは、非常に使い勝手が悪いです。
よって、個人情報を管理するクラス…いわゆるアドレス帳クラスを作り、 個人情報はアドレス帳が管理するようにします。 これまで使ってこなかった「クラスをメンバーに持つクラス」という手法にチャレンジします。
具体的には、これら個人情報オブジェクトを、クラスが持つ配列で管理します。
住所録は、後から追加出来なければ意味がありません。 また、一度作ったら削除出来ないと意味がありません。
修正も出来ると便利ですが、今回は情報の追加と削除機能のみ実装する事にします。
基本的な機能はこれで出揃いました。いよいよ一つずつ実装しましょう。
以前のプログラムでは変数の操作にメンバー関数を使っていたので、 これらをプロパティで置き換えます。
//住所と電話番号を保存するクラス class Address { var userName; var userAddr; var userTel; function Address( name="名無し" , addr="不明" ,tel="") { this.name = name; this.addr = addr; this.tel = tel; } function finalize(){} property name { setter( n ) { userName = n; } getter() { return userName; } } property addr { setter( a ) { userAddr = a; } getter() { return userAddr; } } property tel { setter( t ) { userTel = t; } getter() { return userTel; } } function show() { return name+"\t"+ addr+"\t"+ tel+"\n"; } }
変数「userName」「userAddr」「userTel」は外部から参照せず、プロパティ「name」「addr」「tel」で代替します。
本来なら、プロパティ内で電話番号の桁数を補正する方が望ましいのですが、今回は省略します。
最初に必要なメンバーを整理しましょう。
関数名は出来るだけ短く、分かりやすい名前にすると扱いやすくなりますね。 まずは骨組みだけ作り、一つずつ実装しましょう。
//個人情報を管理する住所録クラス class addrAdmin { var data = new Array(); function addrAdmin(){} function finalize(){} function add(){} function show(){} function nameSearch(){} function nameShow(){} function deleteName(){} }
アドレス帳は特に初期化する処理がありませんので、コンストラクタは空っぽです。
デストラクタは、最後に重要な処理をさせますので、まずは空っぽにして書いておいてください
個人情報を管理するのは配列dataですから、 配列dataの最後に情報を追加していけば良い事が分かります。 (配列の最後に個人情報オブジェクトを作るという事)
配列の最後に追加するには色々方法がありますが、今回は以下の理論を利用します。
配列の最後の箱と配列のサイズを示すプロパティcountは等しい
これを図に示すと分かりやすいですね。
よって、これは次のように書く事が出来ます。
data[data.count] = 値
今回、要素に入れるのはAddressクラスの実物(個人情報クラス)です。よって、次のように追加します。
data[data.count] = new Address(...);
関数addが以下になります。
function add(name) { data[data.count] = new Address(...); System.inform( name+"さんの情報を追加しました"); }
追加した事が分かるように、メッセージを表示して終わりです。
関数addの引数が一つしか書かれていない点に注目してください。 記号「...」は、ここに書かれた引数を渡すのでは無く、 受け取った引数全てを渡す、という動作になります。
Addressクラスのコンストラクタは次のようになっています。
Address( name="名無し" , addr="不明" ,tel="")
そして、例えば関数addを呼び出す際に
住所録.add("田中","大阪府")
と書けば、住所録上に名前=田中、住所=大阪府、電話番号無しという個人情報が作られる事になります。
情報の表示は、個人情報クラスのメンバー関数showを呼び出し、受け取った関数値を表示すれば良い事が分かります。 ただし、このまま表示させてもいけません。
//Addressクラスの関数 function show() { return name+"\t"+ addr+"\t"+ tel+"\n"; }
各変数の値を文字列として返しているだけですから、それが何を指すデータなのか分からないのです。 よって、題名(キャプション)を返す関数を用意しておきましょう。
正確には、これらはプロパティです。 よって、各プロパティのgetter関数によって、内部の変数の値を取得しています。
情報を表示する関数郡は以下になります。
//キャプションを返す関数 function caption() { return "名前\t住所\t番号\n"; } //全データを表示する function show() { if ( !data.count ) { System.inform( "データはありません" ); return; } var tmp=caption(); for ( var i=0; i<data.count; i++) { tmp += data[i].show(); } System.inform( tmp ,"全アドレス情報"); }
配列dataの要素には、Addressクラスの実物が入っています。 よって、Addressクラスのメンバーを参照するには
data[箱番号].show()
としている点に注意してください。 このshowはAddressクラスのメンバー関数です。
実行例は次のようなものです。
この関数は、入力された名前と全個人情報を照らし合わせて、 一致した名前の情報が入っている配列の添え字を返すのが目的です。
方法はいくつか考えられますが、今回は最も簡単な比較を行います。 入力された名前とAddressクラスのプロパティnameとを比較し、 一致したらその箱番号を返す、という作りです。
data[i].name == name
より厳密に探すなら「===」を使っても良いです。
では、これを実装した関数を示します。
function nameSearch(name) { var idx; for ( var i=0; i<data.count; i++) { if (data[i].name == name) { idx=i; break; } } return idx; }
変数idxは、見つかった情報が入っている配列の箱番号を指しています。 情報があればidxに箱番号を代入し、関数値として返します。
見つからなければ何もせず、そのままidxを返しています。 この時、変数idxはvoidになっています。
変数や配列の要素などは、ただ宣言しただけではvoidが入ります。 以前に少し説明しましたが、覚えているでしょうか。
入力された名前を前回作成した関数nameSearchに渡し、 番号が返されたら(名前が存在したら)その人物の情報を表示、 存在しなければその旨メッセージを表示、という動作を行います。
検索結果を得るには次のように行います。
var idx = nameSearch(name);
これで、引数nameに入っている名前で全個人情報を検索し、 該当した情報を持つ配列の箱番号を変数idxに代入します。 もし該当しなければ、idxにはvoidが入っていますので、その判断は容易のはずです。
という事で、実装例は以下のようになります。
function nameShow(name) { var idx = nameSearch(name); if ( idx===void ) { System.inform( "名前が見つかりません" ); return; } System.inform( caption()+data[idx].show(), name+"さんのアドレス情報" ); }
くどいようですが、idxの値を調べるのに次のように書いてはダメです。
if ( !idx )
voidは偽ですからこれでも良いのですが、 配列の箱番号0が返った場合、やはり弾かれてしまいます。(0も偽だからです。)
個人情報を削除するという事は、個人情報オブジェクトを無効化して使えなくするという事です。 まず、オブジェクトの無効化について、リファレンスを紐解いてみましょう。
TJS2 では、オブジェクトが削除される際、オブジェクトの無効化とオブジェクトの削除、という2つの段階を踏みます。
すなわち、使い終わったオブジェクトを無効化しておけば、 そのオブジェクトが削除されるという仕組みになります。
オブジェクトを無効化するには、次の一行を書けば終わります。
invalidate obj;
これで、オブジェクトobjが無効化され、使えなくなります。 今回は、配列の要素が個人情報のオブジェクトなので、次のように書きます。
invalidate data[1];
これでdata[1]というオブジェクトが無効化されます。
C++
オブジェクトの削除にdelete演算子を使わない点に注意しましょう。
TJSでは、delete演算子はローカル変数(およびメンバー)の削除、 invalidate演算子はオブジェクトの無効という動作をします。
よって、invalidate演算子はC++で言うdelete演算子とほぼ同様であると言えます。
さて、個人情報オブジェクトが無効化されたからと言って、終わりではありません。 なぜなら、情報を管理している配列の箱はまだ残っているからです。
例えば、情報が三件あったとして、1番目のオブジェクトを無効化したとします。
このように配列の箱自体は残ります。 当然、この時のdata[1]は使えませんから、配列全てに操作した場合にエラーになってしまう。 よって、配列の箱自体も削除しなければなりません。
しかし、心配はご無用。 配列クラスには便利なメンバー関数が数多くあります。今回使うのはメンバ関数eraseです。
data.erase(1)
たったこれだけで、配列dataの1番目の箱が削除されます。 箱が削除されると、それ以降の箱は順番に前に詰められます。
これで個人情報というオブジェクトを無効化し、対応する配列の箱を削除する事が出来ます。 後は名前で情報を表示した要領で、削除させてみましょう。
function nameShow(name) { var idx = nameSearch(name); if ( idx===void ) { System.inform( "名前が見つかりません" ); return; } //情報の削除 invalidate data[idx]; //↑オブジェクトを無効にしておいてから…… data.erase(idx); //↑箱自体を消す System.inform( name+"さんの情報を削除しました"); }
もし、オブジェクトを無効にする前に箱を消してしまうと、少し問題が残ります。 この点は後ほど説明を入れます。
(この項目は多少難しいので、さらっと流しても構いません)
通常、オブジェクトなどは作成する際にメモリに割り当てられます。 (メモリというのはお使いのパソコンに搭載されているアレです。)
これは、Ctrl+Alt+Delを押して表示されるタスクマネージャーを見れば分かります。
krkr.exeというプログラムが今回使っているファイル名ですから、 約11MBの容量を使っている事になります。
ここから、オブジェクトをいくつか作成して、 メモリの使用量が明らかに増加していれば、オブジェクトがメモリを消費する事の証明になります。
約12MBの容量を使っています。これは、オブジェクトが追加された分だけ、メモリの使用量が増えたと言えます。
裏を返せば、オブジェクトを無効化し、削除するという行為は、 このようにオブジェクトが消費したメモリを解放するという事です。
もし使い終わったオブジェクトを削除しなかったらどうなると思いますか? プログラムは終了しているのにメモリは消費されたまま、なんて状況が起こります。 このような状況をメモリリークと呼び、やってはならない重大な不具合の一つです。 (オブジェクトが残る事からオブジェクトリークとも呼ばれます
いかに、オブジェクトを削除する事が大切か、なんとなく分かっていただけたなら幸いです。
前項で少し脅かしてしまいましたが、吉里吉里は実に親切な設計になっていて、 オブジェクトは不要になった時点で自動的に無効化され、削除されます。 この時、後始末関数であるfinalizeが呼ばれます。 (このような機能はガベージコレクション機能と呼ばれます。)
なんだ~、じゃあ手動で無効化する事は無いじゃないか、と思われるかもしれません。 ですが、リファレンスには次のような記述があります。
TJS2 ではいつオブジェクトが削除されるかの明確な規定が無く、削除や無効化は「いつでもおこりうる」ことになります。
これは後始末関数finalizeがいつ呼ばれるか分からないという事を意味しています。 そのオブジェクトは既に必要無く、違う処理をしている時にfinalizeが呼ばれ、場合によっては大問題になる事もありえます。 だから、使い終わったら手動でオブジェクトを無効化し、finalize関数を呼んでおく、という事が重要になるわけですね。
さて、ようやくfinalize関数の役目を説明する時が来たわけですが、 説明が冗長になって来たので簡単に要約してしまうと、 クラスが管理しているオブジェクトを無効化するのが主な役割です。
今回、アドレス帳クラスは個人情報クラスを管理していますから、 アドレス帳が不要になった時点で、これらのオブジェクトを全て無効化しておくことが望ましいと言えるのです。 これも個人情報配列dataをfor文で操作し、全て無効化していけば良いですね。
for ( var i=0; i<data.count; i++) { invalidate data[i]; }
では、finalize関数を完成させましょう。
function finalize() { for ( var i=0; i<data.count; i++) { invalidate data[i]; } data = void; }
これでアドレス帳が不要になったらfinalizeが呼ばれ、 全ての個人情報オブジェクトが無効化されるという仕組みが出来ました。
アドレス帳の全てのプログラムが完成しました。 最後に、これらを実際に使うためのメニューを作ります。
方法は色々あると思いますが、今回は次のような仕組みにします。
まず、これらのメニュー操作は一度だけしか行えないようでは困るので反復文を使います。
var data = new addrAdmin(); var i=true; while(i) { }
変数iをループ変数として利用し、iが偽になればループが終わる仕組みです。 よって、1~4以外が入力されると偽……例えばfalseを変数iに代入してやればループが終わりますね。
このように、比較する変数が一つの場合、 処理を分岐させるのに便利な文がありますね。switch文です。
switch(menu) { case 1: //全ての個人情報表示 break; case 2: //名前で情報を検索し、表示する break; case 3: //個人情報を追加 break; case 4: //名前で情報を検索し、削除 break; default: i=false; break; }
各処理を一つずつ見ていきましょう
これは単純に、アドレス帳クラスの関数showを呼び出せば終わります。よって
data.show();
と一行書いて終了です。
この機能はメンバー関数nameShowで実装してありますから、 次のように書けばよい事が分かります。
data.nameShow( 名前 )
この名前は住所録を使う側が入力出来ないと困ります。 よって、System.inputStringを使って文字列を入力させ、 その結果を渡してやれば大丈夫ですね。
data.nameShow( System.inputString("データ参照","名前の入力",,) );
この関数の名前はaddです。よってdata.add()と書けば良いのですが、 引数として「名前」「住所」「電話番号」を受け取ります。
よって、それぞれ一つずつ入力させ、結果を引数として渡します。
data.add( System.inputString("データ追加","名前の入力",), System.inputString("データ追加","住所の入力",), System.inputString("データ追加","電話番号の入力",) );
この例では、直接関数addに値を渡していますが、 分かりづらければ次のように書いて構いません。
var name = System.inputString("データ追加","名前入力",); var addr = System.inputString("データ追加","住所入力",); var tel = System.inputString("データ追加","電話番号入力",); data.add( name , addr , tel);
これは「名前で情報を検索し表示」と同じ要領で行えます。 違うのは関数名がdeleteNameである、というだけです。 すなわち
data.deleteName( System.inputString("データ削除","名前の入力",) );
これで完成です。
最後にメニューを作りましょう。 入力された番号に応じて処理をさせるのですから、 使うのはやはりSystem.inputStringです。
ただし、番号が整数値である必要があるので、以前作った関数intInputsを流用します。
function intInputs() { var num; do { num = System.inputString(...) -0; } while( typeof num != "Integer"); return num; }
使い回せる関数を作って来た恩恵が、こんなところで生きてきます。 もし、この関数が局所的にしか使えない代物ならば、 一から作り直す手間が生じます。
switch文は、変数menuの値と比較していきますので、 メニュー画面は以下のようにします。
var menu = intInputs(
"メニュー番号を入力",
"1:全件参照 2:名前検索 3:追加 4:削除 0:終了",);
これで簡易住所録プログラムの全てが完成しました。
プログラム全体をまとめました。
//個人情報を管理するクラス class addrAdmin { var data = new Array(); function addrAdmin(){} function finalize() { for ( var i=0; i<data.count; i++) { invalidate data[i]; } data = void; } function add(name) { data[data.count] = new Address(...); System.inform( name+"さんの情報を追加しました"); } function caption() { return "名前\t住所\t番号\n"; } function show() { if ( !data.count ) { System.inform( "データはありません" ); return; } var tmp=caption(); for ( var i=0; i<data.count; i++) { tmp += data[i].show(); } System.inform( tmp ,"全アドレス情報"); } function nameSearch(name) { var idx; for ( var i=0; i<data.count; i++) { if (data[i].name == name) { idx=i; break; } } return idx; } function nameShow(name) { var idx = nameSearch(name); if (idx === void) { System.inform( "名前が見つかりません" ); return; } System.inform( caption()+data[idx].show(), name+"さんのアドレス情報" ); } function deleteName(name) { var idx = nameSearch(name); if (idx === void) { System.inform( "名前が見つかりません" ); return; } invalidate data[idx]; data.erase(idx); System.inform( name+"さんの情報を削除しました"); } } //住所と名前を保存する個人情報クラス class Address { var userName; var userAddr; var userTel; function Address( name="名無し" , addr="不明" ,tel="") { this.name = name; this.addr = addr; this.tel = tel; } function finalize(){} property name { setter( n ) { userName = n; } getter() { return userName; } } property addr { setter( a ) { userAddr = a; } getter() { return userAddr; } } property tel { setter( t ) { userTel = t; } getter() { return userTel; } } function show() { return name+"\t"+ addr+"\t"+ tel+"\n"; } } function intInputs() { var num; do { num = System.inputString(...) -0; } while( typeof num != "Integer"); return num; } //アドレス帳を使う var data = new addrAdmin(); var i=true; while(i) { var menu = intInputs( "メニュー番号を入力", "1:全件参照 2:名前検索 3:追加 4:削除 0:終了",); switch( menu ) { case 1: data.show(); break; case 2: data.nameShow( System.inputString("データ参照","名前の入力",,) ); break; case 3: data.add( System.inputString("データ追加","名前の入力",), System.inputString("データ追加","住所の入力",), System.inputString("データ追加","電話番号の入力",) ); break; case 4: data.deleteName( System.inputString("データ削除","名前の入力",) ); break; default: i=false; break; } }
一通りの機能が実装でき、住所録らしく完成させましたが、 やはり問題点というか、不便と感じられる事がいくつかありますね。
そこで、TJS2リファレンス、及び吉里吉里2リファレンスを読みながら、以下の改良にチャレンジしてみてください。
今回で基礎知識編は終了です。お付き合いありがとうございました。
……どうしても分からないという方は、 情報のセーブとロードの例を紹介しておきます。 説明はあえてしませんので、リファレンスを参考にしてください。
//個人情報セーブ function saveAddress() { var tmp = []; for (var i=0; i<data.count; i++) { tmp[i] = [data[i].name,data[i].addr,data[i].tel]; } tmp.saveStruct("data.txt"); }
以上を追加し、例えば情報を追加後や削除後に関数saveAddressを呼び出せば、 自動的にデータをセーブするようになります。
//個人情報ロード function loadAddress() { //ファイルdata.txtが無ければ終了 if (!Storages.isExistentStorage("data.txt")) { return; } var tmp = Scripts.evalStorage("data.txt"); for (var i=0; i<tmp.count; i++) { add(tmp[i][0],tmp[i][1],tmp[i][2]); } }
住所録のコンストラクタで、関数addrAdminを呼び出せば、 自動的に読み込んだ情報分のオブジェクトを作成します。
*ファイル名「data.txt」は、startup.tjsの先頭で変数に代入しておくと尚良いです。