条件分岐とジャンプ


条件によって処理を分岐させる

VBAのプログラムは上から下に向かって実行されます。

ここではプログラムの実行中に、条件によってプログラムの一部をスキップしたり、次に実行する処理を変えたりしたい場合の構文である、条件分岐を使ってみます。


if文

条件分岐の最も有名な構文はおそらく if 文でしょう。

これを使ってサイコロの出目によってメッセージを変えるプログラムを書いてみます。

'サイコロの出目によってifで分岐
Sub dice_If()
    Dim kekka(4) As Long
    Dim i As Long
    Erase kekka()
    Randomize
    For i = 1 To UBound(kekka)
        kekka(i) = Fix(Rnd * 6) + 1
        Cells(i, 1) = kekka(i)
        if kekka(i) = 6 then
            cells(i, 2) = "6なので大当たり"
        elseif kekka(i) = 1 then
            cells(i, 2) = "1なので大ハズレ"
        else
            cells(i, 2) = "2~5なので普通のハズレ"
        endif
    Next    
End Sub

if の後に条件式を書き、その後に then 、そして実行する処理のブロックを書くと、条件式が成立したとき(条件式の判定結果がtrueの場合)にだけ then の次のブロックが実行され、その後 end if までスキップする if 文の出来上がりです。

上の例だと kekka配列 の値を判定し、その内容によって処理を分岐させ、セルに異なる文字列を出力しています。

elseif の後の条件式は一つ目の条件式が満たされなかった場合に判定されます。

else 以下は、 if にも elseif にも当てはまらない場合に実行されるブロックです。 else の後には then を付けません。

VBAではif文の締めくくりに必ず end if を書く必要があります。

VBAの 『= (イコール記号)』は、if 文等の条件式では代入ではなく条件を意味しますので、混同しないように注意してください。


select文

判定対象の値によって処理を分岐させる場合、select文を使います。

上の if 文の例を select を使って書くと以下のようになります。

'サイコロの出目によってselect caseで分岐
Sub dice_Select()'
    Dim kekka(4) As Long
    Dim i As Long
    Erase kekka()
    Randomize
    For i = 1 To UBound(kekka)
        kekka(i) = Fix(Rnd * 6) + 1
        Cells(i, 1) = kekka(i)
        select case kekka(i)
        case 6:
            cells(i, 2) = "6なので大当たり"
        case is >= 2: 
            cells(i, 2) = "2~5なので普通のハズレ"
        case else:
            cells(i, 2) = "1なので大ハズレ"
        end select
    Next    
End Sub

select case』の後に条件式を書き、その結果(case)によって行う処理が分岐します。

caseの後には条件式とコロン(:)を書きます。

case内の処理が終了すると、それ以降の case は判定されず、次は end select の行が実行されます。

caseの条件式には『case is >= 2: 』のように範囲を指定することもできますが、他の言語の switch文では意外にもこのような使い方ができません 。


ifとselectの使い分け

if も select も最初に条件を満たしたらその後の条件式は判定されません。

一つの条件式の結果が複数に分かれ、その結果によって処理を分岐させたい場合る場合、if文よりもselect文を使った方が便利なことが多いです。

一方、最初の条件に当てはまらなかったときに全く別の条件式で判定を行いたい場合などはif文が適しています。

ちなみに case が実行された後に 次の case が実行されることをフォールスルーと呼びます。前述の通りVBAでは select文 のフォールスルーはできませんが、言語によっては(switch文のフォールスルーが)許可されていたり、禁止されていたり、明示することで利用することができたりします。


if文によるループの打ち切り

if文の使いどころとして、ループの打ち切りがあります。

for文ではループカウンタが上限に到達するまで繰り返し処理を実行しましたが、途中で処理を中断したいケースは多いはずです。

以下のコードは、特定の条件が成立したときにforループを終了させる処理です。

'forループを打ち切る
Sub test_Break()
    Dim i As Long
    dim hit as boolean
    randomize
    hit = false
    For i = 1 To 100
        if  fix(rnd*100) = 0 then'0~99までの乱数を取得
            hit=true
            exit for
        endif
    Next
    if hit = true then
        msgbox i & "回目に当たりが出ました"
    else
        msgbox "全部外れました"
        exit sub
    endif
    msgbox "おめでとうございます"
End Sub

forループ内の exit for で、forループは強制的に終了します。また、 exit sub でsubプロシージャ自体を終了させることができます。

当たりが出た場合、その時点でfor文を抜けるため、カウンタである i の値は当たったときのままですから、それを見れば何回目に当たりが出たのかを知ることができます。

上記コードを確認して、処理の流れを追ってみてください。


ジャンプ

上から下へと流れるプログラムの流れを変える代表的な命令文には CallGoToがあります。

Call命令

Callは、他の処理(関数)を呼び出すために使う命令です。プログラムの流れを変えると言うよりは、プログラムの途中に別のプログラムを挿入すると言った方が適切です。

VBAのプログラムは、基本的に1つのsubプロシージャ内に全て記述することができますが、余りにも長いプログラムは後で見たときに全体像がつかみにくく、同じ処理を何度も書くことになる場合があり、効率的ではありません。

以下の非効率的なコードを見てください。

'4回分の抽選結果を出力する
Sub test_4rnd()
    Dim i As Long
    dim hit as boolean
    randomize
    
    hit = false
    For i = 1 To 100
        if  fix(rnd*100) = 0 then'0~99までの乱数を取得
            hit = true
            exit for
        endif
    Next
    cells(1,1)="1回目"
    if hit = true then
        cells(1,2)= i & "回目に当たりが出ました"
    else
        cells(1,2)= "全部外れました"
    endif
    
    hit = false
    For i = 1 To 100
        if  fix(rnd*100) = 0 then'0~99までの乱数を取得
            hit = true
            exit for
        endif
    Next
    cells(2,1)="2回目"
    if hit = true then
        cells(2,2)=  i & "回目に当たりが出ました"
    else
        cells(2,2)= "全部外れました"
    endif
    
    hit = false
    For i = 1 To 100
        if  fix(rnd*100) = 0 then'0~99までの乱数を取得
            hit = true
            exit for
        endif
    Next
    cells(3,1)="3回目"
    if hit = true then
        cells(3,2)=  i & "回目に当たりが出ました"
    else
        cells(3,2)= "全部外れました"
    endif
    
    hit = false
    For i = 1 To 100
        if  fix(rnd*100) = 0 then'0~99までの乱数を取得
            hit=true
            exit for
        endif
    Next
    cells(4,1)="4回目"
    if hit = true then
        cells(4,2)=  i & "回目に当たりが出ました"
    else
        cells(4,2)= "全部外れました"
    endif
End Sub

上のコードでは全く同じ処理が何度も行われています。

このようなコードは、とりあえず以下のように書くことができます。ただし、このままでは上記のコードと異なり各回で何回目に当たりが出たかは分かりません。

'4回分の抽選結果を出力する_改
Sub lot100(hit)
    Dim i As Long
    hit = False
    For i = 1 To 100
        If Fix(Rnd * 100) = 0 Then '0~99までの乱数を取得
            hit = True
            Exit For
        End If
    Next
End Sub
Sub kekka(count, hit)
    Cells(count, 1) = count & "回目"
    If hit = True Then
        Cells(count, 2) = "当たりが出ました"
    Else
        Cells(count, 2) = "全部外れました"
    End If
End Sub
Sub test_4rnd_Call()
    Dim hit As Boolean
    Randomize
    
    Call lot100(hit) 'lot100関数を呼び出す
    Call kekka(1, hit) 'kekka関数を呼び出す
    
    Call lot100(hit) 'lot100関数を呼び出す
    Call kekka(2, hit) 'kekka関数を呼び出す
    
    Call lot100(hit) 'lot100関数を呼び出す
    Call kekka(3, hit) 'kekka関数を呼び出す
    
    Call lot100(hit) 'lot100関数を呼び出す
    Call kekka(4, hit) 'kekka関数を呼び出す
    
End Sub

何回も書かれていた処理を1つの関数(subプロシージャ)として定義し、この処理を実行したいタイミングで関数をCallするようにしたものです。

Call命令は、既に定義されている関数を呼び出す(コールする)命令で、呼び出された関数の処理が終了したときには、呼び出し元に戻ってきます。

これまで『msgbox "aaa"』と記述していたものは、『call msgbox("aaa")』のcallを省略したものと考えることができます。

引数

Callする場合、呼び出す関数に対して引数(ひきすう)と呼ばれる情報を渡すことができます。

『msgbox "aaa"』と『call msgbox("aaa")』で言えば、"aaa" が msgbox関数に渡される引数です。

そして上記コード(4回分の抽選結果を出力する_改)では、lot100関数に対して『hit』を、kekka関数に対して『整数とhit』を引数として使用しています。

lot100関数内ではhitの型(boolean型)を宣言していませんが、呼び出される際にhitを引数として受け取っているため、暗黙的に型が解釈されます。

以下は多少難しい話ですが、このケースではhitという真偽値が渡されたのではなく、hitというメモリ上のラベル(アドレス)が渡されている(参照渡し)ため、呼び先(lot100)内で変更したhitの値が戻り先(test_4rnd_Call)でも引き継がれています。

ただし、このように書いた場合に全てが参照渡しである保証はありませんから、引数を呼び先で変更するような場合、値渡し(ByVal)参照渡し(ByRef)かを明示的に指定するのがベターです(※VBAの仕様上は省略した場合参照渡しになる)。

これらを改良し、何回目に当たりが出たかも表示するコードを以下に示します。

'4回分の抽選結果を出力する_改その2
Sub lot100(ByRef hit As Long)
    Dim i As Long
    hit = 0
    For i = 1 To 100
        If Fix(Rnd * 100) = 0 Then '0~99までの乱数を取得
            hit = i
            Exit For
        End If
    Next
End Sub
Sub kekka(ByVal count, ByRef hit)
    Cells(count, 1) = count & "回目"
    If hit > 0 Then
        Cells(count, 2) = hit & "回目に当たりが出ました"
    Else
        Cells(count, 2) = "全部外れました"
    End If
End Sub
Sub test_4rnd_Call()
    Dim i As Long
    Dim hit As Long
    Randomize
    
    For i = 1 To 4
        lot100 hit  'lot100関数を呼び出す(←注目)
        'lot100 (hit)  'こちらは間違い!?
        kekka i, hit 'kekka関数を呼び出す
    Next
    
End Sub

ちなみに、『Call kekka(1, hit)』は『kekka 1,hit』のように call を省略した表現に書き換えています。入門向けとしては少し難しかったかもしれませんが、流れさえ追うことができれば問題有りません。

ところで、コード内のコメント『←注目』のところに注目してください。一行下のコメントアウトされた行では hitに括弧を付けています。

これでも実行可能なので「間違い」というのは微妙ですが、実行結果を見ると期待していた動作(hitをlot100に参照渡しして、lot100内で内容を書き換える)とは異なることが分かります。実際にステップ実行してみるとよいでしょう

この違いによるバグ(←予想と異なる動作をするという意味で)はおそらく発見しづらい部類に入るでしょう。上の場合、『Callを使う場合は引数を括弧内に書き、Callを省略する場合は括弧を付けないで引数を書く』ということに注意してください。

また、VBAでは sub の他に戻り値のある関数である Function も使うことができます。
関数(Function)

GoTo命令

CallはSubプロシージャ単位の処理を途中に挟む命令でしたが、GoToはプログラムの指定した位置へ強制的にジャンプさせる命令です。

処理を中断してジャンプするという意味では『exit for』や『exit sub』と少し似ているかもしれません。

以下のコードを実行してみてください。

'GO TOの使用例
sub skip()
    MsgBox "スキップします"
    GoTo SKIP 'SKIP:というラベルへジャンプ
    MsgBox "ここは実行されません"
SKIP: 'ラベル
    MsgBox "GoToを使ってスキップしました"
end sub

予想できたと思いますが、"ここは実行されません"というメッセージは表示されません。

GoTo の後にラベル名を記述することで、実行中のプログラムをそのラベルまでジャンプさせることができます。ラベルは、ラベル名の後にコロン(:)を付けて表されます。

GoTo文はif文などと組み合わせることでプログラムの実行順序を自由に制御できるので、便利と言えば便利です。ただし、プログラムの流れを把握することが困難になる恐れがあり、プログラマには嫌われていることが多いです。

GoToの使用は必要最低限に抑えるように心がけましょう。


プログラムの高速化について

ifやselectは見た目にも処理が分かりやすい命令ですが、安易に条件を並べていくと修正の際に困ったり、プログラムの実行速度が下がったりする場合があります。

特に処理を高速化したい場合、条件分岐の見直しによって比較的大きな効果を得られるケースが多いです。例えば次のような場合を見てみましょう。


Sub ifTest()
    Dim startTime As Single '開始時間
    startTime = Timer 'プログラム開始時の時間を保存
    Dim count(1) As Long 'カウンタ(0:偶数、1:奇数)
    Erase count 'カウンタの初期化
    Randomize '乱数の初期化
    Dim temp As Long
    Dim i As Long
    For i = 1 To 100000000 '1億回繰り返し
        '※↓ここから
        temp = Fix(Rnd * 100) '0から99までの乱数
        If temp Mod 2 = 0 Then '偶数
            count(0) = count(0) + 1
        Else
            count(1) = count(1) + 1
        End If
        '※↑ここまで
    Next
    MsgBox Timer - startTime 'プログラムの実行にかかった時間を表示
End Sub

0~99までの乱数に対し、偶数であればcount(0)を、奇数であればcount(1)を加算する処理を1億回繰り返して処理時間を表示するプログラムです。

試しに実行してみましょう。

if文のテスト

ポイントは「'※↓ここから」と「'※↑ここまで」の間の処理です。奇数か偶数かをmod演算の結果を見てif文で判定していますが、少し考えれば以下のように修正できるはずです。


        '※↓ここから
        temp = Fix(Rnd * 100) '0から99までの乱数
        temp = temp Mod 2
        count(temp) = count(temp) + 1
        '※↑ここまで

実行結果は以下のようになりました。

if文をはずす

mod演算は割り算の余りを返すので、結果は整数を2で割った結果は0か1になり、演算結果をそのままcount配列の添え字として使うことができます。

乱数取得~mod演算~カウンタ加算という流れだけみると同じように見えるかもしれませんが、if文がなくなっただけで処理が高速化できました。

ただし、この例では0から99の値だったtemp変数の値を書き換えているため、もし乱数値自体を他で参照したいのであれば別の変数を用意する必要があります。

参考までに、さらに高速化を目指すのであれば、以下のように書き換えることができます。


        '※↓ここから
        temp = Fix(Rnd * 100) '0から99までの乱数
        temp = temp And 1
        count(temp) = count(temp) + 1
        '※↑ここまで
And演算を使う

整数と1をAnd演算した結果は偶数なら0、奇数なら1になるため、今回のケースではMod演算よりも高速に望む結果を得ることができます。

さらに、temp変数に乱数をいったん格納するのをやめ、以下のように演算結果の0か1を直接代入することも考えられます。


        '※↓ここから
        temp = Fix(Rnd * 100) And 1
        count(temp) = count(temp) + 1
        '※↑ここまで
演算結果を直接代入

プログラムの高速化は面白い作業ではありますが、やりすぎて可読性が下がることも考慮していろいろと試してみるとよいでしょう。

条件分岐を説明したページで言うのも少し変かもしれませんが、一般的に条件分岐を省略して記述する方法を検討することが高速化の近道であることが多いです。

最後に、上記のコードをさらに以下のようにした場合、どうなるでしょうか。


        '高速化?
        '※↓ここから
        count(Fix(Rnd * 100) And 1) = count(Fix(Rnd * 100) And 1) + 1
        '※↑ここまで

temp変数自体を使わないようにしただけで一見同じような処理が行われるように見えるかもしれませんが、これは初心者がやってしまいがちな誤りです。

何がいけないのか、簡単な問題ですが考えてみてください。