VBAで変数宣言をまだプロシージャの先頭にまとめてしてますか?
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ヶ月でいいので試してみてください。それでも、やはり、先頭宣言の方かいいというのなら、止めませんが、きっと、メリットを実感できると思います。
えっ、「変数は宣言しなくてもそのまま使える。」って!それで今まで問題がなければ、それでもいいでしょう。しかし、将来、長いコードを書くようになると、きっとどこかで痛い目にあうことは覚悟しておいてください。
ディスカッション
コメント一覧
Duetさん、コメントありがとうございます。
記事がお役にたててうれしい限りです。
いままで慣れ親しんだ書き方から変えるのは、最初は違和感を感じますよね。
慣れてくると気にならなくなる。
ループ変数、カウンタ等に関しては、私の場合、Accessでの開発がメインですので、そんなに使う機会がないんですよね。データ操作はたいていSQLですることになりますので。ですので、いままで気になったことはないので深く考えてはきませんでした。
Excelならループで操作する場面はよくありますね。
私は For Each を使える場合はなるべくそれを使うようにしています。
そうすれば、iという抽象的な名前でなく実体に即した名前にしようという気になります。
今回、考えてみて、
ループ変数はアルファベット一文字でというシンプルなルールにしておいて、
i, j, k, l, m, n・・・・とどんどん直前宣言する、
関数化するときも変更せずにそのまま使用すればいいように思えてきました。
ループ変数は i, j でなければならないという合理的な利用は何もないですよね。
m, n でも x, y でもなんら問題はない。
記事の最初のリンク先の人も、関数化するときに、i に変更していますが、
変更する合理的な利用はないですよね。
ループ変数は i でないと気持ち悪いぐらいの理由しかない。
私も含めてそうなんですか、
習慣的にしていること、皆がしていることは、
知らず知らずにそうあるべきだと思い込んでしまう、
さしたる根拠もないのに。
自戒をこめて頭を柔らかくと思いますね。
(かといってリンク先の内容が間違っているとは思いませんが)
> 感じたメリットを挙げましたが、他者が読みやすいかどうか。という点は常に考える必要があると感じました
私も同感です。
自分が直前宣言にメリットを感じたといっても、VBAの世界では先頭宣言がまだまだ主流ですので、それも考慮しつつ折り合いをつけていく必要はあるでしょうね。
> あと、これは愚痴ってもしょうがないことですが
他言語を触ることが最近多いのですが、その後、VBAでコーディングすると、
なんとかしてよ、MSさんと思ってしまいます。
VBAエディターもまったく変更なしで放置状態。
他言語でコーディングするときは、Visual Studio Code を使ってますが、機能、使いやすさに雲泥の差があります。
同じMS社製のなのでその気になれば簡単に実装できそうに思うですが。
> 1か月時点でもメリットは十分に感じたので、今後も気楽に試し続けてみようと思います。
使い続けて、また、何かお気づきの点がありましたか、お気軽にコメントしてください。
今後ともよろしくお願いします。
初めまして。いつもお世話になっております
先月この記事をお見掛けして、物は試しと一か月間試してみました。
総評として、私にはドンピシャでした。
・以前のコーディングスタイル
‘処理Aの変数宣言
‘処理Bの変数宣言
‘処理Aの初期化
‘処理Bの初期化
‘処理A
‘処理B
‘後処理
よくある形ですが
いつも最初は関数化を意識せずにだらーっと実装→全体像が見えてきたタイミングで関数化を考える為
例えば上記の処理B関連を関数化しようとすると、記述が飛び飛びで多少面倒なことになります。
関数化をあまり意識してないので処理AとBが混ざったような記述になってしまっていたり、
あとコーディングの期間が空いたりすると猶更困ったことになります。
直前宣言の場合、最初からプロシージャ内に別の関数を作っているようなものなので
関数化が単純なコピペで済む というのは私にとって実装面で大きなメリットを感じました
実際に試した手触りとしては、やはり最初は読みにくさが先行しました。
(ここにあるべき筈の処理が無い…なぜだ… みたいな)
ただ、「ここはAの処理ブロック」「ここはBの処理ブロック」という感覚が分かってくると
処理ブロック同士が癒着しないコーディングが自然と行えるようになってきました
(以前はしっかり意識しないといつの間にか処理Aと処理Bが融合していた、なんてことがよくありました)
ループ変数、カウンタ等については色々試したのですが
ループ変数はi,jまでは先頭宣言、3個以上なら直前宣言
カウンタは変数1個で済む場合のみ先頭宣言、それ以外 (例:cnt_A、cnt_B) は直前宣言
がしっくりきました。この辺りは好みが強く出そうだな、と感じます。
(私も自分ルールに完全に沿う訳ではないですし)
感じたメリットを挙げましたが、他者が読みやすいかどうか。という点は常に考える必要があると感じました
>For文内で使用する変数をFor文内で宣言すると、ループするたびに初期化されると誤解される場合があるの>で、For文の直前で宣言するほうがいいでしょう。
ここも直前宣言に沿うとFor文内で宣言したくなりますが、やはり誤解を生みやすくなるので避けたいですね
あと、これは愚痴ってもしょうがないことですが
いくら変数スコープを意識しても処理Aブロックが終わり処理Bブロックが始まったとき
処理A関連の変数は実際には残ったまま という点も忘れてはいけないですね(私の例ですみません)
1か月時点でもメリットは十分に感じたので、今後も気楽に試し続けてみようと思います。
ご紹介ありがとうございました