VBAで変数宣言をまだプロシージャの先頭にまとめてしてますか?

2019年9月3日VBA全般

Do you still group variable declarations at the top of the procedure?

hatenaも1年前までは変数をプロシージャの先頭にまとめて記述してました。コードの途中に変数が宣言してあるとコードが読みにくいと思ってました。あるきっかけで直前で宣言する派に転向しました。直前で宣言するようになって1年たった今、直前で宣言したほうかメリットが多いということを確信しました。

なぜプロシージャの先頭で宣言していたのか?

hatenaが最初に本格的に取り組んだプログラム言語は、Access VBA です。 Access 1.1 からですので相当昔ですね。その当時、参考にしたヘルプや書籍、WEB上のコードはすべて変数を先頭でまとめて宣言していました。なんの疑問も持たずにそういうものだと思っていました。

その後、Delphi の Object Pascal も使い始めました。これは言語仕様上変数はプロシージャの先頭でしか宣言できないというものでした。

ということで、20年以上変数を先頭で宣言してコーディングしてきた生粋の先頭宣言派でした。

直前宣言派に転向したきっかけ

VBA関係の掲示板徘徊をしていてもほとんど先頭宣言のコードした。ただ、数年前から徘徊し始めた teratail【テラテイル】 ではたまに直前宣言のコードをみかけることがありました。そのときの感想は、コードの流れが分断されて読みにくいな、というものでした。

考えが変わったきっかけは下記のページを読んでからです。

実例を交えて、分かり安く解説してくれてます。 すごく納得がいきました。それから、すこしずつ直前宣言に変更していきました。

下記の teratail のQ&Aの回答も後押しになりました。 ちょうど転向した直後のものなので他の人の考え方も参考になりました。

imihitoさんの回答に直前宣言のメリットがうまくまとめられてます。

モダンプログラミング言語では直前宣言がデフォルト

直前宣言が読みづらいと感じたのは、宣言と代入の2行になるからです。

Dim quantity As Integer
quantity = 10
Dim message As String
message = "Just started"

現在のモダンプログラミング言語はたいてい宣言と初期化(値の代入)が同時にできます。例えばVB6(≒VBA)の後継のVB.Netだとそれが可能です。

Dim quantity As Integer = 10
Dim message As String = "Just started"

これなら読みやすいです。VBAでも :(コロン) を使うと複数のコマンドを1行に記述できるのでそれを使って下記のように記述できます。

Dim quantity As Integer: quantity = 10
Dim message As String:   message = "Just started"

VB.Netほどスマートではないので、2行で記述するのと比べてどちらが読みやすいかは好みや慣れもあると思いますので、こんな書き方もできるということは覚えておいて損はないでしょう。

さらにVB.NETだとFor Nextループのカウンター変数も下記のように宣言と代入が同時にできます。しかも、このカウンター変数(x)はFor Nextループ 内のみ有効です。

For x As Integer = 0 To 9
  ' Forステートメント(変数が宣言されているブロック)の内側では変数xを参照できる
  Console.WriteLine(x)
Next
' Forステートメント(変数が宣言されているブロック)の外なので変数xは参照できない

このようにモダンな言語では言語仕様としてなるべく有効範囲(スコープ)を狭くできるようになっています。

この変数の有効範囲(スコープ)はなるべく狭くするという考え方はモダンプログラマーにとってはほぼ常識といえるでしょう。複雑で長大なソースコードを書く時はこれはとくに重要です。

実例

下記は最近 VBA – ExcelVBAのフィルタによる文字と背景色の複数条件検索|teratail の質問に回答したコードです。

Sub NameCopy()
    Dim ws01 As Worksheet: Set ws01 = Sheet1 '一覧があるシート
    Dim ws02 As Worksheet: Set ws02 = Sheet2 '書き出し先のシート
    Dim col_Num As Long:   col_Num = ws02.Range("A1") '検索列
    Dim keyVal As String:  keyVal = ws02.Range("A2")  '検索値
    Dim maxRow As Long:    maxRow = ws01.Cells(1, 1).End(xlDown).Row
    Dim aryName() As String: ReDim aryName(maxRow - 2, 0)
    
    Dim i As Long, cnt As Long
    For i = 2 To maxRow
        With ws01.Cells(i, col_Num)
            If .Interior.Color = vbYellow And .Value = keyVal Then
                aryName(cnt, 0) = ws01.Cells(i, 1)
                cnt = cnt + 1
            End If
        End With
    Next
    
    ws02.Range("B:B").ClearContents
    ws02.Range("B1").Resize(cnt).Value = aryName
End Sub

補足: teratailの回答のコードから少し修正しています。

これは直前宣言仕様のコードになってます。これを先頭宣言仕様で記述すると下記のようになります。

Sub NameCopy1()
    Dim ws01 As Worksheet
    Dim ws02 As Worksheet
    Dim col_Num As Long
    Dim keyVal As String
    Dim maxRow As Long
    Dim aryName() As String
    Dim i As Long, cnt As Long
    
    Set ws01 = Sheet1 '一覧があるシート
    Set ws02 = Sheet2 '書き出し先のシート
    col_Num = ws02.Range("A1")  '検索列
    keyVal = ws02.Range("A2")   '検索値
    maxRow = ws01.Cells(1, 1).End(xlDown).Row
    ReDim aryName(maxRow - 2, 0)
    
    For i = 2 To maxRow
        With ws01.Cells(i, col_Num)
            If .Interior.Color = vbYellow And .Value = keyVal Then
                aryName(cnt, 0) = ws01.Cells(i, 1)
                cnt = cnt + 1
            End If
        End With
    Next
    
    ws02.Range("B:B").ClearContents
    ws02.Range("B1").Resize(cnt).Value = aryName
End Sub

どうでしょうか。短めのコードですのでメリットが明確に分かりづらいですか、前者の方が読みやすいと思いませんか。(一年前のhatenaが見たら前者は読みづらいと思うだろう。結局、慣れたということかな。)

直前宣言を突き詰めると、For … Next内のCnt変数も直前に宣言すべきかもしれません。

    Dim i As Long
    For i = 2 To maxRow
        With ws01.Cells(i, col_Num)
            If .Interior.Color = vbYellow And .Value = val Then
                Dim cnt As Long
                aryName(cnt) = ws01.Cells(i, 1)
                cnt = cnt + 1
            End If
        End With
    Next

ループ内で宣言すると繰り返し宣言されることになると思われるかもしれませんが、ループ内に記述しても宣言は一回のみになりますので、この書き方でも正常に動作します。ただ、あらぬ誤解を生みそうなのでここまですることはないかなと、ループ内で使用する変数ということでループの前で宣言しておくことでいいかと思います。

1年間、直前宣言でコーディングした結果

既に、上で紹介した2つのリンク先でメリットは言い尽くされてますので、付け加えることはありませんが、素直な感想としてメリットを実感できて、今後、先頭宣言に戻ることはないだろうということです。

大きなシステムをコーディングしているとき、仕様変更で一部を修正したり、似たような処理を他で使いまわすということはよくあります。そのようなとき、先頭宣言だと、使用しなくなった変数を削除し忘れて幽霊変数が多数存在しているとかなりがちです。使いまわすためにコピーする場合も変数と処理部分を2回コピーするという手間がかかります。直前宣言だとそのような場合の手間が大幅に省略出来て楽できます。

少し補足

メリットや具体的な宣言位置について説明不足の部分かありましたので、補足しておきます。

直前宣言のメリット

現在のプログラミングの常識として「変数のスコープは狭いほどよい」とされてます。その理由としては下記のようなことがあげられます。

  • コードを読むときに考慮する範囲が狭くなり可読性があがる。
  • 関数化などの再利用性がアップする、仕様変更時のメンテナンス性が高くなる。
  • 処理ブロックの独立性が高くなり、予期しない誤動作を抑制することができる。

この重要性は、ほとんどのモダンプログラミング言語では、ブロック単位でスコープを制限できるようになっていることからも分かります。

また、古くからあるプログラミング言語でもブロック内スコープが利用できるように拡張されてきています。(VB.Net 、JavaScript など)

VBAは言語仕様上、スコープは宣言した位置からEnd Subまでとなるので、プログラマーは意識して変数の使用範囲を限定することで同様のメリットを享受することができます。

また、Withステートメントは、スコープ(寿命)をEnd Withまでと制限できるので積極的に使用すべきです。Withをネストして使用することも可能ですが、ネストが深くなると読みづらくなるので、直前変数宣言と組み合わせてあまり深くならないようにすると読みやすくできます。

直前宣言、具体的にどの位置か

直前宣言といっても何がなんでも初めて代入するすぐ前で宣言しなければいけないということではありません。場合によっては密着度の高いあまり長くない処理ブロックの前で宣言しても問題ないです。

For文内で使用する変数をFor文内で宣言すると、ループするたびに初期化されると誤解される場合があるので、For文の直前で宣言するほうがいいでしょう。

変数とそれを使用する処理ブロックが離れていると可読性が落ちるので避けたほうがいいでしょう。

まとめ

まだ、変数を先頭で宣言しているなら、一度、直前宣言でのコーディングを1ヶ月でいいので試してみてください。それでも、やはり、先頭宣言の方かいいというのなら、止めませんが、きっと、メリットを実感できると思います。

えっ、「変数は宣言しなくてもそのまま使える。」って!それで今まで問題がなければ、それでもいいでしょう。しかし、将来、長いコードを書くようになると、きっとどこかで痛い目にあうことは覚悟しておいてください。

VBA全般VBA

Posted by hatena