プログラミング初心者向けのTypeScript入門連載。第7回は繰り返し処理のうちfor文とfor ... in文を詳しく解説する。TypeScriptでプログラミングへの理解を一歩深めよう。
前回は、TypeScriptの繰り返し処理のうちwhile文とdo ... while文について説明した。今回は、for文とfor ... in文を基礎から詳しく解説していく。
前回の概要と重複するが、繰り返し処理のうち、for文とfor ... in文の機能は以下の通りである。
まずは、簡単な例を使ってfor文の書き方と処理の流れを見てみよう。
以下のプログラムは、ブラウザーに「ピザ」という文字を10回表示するプログラムだ。特に役に立つプログラムではないが、for文の機能が確認できる。
for (var i = 0; i < 10; i++) { // (1)
document.body.innerHTML += "ピザ<br/>"; // (2)
}
(1)が10回繰り返すためのfor文、(2)が文書に「ピザ<br/>」というHTMLの記述を追加する文だ。Playgroundを開いて上のプログラムを入力し、[Run]ボタンをクリックすると、図1のような結果が表示される。for文の範囲内に書かれた文が10回実行されるので結果は言わずもがなだが、一応確認しておこう。
for文の書き方については後回しにすることとして、プログラムの細かいところを見ておこう。(2)の「document.body.innerHTML」は、文書の<body>タグ内に含まれるHTMLの記述という意味だ。
「+=」は、左辺と右辺の両方が数値ならば加算して代入する演算子だが、いずれかが文字列なら、文字列を連結して代入する演算子となる(もちろん、左辺が数値型の変数の場合に右辺に文字列型のデータを指定するとエラーになる)。
</br>は改行を表すタグである。HTML文書では改行文字(\n)があっても改行されるわけではないので、</br>タグを挿入して改行する。若干脇道にそれるが、以下のように書いても改行されないことに注意しよう。
document.body.innerHTML += "ピザ\n";
document.writeln("ピザ");
writelnメソッドは文字列に改行文字を付加してHTML文書に挿入するが、やはり改行はされない(alertメソッドで表示されるメッセージの場合は改行される)。
では、for文の書き方をまとめておこう。実行する文が1つのときには、形式1のような書き方となる。実行する文が複数のときには、形式2のように{ }で囲んで複合文にすればよい。複合文にしたブロックも1つの文として取り扱われるので、実質的に形式1と形式2に違いはないのだが、分かりやすいように別々に示しておいた。
形式1のフローチャートも併せて示しておく。「文」の部分は複合文でも構わないので、形式2でもこの流れになる。
式1、式2、式3の働きを以下にまとめておく。
実際のところ、for文のフローチャートや処理の流れなどを知らなくても、以下の形式でコードを書けば、必ずn 回の繰り返しができる。
for(var i = 0; i < n ; i++){
文1;
文2;
…
}
上の図では制御変数の変数名にiを使っているが、もちろん別の変数名でも構わない。nの部分には繰り返したい回数を書く。
多少プログラミングの経験を積めば間違うことはないのだが、初心者に多い間違いに、for文の「()」の後にセミコロンを書いてしまうというものがある。セミコロンは文の終わりを示すので、for文の中で実行される「文 」として「何もしない」という文(空文)が指定されたものと見なされる。例えば、以下のプログラムを実行してみると、どうなるか考えてみてほしい。
for (var i = 0; i < 10; i++); { // (1)
document.body.innerHTML += "ピザ<br/>";
}
実際にやってみると、「ピザ」が1回しか表示されない。(1)のfor文で空文が10回繰り返し実行された後、{ }の部分が実行される。{ }は単に複合文のブロックを作っているだけで、for文の範囲外であることに注意しよう。
PlaygroundでJavaScriptにコンパイルされたプログラムを見ると、以下のようになっているので、期待した通りに動かないことが推測できる。
for (var i = 0; i < 10; i++)
; // 空文
{
document.body.innerHTML += "ピザ<br/>";
}
TypeScriptでは、変数の宣言時にデータ型を指定しなくても、初期値を設定すればデータ型が推測されます。この機能を型推測と呼びます。例えば、サンプルプログラムでは、for文の中で制御変数iを宣言していますが、データ型は指定されていません。しかし、0を代入しているので数値型として扱われます。
型推測によってデータ型が決められた変数に、異なるデータ型の値を代入しようとするとエラーになります。それでも、データ型を指定して宣言した方が間違いを防ぐのには役立ちます。ただし、for文の制御変数のように局所的にしか使われないことが確実に分かっている場合は、型推測を使った方が簡潔な記述になります。
注意しないといけないのは、宣言時にデータ型も指定せず、初期値も代入していない場合です。そのような場合にはany型と見なされ、どのようなデータ型の値でも代入できるようになります(後で数値を代入したからといって、数値型にされるわけではありません)。何でも代入できるので一見便利なように思われますが、誤って異なるデータ型の値を代入してしまう可能性があるので、特別な理由がない限り、そのような書き方は避けた方がいいでしょう。
以下の例は、最初に示したプログラムと同じ意味になります。
for (var i: number = 0; i < 10; i++) {
…… 省略 ……
}
しかし、以下の例は、any型と見なされるので、変数iには文字列も代入できてしまいます。普通に考えると、変数iに文字列を代入することはないでしょうが、潜在的なエラーの可能性を持っていることは確かです。
var i;
for (i = 0; i < 10; i++) {
変数iをfor文の外で宣言する必要がある場合には、「var i」の後に「:number」と書き、数値であることを明示した方がいいでしょう。
for文の中にfor文を書くと「繰り返しの繰り返し」ができる。if文のときにも触れたが、このような形のコードをネスト(入れ子)と呼ぶ。for文をネストさせると、行列形式のデータを自然な感覚で取り扱うことができる。
以下の例は、RPGゲームによくあるダンジョン(地下の迷宮)内の1つの部屋を表示するプログラムである。部屋の広さは10列×10行として、文字「.」が特に何もない床を表すものとする。
document.body.style.fontFamily = '"Courier", "monospace"';
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
document.body.innerHTML += "."; // (1)
}
document.body.innerHTML += "<br/>"; // (2)
}
1行目は、フォントを固定ピッチフォントにするための設定である。
多少厳密さには欠けるが、for文をネストした場合は、内側の繰り返しが先に実行されると覚えておくといい。以下のような2桁のカウンターのイメージで捉えると分かりやすいだろう。
イメージがつかめたところで、プログラムの処理を、以下の表で 順を追って細かく見てみよう。
制御変数i | 制御変数j | 動作 |
---|---|---|
0 | 0〜9 | 制御変数iの値は最初0になっていて、制御変数jについて以下が行われる ・ 制御変数jの値が0から9まで変わる間、(1)の文によって「.」が1個ずつ表示される。従って、「.」は10個表示される ・ 制御変数jの値が10になると内側の繰り返しを抜ける |
0 | 10 | (2)の文によって「<br/>」が追加され、改行される |
1 | 0〜9 | 次に制御変数iの値が1になり、制御変数jについて以下が行われる ・ 制御変数jの値が0から9まで変わる間、(1)の文によって「.」が1個ずつ表示される。従って、「.」は10個表示される ・ 制御変数jの値が10になると内側の繰り返しを抜ける |
1 | 10 | (2)の文によって「 」が追加され、改行される ……以下同様に繰り返される |
ネストしたfor文の動作 |
実行結果は以下の通り。
さすがにこれだけでは味気ないので、部屋の中のランダムな位置にモンスターを1匹出現させよう。英字1文字がモンスターなどのキャラクターを表すものとする。本来ならばモンスターを表すクラスを作るなどして、汎用的なプログラムにするのだが、ここでは、できるだけコードを単純にしてある。
var m_name: string = "D";
var x: number = Math.floor(Math.random() * 10); // (1)
var y: number = Math.floor(Math.random() * 10); // (2)
document.body.style.fontFamily = '"Courier", "monospace"';
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i == y && j == x) { // (3)
document.body.innerHTML += m_name;
} else {
document.body.innerHTML += ".";
}
}
document.body.innerHTML += "<br/>";
}
(1)でモンスターのx位置を決め、(2)でモンスターのy位置を決めている。x位置、y位置とも、0以上10未満のランダムな整数となる。for文のネストは最初の例とほとんど同じだが、(3)で制御変数iの値と変数yの値が等しく、かつ制御変数jの値と変数xの値 が等しいときだけ、モンスターの名前を表示する。それ以外の場合は「.」を表示する。
では、次にfor ... in文を見てみよう。for ... in文はオブジェクトのプロパティ名を1つずつ取り出して順に処理していく。そして、取り出すプロパティ名がなくなると繰り返しの終了となる。こちらも簡単な例から見ていこう。オブジェクトに関してはまだ何も学んでいないが、繰り返し処理の書き方と働きは分かるはずなので、心配せずに読み進めていってほしい。
以下の例は、モンスターの名前、HP(生命力)、MP(魔力)を表示するプログラムである。
class Monster { // (1)
name: string;
hp: number;
mp: number;
}
var m: Monster = new Monster(); // (2)
m.name = "Dragon";
m.hp = 100;
m.mp = 200;
document.body.innerHTML = "モンスターのステータス一覧<br/>";
for (var x in m) {
document.body.innerHTML += x + ":" + m[x] + "<br/>"; // (3)
}
(1)ではクラスを定義している。(2)ではクラスからオブジェクトを作成し、変数mで参照できるようにしている。これらのコードの書き方は回を改めて説明するが、以下のような図のイメージで働きを捉えておくといい。
若干の語弊はあるが、クラスとはある機能を持った部品の設計図のようなもので、その設計図から作られた実際の部品がオブジェクトに当たる。クラスにはその性質を表すプロパティと、動きを表すメソッドがある。
といった一般的な説明はさておき、要するに、変数mで参照されるオブジェクトにはname、hp、mpという3つのプロパティがあるということだ。(3)では、for ... in文で順に取り出されたプロパティ名(変数xに代入されている)を表示する。つまり、最初は変数xに“name”が代入され(3)の文が実行される。次は変数xに“hp”が代入され(3)の文が実行される。続いて、変数xに“mp”が代入され(3)の文が実行される。そして、それ以上プロパティがないので、繰り返しを抜ける。
なお、(3)ではオブジェクト名[プロパティ名を表す文字列]という書き方で、プロパティの値も取り出し、併せて表示している。実行例は以下の通り。
では、for ... in文の書き方を確認しておこう。for文の場合と同様、形式1と形式2の違いは、文が単一の文であるか複合文にしているかだけなので、実質的に同じことを表している。
for ... in文の働きは、オブジェクトのプロパティ名を1つずつ取り出して順に処理していくことであると述べたが、実際にはプロパティ名だけでなくメソッド名も取り出される。が、ここではこれ以上深入りしないことにしよう。
最後に、for ... in文の使い方に関する留意事項について触れておこう。for ... in文を使えば、配列の全ての要素を処理することもできる。記述が簡略になるので、for文の代わりにfor ... in文を使いたくなるが、配列に対してfor ... in文を使うのは避けた方がいい。
配列についても、まだ本連載では取り扱っていないが、実例を見ればだいたい意味が分かるだろう。以下の例は、配列の隣り合う要素を足し合わせて、その結果を表示するプログラムである。かなり強引ではあるが、for ... in文を使って書いてみたものだ。しかし、残念ながら期待した結果は得られない。
var data: number[] = [10, 20, 30, 40, 50];
var sum: number;
for (var i in data) {
if (i >= data.length - 1) break; // (1)
sum = data[i] + data[i + 1]; // (2)
document.body.innerHTML += sum + "<br/>";
}
dataという名前の配列には5つの要素がある。配列の要素は0から始まるインデックスを[]の中に書いて指定する。
繰り返し処理の中では、(2)で0番目の要素と1番目の要素を足し合わせ、次に1番目の要素と2番目の要素を足し合わせ...という順に処理が進められる。(1)では、最後から1つ手前までの要素を処理するために、最後の要素にたどり着いたら繰り返しを抜けている。……というつもりのコードなのだが、(2)の部分が正しく動かないので、結果は以下のようになってしまう。
期待通りに動かなかった理由は、for ... in文の場合、変数に代入されるオブジェクトのプロパティ名は文字列であるということだ。配列の場合はインデックスが代入されるのだが、やはり文字列として代入される。従って、最初は変数iに0ではなく"0"が代入される。最初の繰り返しでは、(2)は以下のようになる。
sum += data["0"] + data["0" + 1];
そのため、配列のインデックスは以下のように計算される。
sum += data["0"] + data["01"];
data["0"]はdata[0]と見なされるが、data["01"]はdata[1]とは見なされない。存在しないデータを利用しようとしているので、NaNという結果が表示されたというわけだ。それ以降も同様である。
そういうわけで、配列の処理には素直にfor文を使った方がいい。正しく動くプログラムと実行結果は以下の通りだ。
var data: number[] = [10, 20, 30, 40, 50];
var sum: number;
for (var i = 0; i < data.length - 1; i++) {
sum = data[i] + data[i + 1];
document.body.innerHTML += sum + "<br/>";
}
JavaScript 1.6以降では、オブジェクトのプロパティの値を取り出すfor each ... in文が使えます。for ... in文ではプロパティの名前を取り出すのに対して、for each ... in文はプロパティの値を取り出すことに注意してください。ただし、for each ...in文はECMAScriptやTypeScriptでは使えません(当然のことながらエラーになります)。プロパティの値を取り出すには、サンプルプログラムで見たような方法を使ってください。
今回はTypeScriptの繰り返し処理のうち、for文とfor ... in文を取り上げた。一定回数の繰り返しや、オブジェクトのプロパティ名を列挙するのに便利であるということが分かったと思う。
次回は同じ目的の変数を数多く使うときに便利な配列について解説する。配列とfor文は相性のいい組み合わせなので、今回の話のおさらいも含めて詳しく解説する。
Copyright© Digital Advantage Corp. All Rights Reserved.