表形式のデータを扱う際に、
Cells(R, 4) = Cells(R, 2) * Cells(R, 3) Range("D" & i) = Range("B" & i) * Range("C" & i)
こんな感じのマクロを書いていませんか?
2や3とかじゃわからないからコメントをつけたり、
シートとコードを交互に見ながら列番号を入力したり。
そして完成した後、列の挿入や削除が発生し、
3→4、4→5、…と書き換える羽目になって、
涙目になったことがある方は必見です。
定数を使えば、このマクロを劇的に「読みやすく、変更に強く」することができます。
お忙しい方のために最終目標コードを先に書きますと、
' 列番号を先に定数にしておく Enum 売上データの列 購入日 = 1 単価 ' Enum(列挙型の定数)は、ここから先が= 2,3,4と自動連番されるしくみ 個数 金額 End Enum ' 冒頭と同じ処理 Cells(R, 売上データの列.金額) = Cells(R, 売上データの列.単価) * Cells(R, 売上データの列.個数)
こんなコードを書くことを目指します。
一番下のコードは、金額/単価/個数に4/2/3が代入されるため、
冒頭の「D列 = B列×C列」と同じ処理をしていることになります。
ですが、こっちは「金額 = 単価×個数」というのがすぐにわかりますね。
このように定数を使うと、列番号がただの数字から読めるテキストになります。
しかも列に挿入や削除があっても、宣言部分の1箇所だけいじれば良くなります。
いいことづくめですね!
一見、書くコードの量が増えて手間に感じるかもしれませんが、
プログラムは書く時間より読んで書き換える時間の方が圧倒的に長いので、
トータルではこっちの方が手間なく、早くプログラミングすることができます。
書き方自体はすごく簡単なので、しっかりマスターしましょう。
なお、「定数が大事ってのはもう知ってるから、列挙型だけ学びたい」という方は、
ここよりも列挙型専用の解説ページをおすすめします。
定数に関する丁寧な解説から読みたい方は、このまま読み進めてください。
ではいきましょう。
題材に使うサンプルマクロ(定数を使わずに書く)
まずは題材として、
「データシートの行番号を指定して、領収書を出力する」
プログラムを書きます。
◇ データシート
◇ 領収書
◇ データシート⇒領収書 への印字マクロ
Sub 領収書を出力する(出力行 As Long) Worksheets("領収書").Activate Range("D3") = Worksheets("購入歴データ").Cells(出力行, 1) ' No Range("C7") = Worksheets("購入歴データ").Cells(出力行, 2) ' 購入者 Range("G3") = Worksheets("購入歴データ").Cells(出力行, 3) ' 日付 Range("F11") = Worksheets("購入歴データ").Cells(出力行, 4) ' 品物 Range("F6") = Worksheets("購入歴データ").Cells(出力行, 7) ' お支払い End Sub
まずはこんな感じでしょうかね。
ここで列を挿入することによる改修発生
さて、完成したマクロで効率よく領収書を作っていましたが、ここで問題が発生です。
領収書に社名を印字するために、データのB列に社名を追加することになりました。
この変更に対応するために、上のコードを↓のように書き換えます。
Sub 領収書を出力するVer2(出力行 As Long) Worksheets("領収書").Activate Range("D3") = Worksheets("購入歴データ").Cells(出力行, 1) ' No Range("C5") = Worksheets("購入歴データ").Cells(出力行, 2) ' 社名 ← これが書き加えられた。 Range("C7") = Worksheets("購入歴データ").Cells(出力行, 3) ' 購入者 ↓ここから下は列数が1増えた。 Range("G3") = Worksheets("購入歴データ").Cells(出力行, 4) ' 日付 Range("F11") = Worksheets("購入歴データ").Cells(出力行, 5) ' 品物 Range("F6") = Worksheets("購入歴データ").Cells(出力行, 8) ' お支払い End Sub
よくある改修ですね。
しかしこの改修、もっと規模が大きいと大変なことになります。
実際のマクロでは、同じセルが1つの関数内で何回も登場しますし、
たくさんのマクロで同じデータシートを参照していることもあるでしょう。
そのすべての「2,3,4,7」を「3,4,5,8」に書き換えるのはものすごく大変です。
面倒なだけならまだしも、書き間違えや変え忘れがあると、バグになってしまいます。
マクロ中に登場するすべての「7」という数字を、
「これって購入歴データG列を表す7かな??」って考えて「8」にしていくのは、
「面倒・危険・無駄」の3拍子そろった、典型的ダメプログラミングです。
定数化のやり方とメリット
ここで定数が登場します。
例題のマクロは、定数を使って↓のように書き換えることができます。
' 定数の宣言 Const 購入歴データの列_No = 1 Const 購入歴データの列_社名 = 2 Const 購入歴データの列_購入者 = 3 Const 購入歴データの列_購入日 = 4 Const 購入歴データの列_品物 = 5 Const 購入歴データの列_価格 = 6 Const 購入歴データの列_個数 = 7 Const 購入歴データの列_お支払い = 8 ' 書き換えたマクロ Sub 領収書を出力するVer3(出力行 As Long) Worksheets("領収書").Activate Range("D3") = Worksheets("購入歴データ").Cells(出力行, 購入歴データの列_No) Range("C5") = Worksheets("購入歴データ").Cells(出力行, 購入歴データの列_社名) Range("C7") = Worksheets("購入歴データ").Cells(出力行, 購入歴データの列_購入者) Range("G3") = Worksheets("購入歴データ").Cells(出力行, 購入歴データの列_購入日) Range("F11") = Worksheets("購入歴データ").Cells(出力行, 購入歴データの列_品物) Range("F6") = Worksheets("購入歴データ").Cells(出力行, 購入歴データの列_お支払い) End Sub
変更時には宣言部分だけ書き換えればOK
コードを書き換えたことによる最大の効果は「変更に強くなった」ことです。
例えば5列目に新たに列を挿入するときは、
上の定数部分の「5,6,7,8」を「6,7,8,9」に変更すればいいだけになります。
本体のコードは見なくてもOKというのが素晴らしいですね。
この後コードがズラズラ続いていくとしても、
さらにはこの表を使うたくさんマクロがあっても、
一番上の定数をいじるだけで改修ができるわけです。
「変更時に定数宣言部だけをいじれば済むようになる」
というのが定数を使う最も大事な目的です。
定数を使うとコメントが減る
もう一つの重要な効果が「コメントが減る」ということです。
よく見ると、
Range("G3") = Worksheets("購入歴データ").Cells(出力行, 4) ' 購入日 ' ⇩ Range("G3") = Worksheets("購入歴データ").Cells(出力行, 購入歴データの列_購入日)
と、右側の補足コメントがすべて消えていますね。
定数のおかげで、どの列なのかが「見ればわかる」状態になったからです。
これもとても大事な定数の効力です。
定数を使うと「値に名前を付けられる」ので、
うまく名付けることができれば、とても読みやすいコードにすることができます。
定数は複数のマクロで共有しよう
定数宣言のポイントは、Subの外側(モジュールの一番上)で宣言することです。
定数や変数はモジュールの先頭で宣言すると、
そのモジュール内のすべてのSubやFunctionで使えるようになります。
さらに、「Const」を「Public Const」としてあげれば、
ブック内のすべてのモジュールで使えるようになります。
シートの列番号など、どこで使っても同じになるはずの定数を、
マクロごとに定義していたら定数にした意味がありません。
共有できるものは、とことん共有するのが大事です。
※ 「Publicはダメ」と記憶している方がいましたら、正しくは「Public変数はダメ」です。
ブック内で常に同じ値の定数は、むしろPublicにしないといけません。
さらに強力な定数:列挙型
VBAにおける「定数」の中で、一番重要なのが今回の「列番号」です。
表形式のデータにおいて、データの種類を示すキーになりますからね。
さてこの列番号ですが、これをさらに便利に扱える「列挙型」という定数があります。
定数を使っていたこの部分↓を、
Const 購入歴データの列_No = 1 Const 購入歴データの列_社名 = 2 Const 購入歴データの列_購入者 = 3 Const 購入歴データの列_購入日 = 4 Const 購入歴データの列_品物 = 5 Const 購入歴データの列_価格 = 6 Const 購入歴データの列_個数 = 7 Const 購入歴データの列_お支払い = 8
列挙型を使うとこのように書き換えることができます↓
Enum 購入歴データの列 No = 1 購入者 購入日 品物 価格 個数 お支払い End Enum
列挙型は初期値の「=1」以降は自動で連番になるため、
宣言部がとてもスッキリして見やすくなりますね!
この自動連番というのが列番号のために存在するといってもいい機能で、
セル結合などがなければ、見出しをコピーして縦にするだけで、
「購入者=2(B列)」「購入日=3(C列)」…と自動で採番してくれます。
実際に呼び出すコードがどうなるかというと、
Range("G3") = Worksheets("購入歴データ").Cells(出力行, 購入歴データの列_購入日) ' ⇩ Range("G3") = Worksheets("購入歴データ").Cells(出力行, 購入歴データの列.購入日)
このように書き換わります。
列挙型定数は「列挙型名.項目名」で呼び出しますので、
見た目は_が.になっただけですね。
この呼出しで強力なのが、入力する時に選択肢が表示される点です。
この機能がすさまじく便利です。
これの見出しと同じ選択肢が出るわけですからね↓
シートを見に行く回数が減って入力もしやすい上に、
書き間違え取り違えも激減します。
これだけでも十分「列挙型が最強!」って感じなのですが、
さらなる決定打になるのが改修の容易さです。
今回の題材であった「B列に社名を挿入」をしてみましょう。
なんとEnum内に列名を足すだけでいいのです↓
Enum 購入歴データの列 No = 1 社名 ' ←これを挿入した 購入者 ' ← B列だった購入者がC列になるが、連番機能で勝手に3になっている! 購入日 ' 以降の3→4、4→5も同様に対応済 品物 価格 個数 お支払い End Enum
「No」と「購入者」の間に「社名」を入れれば、
社名が2になって、それ以降は3,4,…に再採番されます。
なんと素晴らしい!
列挙型定数を使うと、コードの書きやすさ、メンテナンス性ともに、
数値のベタ打ちに比べてケタ違いによくなります。
表形式のシートは、列番号を必ず列挙型で定義しておく癖をつけましょう。
マジックナンバーとは
プログラミングにおいて、マジックナンバーという用語があります。
コード内に直接打ち込まれた数字(とか文字)のことで、
まさに今回取り上げた、
Worksheets("購入歴データ").Cells(出力行, 3)
における「3」のことになります。
用語の由来は
~この3という数字は何のことかわからない。
でもなぜかプログラムは動いている。まるで魔法だ。~
という皮肉です。ひどいですね。
マジックナンバーは「定数にすべきなのにしなかったやつ」なので、
定数のメリットをそっくり反対にしたデメリットを持ちます。
つまり、変更に弱くて、バグに気づきにくくて、コメントがないと読めません。
ちゃんとしたプログラマさんやシステム会社さんでは、
コーディング規約(コードを書くときのルール)で、
マジックナンバーの使用を禁止しています。
ちょっと検索するとわかりますが、
マジックナンバー=ヘボプログラマの証みたいな扱いをされます。
ヘボ扱いは悲しいですが、仕方がないのです。
「3」ならまだいいんですが、
実際の業務はAC列みたいな大きいデータもたくさんありますからね。
For i = 15 To 24 Cells(i, 23) = Cells(i, 32) * Cells(i, 34) Next
とかが延々と続くマクロは、他の人はおろか、
書いた本人にすら1ヶ月経ったら読めませんからね(´∀`;)
「マジックナンバーを無くす = すべて定数にする」
ことは、最初は面倒に思うかもしれません。
しかし、マジックナンバーだらけのプログラムを改修することのほうがはるかにオエーってなります。
マジックナンバーを減らすことは未来の自分への贈り物だと思って、
ちゃんと定数を使っていきましょう。
マジックナンバーを完全になくしたマクロ
先ほど定数を使ってレベルアップさせたマクロも、
実はまだまだマジックナンバーが残っています。
Worksheets("領収書").Range("D3")
↑この"領収書"や"D3"もマジックナンバーですからね。
もちろん「列挙型」も使ったうえで、
思いつく限り「読みやすく」「変更に強く」したマクロがこちらです↓
' 購入歴データの列レイアウト Enum CNo購入歴データ No = 1 購入者 購入日 品物 価格 個数 お支払い End Enum ' 領収書のセルアドレス Const Adrs領収書_No = "D3" Const Adrs領収書_社名 = "C5" Const Adrs領収書_購入者 = "C7" Const Adrs領収書_購入日 = "G3" Const Adrs領収書_品物 = "F11" Const Adrs領収書_お支払い = "F6" ' 印字するマクロ Sub 購入歴データの指定行を領収書に印字する _ (R As Long, ws購入歴データ As Worksheet, ws領収書 As Worksheet) With ws購入歴データ ws領収書.Range(Adrs領収書_No) = .Cells(R, CNo購入歴データ.No) ws領収書.Range(Adrs領収書_社名) = .Cells(R, CNo購入歴データ.社名) ws領収書.Range(Adrs領収書_購入者) = .Cells(R, CNo購入歴データ.購入者) ws領収書.Range(Adrs領収書_購入日) = .Cells(R, CNo購入歴データ.購入日) ws領収書.Range(Adrs領収書_品物) = .Cells(R, CNo購入歴データ.品物) ws領収書.Range(Adrs領収書_お支払い) = .Cells(R, CNo購入歴データ.お支払い) End With End Sub
だいぶ見やすくなりましたね。
シート名は定数にするだけだと、別のブックを参照したりする改修に弱いままなので、引数にしてしまいました。
これで呼び出すマクロで自由に印字/参照シートを指定できるようになります。
また、何でもかんでも「~の列/~の行」と書くと、逆に読みづらくなります。
行列の番号をRNo、CNoなどにして、
Cells(R, C)っぽく見えるようにすると見やすくなりますね。
「指定の行」なんかは「R」1文字にした方が読みやすいと思います。
ちなみにこの例のように、変数名に「ws/Adrs/CNo」など英字を接頭することを、
アプリケーションハンガリアン記法と呼びます。
詳しい解説は専用の記事に任せるとして、やりたいのはこれです↓
このアニメのように、Ctrl+Spaceのコード補完機能をつかって、
ws+Ctrl+Spaceによる選択肢からの入力が可能になります。
- wsから始めるとシートの候補を
- Adrsから始めるとセルアドレスの候補を
と、欲しいもののリストをくれるのが素晴らしいですし、
接頭が英語だと日本語入力をOFFのままで日本語変数が入力できます。
さらに読む際には、
- 「何のオブジェクト?プロパティ?」っていうシステム的な意味づけ
- 「何の業務?帳票?」っていうビジネス的な意味づけ
これを「英語/日本語」という文字の種類で見分けれるようになります。
便利だし見やすいし言うことないので、興味があればぜひこの記事をよんでみてください。
以上で定数を使ったマクロ作成の解説を終わります。
皆さんお疲れさまでした。
定数を使うと「読みやすく変更に強い」プログラムを書けるようになります。
書き始めは面倒に感じるかもしれませんが、
読みやすさと書き換えやすさは未来への贈り物です。
しっかりマスターしてメンテナブルなコードを書けるようになりましょう!
しかしこの、
「定数をつかえ。マジックナンバーはダメ。ゼッタイ」
っていう話は、みんなどこで習うんですかね?
体系立ててプログラミングを学んだり、会社に入って先輩から教わる人は、
当然のように身につくものなのかな?
自分は
- 周りに自分しかプログラムやってる人がいない
- 自分も本業ではない
- 必要に迫られたときに、そこだけを勉強する
というお手本のようなVBAエンジニアだったので、
割と複雑な処理が書けるようになってからも、定数の存在に気づいていませんでした。
ネットで出てくるサンプルコードは、その機能だけを説明しているコードなので、
定数が出てこないんですよね…。
ノンプログラマーがメインユーザーのVBAは、
こういう「コードを書くためのスキル」を学ぶ機会がなかなかないのがつらいところです。
書いたコードを添削してもらうなんて夢のまた夢ですし。
結果的に私は、「2,3,4,5」を「3,4,5,6」に書き換える改修をちまちまやってました。
いやそんなレベルではなく、
「23,24,25,26」を平気で「31,32,33,34」に書き換えていました。
私にとって、列の挿入は一大アップデートでしたね(笑)
こういう阿呆が減るようにと思いを込めてこの記事を書いたので、
少しでも犠牲者が減ってくれると嬉しいです。