🍖

ここが変だよVBA

2022/06/17に公開

SESをやってると、影響調査とかでEXCEL設計書から機械的に情報を抽出したい場合が結構ある。
エンジニアたるもの、そんな時には泥臭くコピペをして資料をまとめるのではなく、サクッとプログラミングで解決したい。

そこでVBAでちょっとした処理を書き始めるのだが、VBAには他の言語の感覚からすると正気を疑うような変わっている仕様がいくつかあって、久しぶりに使うと思わぬ所でハマって無駄に時間を使ってしまい、この世のすべてを呪う羽目になることが多いので、愚痴と備忘録を兼ねて記事を書いておく。

筆者は別にVBAのスペシャリストではないので、これらの仕様にメリットや何か深遠な理由があるという指摘があれば、是非教えていただきたい。

オブジェクトを代入したいときはsetをつける必要がある

有名だと思うけど、VBAではオブジェクトを変数に代入したいときはsetをつける必要がある。
dimで型を宣言していても、コードを書いているときには検知してくれず、実行時までわからない。おまけにエラーメッセージが死ぬほどわかりづらいと、VBAで一番嫌いなところ。

なにいってんだこいつ

相変わらず何を言っているかはわからん(引数…?)が、dimを付ければ一応エラー箇所はわかるっぽい
コンパイルエラーって書いてあるのに、なんで実行するまで教えてくれないんだろう。だれか詳しい人いたら教えてください。
thenの付け忘れとかは行を移動しただけで鬱陶しくメッセージボックスで指摘してくるのに、これに関してはダンマリなのがより腹立つ。まあこれはVBAっていうよりVBEについての愚痴か。
(追記:FOR~NEXTのNEXTの付け忘れとかも実行時にしか教えてくれなかったので、単純に一行で間違いがわかる場合以外は、実際にコード実行を実施するまで教えてくれないっぽいです)

Functionの呼び出し方法

個人的に一番特殊かつ複雑だと思うのがこれ、引数の有無や呼び出し方によって微妙に書き方が違う上に省略記法がたくさんある。

引数がないFunctionを呼び出したいとき

例えば、引数や戻り値のないメソッドhogeを作ったとして、他言語の感覚で

hoge()

みたいにして呼び出そうとしてもエラーになってしまう。

メソッドを呼び出すときはcallステートメントを使用し、

call hoge

のように書く。

このcallは省略することもできる

hoge

とすることで、メソッドを呼び出すことができる。

ちなみに、この際に

call hoge()

と書いても末尾の()はエディタに消されてしまう、これはこの()がメソッド呼び出しとは無関係の空の括弧とみなされるからだろう。

ここまでだと、call hogeが正式な書き方で、callは省略してもいいのね。という風に考えられるし別段複雑じゃない。
しかし、この認識でいられるのは引数がないとき限定である。

引数を持つFunctionを呼び出したいとき

引数がある場合は以下のように書く。

call hoge(a,b)

もしくは

hoge a,b

そして、以下のように書くとエラーになる

'NG
call hoge a,b
'NG
hoge(a,b)

ここまでを総合すると、
callステートメントあり、引数なしの場合は()は不要だが、(付けても勝手に消される)
callステートメントあり、引数ありの場合は()を付けなければいけない。
そして、callを省略するのであれば()は付けてはいけない。とかなりややこしい。

引数なし 引数あり
callあり ()はつけてもつけなくてもいい ()は必須
callなし ()はつけてはいけない ()はつけてはいけない

さらにややこしいことを言うと、hoge(a,b)の書き方でも引数が1個の時だけは例外的にエラーにならない。

上の画像を見てもらうとわかるが、hogeと(a)の間にスペースが入っている。(行を移動すると、VAEが勝手に入れてくれる)

なので、この場合は、hoge aa()がついているという扱いのようだ。引数なしの項で紹介した、call hoge()の場合と同じである。引数が複数のhoge(a,b)だとNGになるのは(a,b)という括弧の付け方は存在しないから。
(じゃあなんで一番上で書いた、引数なしのhoge()がエラーになってしまうのかは後述する)

この時点でもう相当お腹いっぱいだが、これで終わりではない。

関数の戻り値を変数に格納したいとき

関数の返り値を変数に格納したい場合、callステートメントは使用することができない。

'NG
fuga = call hoge(a,b)

じゃあ、上で書いたcallを省略した記法で書けばいいのかといわれるとそれも違う。以下のような書き方はコンパイルエラーとなる。

'NG
fuga = hoge a,b

じゃあどうすればいいのかというと、なんと、戻り値を格納したい場合はメソッドの後に()を付けなければいけない。
つまり、戻り値を受け取りたい場合は、戻り値を受け取らない場合にはエラーになっていたhoge(a,b)の書き方が正解となる。

'これはNGだけど
hoge(a,b)
'これはOK!
huga = hoge(a,b)

そして、当然のようにこっちにも例外があって、引数なしの場合は()は付けてもつけなくてもいい。(call hoge()の時と違って()が消えるわけではないので、空の括弧として認識しているわけではない。おそらくどちらも書き方として許容されているということだろう)

'OK
huga = hoge()
'OK
huga = hoge

これが冒頭のhoge()だとエラーになる理由。エラーメッセージの「修正候補 =」は「()を使っているのに変数に格納してないよ」という意味だったわけだ。わかるわけねえだろ。

引数なし 引数あり 引数なしのメソッドの返り値 引数ありのメソッドの返り値
callあり ()はつけてもつけなくてもいい(つけても勝手に消される) ()は必須 - -
callなし ()はつけてはいけない ()はつけてはいけない ()はつけてもつけなくてもいい ()は必須

どうだろうか。
ただメソッドを呼び出すだけにしては、あまりにも複雑すぎる。

個人的には、call省略できる 場合と、call書いてはいけない 場合があることが混乱を生み出していると思う。

returnがない

vbaにはreturnがない。

関数名 = 返り値

とすることで、メソッドの返り値を設定することはできるのだが、他の言語のreturnと違ってその場で関数が終了しない。
よって、関数名 = 返り値で返り値を設定した後、再度関数名 = 返り値 が呼び出されると返り値は上書きされてしまう。
他の言語の感覚で

Function hoge()
    If True Then
        hoge = "expect res"
    End If
    hoge = "actual res"
End Function

とか書いたときに"expect res"が返ってくる想定でいると痛い目を見る。
returnの挙動をさせたいのであれば、返り値の設定の次の行にexit functionを付けることで再現できる。

Function hoge()
    If True Then
        hoge = "expect res"
        Exit Function
    End If
    hoge = "actual res"
End Function

昔、VBAで再帰処理を書いたときにこの仕様を見落としてて、わけのわからん戻り値が返ってきてえらい目にあった。
この仕様があることで、関数を終了したくないときにも戻り値をセットできるが、正直それがメリットになる状況が思い浮かばない。

try-catchがない

vbaにはtry-catchもない。
on error GoTo Labelを使って実装していく必要があるのだが、これがなかなかめんどくさい。
というのも、:Label部分は他言語のcatch節と違ってエラーが起こらなくても実行されてしまうからだ。
エラー時に強制終了させたい場合は、exit subの外側にラベルを付けてあげて

Sub Hoge()
    On Error GoTo myError
	....
    Exit Sub
myError:
    MsgBox "エラーが発生しました"
End Sub

のようにすればいいだけなのだが、問題は例外処理をしたらそのまま後続の処理を続けたい場合である。
たとえば、forループ内で各wsに対してworksheetfunction.matchを使う場合などは大変。(worksheetfunctionは結構気軽に例外を吐き出す。例えばmatchだと検索値が存在しないだけでエラーになる)

Sub Hoge()
	....
	for each ws in worksheets
		on error goto myError
		'"hoge"が見つからなかった場合も、続けて他のシートの検索はしたい
		worksheetfunction.match("hoge",ws.range("A:A"),0)
		'エラーが発生したシートではここの処理は行いたくない
		call dosomething()
myerror:
		msgBox "エラーが発生しました"
	next
End Sub

などとしてしまうと"hoge"が見つかるか見つからないかにかかわらず、各シートごとに"エラーが発生しました"というメッセージボックスが発生してしまう。

なのでif文で制御をすることになる。

Sub Hoge()
	....
	for each ws in worksheets
		on error goto myError
		'"hoge"が見つからなかった場合も、続けて他のシートの検索はしたい
		worksheetfunction.match("hoge",ws.range("A:A"),0)
		'エラーが発生したシートではここの処理は行いたくない
		call dosomething
myerror:
		If Err.Number <> 0 Then
			msgBox "エラーが発生しました"
			'err.clearを呼び出さないと永遠にerr.numberに値は入ったまま
			call err.clear
		end if
	next
End Sub

gotoでなくOn Error Resume Nextifの組み合わせでも同じようなことができる。

	for each ws in worksheets
		'On Error Resume Next以降は例外が発生しても次の処理が実行される
		 On Error Resume Next
		'"hoge"が見つからなかった場合も、続けて他のシートの検索はしたい
		worksheetfunction.match("hoge",ws.range("A:A"),0)
		If Err.Number = 0 Then
			'エラーが発生したシートではここの処理は行いたくない
			call dosomething
		else
			msgBox "エラーが発生しました"
			'err.clearを呼び出さないと永遠にerr.numberに値は入ったまま
			call err.clear
		end if
		'On Error GoTo 0以降は、再び例外発生の場合は処理が終了するようになる
		On Error GoTo 0 	
	next
End Sub

面倒なことこの上ない。
あって当たり前だと思っていた、try-catchのありがたさがよくわかる。

文字列連結は"+"ではなく"&"

メンドクセ

インクリメントとデクリメントがないしi += 1とかもない

メンドクセ

結論

pythonを使おう

Discussion