配列とファイル入出力
前回までに配列の使い方の基本を覚えましたね。 今回は配列のメンバー関数を使った配列の応用例を紹介します。
メンバー関数を扱うと、配列の要素を並び替えたり、データをファイルに保存、読み込み…等が行えるようになります。
ストレージを知る
ファイルの入出力を行うには、そのファイルが何処にあるのか、 どこにファイルを保存するのか、それを正確に指示しなければなりません。 例えば、次のように書いても、吉里吉里は理解できません。
var path = "sample.txt";
sample.txtが何処にあるのか、吉里吉里は知らないからです。
吉里吉里では、各ファイルやフォルダを総称してストレージと呼び、 そのファイルの位置情報を示す文字列を統一ストレージ名と呼んでいます。 つまり、ファイル名を指定しなさいと言われたら、この統一ストレージ名で指定すれば良いのです。
詳しいストレージの仕様についてはリファレンスを参照頂くとして、 今回はSystemクラスのプロパティを参照して、二つのストレージ名を知ることに留めます。
System.exePath …吉里吉里本体があるフォルダ
System.dataPath…データ保存場所
吉里吉里本体とは、プログラムを実行する時にクリックしたお魚アイコン「krkr.eXe」を指します。 (ファイル名を変えている場合は読み替えてください。)
データ保存場所とは、吉里吉里本体と同じ場所に作られる、ユーザデータ保存用のフォルダを指します。 (標準で savedata という名前になり、一度でも吉里吉里を実行すると自動的に作られます。)
最初に示した sample.txtの場所を正確に指すには、次のようにします。
System.exePath+"sample.txt" …吉里吉里本体と同じ場所にあるsample.txt
System.dataPath+"sample.txt" …データ保存用フォルダと同じ場所にあるsample.txt
統一ストレージ名は文字列ですから、このように+記号で加算します。 ファイルはデータ保存場所へ保存するのが基本ですから、 今後はSystem.dataPathを原則として使用することにします。
本入門では、統一ストレージ名をストレージ名、 System.dataPathを、単にデータフォルダと書く事があります。
ファイルに書き出す
ゲームやツール類を作ると、必ず必要となるのがファイル操作です。 データの読み書きが出来ないと、データベースソフトとして開発しても、 ソフトを終了すれば全データが消えてしまい、使い物になりませんよね。
TJSでは配列のメンバー関数の呼び出しのみでこれが行えます。
save(ファイル名)
これで、配列内のデータが指定ファイル名で保存されます。 では、配列データをデータフォルダ内に保存する例を見てみましょう。 とは言っても、たった二行の簡単なものです。
var dim = [1 , 2 , 3 , 4 , 5];
dim.save(System.dataPath+"sample.txt");
保存されたファイルsample.txtをご覧ください。 配列のデータは次のようになっている点を確認しましょう。
1 2 3 4 5
このように、一行目に最初の要素、二行目以降は順番に要素が保存されます。
関数の第二引数に"c"を指定すると、内容が暗号化されます。 "z"を指定するとファイルが圧縮されます。
save(System.dataPath+"sample.txt" , "c") …暗号化
いずれの場合も、通常のテキストエディタでは読み込めませんので、 内容を知られたくないデータを保存する時に用います。
※ただし、プログラムに詳しい方なら、簡単に復号(内容を元に戻すこと)が出来ますので、 この機能に頼りきった仕様にするのはいけません。
ファイルを読み込む
データを保存するだけでは使い道が限られますので、 保存したファイルを読み込む方法も覚えましょう。
ファイルの読み込みも、配列の関数で実装されています。
load(ファイル名)
最初に次のファイルをデータフォルダに作ってください。 名前はloadtest.txtとします。 (三行目は空白にしてください。何も入力せずにリターンキーを押します。)
読み込みのテストです。 これは二行目の文字列です。 9999 void このように、文字列以外も読み込めます。
では、上のファイルを配列に読み込ませましょう。
var dim = []; dim.load(System.dataPath+"loadtest.txt"); var str="読み込み結果\n\n"; for (var i=0; i<dim.count; i++) { str += dim[i]+"\n"; } System.inform(str);
実行結果から、一行目が配列の要素番号0、 二行目以降は順番に配列の要素に代入されているのが分かります。
また、voidと書いた行は文字列のvoidとされている点に注意してください。 空白やvoidと書いても、全て文字列として読み込まれるという事です。
匿名配列と関数
今回の利用の場合、配列を作って変数に入れ、メンバー関数を呼び出すという、少し手間な事をしています。 これを一文にまとめると、次のように書けます。
var dim = [].load(System.dataPath+"loadtest.txt");
※[].loadは配列クラスを作り、load関数を呼び出します。 その結果を変数dimに入れているので、変数dimはloadtest.txtの内容が入った配列になります。
同様に、関数も名前をつけずに利用できます。
test( function(a){ System.inform(a); } ); function test( f ) { f("匿名関数の呼び出し"); }
このように、関数や配列といったオブジェクトは、名前を付ける事無く実物を作り、利用出来ます。 これを匿名関数、匿名配列と呼ぶ事があります。
最初は扱いが難しいですが、この考え方を利用すれば、 関数や配列を宣言する事無く、関数に渡すという事も可能です。 今は分からなくても、頭に片隅にでも入れておいてください。
func( function(a){}) …関数を関数に渡す
func( [1,2,3,4,5] ) …配列を関数に渡す
ファイルの存在確認
先ほど、loadtest.txtというファイルを作り忘れたり、 名前を打ち間違えたりして、エラーが出なかったでしょうか。
プログラムを作る際は、起こり得る事象を全て考慮して作らないと、 時にはとんでもない不具合が起こったりします。 (飛躍すれば、ウィルスやOSの暴走で、ファイルが勝手に消される可能性もあります。)
よって、今回の場合は読み込むファイルが存在するか…正確に言えば、 指定したストレージが有効かどうかを確かめる必要があります。
Storages.isExistentStorage(ストレージ名)
isExistentStorageはStoragesクラスの関数で、 指定したストレージが存在するなら真、無ければ偽を返してくれます。 これを利用して、if文で処理を分岐させます。
Storagesは吉里吉里のストレージに関する操作や情報を得る事が出来るクラスです。 詳しくはリファレンスを参照してください。
var path = System.dataPath+"無効なファイル.txt"; // ストレージの存在によって分岐 if ( Storages.isExistentStorage(path) ) { System.inform("ストレージは存在します。"); } else { System.inform("ストレージ「"+path+"」は存在しません。"); }
この関数を使えば、特定ファイルが存在すれば読み込み、 存在しなければ新しくファイルを書き込んでから、それを読み込み…といった処理が実現できます。 これは、アプリケーションの設定ファイルなどに応用できますね。
配列構造を維持した入出力
loadとsaveを使った読み書きでは、配列が二次元配列、 またはそれ以上の構造になっていた場合、非常に不便です。 関数splitやjoin(当入門では説明しません。)を駆使すれば実現できますが、 今回は配列の構造をそのままに読み書きする関数を紹介します。
まず、配列のデータ構造を維持したまま保存する関数です。
saveStruct(ファイル名)
saveの後ろにStructがくっ付いている以外は、save関数と使い方は変わりません。
var dim =
[
[1,2,3,4,5],
[6,7,8,9,10]
];
dim.saveStruct( System.dataPath+"struct.txt" );
dimは配列を二つ持った配列です。 保存したstruct.txtの中を覗いて見ると、頭に(const)がくっ付いていますが、 二次元配列の構造になっている事が確認出来ます。
この構造を読み込む方法ですが、 配列クラスのload関数では、一行ずつ読み取ってしまうので、うまく動作しません。 ここは、新しいクラスとその関数を使います。
Scripts.evalStorage(ストレージ名)
この関数について、リファレンスには次のように書かれています。
指定されたストレージを読み込み、その内容を TJS2 式として評価します。
すなわち、ファイルの内容自体をTJSとして処理して、 その結果を返してくれる関数という事ですね。 保存したstruct.txtの内容は、配列構造になっていますから、 TJSの式として有効なものなので、この関数を利用できます。
var dim = Scripts.evalStorage( System.dataPath+"struct.txt" );
配列の並び替え
配列は0から始まる添え字を利用して、要素を並べています。 この原理を利用して、要素の値が大きい順、または小さい順に並べ変える事が出来ます。
このような並び替えなら、反復文を利用すれば可能ですが、 ここはもっと簡単に並び替えるための関数を使います。
sort();
これだけで、データを小さい順に並びを変えられます。
// 順不同データを小さい順に並べる var dim = [3,4,2,5,1]; dim.sort(); var str; for (var i=0; i<dim.count; i++) { str += dim[i]+"\n"; } System.inform(str);
結果は1,2,3,4,5という表示になったと思います。 このような配列を並び替える事をソートと呼びます。 また、データを小さい順に並べることを昇順ソートと呼びます。 (値が大きいほうへ昇っていくから昇順と覚えましょう。)
今度は値が小さい順に並べます。 sort関数に引数を指定すれば可能ですが、今回は次の関数を使います。
reverse();
この関数で、配列の並びが後ろから頭に…つまり、逆順になります。 よって、sort()呼び出しの後にreverse();を呼べば、データは大きい順に並び替えられます。
この方法では、データが文字だった場合にうまく動作しません。 文字列を降順…Z~Aに並べたい場合は、後述のsort("z")を使用してください。
// 順不同データを大きい順に並べる var dim = [3,4,2,5,1]; dim.sort(); dim.reverse(); var str; for (var i=0; i<dim.count; i++) { str += dim[i]+"\n"; } System.inform(str);
表示が5,4,3,2,1となった事を確認しましょう。 データを大きい順に並べることを降順ソートと呼びます。 (値が小さいほうへ降りていくから降順と覚えましょう。)
sort()関数への引数
第一引数に以下の文字を指定すると、それぞれ並び替える方法を変えられます。 うまく並び替えられない場合、要素のデータ型を考えて、 以下の指定を行って見てください。
sort("+") | 通常の昇順ソート |
---|---|
sort("-") | 通常の降順ソート |
sort("0") | 数値の昇順ソート( 1,2,3,4,5 ) |
sort("9") | 数値の降順ソート( 5,4,3,2,1 ) |
sort("a") | 文字の昇順ソート( a,b,c,d,e ) |
sort("z") | 文字の降順ソート( e,d,c,b,a )) |
ソート関数
単なる昇順や降順ではなく、希望のソートを行う場合、 sort関数の第一引数に関数を指定することが出来ます。
ソートに使う関数をソート関数と呼び、次のような構造をしています。
// 昇順ソートを行うソート関数 // 常に引数を二つ取ります。 function sortFunc( a , b ) { // 関数値は真偽値 // aがbより小さければ真を返します return a < b; }
この関数をsort関数の第一引数に渡すと、 関数で指定した並び順で、配列がソートされます。
dim.sort(sortFunc);
※関数を呼び出すわけではないので、後ろに()を付けない点に注意しましょう。
分かりづらいと思うので、順に説明します
- 1.引数のaとbは配列の要素が渡される
-
指定配列の要素が整数だった場合、aとbには1と6や、100と90といった値が渡されます。
dim = [1,2,3,4,5]
この場合、aとbは2と5、3と4…というように、全要素が順に渡されていきます。
- 2.aとbを比較させる
-
渡された要素aとbで、希望の比較をします。 ここでは比較するための演算子「>」「<=」等が使えますね。
sortFunc関数では、「a<b」という演算式によって、 aがbよりも小さければ真、そうでなければ偽を返すという処理になっています。 この真偽値によって、配列要素aとbのどちらが前に置かれるのかが決定されます。
- 3.真か偽を返す
-
ソート関数は常に真偽値を返します。真を返すとaとbのどちらかが前に、偽を返すと後ろに置かれます。
sortFunc関数では「a<b」が返す真偽値を元に関数値としていますので、 aがbより小さければ真…すなわち、aとbの値が小さい方を前に置く、という意味になります。 結果として、このソート関数は値を小さい順にソートするという関数になるのです。
実際に動かして、昇順にソートが行われる事を確認しましょう
//昇順ソートを行うソート関数 function sortFunc( a , b ) { return a < b; } var dim = [3,4,2,5,1]; dim.sort(sortFunc); var str; for (var i=0; i<dim.count; i++) { str += dim[i]+"\n"; } System.inform(str);
ソート関数はソート専用の関数なので、他の場所で使われることはまずありません。 よって、匿名関数のコラムで紹介した方法で、 関数を直接渡す方法がよく使われます。
dim.sort( function ( a , b ) { return a < b; } );
尚、この関数に限っては処理がはっきりと分かるので、 一行で書いても構いません。
dim.sort( function ( a , b ){ return a < b; } );
配列オブジェクト
説明してきませんでしたが、配列の中にオブジェクトを入れてしまった方…例えば、 次のような配列を作った方がいるかもしれませんね。
class human { var human_name; var human_age; function human( name,age ) { human_name = name; human_age = age; } property name { getter() { return human_name; } } property age { getter() { return human_age; } } } var dim_h = []; dim_h[0] = new human("山田",33); dim_h[1] = new human("斉藤",16); dim_h[2] = new human("田中",54);
humanは人間という設計図で、名前と年齢を入れるメンバー変数と、 それぞれを表示するプロパティを持つだけのクラスです。
配列でオブジェクトを管理する事で、オブジェクトの扱いを楽にしてくれるだけではなく、 全オブジェクトに対して、配列のメンバー関数が使えるというメリットが生まれます。
dim.h.count …… 全オブジェクトの数を知る
dim.h.clear() …… 全オブジェクトを消去する
今回のdim.h.clear()という使い方は、少しまずいです。 何がいけないのか…それはもう少し後に分かります。 気になる方はリファレンスを覗いて、考えて見てください。
という事は、オブジェクトのメンバーの値を元に並び替える事も可能という事です。 ここでは、各オブジェクトの年齢が高い順(降順)に並び替える例を示します。
dim_h.sort( function ( a , b ) { return a.age > b.age; } ); var str; for (var i=0; i<dim_h.count; i++) { str += dim_h[i].name+"\t"+dim_h[i].age+"\n"; } System.inform(str);
着目点は、ソート関数の次の処理です。
return a.age > b.age;
引数aとbは、配列の要素が順に渡されると書きました。 ここで渡される要素とはオブジェクトなので、 そのオブジェクトのプロパティage同士を比較する…という方法を取っています。
このように、配列でオブジェクトを扱うようになると、 大量のオブジェクトを容易に管理できるようになって効率的です。 例えばカードゲームでは、全カード52枚を配列化し、ソート関数を使ってシャッフルする…というような処理が実現できそうですね。
はみ出しコラム
ここまで色々複雑で難しい仕様を覚えてきましたが、 まだウィンドウどころか、グラフィック機能すら扱えていないじゃないか、 というお声がそろそろ聞こえてきそうですね。
確かに、表示はSystem.informに頼りっきりですし、 ある程度のプログラムが作れるようになってくると、物足りないかもしれません。
ただ、今一度考えて見てください。 プログラムに初めて触れたというあなた、どれだけのプログラムを作ってきましたか? 100?200?300?きっとそれより少ないでしょう。
数多くのプログラムを自分なりに考えて作った人と、そうでない人との決定的な差は応用力です。 今楽しめている方はきっと、これまでの知識だけでも素晴らしいモノを作れる方でしょう。 少し物足りない方は、まだまだ余力が残っているはずです。
…と偉そうに書いている私ですが、プログラムの世界全体で言えば、一割くらいの知識しか持っていないと思います。 プログラミングというのは、それだけ広大で、迷宮であり砂漠であり、沼であるのです。
ただし、今は少し苦痛でも、この先きっとオアシスが見えてきます。 そして、素晴らしい世界が待っています…と自身を持って言えます。 今はとにかく、苦痛を楽しみましょう。