表形式のデータを扱う際に、
Cells(R, 4) = Cells(R, 2) * Cells(R, 3) Range("D" & i) = Range("B" & i) * Range("C" & i)
こんな感じのマクロを書いていませんか?
2や3とか、BやCじゃわからないからコメントをつけたり、
わざわざシートを見に行って、照らし合わせながらコードを書いたり。
そして完成した後、列の挿入や削除が発生し、
3→4、4→5と書き換えていくのが面倒と感じている方は必見です。
定数を使えば、このマクロを劇的に「読みやすく、変更に強く」することができます。
お忙しい方のために最終目的を先に書きます。
' 列番号を先に定数にしておく Enum 売上データの列 購入日 = 1 単価 ' Enum(列挙型の定数)は、ここから先が= 2,3,4と自動連番されるしくみ 個数 金額 End Enum ' 冒頭と同じ処理 Cells(R, 売上データの列.金額) = Cells(R, 売上データの列.単価) * Cells(R, 売上データの列.個数)
一番下のコードは、冒頭の「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
これで、このマクロはデータの改修にとても強くなりました。
列のレイアウトが変わったら、上の定数を変更すればいいのです。
領収書に関係のない変更であれば、このマクロの中身は見る必要もありません。
この後コードがズラズラ続いていても、
このデータを扱うたくさんマクロがあっても、
一番上の定数だけをいじればいいわけです。
よく使うデータは、後で改修がかかる可能性も高いですし、必ず定数にしておきましょう。
定数を使うとコメントが減る
定数なしマクロで右側にあった、補足のコメントがすべて消えていますね。
定数にしたことで、どの列のデータなのかが「見ればわかる」状態になったからです。
これもとても大事な定数の効力です。
定数を使うと、「値に名前を付けられる」ので、
うまく名付けることができればとても読みやすいコードにすることができます。
定数は複数のマクロで共有しましょう
定数宣言のポイントは、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
値を呼び出すときは、「購入歴データの列.No」と「.」でつなぎます。
これが、「No = 1」とした通りに「1」で計算されます。
列番号なので、「No = 1」 が「NoがA列である」ことを意味しますが、
便利なのが「=」を省略すると、そのあとは自動で連番が振られる点です。
セル結合などがなければ、見出し名をたてに並べるだけで、
「購入者=2(B列)」「購入日=3(C列)」…と自動採番されるため、
まさに列番号のための機能といっても過言ではないですね。
今回の題材であった「列の挿入」も列挙型だと超簡単です。
なんと「列名を足す」だけ。
Enum 購入歴データの列 No = 1 社名 ' ←これを挿入した 購入者 ' ← B列だった購入者がC列になるが、連番機能で勝手に3になっている! 購入日 ' 以降の3→4、4→5も同様に対応済 品物 価格 個数 お支払い End Enum
「No」と「購入者」の間に「社名」を入れれば、社名が2になって、それ以降は3,4,…に再採番されます。
素晴らしい。
実際にセルアドレスとして書いた例は↓の通りです。
Cells(R, 購入歴データの列.購入者) ' B列を意味する2が呼び出され、Cells(R, 2)として扱われる
読みやすくていいですね。
しかも入力する時は選択肢が表示され、↓のように選べます。
この機能もとても便利です。
入力もしやすい上に書き間違えもないです。
コードの書きやすさ、メンテナンス性ともに、数値のベタ打ちに比べてケタ違いによくなるので、表形式のシートは、列番号を必ず列挙型で定義しておく癖をつけましょう。
マジックナンバーとは
プログラミングにおいて、マジックナンバーという用語があります。
コード内に直接打ち込まれた数字(とか文字)のことで、
まさに今回取り上げた、
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などと、英字を接頭することを、アプリケーションハンガリアン記法と呼びます。
詳しい解説は専用の記事に任せるとして、やりたいのはこれです。
シートが欲しいときにシートの、セルアドレスが欲しいときにセルアドレスの入力候補をくれるうえに、
接頭が英語だと日本語入力をOFFのままで日本語変数が入力できます。
さらに読む際には、
- 「何のオブジェクト?プロパティ?」っていうシステム的な意味づけ
- 「何の業務?帳票?」っていうビジネス的な意味づけ
これを「英語/日本語」という文字の種類で見分けれるようになります。
便利だし見やすいし言うことないので、興味があればぜひこの記事をよんでみてください。
定数を使って仕様変更に強いマクロを作る話はこれで終了です。
皆さんお疲れさまでした。
しかしこの、「定数をつかえ。マジックナンバーはダメ。ゼッタイ」っていう話は、
みんなどこで習うんですかね。
体系立ててプログラミングを学んだり、会社に入って先輩から教わる人は、
当然のように身につくものなんでしょうか?
自分は
- 周りに自分しかプログラムやってる人がいない
- 自分も本業ではない
- 必要に迫られたときに、そこだけを勉強する
というお手本のようなVBAエンジニアだったから、
割と複雑な処理が書けるようになってからも、定数の存在に気づいていませんでした。
ネットで出てくるサンプルコードは、その機能だけを説明しているコードなので、定数が出てこないんですよね…。
ノンプログラマーがメインユーザーのVBAは、こういう「コードを書くためのスキル」を学ぶ機会がなかなかないのがつらいところです。
書いたコードを添削してもらうなんて夢のまた夢ですし。
結果的に、私は「2,3,4,5」を「3,4,5,6」に書き換える改修をちまちまやってました。
いや、そんなレベルではなく、23,24,25,26 を平気で31,32,33,34に書き換えていました。私にとって、列の挿入は一大アップデートでした。
こういう阿呆が減るようにと思いを込めてこの記事を書いたので、
少しでも犠牲者が減ってくれると嬉しいです。