和風スパゲティのレシピ

日本語でコーディングするExcelVBA

If文は判定回数より読みやすさを最適化すべし

コードの読みやすさと、マクロの処理速度のバランスに関するお話です。

今回のテーマは「Ifの速度=判定回数にどれだけこだわるか」です。

ひとくちに「IFの処理時間」といっても、3つの要素がある

まずはじめに

If A = B Then
    処理C
End If

というIf文があったとします。

この処理速度を考えようと思ったときは、
このIfを3つの部分に分けて考えなければいけません。

A,B AとBを求める時間
= 比較する時間
処理C Ifで分岐したブロックの処理時間


まずはこのことを念頭に置いて、読み進めてください。

=で比較する部分の処理時間は0

今回のテーマで、まず意識すべき点は、
②の「=で比較する時間」は、0だと思っていいということです。


単純な判定をするだけなら、

For i = 1 To 100000000
    If 1 = 2 Then MsgBox ("常にFalseだから実行されない")
Next

↑これは1秒かかりません。

0だと思っていいというか、もう0ですね。


「If文をたくさん使うと遅くなるかな?」

っていうのは、考えなくてもいいのです。

If文で最適化すべきは読みやすさ

ということで、②の比較時間が0でしたから、
「If文の実行時間」=「①A,Bを求める時間」
であることが分かりました。


ここでひとまず、この「A,Bを求める時間」は置いておきましょう。

If Cells(R, 2) = 1 Then

のような0ではないけどだいたい0くらいの処理だと仮定しておいてください。


さてこの前提で、以下のコードを比べてください。
どちらも結果は同じになるコードです。

' ①
If Cells(R, 2) = "みかん" Then
    If Cells(R, 3) = "愛媛" Then
        処理X:10行くらい
    
    ElseIf Cells(R, 3) = "和歌山" Then
        処理Y:10行くらい
    End If
End If
' ②
If Cells(R, 2) = "みかん" And Cells(R, 3) = "愛媛" Then
    処理X:10行くらい
End If

If Cells(R, 2) = "みかん" And Cells(R, 3) = "和歌山" Then
    処理Y:10行くらい
End If

①と②で、If文の書き方を変えてあります。


数学的には①の方が美しいですね。

Ifの判定回数でも、①の方が圧倒的に優れています。

VBAのAnd演算子は、Falseを見つけても最後まで判定しますので、②は

  • そもそもみかんかどうか2回判定している
  • みかんじゃないときでも、愛媛・和歌山産か判定している
  • 愛媛産とわかったあとでも、和歌山産か判定している

と、無駄な判定をしまくっています。


がしかし。


もう何を言いたいかわかると思いますが、
プログラムとしては②の方が優れています。

なぜなら、
「管理しなければいけないElse、EndIfの数が少ない」
  ⇩
「If ○○ の部分まで遡って見に行く必要が減る」
  ⇩
「一度に見なければいけない範囲が狭くて済む」

からです。


「ElseとEndIfの場所を間違っちゃう><」とか、
「EndIfの数が足りない><」とか、

そういう初歩的な話ではありません。
(そういうミスも往々にしてやるけど)


①のプログラムは、和歌山みかんの処理Yを書いている最中に、
「みかんのことを書いている」ことが視界に入らない可能性がある
のです。

↓こんなイメージ

        処理X6
        処理X7
        処理X8
        処理X9
        処理X10
        処理X11
    
    ElseIf Cells(R, 3) = "和歌山" Then
        処理Y1
        処理Y2
        処理Y3
        今ここを書いている。私はみかん?

    End If
End If


和歌山だからまだみかんだとわかりますが、山梨とかだと危ないです。

なーんて油断していると、まさかの和歌山産「紀州南高梅」が登場し、
梅干をみかん箱に詰めてしまう不具合を書いてしまうかもしれません。


まあさすがに今回の例は、Ifの分岐内容がとても簡単なので、
本当にこの程度のIf文なら、①の方がきれいで良いと思います。
(というか、ここまで単純ならSelect Case文にした方が見やすいです)


しかし実際のプログラムでは、こんな単純に書けないIf文はいっぱいあります。

そして、バグ修正や仕様変更などが発生すると、
真っ先に手が入るのがIFの分岐です。


それを「IFが重複してはいけない縛り」プレイで、ElseIf、And、Orを見事に組んでいこうとすると、
どこがどの順番で処理されていくかわからない、いわゆるスパゲティコードが誕生します。

皆さんも一度くらい経験したことがあるのでは?

美しすぎて手が出せないElse地獄を、
キーボードよりも長い時間、マウスのホイールを回しまくって改修する悪夢を。


別にIfは重複しても問題ないのです。

②の方が(無視できる範囲で)遅いプログラムと見せかけて、
思考時間もコストに加えれば、②の方が圧倒的に高速なプログラムだったりしますよ。

共通の分岐も同じように分けることができる

さて、先ほど比較したコードを再掲します。

' ①
If Cells(R, 2) = "みかん" Then
    If Cells(R, 3) = "愛媛" Then
        処理X
    
    ElseIf Cells(R, 3) = "和歌山" Then
        処理Y
    End If
End If
' ②
If Cells(R, 2) = "みかん" And Cells(R, 3) = "愛媛" Then
    処理X
End If

If Cells(R, 2) = "みかん" And Cells(R, 3) = "和歌山" Then
    処理Y
End If

ここで、「みかん共通の処理があったら、②では書けなくない?」という疑問が浮かびますね。

つまりこういうことです。

' ①に共通処理を追加
If Cells(R, 2) = "みかん" Then

    共通の処理Z

    If Cells(R, 3) = "愛媛" Then
        処理X
    
    ElseIf Cells(R, 3) = "和歌山" Then
        処理Y
    End If
End If

たしかに②にこの共通処理は書きづらい気がしますが、
実はそんなことはありません。

↓こう書くことで②でも対応ができます。

If Cells(R, 2) = "みかん" Then
    処理Z
End If

If Cells(R, 2) = "みかん" And Cells(R, 3) = "愛媛" Then
    処理X
End If

If Cells(R, 2) = "みかん" And Cells(R, 3) = "和歌山" Then
    処理Y
End If


これも慣れないと気持ち悪い書き方かもしれませんが、メリットはあります。
共通の処理がどこにあるか一目瞭然です。

しかし、これは前ほど②が優秀とは言い切れません。

なぜなら、「順序関係が希薄になる」からです。


この辺は場合によりけりで、

  • 処理Z⇒処理X,Y の順番で実行する必要がある場合は、
    それが明確になっている①の方がいい気がします。
  • 逆にどの順番でやってもいい場合は、②の方がいい気がします。


「読みやすいコード」を突き詰めれば、当然正解なんてありません。
自分が読みやすいと思う書き方をすればOKです。

大事なのは、「考慮に値しない早さに支配されて、読みやすさの大事さを忘れてしまう」ことが無いように意識することです。

A,Bの処理が遅い場合はどうするか

さて、ひとまず置いておいた「A=Bの実行速度」問題ですが、こちらは単純です。

必要な判定である以上、どうせ1回は実行しなければいけません。
なので、最初に変数に入れてしまいましょう。


そうすれば、Ifの速度とは無関係の話になります。

②のコードを書き換えるとこんな感じ

Dim KEY商品名 As String: KEY商品名 = Cells(R, 2)
Dim KEY産地 As String: KEY産地 = Cells(R, 3)

If KEY商品名 = "みかん" And KEY産地 = "愛媛" Then
    処理A
End If

If KEY商品名 = "みかん" And KEY産地 = "和歌山" Then
    処理B
End If

これでA,Bが時間がかかる処理かどうか問題は解決しました。

A,Bの処理が重かろうが、1回で済むので、Ifの書き方は関係なくなります。


余談ですが、ただ「商品名」という変数名にせず、
「分岐条件の商品名」のようにすると、変数の意図が明確になって読みやすく、書きやすくなります。

「分岐条件の」といちいち打つのはだるいので、↑の通り私は「KEY」を接頭しています。

この変数は当然かなり頻繁に出てくるわけですが、

分岐用のKEY変数

これで日本語入力OFFのまま打てるので便利です。

おまけ:本質的にはプロシージャに分ける

この手の話は大体このおまけで締めくくってますね。

最終的には、プロシージャに分割するのが理想です。

If Cells(R, 2) = "みかん" Then
    If Cells(R, 3) = "愛媛" Then
        Call 愛媛みかんに処理Xを行う
    
    ElseIf Cells(R, 3) = "和歌山" Then
        Call 和歌山みかんに処理Yを行う
    End If
End If

こう書くことができれば、
処理XとYがどれほど複雑だろうとメインコード上は1行なので、
①で書いても十分わかります。

むしろ分岐の並列関係が明確なので、
(Elseのおかげで、処理XとYが同時に処理されないことがすぐわかる)
ここまでくれば②より①が読みやすいですね。


また、愛媛と和歌山で処理が大きく変わらないようなときは、

If Cells(R, 2) = "みかん" Then
    Call みかんの共通処理
    Call みかんの産地ごとの処理(Cells(R, 3))
End If

と、産地で分岐する部分をIf文ごと関数にすると、
より分かりやすいコードになるかもしれません。

共通処理は引数がなく、
産地ごとの処理には、産地を引数に渡しているのがわかりやすいですね。


関数は「何回も出てくるコードをまとめるもの」という説明が一般的なようですが、

今回の「どんなに複雑な処理でも、メインコードでは1行のままに保つ」の方が、
便利だし、よく使う使い方だと思います。

こちらの使い方の方が、書くのも簡単です。