Quantcast
Channel: t-hom’s diary
Viewing all articles
Browse latest Browse all 493

VBA 中級者を悩ませるプロシージャ分割をマスターする極意

$
0
0

タイトルで大きく出てしまった。極意だなんてまあよく恥ずかしげもなく。
「だって教えるプロの~」よりマシか。。なんちゃって。

ま、是非知ってほしい内容ではあるので、釣ってみた感じ。

さて、それなりにVBAを書けるようになった方が次に悩むこととして、プロシージャの分割方法が分からないというものが多い。Functionの使い方が分からないという相談もよくいただくけれど、これもプロシージャ分割の問題。

上級者のコードは1つのマクロで複数のプロシージャを呼び出していたりするので、「ああ自分のコードと違う」と最初に気付くのがこのプロシージャの分割なのかもしれない。

よくある相談として、「どこをどう分けていいかわからない」というものがある。

これ、まずこの日本語を分けよう。

  • どこを分けていいかわからない
  • どう分けていいかわからない

この2つは別物で、前者は設計の話であるし、後者は具体的なコーディングテクニックの話である。これは明確に区別しておきたい。

実際にプロシージャの分割を学ぶ順としては、まずコーディングテクニックから学んだほうがスッと入ってくると思う。「分割できる」という実感がなければ、どこで分けるべきかという設計の話をしてもいまいちピンとこないんじゃないだろうか。だからまずどこでも分割できるだけのテクニックを身につけると良い。

ということで、今回はこの分割のためのテクニックを中心に解説していく。
設計についても書くつもりだったけど、テクニックを書いたら疲れてしまったのでそれはまた別の機会に。。

具体的なプロシージャ分割テクニック

さて、ここからは実際にプロシージャの分割方法について説明していく。
サンプルに使用するコードはこちら。

Subじゃんけん()Dim you AsIntegerDim com AsIntegerDo
        you =CInt(InputBox("じゃんけんの手を数字で入力"&vbNewLine& _"1:グー、2:チョキ、3:パー"))
        com = WorksheetFunction.RandBetween(1,3)SelectCase com
        Case1MsgBox"相手はグーを出しました。"Case2MsgBox"相手はチョキを出しました。"Case3MsgBox"相手はパーを出しました。"EndSelectIf you = com ThenMsgBox"あいこです。もう一度。"ElseIf(you =1And com =2)Or(you =2And com =3)Or(you =3And com =1)ThenMsgBox"あなたの勝ちです。"ElseMsgBox"あなたの負けです。"EndIfLoopWhile you = com
EndSub

まずは空行に注目する。皆さんもコードを書くとき、ある程度処理のまとまりごとに適宜空行を入れていると思う。つまりここが処理が切り替わる地点だと認識しているわけだ。
そもそもこの程度のマクロをわざわざ分割すべきかどうかという話は一旦おいといて、とりあえず分割してみよう。

最初にやることは、マクロをそっくりそのままバックアップしておくこと。マクロを分割するということは、下手をすると動かなくなってしまうからバックアップは大事。
別のモジュールを挿入してコピー&ペーストしておこう。

次にやることは、メインコードと同じモジュールに新しいプロシージャを作ることだ。

Sub手の入力()EndSub

まあこれは当たり前。
次に元のコードから切り出したい部分を、文字通り切り出してくる。

元のコードの該当部分は、作成した「手の入力」プロシージャに変更する。

Sub手の入力()
    you =CInt(InputBox("じゃんけんの手を数字で入力"&vbNewLine& _"1:グー、2:チョキ、3:パー"))
    com = WorksheetFunction.RandBetween(1,3)EndSubSubじゃんけん()Dim you AsIntegerDim com AsIntegerDoCall手の入力
        
        SelectCase com
        Case1MsgBox"相手はグーを出しました。"'----以下略

この時点ではまだ正しく動作しない。じゃんけんプロシージャの変数you、comと手の入力プロシージャの変数you、comはプロシージャを分割した時点でまったく関係がなくなってしまうからだ。単に名前が同じなだけ。

関係ないものを同じ名前にしておくとややこしいので、別の名前に変更してしまおう。

Sub手の入力()
    your_hand =CInt(InputBox("じゃんけんの手を数字で入力"&vbNewLine& _"1:グー、2:チョキ、3:パー"))
    computers_hand = WorksheetFunction.RandBetween(1,3)EndSub

次に外からデータを受け取れるように、手の入力プロシージャのカッコ内にこの変数を入れる。

Sub手の入力(your_hand, computers_hand)

ここに入力した変数は仮引数(かりひきすう)と呼ばれ、外部から渡されたデータが代入される専用の変数として宣言したことになる。
仮引数(かりひきすう)はプロシージャ内ではふつうの変数(専門的にはローカル変数という)と同じように使えるが、一点注意として、特に指定しなければ仮引数(かりひきすう)は参照という方法でデータを受け取るということ。

※いちいち(かりひきすう)と振り仮名を打ってるのは、私が「いんすう」と読む癖がなかなか抜けなかったので。最初の思い込みはなかなか消えないものだ。

手の入力プロシージャを呼び出すときに変数youとcomを渡すと、your_handとcomputers_handはそれぞれyouとcomと同じものとして扱われ、たとえばyour_handに値を代入するとyouにも同じ値が代入される。これが参照渡しである。

参照渡しの詳しい仕組みは以下の記事に書いたので興味があればどうぞ。
thom.hateblo.jp

次に、じゃんけんプロシージャから手の入力を呼び出すコードのカッコ内に、youとcomを記入する。

Call手の入力(you, com)

これは実引数(じつひきすう)と呼ぶ。仮引数(かりひきすう)と実引数(じつひきすう)はどちらも単に引数(ひきすう)と呼ばれることが多いが、説明の都合上分けておいたほうが理解しやすいのであえて用語を紹介した。

さて、これで1箇所分割できた。

他の箇所も分割するとこのようになる。

Sub手の入力(your_hand, computers_hand)
    your_hand =CInt(InputBox("じゃんけんの手を数字で入力"&vbNewLine& _"1:グー、2:チョキ、3:パー"))
    computers_hand = WorksheetFunction.RandBetween(1,3)EndSubSub相手の手を表示(computers_hand)SelectCase computers_hand
    Case1MsgBox"相手はグーを出しました。"Case2MsgBox"相手はチョキを出しました。"Case3MsgBox"相手はパーを出しました。"EndSelectEndSubSub勝敗判定(your_hand, computers_hand)If your_hand = computers_hand ThenMsgBox"あいこです。もう一度。"ElseIf(your_hand =1And computers_hand =2) _Or(your_hand =2And computers_hand =3) _Or(your_hand =3And computers_hand =1)ThenMsgBox"あなたの勝ちです。"ElseMsgBox"あなたの負けです。"EndIfEndSubSubじゃんけん()Dim you AsIntegerDim com AsIntegerDoCall手の入力(you, com)Call相手の手を表示(com)Call勝敗判定(you, com)LoopWhile you = com
EndSub

あ、それと今回はちょっと例が悪いので紹介できないけれど、プロシージャ分割した時点で、元の変数がそのプロシージャ内だけで使う一時的な変数になることもある。つまり仮引数として外部から持ってこなくても、そのプロシージャ内で宣言して、そのプロシージャ内で使い終わるような変数。

これをローカル変数と呼ぶが、その前段として以下の準備が必要になる。
thom.hateblo.jp

関数分割するかどうかにかかわらず、普段から変数は使用する直前で宣言するようにしておくと良い。

プロシージャを関数化するテクニック

VBAにおいて関数というのは、要するにデータを返すプロシージャで、ふつうはFunctionプロシージャで作る。
Functionをどういうときに使うのかという質問もよく受けるけど、その前にプロシージャの分割ができていることが前提となる。
さて、プロシージャの分割までは前項で完了したので、これの一部を関数化していこう。

まずはこちら。

Sub相手の手を表示(computers_hand)SelectCase computers_hand
    Case1MsgBox"相手はグーを出しました。"Case2MsgBox"相手はチョキを出しました。"Case3MsgBox"相手はパーを出しました。"EndSelectEndSub

まずはSubをFunctionに書き換える。

Function相手の手を表示(computers_hand)SelectCase computers_hand
    Case1MsgBox"相手はグーを出しました。"Case2MsgBox"相手はチョキを出しました。"Case3MsgBox"相手はパーを出しました。"EndSelectEndFunction

まだこの時点では値を返すことはできない。

そして、MsgBoxを表示させていたところを、プロシージャ名への代入式に変更する。

Function相手の手を表示(computers_hand)SelectCase computers_hand
    Case1相手の手を表示 ="相手はグーを出しました。"Case2相手の手を表示 ="相手はチョキを出しました。"Case3相手の手を表示 ="相手はパーを出しました。"EndSelectEndFunction

「相手の手を表示」プロシージャの変更はとりあえずこれだけでもOK。

プロシージャに代入ってところがイメージできにくいかもしれないのでもう少し簡単なサンプルで例を示す。

まずはSubで参照渡しを使った値の取得からおさらい。

Subヨブ()Dim ret AsLongCallヨバレール(10, ret)MsgBox ret
EndSubSubヨバレール(a, return_value)
    return_value = a *2EndSub

ヨブを実行すると、ヨバレールに実引数10とretが渡り、仮引数aとreturn_valueで受け取る。
このときヨバレールのreturn_valueはヨブのretと同じものを指しているのでreturn_valueに10*2が代入されるということは、retにも20が入る。

…という面倒な処理を頻繁にしなくて良いように、もうreturn_valueは書かなくても使えるようにしない?って生まれたのがFunction。
Functionを使って書き直すとこうなる。

Subヨブ()Dim ret AsLong
    ret =ヨバレール(10)'呼出しから戻ると見えないreturn_valueがretに入るMsgBox ret
EndSubFunctionヨバレール(a)'見えない仮引数return_valueがある。ヨバレール = a *2EndFunction

プロシージャ名自体が、return_valueのように機能する。
ここで呼出し元に戻す値を「戻り値(もどりち)」と呼ぶ。

戻り値はどんなデータ型を戻すか指定することもできる。
それにはプロシージャ名のカッコの後ろに[As データ型]を付与すれば良い。

Functionヨバレール(a)AsLong

ちなみに引数にもデータ型を指定することができる。

これらを「相手の手を表示」プロシージャに反映させるとこうなる。

Function相手の手を表示(computers_hand AsInteger)AsStringSelectCase computers_hand
    Case1相手の手を表示 ="相手はグーを出しました。"Case2相手の手を表示 ="相手はチョキを出しました。"Case3相手の手を表示 ="相手はパーを出しました。"EndSelectEndFunction

ちなみに私は戻り値は一旦retという変数に入れて最後でプロシージャに代入することが多い。

Function相手の手を表示(computers_hand AsInteger)AsStringDim ret AsStringSelectCase computers_hand
    Case1
        ret ="相手はグーを出しました。"Case2
        ret ="相手はチョキを出しました。"Case3
        ret ="相手はパーを出しました。"EndSelect相手の手を表示 = ret
EndFunction

そうすればプロシージャ名を変更したときに、2箇所の書き換えで済むから。

さて、呼出し側にはString型でメッセージが戻るので、直接MsgBoxに引き渡してやるとそのまま画面表示される。
このようなコードになった。

Subじゃんけん()Dim you AsIntegerDim com AsIntegerDoCall手の入力(you, com)MsgBox相手の手を表示(com)Call勝敗判定(you, com)LoopWhile you = com
EndSub

勝敗判定の関数化は皆さんでやってみてほしい。
いろんなやり方がある。たとえば、

  • メッセージを返す。
  • 結果を1(あいこの場合)、2(勝ちの場合)、3(負けの場合)とLong型で返し、呼出し元のSelect文でメッセージを分ける。
  • 結果を"あいこ"、"勝ち"、"負け"とString型で返し、呼出し元のSelect文でメッセージを分ける。
  • あらかじめ列挙型定数GameResultを宣言し、Win、Even、Loseを含める。呼出し元のSelect文でメッセージを分ける。

などなど。

どれが良いかという議論の前に、いろんなやり方、引き出しを持っておくことが重要だと思う。

どこを別プロシージャに分割するかについての参考書

以下の書籍が非常に参考になった。

ゲームプログラマのためのコーディング技術

ゲームプログラマのためのコーディング技術

書かれているコードはC++なのでVBAしかしない方は購入を躊躇するかもしれないけど、P67~89(初版 第1刷の場合)の関数化のパターンは中級者にとって非常に有益な情報が掲載されている。

一部引用

コードの重複部分をまとめるだけが関数化ではありません。

~ 中略 ~

関数化するポイントにはパターンがあります。ここでは、次の関数化のパターンを初心者でもわかりやすいように紹介します。

・条件式の関数化
・計算式の関数化
・ループの関数化
・ループのブロック内の関数化
・データ変換の関数化
・データ確認の関数化
・配列アクセスの関数化
・コメント部分の関数化

このパターンを身に付けるだけで格段にコードの保守性が高まります。

以降のページでこれらについて詳しく解説されている。

また、多過ぎる引数の問題、小さな関数の必要性、関数化の目的は再利用だけではない、などの非常にためになるトピックを扱っている。

ちなみにこの書籍でいう関数とは、Functionはもちろん、Subプロシージャも含むと思って良い。C++言語の用語ではどちらも関数なのだ。

ゲームプログラマのための」とタイトルについてるけど、具体的にゲームを作る話は出てこず、専らコーディング技術に焦点を当てた書籍なので、「すべてのプログラマのための基本コーディング技術」というタイトルの方が売れたかもしれない。

Amazonレビューでは「今更感の強い内容」といったレビューもあるのだけど、あくまで経験を積んだ現役バリバリのプログラマーにとって今更だというだけで、事務職や運用でVBAやってる方々からしたら目から鱗なお宝が盛り沢山だ。

一方で、クラスに関してはC++を前提にしているのでVBAで参考にできる部分とそうでない部分がある。VBAには継承が存在しないためだ。また、STLラムダ式など、VBAに無い機能を前提に書かれている箇所もあるのですべて参考にできるわけではない。

そのへんを割り切って、コードではなく解説をメインに読むと色々と学べるところがあると思う。

あ、あとインストラクターのネタ帳の伊藤 潔人さんがちょうど先ほどTwitterでタイムリーに以下の記事を紹介されていたのでこれも参考に追記。
www.publickey1.jp


Viewing all articles
Browse latest Browse all 493

Trending Articles