第15回 いつも心にカーソルを「4.高度な繰り返し」

15−4 高度な繰り返し

ここで、ちょっと妙な考えを起こしてみましょう。
Do〜Loopでは簡単に作ることができた無限ループを、For〜Nextを使ってみる事にします。

そもそも、For〜Nextは、カウンタが最終値に到達すればループを抜けますので、それを逆手にとって最終値に達しないようにすれば無限ループになりますね。
まずは、1回ループするごとに最終値を増やしてみることにします。
    
    n=1
    
    For i=0 to n
        n=n+1
    Next
    
一見すると、常にカウンタ i は、最終値の n に達しないため、無限のループになるように見えますが、実際にこれを実行しても無限ループにはなりません。
なぜなら、Do〜LoopとFor〜Nextでは、そのループの方法が若干異なるからです。

For〜Nextの繰り返しは、具体的には次のような仕組みになっています。
この図の通り、For〜Nextでは、初期値と最終値をループが始まった時点で設定してしまいますので、途中で最終値が増えようが減ろうが、そのループは当初の設定どおり終わってしまいます。
したがって、どうしても無限ループにしたい場合、最終値を変更するのではなく、カウンタの方を変更しなければなりません。
    
    n=1
    
    For i=0 to n
        i=i-1
    Next
    
こうすることで、カウンタ用の変数iは、1回のループする間に相殺されて0となり、永遠に終わらない仕組みが出来上がります。
通常は、わざわざこんなことをする人もいないでしょうが、プログラムを組んでいたら『結果的になってしまう』可能性は十分あります。こういう無限ループにならないよう、注意するに越したことはありません。
ちなみに、Do〜LoopにはFor〜Nextのような明示的なカウンタが存在しません。そのため、必ずWhileもしくはUntilの条件式をキチンと評価して判定しますので、ループ内で条件が変わっても、それに対応した判定が行われます。
ある意味、こちらも『結果的に』無限ループに陥ってしまう可能性を持っているわけです。
どちらも気をつけましょう。


ところで、先ほどのフローチャートをよく眺めてみると、変だなぁ、と感じる部分が1ヶ所あります。
1回ループをした後、ループを続けるか否かをどこで判定しているんだろう?という『具体的な場所』です。
いや、本来はどうでもいいことなんですが、ここでは、ちょっとこだわってみましょう。
    
    For i= 0 to 10
        {処理}
    Next
    
For 〜 Next では、最初に初期値と最終値を決めてしまう、ということでしたが、この図に実際のコマンドを当てはめてみると、不可解なことになります。
つまり、ループの1回目が終わり、Nextから前に戻る『場所』がどこなのか?という点です。
Forの行は、初期値と最終値を決める行なので、Nextから戻るのは、その『次の行』、すなわち For〜Nextとは無関係な『普通のコマンド』がかかれている行です。
そうすると、実際にカウンタの加算(減算)と条件判定を「Next」が行っていると考えるのが自然ですね。
    
    ???
    
しかし、次の場合を考えてみます。
前項で登場した1回もループしないループ(?)ですが、もし、先ほどの図のように「Next」がカウンタ処理と条件判定を行っているならば、最低1回は「In」が表示されるはずです。
    
    For i=1 to 0
        MsgBox "In"
    Next
    MsgBox "Out"
    
    
    結果は、
    
        「Out」が1回のみ
    
    
しかし、この場合は結果として「Out」が1回表示されるだけです。
とすれば、このフローのように、ループに入る前のForの段階と、Nextの2ヶ所で、ループのチェックを行っていると考えても良さそうです。
しかし、何処かしらスマートさに欠けますよね。

ここで、存在を忘れかけていた「To」に注目してみましょう。
実は「To」が条件判定、「Next」がカウンタの処理をしていると考えれば、かなりスッキリします。
実際にランタイムなどが、BASICコマンドをどのように解釈しているかという事とは無関係に盛り上がってしまいましたが、このように、フローチャートを使って考えるとそれなりに理解がしやすくなるのがお分かり頂けるかと思います。
特に、全体図もそうですが「今、どこを処理しているの?」という具体的な場所をイメージするには最適な道具です。
とは言え、これほどFor〜Nextにこだわった解説は不要ですよね。一般の解説書のように「Nextでは、Forの次の行に戻ります」としておく程度で十分です。


 さて、脱線しましたが、元に戻りましょう。
ループといっても、実際のプログラミングにおいては、直線的に1つのループを実行するだけではありません。
場合によっては2重、3重のループを扱う場合があります。
例えば、画面に掛け算の「九九」を表示することを考えてみましょう。

ご存知のとおり、九九は1から9を掛け合わせた81通りの数値の羅列で、一覧表にするならば、縦、横、それぞれ9つずつの数字が並ぶことになりますが、このように縦、横に展開する値を扱う場合、繰り返しを1重で考えるより、次のように2重に考えたほうが、はるかに効率よく処理することが可能です。
    
    For Y=1 To 9
        For X=1 To 9
            DrawChars Str(X*Y),X*15,Y*15
        Next
    Next
    
これを適当なボタンに貼り付けて実行すると、このような結果になります。
単なる数字だけですが、九九の表が出来上がっていることが分かります。
もし、1重のループだけで作ろうと思うと、結構面倒です。

ところでこのループの順番、プログラムをよく見ればわかるかと思いますが、外側のループが1つ進む間に、内側のループが1回完結します。
内側のループの変数Xは、描くべき文字列の横方向を指定していますから、プログラム的には、1行描いては下へ、また1行描いては下へ、を繰り返しています。
もちろん、XとYのループを入れ替えても動作しますが、入れ替えた場合は、1列描いては横へ、という動きに変わることは理解できると思います。
         
どちらの場合でも九九の表が画面に描かれる訳ですが、変数を入れ替えることで描く順番が異なることに注意しましょう。
特に、実行の順番が動作上、何らかの問題になる場合、その結果に大きく影響します。

多重ループのように、内側、外側がキチンと決まっている構造を専門的には「ネスト(入れ子)構造」と言います。
そして、多重ループのネストが重なることを「深さ」や「段」という呼び方で呼ぶこともあります。
NS Basicの多重ループのネストは、最高10段までの深さが許可されています。
    
    For a=1 to 10
        For b=1 to 10
            For c=1 to 10
                For d=1 to 10
                    For e=1 to 10
                         For f=1 to 10
                             For g=1 to 10
                                 For h=1 to 10
                                     For i=1 to 10
                                         For j=1 to 10
    
                                         Next
                                     Next
                                 Next
                             Next
                         Next
                     Next
                 Next
             Next
        Next
    Next
    
10段とは思ったより少なく感じるかもしれませんが、内側がループしている間も外側のループ中の値を記憶し、さらに外側のものも記憶しなければならないため、一時的に記憶しておく領域に思った以上に負担がかかります。
そのため、多重ループにも制約があるわけですが…ひとまず、10段もあれば大丈夫ですね。

さて、ネストについて「内側、外側がキチンと決まっている構造」と書きましたが、キチンと決まらない場合もあるんでしょうか?
あります。ループが重なってしまう場合です。
例えば、
    
    For i=0 to 5
        Do Until j=5
            {処理}
    Next
        Loop
    
あえて対応するよう、先頭をそろえてみましたが、このようにループが交差する構造はエラーになります。
順に考えてみましょう。
まず、最初の、For〜Nextの処理は問題なく行われるはずです。途中にあるDo untilはネストが許す限りは関係ないですから。
一方、無事にFor〜Nextのループを抜けた後、Loopにたどり着きますが、これもDoに戻るでしょう。しかし、その次に登場するNext、これには戻るべきForがありません。
こういう場合、コンパイル時にエラーになるか、実行時にエラーになります。

まぁ、ループのだけの処理で交差するような構造を書いてしまうことは少ないでしょうが、このネストというのは分岐のコマンドでも重要な意味を持っています。
    
    If A=1 Then
    
    For i=0 to 9
    
       {処理}
    
    End If
    
    Next
    
このコードのEnd IfとNextの位置に注意しながら、順を追ってみていけばどういう結果になるか分かると思いますが、こういう簡単なミスを防ぐために、プログラムを書く時はネストごとに先頭を揃えて書くように心がけましょう。
旧来のBASICでは、Nextの後に、どの変数をループさせているのか省略できなかったので、このようなループの交差について、比較的大きな解説ポイントとして取り上げられていました。
    
    昔のBASICでは…
    
    FOR A=0 TO 9
        FOR B=0 TO 9
            {処理}
        NEXT A ← 変数名は省略不可
    NEXT B   ← 変数名は省略不可
    
しかし、Nextの後の変数が省略できるようになったことと、先頭揃えをする書き方が定着してきましたので、話題になることが少なかったように思えます。
その一方、ある分野では「ネスト」の話題をよく見かけます。
それは、構造化言語XMLのタグです。
入門的な解説書などでは、同じようなタグのHTMLが結構省略可能だったのに対して、XMLはキチンとネストを守らなければならないよ、と比較して説明されていたりします。
元々、プログラミングなどでキチンとネストを守ってきた人たちから見れば、むしろ自然でしょうが、省略OKだったHTML派などから見れば、面倒だなぁ、と思うことでしょう。
どちらにしろ、頭の固いコンピュータには、人間の都合で省略するよりも、キッチリ指示する方が効率が良いのかな、などとも思ってしまいます。

えっと、話が外れてしまいましたが、多重ループが交差することによって発生するエラー、それは繰り返しだけではなく、分岐などあちらこちらで発生する可能性を持っています。
プログラムコードの先頭を、ネストごとに揃えるなどして、複雑な処理中でも、間違いを最小限に押さえるような工夫を心がけましょう。

前へ     目次へ     次へ

第15回 いつも心にカーソルを「4.高度な繰り返し」