t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA ローカル変数は使用する直前で宣言する

ネットや書籍で見かけるマクロでは、変数宣言はプロシージャの先頭にまとめられているものが多い。一般的に変数宣言はプロシージャの先頭にまとめて書くものだと紹介している記事さえある。

それはなぜかというと、みんながそうしてるからだ。

先頭にまとめるとコードが読みやすいとか、変数を一覧管理できて便利とかそういうことではない。書いてる本人も、なぜ先頭にまとめるべきなのか、確たる根拠を持っていないことが多いように思われる。

じゃあなぜ先頭に書くのが一般的になったのかというと、おそらくこれは初期のC言語に由来するものだろう。初期のC言語では、変数宣言は先頭に書かないとコンパイルできなかったようだ。そのようなコードでプログラミングを学んだ人たちが、VBAに同様の習慣を持ち込み、それが標準的な作法として扱われるようになったものと思われる。

現在ではローカル変数は使用する直前で宣言するのが主流

まずこちらの記事で述べられているレガシープログラマの判断項目「使われるローカル変数をすべてメソッドの最初に宣言する。」をご覧いただきたい。
blog.jnito.com
「できるだけ変数の寿命が短くなるように」とあるが、先頭で宣言してしまうとプロシージャの始まりから終わりまで変数の寿命が続くことになる。

たとえばバブルソートを例に、一般的とされる先頭での変数宣言で書いてみる。

Sub バブルソート()
    Dim arr(1 To 10) As Long
    Dim i As Long
    Dim j As Long
    Dim tmp As Long
    Dim min As Long
    Dim max As Long
    Dim LOWERBOUND As Long
    Dim UPPERBOUND As Long
    min = LBound(arr)
    max = UBound(arr)
    LOWERBOUND = 10
    UPPERBOUND = 99
    
    Debug.Print "ソート前"
    For i = min To max
        arr(i) = Int((UPPERBOUND - LOWERBOUND + 1) * Rnd + LOWERBOUND)
        Debug.Print arr(i)
    Next
    
    Debug.Print "ソート後"
    For i = min To max - 1
        For j = i + 1 To max
            If arr(i) > arr(j) Then
                tmp = arr(i)
                arr(i) = arr(j)
                arr(j) = tmp
            End If
        Next
    Next

    For i = min To max
        Debug.Print arr(i)
    Next
End Sub

ループ変数のiとjを例にとってみてみると、宣言からプロシージャの終了まで実に広い範囲で寿命を持っていることがわかる。
f:id:t-hom:20161212213254p:plain

次に、変数宣言を使用の直前に変更してみた。
また、変数の使い回しをやめて新たにループ変数 k、l を作成し、minとmaxは変数を作らずに都度関数で求めるようにした。※配列の上限と下限を都度関数で計算させるのは賛否あると思うが後のコード分離を見越して余計な変数を減らしている。

Sub バブルソート()
    Dim arr(1 To 10) As Long
    
    Debug.Print "ソート前"
    Const LOWER_BOUND As Long = 10
    Const UPPER_BOUND As Long = 99
    Dim i As Long
    For i = LBound(arr) To UBound(arr)
        arr(i) = Int((UPPER_BOUND - LOWER_BOUND + 1) * Rnd + LOWER_BOUND)
        Debug.Print arr(i)
    Next
    
    Debug.Print "ソート後"
    Dim j As Long, k As Long, tmp As Long
    For j = LBound(arr) To UBound(arr) - 1
        For k = j + 1 To UBound(arr)
            If arr(j) > arr(k) Then
                tmp = arr(j)
                arr(j) = arr(k)
                arr(k) = tmp
            End If
        Next
    Next

    Dim l As Long
    For l = LBound(arr) To UBound(arr)
        Debug.Print arr(l)
    Next
End Sub

すると、各変数の寿命はこのとおり。
f:id:t-hom:20161212213440p:plain

さらに寿命ではなく「使用範囲」と考えるとこのように狭い範囲に絞られる。
f:id:t-hom:20161212213805p:plain

一つのマクロのなかにも、配列にランダム値を入れる、ソートする、表示するという風にいくつかの処理が存在するが、このように変数の使用範囲を狭めることで各処理の独立性が高まり、ほかに影響を与えにくくなる。

さて、各処理ごとにコメントを書きつつ、最初の表示部分を別処理に分離したものがこちら。

Sub バブルソート()
    Dim arr(1 To 10) As Long
    
    Debug.Print "ソート前"
    
    '■配列をランダムな整数で埋める。
    Const LOWER_BOUND As Long = 10
    Const UPPER_BOUND As Long = 99
    Dim i As Long
    For i = LBound(arr) To UBound(arr)
        arr(i) = Int((UPPER_BOUND - LOWER_BOUND + 1) * Rnd + LOWER_BOUND)
    Next
    
    '■配列を表示させる。(上から分離して単機能化)
    Dim m As Long
    For m = LBound(arr) To UBound(arr)
        Debug.Print arr(m)
    Next
    
    Debug.Print "ソート後"
    
    '■配列をソートする。
    Dim j As Long, k As Long, tmp As Long
    For j = LBound(arr) To UBound(arr) - 1
        For k = j + 1 To UBound(arr)
            If arr(j) > arr(k) Then
                tmp = arr(j)
                arr(j) = arr(k)
                arr(k) = tmp
            End If
        Next
    Next

    '■配列を表示させる。
    Dim l As Long
    For l = LBound(arr) To UBound(arr)
        Debug.Print arr(l)
    Next
End Sub

処理の独立性が高まると次に何ができるかというと、前回紹介したプロシージャの分割である。
thom.hateblo.jp

まずは配列の表示から外だししてみる。この時ローカル変数はスコープを縮めているのでそのまま切り取ってもほかに影響を与えない。ただし配列arrはそうもいかないので、外から持ってこられるように引数にしておく。

Sub 配列を表示させる(arr)
    Dim m As Long
    For m = LBound(arr) To UBound(arr)
        Debug.Print arr(m)
    Next
End Sub

メインコードはこうなる。

Sub バブルソート()
    Dim arr(1 To 10) As Long
    
    Debug.Print "ソート前"
    
    '■配列をランダムな整数で埋める。
    Const LOWER_BOUND As Long = 10
    Const UPPER_BOUND As Long = 99
    Dim i As Long
    For i = LBound(arr) To UBound(arr)
        arr(i) = Int((UPPER_BOUND - LOWER_BOUND + 1) * Rnd + LOWER_BOUND)
    Next
    
    Call 配列を表示させる(arr)
    
    Debug.Print "ソート後"
    
    '■配列をソートする。
    Dim j As Long, k As Long, tmp As Long
    For j = LBound(arr) To UBound(arr) - 1
        For k = j + 1 To UBound(arr)
            If arr(j) > arr(k) Then
                tmp = arr(j)
                arr(j) = arr(k)
                arr(k) = tmp
            End If
        Next
    Next

    Call 配列を表示させる(arr)
End Sub

この時配列の表示で使用していた変数mは、一般的なループ変数であるiに戻しておく。プロシージャを独立させたためメインコードのiと競合しなくなるからだ。

Sub 配列を表示させる(arr)
    Dim i As Long
    For i = LBound(arr) To UBound(arr)
        Debug.Print arr(i)
    Next
End Sub

このようにどんどん分離させていくと最終的に以下のようになった。

Sub バブルソート()
    Dim arr(1 To 10) As Long
    Call 配列をランダムな整数で埋める(arr)
    
    Debug.Print "ソート前"
    Call 配列を表示させる(arr)
    
    Call 配列をソートする(arr)
    
    Debug.Print "ソート後"
    Call 配列を表示させる(arr)
End Sub

Sub 配列を表示させる(arr)
    Dim m As Long
    For m = LBound(arr) To UBound(arr)
        Debug.Print arr(m)
    Next
End Sub

Sub 配列をランダムな整数で埋める(arr)
    Const LOWER_BOUND As Long = 10
    Const UPPER_BOUND As Long = 99
    Dim i As Long
    For i = LBound(arr) To UBound(arr)
        arr(i) = Int((UPPER_BOUND - LOWER_BOUND + 1) * Rnd + LOWER_BOUND)
    Next
End Sub

Sub 配列をソートする(arr)
    Dim i As Long, j As Long, tmp As Long
    For i = LBound(arr) To UBound(arr) - 1
        For j = i + 1 To UBound(arr)
            If arr(i) > arr(j) Then
                tmp = arr(i)
                arr(i) = arr(j)
                arr(j) = tmp
            End If
        Next
    Next
End Sub

ローカル変数を使用する直前で宣言することで、各フェーズごとにコードの独立性が高まり、このようにコード分割の前準備にもなる。

コードをうまく分割できない、Functionが使いこなせないという方は、まず変数のスコープが大きすぎて他のフェーズとの依存関係を断ち切れないからじゃないかなと思う。VBAで一つのプロシージャに長々とコードを書いてしまうのは、ローカル変数の宣言を先頭でやっているのも原因の一つかもしれない。

また、コード分割しないにしても、変数宣言がスコープ開始の目印になるので、それ以前の箇所で変数がいじられていないことが明白になる。つまり各フェーズで把握すべき変数が絞られるのでコードの可読性が向上する。

いいことずくめ。

でもまあ、なんとなく変数宣言はプロシージャの先頭に書かないと気持ち悪いという方もいるだろう。
それはきっと単にそのようなコードに見慣れているからであり、なぜ見慣れているかといえばVBAでは皆そのようにコーディングしているからであり、突き詰めれば古いC言語の言語的制約でそのように書かれているものを見よう見まねでVBAに持ち込んだためである。

職場のコーディングルールなどがあって変数を先頭で宣言している方もいるだろうから、それはそれで間違っているとは思わないけれど、「一般的だから」という理由で採用しているのであれば、ぜひ再考してみてほしい。

単にVBAにおいて、いまだに変数をプロシージャの先頭で宣言するのが一般的だというだけであり、他言語のプログラマーはもうその書き方は古いものと見做している。

以下、変数のスコープを縮めることに言及している書籍紹介

良いコードを書く技術 ?読みやすく保守しやすいプログラミング作法 (WEB+DB PRESS plus)

良いコードを書く技術 ?読みやすく保守しやすいプログラミング作法 (WEB+DB PRESS plus)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

VB.NETルールブック ?読みやすく効率的なコードの原則

VB.NETルールブック ?読みやすく効率的なコードの原則

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。