ここから本文です

PHPのfor文&怖い無限ループから脱出できるbreak/スキップできるcontinue

6/27(火) 8:40配信

@IT

●ループ中のbreak文

 オープンソースのWeb開発向けスクリプト言語「PHP」の文法を一から学ぶための入門連載「Web業界で働くためのPHP入門」。

 今回はループをよりスッキリと書き表すための構文について解説します。

 なお、PHP 7.1.6が2017年6月8日にリリースされています。本連載の範囲では特に影響はないと思いますが、幾つかバグフィックスされていますので、アップデートしておきましょう。

○【お題】素数かどうか調べるロジックを考えてみよう

 ちょっと複雑なループの例として、素数かどうか調べるロジックを考えてみましょう。素数というのは、1と自分自身以外の数で割り切れない、1より大きい整数のことです。例えば、2、3、5、7、11などが素数になります。4は2で割り切れ、6は2または3で割り切れるため素数ではありません。

 素数かどうかの判定方法はいろいろありますが、最も単純な方法は、実際に割り切れる数があるかどうかを総当たりで探してみることです。割る数を2から始めてインクリメントするループを作り、割り切れる数があるかどうかを探すのです。割り切れるかどうかは、割ったときの余りを求める算術演算子「%」を使います。余りが0のときは割り切れるということです。

 まず、ここまでのロジックを実装してみます。素数かどうかを調べたい数値を$num変数に格納するものとすると、次のようになります。

・リスト1 phplesson/chap07/checkPrimeNumber.php
――――――
<?php
$num = 15; // 素数かどうかを調べたい数
$i = 2;
while($i < $num) {
if ($num % $i == 0) {
print($num."は素数ではありません<br>");
}
$i++;
}
print("素数判定が終了しました。");
――――――

 while文では$i変数をカウンターにして、2から$num未満までのループを作っています。5行目のif文では、$numを$iで割った余りを求め、0の場合は素数ではないという表示を行っています。この例の$numは15ですから素数ではありません。この表示が行われることが期待できます。それでは実行してみましょう。実行結果は以下のようになります。

――――――
15は素数ではありません
15は素数ではありません
素数判定が終了しました。
――――――

 2回表示されてしまいました。15は$iが3と5のときに割り切れるため、ループ中のその2回でprint文が実行されたためです。これでは、いまいちですね。

 表示1回目の3で割り切れた時点で素数ではないので、その先のループを実行する必要はありません。つまり途中でループから抜けたいわけです。

○switch文だけではなく、ループ処理からも抜けられるbreak文

 こういった目的のために、break文があります。break文はswitch文から抜ける目的で使いましたが、それと同様にwhile文からも抜けることができます。

 では、リスト1にbreak文を追加して、割り切れた場合は表示を行った後、whileループから抜けることにしましょう。

・リスト2 phplesson/chap07/checkPrimeNumberWithBreak.php
――――――
<?php
$num = 15;
$i = 2;
while($i < $num) {
if ($num % $i == 0) {
print($num."は素数ではありません<br>");
break; // 【1】
}
$i++;
}
print("素数判定が終了しました。");
――――――

 【1】が追加したbreak文です。

 これを実行すると、以下のようになります。
――――――
15は素数ではありません
素数判定が終了しました。
――――――

 ちゃんと1行だけ表示されるようになりました。

○$numが素数だった場合

 次に、$numが素数だった場合を考えてみます。その場合は、「素数判定が終了しました。」としか表示されなくなってしまいます。素数の場合には、「〇〇は素数です。」という表示もするように改造してみましょう。

 この場合は最後のループまで実行されるため、カウンターの$iは$numと等しい値になっているはずです。そこでwhile文の後にその条件分岐を付け加えます。同時に、$numを固定値ではなく、2~20の乱数を使うことにしましょう。

・リスト3 phplesson/chap07/checkPrimeNumberWithRand.php
――――――
<?php
$num = rand(2, 20);
$i = 2;
while($i < $num) {
if ($num % $i == 0) {
print($num."は素数ではありません<br>");
break;
}
$i++;
}
if($i == $num) {
print($num."は素数です。<br>");
}
print("素数判定が終了しました。");
――――――

 乱数を使うので、実行するたびに結果が変わりますが、ここでは、素数の場合の結果を掲載しておきましょう。

――――――
7は素数です。
素数判定が終了しました。
――――――

 何度か実行して、結果を確認してみてください。

●break文+整数で抜けるループの指定

 次に、2~100の全ての整数について素数判定をすることを考えてみましょう。上記の例をさらにループで囲んでネストすれば実装できます。$numをカウンターにして2から100まで実行するだけです。

・リスト4 phplesson/chap07/checkPrimeNumberBetween2And100.php
――――――
<?php
$num = 2;
while($num <= 100) {
$i = 2;
while($i < $num) {
if ($num % $i == 0) {
print($num."は素数ではありません<br>");
break;
}
$i++;
}
if($i == $num) {
print($num."は素数です。<br>");
}
$num++;
}
print("素数判定が終了しました。");
――――――

 インデントが1段深くなっていますが、4~14行目はリスト3と同じものです。実行結果は以下のようになります。長いので途中を省略しています。

――――――
2は素数です。
3は素数です。
4は素数ではありません
5は素数です。
6は素数ではありません
~省略~
99は素数ではありません
100は素数ではありません
素数判定が終了しました。
――――――

 100までの範囲にある素数は、Wikipediaの素数のページに載っていますので、この結果が正しいか確認してみましょう。

 この例ではwhile文がネストして2重になっています。こうなると8行目のbreak文はどちらのwhileループから脱出するのだろうか、という疑問が生じるかもしれません。

 break文は、そのbreak文から見て最も内側のwhile文から脱出するようになっています。従って、この場合は意図した通りに動作します。

 仮に、素数ではない最初の数が見つかった時点で$numのループを終了したいとすると、内側ではなく外側のループから脱出しなければなりません。このような場合、break文にどのループを抜けるかを指定します。指定は何段目のループを抜けたいのかを整数で表記します。そのbreak文から見て最も内側のループを1段目と数え、そのすぐ外側のループであれば2となります。「break」は「break 1」と同等です。

 では、外側のループ、つまり、「break 2」を行うようなソースコードを記述してみましょう。

・リスト5 phplesson/chap07/checkPrimeNumberWithBreak2.php
――――――
<?php
$num = 2;
while($num <= 100) {
$i = 2;
while($i < $num) {
if ($num % $i == 0) {
print($num."は素数ではありません<br>");
break 2; // 【1】
}
$i++;
}
if($i == $num) {
print($num."は素数です。<br>");
}
$num++;
}
print("素数判定が終了しました。");
――――――

 先ほどのリスト4から変わったところは【1】のみです。これを実行すると、以下のようになります。

――――――
2は素数です。
3は素数です。
4は素数ではありません
素数判定が終了しました。
――――――

 $numが4のときに8行目で外側のループからも抜けているのが分かると思います。

●ループのスキップ

 では、リスト4の100までの素数判定に立ち返って、少しばかり効率の点で考えてみることにしましょう。

○偶数を除外する

 偶数は2で割り切れますので、$numが偶数の時点で素数ではないと判定できます。これを利用すれば偶数のときは内側のwhileループを実行せずに済みます。さらに、内側のループを3から開始できます。

 これをif文の条件分岐で組み込んでみましょう。$numが2のときについては偶数かつ素数なので例外的に扱わなければなりませんが、今回は説明のために簡略化し、3から開始することにします。

・リスト6 phplesson/chap07/checkPrimeNumberBetween3And100.php
――――――
<?php
$num = 3;
while($num <= 100) {
if($num % 2 == 0) {
print($num."は素数ではありません<br>");
} else {
$i = 3;
while($i < $num) {
if ($num % $i == 0) {
print($num."は素数ではありません<br>");
break;
}
$i++;
}
if($i == $num) {
print($num."は素数です。<br>");
}
}
$num++;
}
print("素数判定が終了しました。");
――――――

 実行結果は、最初の「2は素数です。」の表示がないことを除けば、リスト4と同じです。

 4行目が偶数のときの処理です。6行目からが偶数でないときの、これまでと同じ素数判定のループです。このやり方でも間違いはないのですが、素数判定のループ部分のインデントがさらに深くなってしまっています。むやみにインデントを深くするのは読みやすさの点で不利です。

○ループをスキップできるcontinue文

 こういったケースでは、continue文を使う方がいいでしょう。continue文はループ内のそれ以降の実行をスキップし、次回のループを始めます。continue文を使って書き直すと、次のようになります。
・リスト7 phplesson/chap07/checkPrimeNumberWithContinue.php
――――――
<?php
$num = 3;
while($num <= 100) {
if($num % 2 == 0) {
print($num."は素数ではありません<br>");
$num++;
continue; // 【1】
}
$i = 3;
while($i < $num) {
if ($num % $i == 0) {
print($num."は素数ではありません<br>");
break;
}
$i++;
}
if($i == $num) {
print($num."は素数です。<br>");
}
$num++;
}
print("素数判定が終了しました。");
――――――

 実行結果はリスト6と同じです。

 偶数の場合、【1】のcontinue文でその回のループは終了し、次回のループに移ります。continue文があることで、素数判定のループはelseの中に入れる必要がなくなるわけです。

○continue文+整数でスキップするループの指定

 なお、continue文もbreak文と同様に整数を指定することで、どのループをスキップするか指定できます。

 break文やcontinue文はループにおいて絶対に使わなければならないわけではありません。使わずにループを書くこともできますが、複雑なループになるほど無理やりな感じになり、読みにくく、メンテナンスしづらいものになります。うまく活用してループをスッキリと記述するように心掛けましょう。

□□
●コラム「無限ループ」

 while文の条件はループ実行前に評価されるので、もし条件が最初から偽であれば、ループは一度も実行されません。

 また、次のようにするとループから抜け出せない、いわゆる無限ループになります。

――――――
while(true) {
……
}
――――――

 これは意図した無限ループですが、条件や、条件に関わる変数の扱いを間違えてしまって、条件が偽にならず無限ループのバグとなることもあります。

 一方、意図した無限ループを実際に使うこともあります。例えば、以下のように、「ループの終了条件がループ中のさまざまな箇所で複数記述し、ループは無限ループにしておき、if文とbreak文でループを終了する」というような書き方です。

――――――
while(true) {
……
if (ループ終了条件1) {
break;
}
……
if (ループ終了条件2) {
break;
}
……
if (ループ終了条件3) {
break;
}
……
}
――――――
□□

●for文を使ったループ処理

 ここで、もう1つのループであるforを紹介しておきましょう。

○ループの考え方とfor

 今まで扱ってきたwhileループをもう一度見直してみてください。すると、ループ処理には以下の3種の処理が含まれているのが分かると思います。

・ループが始まる前の準備
・ループを続けるための条件
・ループが1回処理されるごとにその末尾で行う処理

 例えば、最初に行ったリスト1では、以下のようになります。

・ループが始まる前の準備→3行目の「$i = 2」
・ループを続けるための条件→4行目の( )内の「$i < $num」
・ループが1回処理されるごとにその末尾で行う処理→8行目の「$i++」

 ループ処理というものは上記3点セットが頻繁に起こります。whileループの場合は、それらがバラバラの位置に記述されています。これをまとめて記述できる構文があります。それがfor文です。for文の書き方は以下の通りです。

□□
●構文 for
for(ループ開始前の処理; 繰り返しを続ける条件; ループ1回ごとの末尾で行う処理) {
 繰り返す処理
}
□□

○while文をfor文で書き換えると

 では、リスト7をforを使って書き換えてみましょう。

・リスト8 phplesson/chap07/checkPrimeNumberWithFor.php
――――――
<?php
for($num = 3; $num <= 100; $num++) {
if($num % 2 == 0) {
print($num."は素数ではありません<br>");
continue;
}
for($i = 3; $i < $num; $i++) {
if ($num % $i == 0) {
print($num."は素数ではありません<br>");
break;
}
}
if($i == $num) {
print($num."は素数です。<br>");
}
}
print("素数判定が終了しました。");
――――――


 実行結果はリスト6と同じです。

 全体を通してかなり見通し良く書けるようになりました。while文では、偶数だった場合、continue文の前にインクリメントをしなければなりませんでしたが、for文ではそれもなくなります。なぜでしょうか。for文を模式化すると、次のようになります。

――――――
for(A; B; C) {
ループしたい文1;
ループしたい文2;
……
}
――――――

 continue文を考えないのであれば、これは先ほどの解説通り、次のwhileループと同じです。
――――――
A;
while(B) {
ループしたい文1;
ループしたい文2;
……
C;
}
――――――

 このwhileループの中でcontinue文を使った場合、「C」は実行されません。一方、for文では「continue文を使ったかどうか」にかかわらず、各ループの終了時は常に「C」を実行する点が異なります。

□□
●コラム「for文の省略」

 for文において、本文中のA、B、Cはそれぞれ省略することもできます。AとCを省略した場合は実行されないだけですが、ループ継続の条件式であるBを省略した場合、真と見なされます。つまり無限ループとなります。従って、while文による無限ループである

――――――
while(true) {
……
}
――――――

は、次のように書くこともできます。

――――――
for(;;) {
……
}
――――――

 意図的に無限ループを作る場合、どちらの書き方も見受けられます。また、これまでの連載で、ifで波かっこを省略できる場合や、whileで波かっこを省略できる場合について触れてきましたが、forについても全く同様です。
□□

最終更新:6/27(火) 8:40
@IT