05 反復処理
Contents
- ループとは?
- while文
- 無限ループ
- do文
- for文
- foreach range文
- foreach_reverse
- ループを抜ける, 次に進める
- ループから抜け出す
- ループを次に進める
- 問題 -> 解答
- おわりに
- キーワード
- 仕様
ループとは?
反復処理(ループ)というのは、作業の繰り返しのことです。 プログラムでのループは、「決まった状態になるまで続ける」という処理を記述します。
while文
while文は典型的なループ処理を記述できる文です。 たとえば、自然数を足していき、和が100を超えた時点での値を出力したいとします。
/// test00601.d
import std.stdio;
void main()
{
int n = 1, sum;
while(sum < 100){ // (sum < 100) がtrueの間は繰り返す
sum += n;
++n;
}
writeln(n-1);
writeln(sum);
}
$ rdmd test00601.d
14
105
例のwhileは、数学をしらない人間が愚直に100になるまで足していく、という作業をそのまま表しています。
実際に14までの和は、14 * (14 + 1) / 2 => 105ですし、13までの和は105 - 14 => 91となりますから、正常に計算できているようです。
最後のほうでwriteln(n-1);となっていますが、なぜnじゃなくてn-1なのでしょうね?
少し考えればわかると思いますので、処理の流れを理解するいい練習になると思います。
さて、whileの詳細ですが、whileはif同様にwhile(<expression>) <statement>という形式を取ります。
whileの意味は、「<expression>がtrueの間、<statement>を実行する」ということです。
もし、最初から<expression>がfalseであればどうなるでしょうか?
<statement>は一度も実行されなくなります。
/// test00602.d
import std.stdio;
void main()
{
while(false)
writeln("foooooooo");
writeln("bar");
}
$ rdmd test00602.d
bar
無限ループ
無限ループとは、入ったら永続的に実行され続けるループです。
whileは無限ループを簡単に表現できます。
while(1) //while(true)でもOK
<statement>
無限ループは後述するforでfor(;;)とも書かれたりします。
do文
while文の場合、条件<expression>が最初からfalseであれば一度も実行されないと書きました。
でも、一度は必ず実行して欲しい時があります。
その要望に答えるために、do文といいdo <statement> while(<expression>);という文があります。
/// test00603.d
import std.stdio;
void main()
{
int i;
do
writeln(i);
while((++i) < 5);
}
$ rdmd test00603.d
0
1
2
3
4
do文の処理の流れは、一度<stamenet>を実行してから、<expression>を評価し、trueであればまた<statement>を実行し、<expression>を評価し、...というようになります。
while文とは、<statement>の実行と<expression>の評価の順序が入れ替わっただけですが、これによって絶対に1回は<statement>が実行されます。
for文
一番最初に示した例test00601.dのループでは、条件式sum < 100と更新式++nを持つことがわかります。また、よく考えるとint n = 1, sum;というのはn = 1; sum = 0;を表しているとも考えられます。これを初期化式といいます。
以上をまとめると、test00601.dのループには条件式と更新式、初期化式があることがわかりました。
実は、この3つを綺麗に書ける文があります。
for文といいます。
/// test00604.d
import std.stdio;
void main()
{
int n, sum;
for(n = 1, sum = 0; sum < 100; ++n)
sum += n;
writeln(n-1);
writeln(sum);
}
$ rdmd test00604.d
14
105
for文は、for(初期化子; 条件式; 更新式) <statement>という形式をとります。
処理の流れは、まず初期化子が実行され、条件式により判定されます。条件式がtrueであれば、文<statement>が実行されます。
<statement>が終われば、更新式が評価され、また条件式が通れば<statement>が実行され、更新式が評価され、....を繰り返します。
for文をwhile文で書き換えてみると、次のようになります。
{ //新しいスコープを作る
初期化子;
while(条件式){
<statement>
更新式;
}
}
初期化式と書かずに初期化子と書いたのには理由があって、初期化子では変数の宣言ができます。
次のfor文を使ったループの記述は、C言語の至る所で見る典型的なforの使い方です。
(D言語では、後述するforeachを普通は使います)
/// test00605.d
import std.stdio;
void main()
{
for(int i = 0; i < 5; ++i)
writeln(i);
}
$ rdmd test00605.d
0
1
2
3
4
変数iのスコープは、for文内のみなので、for文の外側ではiにアクセスできません。
foreach range文
foreach range文は、for文の特殊形式だと言えます。
foreach(<identifier>; <exprLower> .. <exprUpper>) <statement>という形式をとります。
for文でforeach range文を表すと、次のようになります。
foreach(<identifier>; <exprLower> .. <exprUpper>)
<statement>
と、以下は同等
{ // 新しいスコープを作る
auto index = <exprLower>; // autoは型を自動でつけてくれる
auto exprUpper = <exprUpper>;
for(; index < exprUpper; ++index){
auto <identifier> = index; // <identifier>はindexのコピー
<statement>
}
}
さらにwhileで書き直すと
{ // 新しいスコープを作る
auto index = <exprLower>; // autoは型を自動でつけてくれる
auto exprUpper = <exprUpper>;
while(index < exprUpper){
auto <identifier> = index; // <identifier>はindexのコピー
<statement>
++index;
}
}
たとえば、「1から10までの総和を取りたい」のなら、次のように記述します。
/// test00606.d
import std.stdio;
void main()
{
int sum;
foreach(i; 1 .. 11) //[1, 11)
sum += i;
writeln(sum);
}
$ rdmd test00606.d
55
foreach range文は、for文より用法が限られますが、単純な範囲を回すループを記述するのに役立ちます。
- 明示的に
<identifier>の型を記述することも可能です。
foreach(ulong i; 0 .. 100)
foo();
foreach(ref <identifier>; <exprLower> .. <experUpper>)とすることで、ループのインデックスを操作可能です。
foreach(ref <identifier>; <exprLower> .. <exprUpper>)
<statement>
と、以下は同等
{ // 新しいスコープを作る
auto <identifier> = <exprLower>; // autoは型を自動でつけてくれる
auto exprUpper = <exprUpper>;
for(; <identifer> < exprUpper; ++<identifier>)
<statement>
}
さらにwhileで書き直すと
{ // 新しいスコープを作る
auto <identifier> = <exprLower>; // autoは型を自動でつけてくれる
auto exprUpper = <exprUpper>;
while(<identifer> < exprUpper){
<statement>
++<identifier>;
}
}
例を示すと、以下のようになります。
~~~~d
/// test00607.d
import std.stdio;
void main()
{
foreach(i; 0 .. 10){
writef("%s, ", i);
++i; // refナシなので意味なし
}
writeln();
foreach(ref i; 0 .. 10){
writef("%s, ", i);
++i; // refアリなので見かけ上2ずつ進む
}
}
$ rdmd test00607.d
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
0, 2, 4, 6, 8,
- イテレート可能な型
foreach range文で、辿る範囲の型はインクリメント++と等価テスト==が定義されていればどのような型でも可能です。 たとえば、doubleなどの浮動小数点型や、以下の例中に定義されているstruct Incrementableは++と==が可能なのでforeach rangeで使えます。
/// test00608.d
import std.stdio;
void main()
{
foreach(i; 0.5 .. 5.5)
writeln(i);
foreach(e; Incrementable(0) .. Incrementable(10))
writeln(e);
}
struct Incrementable
{
ref typeof(this) opUnary(string s : "++")()
{
++_value;
return this;
}
private:
int _value;
}
$ rdmd test00608.d
0.5
1.5
2.5
3.5
4.5
Incrementable(0)
Incrementable(1)
Incrementable(2)
Incrementable(3)
Incrementable(4)
Incrementable(5)
Incrementable(6)
Incrementable(7)
Incrementable(8)
Incrementable(9)
foreach_reverse
foreach range文には、逆順に辿るforeach_reverseというものがあります。
/// test00609.d
import std.stdio;
void main()
{
foreach(i; 0 .. 5)
writeln(i);
foreach_reverse(i; 0 .. 5)
writeln(i);
}
$ rdmd test00609.d
0
1
2
3
4
4
3
2
1
0
ループを抜ける, 次に進める
現実から抜け出したいことはよくありますよね。 少なくとも私は、毎日のように「現実を壊したい、人生をコンティニューしたい」と思ってます。 プログラムでも、ループから抜けだしたいことはありますし、コンティニューしたいことがあるんです。
ループから抜け出す
break文は、ループという檻を破壊します。
つまり、ループから抜け出します。
/// test00610.d
import std.stdio;
void main()
{
foreach(i; 0 .. 100){
if(i > 10)
break;
writeln(i);
}
}
$ rdmd test00610.d
0
1
2
3
4
5
6
7
8
9
10
break文は実行されれば、最も内側のループを抜け出します。
ネストされた複数のループを抜けたい場合には、ループ文にラベルをつけて解決します。
import std.stdio;
void main()
{
LbreakLabel0:
while(1)
LbreakLabel1:
while(1)
while(1)
break LbreakLabel0;
}
break LbreakLabel0;が無ければ、無限ループになって何時までたっても終わらなくなります。
ループを次に進める
たとえば、1~50までの偶数のみの総和はcontinueを使うと次のようになります。
void main()
{
int sum;
for(int i = 0; i <= 50; ++i){
if(i % 2 != 0)
continue;
sum += i;
}
writeln(sum);
}
どういうことかというと、continueは文の実行を中止して、更新式++iと条件式i <= 50を評価します。
そして、その後また最初から文を実行します。
別の表現を使用すると、「ループされる文の最後までジャンプ」します。
例では、sum += i;の後までジャンプすると捉えることもできます。
また、continueもラベルを指定することができます。
import std.stdio;
void main()
{
LcontinueLabel:
do{
while(1)
LcontinueLabel1:
while(1)
continue LcontinueLabel;
}while(0);
}
問題 -> 解答
test00601.dで、writeln(n);でなくてwriteln(n-1);となっている理由は?foreach_reverseをfor文で書き直すとどうなるでしょうか?1000未満の自然数で、3の倍数もしくは5の倍数の総和を計算するプログラムをループを使って作ってください。(Project Euler Problem 1より)
フィボナッチ数列
1, 2, 3, 5, 8, ...を考える。数列の項の値が400万以下の偶数である項の合計を求めるプログラムを作ってください。(Project Euler Problem 2より)最初の100個の自然数の2乗の総和と、総和の2乗の差を出力するプログラムを
forを使って作ってください。その後、foreachを使用するように書き換えてみましょう。(Project Euler Problem 6より)while(1),break,continueを使って、「標準入力から得点を受け取り、平均を計算する。ただし、負の数を受け取った場合には平均と合計点を出力して終了する。また、10未満の得点は無視して平均や合計には含めないとする」ようなプログラムを作ってください。
おわりに
おつかれさまです。 ループが書けるようになると、相当プログラムの幅が広くなります。 そのため、今回の問題の量はいつもより多いと思います。
次はその他の制御文として、goto, switchの紹介をしたいと思います。
キーワード
whiledo-whileforforeach rangebreakcontinue