t-hom’s diary

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

VBA 変数のスコープを絞る

基本的に、他人の書いたソースコードというのは分かりづらい。
原因の一つに、変数がある。

VBAのような手続き型言語において、変数は、宣言されてからあらゆる場所で変更される可能性を持っており、何がどうなってその値になったのか、順番に追っていかないと分からない。

前回紹介した関数型言語ではそうしたことは少ない。
thom.hateblo.jp

特にSchemeではそれ以降その変数に別の値を代入することはできない。多分Haskellもそう。
それでどうやってプログラムしろというのか、サッパリ分からないという方は、一度関数型言語をやってみることをオススメする。
きっと新しい世界が開けてくるだろう。

さて、今回は変数のスコープについてのお話である。
VBAでは前述のように変数はあらゆる場所で変更される可能性がある。

変数がいつから有効で、いつまで参照できるかの範囲をスコープという。
VBAのプロシージャ内で宣言した変数の場合、宣言からプロシージャの終わりまでがスコープである。
これを狭めてやることが、わかりやすいプログラム作成の第一歩である。

変数は原則、使う直前で宣言し、速やかに値を代入するのが良い。
昔はコードの初めの方で全ての変数を宣言していたが、宣言と使用箇所が一画面に収まっていたほうが見やすい。
トヨタのJust In Time方式「必要なものを、必要なときに、必要なだけ」という考えはプログラミングにも当てはまる。

以下、悪い例

Sub 私が考える悪いプログラム()
    Dim a(1 To 2) As String
    Dim b As String
    Dim c As Integer
    Dim d As Date
    
'~中略~
    
    a(1) = "Good morning!"
    b = "Hello!"
    c = 123
    d = Date

'~中略~

    Debug.Print a(1)
    Debug.Print (b)
    Debug.Print (c)
    Debug.Print d
End Sub

中略の部分には様々な処理が入ると考えてほしい。
時々、たとえばdの出力が不要になったにも関わらず、宣言と代入だけそのまま放置されているようなソースコードを見かける。
これは宣言と使用箇所が離れているため、メンテナンスをする人が「念のため」残しているからだと思う。
人の作ったプログラムなど、どこでその変数が参照されているか分からないので怖くて削除できない。

一方、私が考える良いプログラムは次のようなもの。

Sub 私が考える良いプログラム()
'~前略~
    Dim a(1 To 2) As String: a(1) = "Good morning!"
    Debug.Print a(1)
    
    Dim b As String: b = "Hello!"
    Debug.Print (b)
    
    Dim c As Integer: c = 123
    Debug.Print (c)
    
    Dim d As Date: d = Date
    Debug.Print d
End Sub

宣言と同じ行で代入し、次の行ですぐ使っているので、イミディエイトウインドウに出力するために宣言していることが明確である。

では使い終わりはどうかというと、VBAには適切に変数を廃棄する手段が無い。
そこで機能的には制限できないものの、明示的に変数の中身を破棄するプロシージャを作ってみた。

Public Sub Unset(ByRef variable As Variant)
    If IsArray(variable) Then
        Erase variable
    ElseIf IsObject(variable) Then
        Set variable = Nothing
    Else
        Let variable = Empty
    End If
End Sub

Unsetという類似の命令がPHPに存在しているようなので、プロシージャ名はそれをそのままパクった。
一般的に知られている同等効果の命令がある場合、自作したプロシージャもそれに倣うのが良い。

これを使ったサンプルは次のとおり。

Sub 変数を廃棄する処理のサンプル()
    Debug.Print "---- before↓ ----"
    Dim a(1 To 2) As String: a(1) = "Good morning!"  '作る
    Debug.Print a(1)  '使う
    Unset a  '捨てる
    
    Dim b As String: b = "Hello!"  '作る
    Debug.Print (b)  '使う
    Unset b  '捨てる
    
    Dim c As Integer: c = 123  '作る
    Debug.Print (c)  '使う
    Unset c  '捨てる
    
    Dim d As Date: d = Date  '作る
    Debug.Print d  '使う
    Unset d  '捨てる
    
    Dim e As Object: Set e = ThisWorkbook  '作る
    Debug.Print (e Is Nothing)  '使う
    Unset e  '捨てる
    
    Debug.Print "---- After↓ ----"
    Debug.Print a(1)
    Debug.Print b
    Debug.Print c
    Debug.Print d
    Debug.Print (e Is Nothing)
    Debug.Print "------ Fin ------"
End Sub

この方法なら変数の使用範囲がパッと見て分かるので、たとえばdの出力が不要になったら宣言から廃棄までをごそっと消すことができる。

ただし、中身が空になったというだけで再代入や参照はできてしまう。
サンプルでは空になったことを示すためあえてUnset後にDebug.Printで参照しているが、実際に活用する場合、Unset以降は一切参照しないというコーディング規約も併せて適用しないと意味がない。
VBAの仕様上のスコープを狭めることはできないが、「Unsetされた変数は絶対に参照しない」と規約で決めておけば、実務上は「宣言からUnsetまで」が変数スコープと言える。

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