オブジェクト変数の宣言と使用については、以下の2パターンが存在する。
■パターン1 宣言と同時にNewしてしまう方法
Dim C As New Collection
■パターン2 宣言とオブジェクトのSetを分ける方法
Dim C As Collection
Set C = New Collection
私はこれまで大体のケースでパターン1を用いてきた。そのほうがコードが短くまとまってスッキリするからだ。
たかが1行とあなどるなかれ。1つのプロシージャのサイズはスクロールせずに全体が見渡せるくらいの長さが理想的であるが、特にノートPCのように画面が狭い環境では1行といえど貴重なスペースである。
ところがちょうど今日、VBAのお膝元であるMicrosoftのサイトでVB6.0においては変数宣言時にNewするのは避けるべきであるとの記述を発見してしまった。
https://msdn.microsoft.com/ja-jp/library/dd297716.aspx
VBAも実質はVB6.0なのでもちろんこれに該当する。
短く書けなくなるのは残念であるが、MSの公式見解である以上は方針転換を検討せざるを得ない。
そもそも変数宣言でNewした場合の動作について
前述のページには「Visual Basic 6.0 では、このコードのある行ではオブジェクトは生成されずに、それを利用するタイミングに、オブジェクトがすでに作られているかどうかをチェックし、なければ作成されるというものでした。これによる利点と欠点は次の通りです。」とある。
まぁここまでは知っていた。
驚いたのはここからで、「必ずオブジェクトが存在することが保証される (Nothing を設定してオブジェクトが破棄されたとしても、オブジェクト変数を再利用しようとすると、再作成される)」という記述である。
実際にやってみた。
Sub あれれー() Dim C As New Collection Set C = Nothing C.Add "あれ?なんでNothing入れたのにAddできるん?" MsgBox C.Item(1) End Sub
マジだ。
オブジェクトを明示的にNothingで破棄しているにもかかわらず、C.Addができてしまう。
ちなみに、念のため宣言とNewを分けて書いてみたところ、エラーになる。これは予想どおり。
Sub こっちはエラーになる() Dim C As Collection Set C = New Collection Set C = Nothing C.Add "このAddは無理" MsgBox C.Item(1) End Sub
さて、この実験により変数宣言時にNewするとオブジェクト変数にNothingを設定しても、そのオブジェクト変数を再利用しようとすると自動でオブジェクトが生成されてしまうことが判明した。
ところで、オブジェクトの利用とはプロパティの設定やメソッドの実行だけを指しているのではない。単にオブジェクト変数を参照するだけで利用したことになってしまうのだ。
以下のコードはオブジェクト変数にNothingを代入しているにも関わらず、Nothingと比較するとFalseになってしまうというもの。
Sub Nothingになってくれない() Dim C As New Collection Set C = Nothing Debug.Print C Is Nothing Set C = Nothing Debug.Print C Is Nothing End Sub
Set C = Nothingの時点ではちゃんとNothingが入っているけど、いざ比較しようとしたらやはりオブジェクト変数の中身を参照する必要があるので、その時点で中身が作られてしまいNothingではなくなってしまうのだ。
つまりCはNothingになるけれど、観測した瞬間Nothingではなくなってしまう。なんだか量子力学にでてくるシュレーディンガーの猫の話と似てる。まぁ、VBAの場合はローカルウインドウを使えばちゃんとNothingになっている瞬間を観測できるけどね。
オーバーヘッドについて
変数宣言するときにNewした場合、オブジェクト変数が参照されるたびにNothingかどうかをチェックするのでオーバーヘッドが発生する。
MSDNには変数宣言時にNewする方法について、「オーバーヘッドが大きいので、使うべきではありません。」と書かれているが、はたしてどれくらい違うものなんだろうか。
実際にやってみた。
Sub 変数宣言とNewを分ける() t = Timer Dim C As Collection Set C = New Collection For i = 1 To 10000000 C.Add i Next Debug.Print Timer - t End Sub Sub 変数宣言と同時にNew() t = Timer Dim C As New Collection For i = 1 To 10000000 C.Add i Next Debug.Print Timer - t End Sub
結果は、大して変わらない。
いちおう変数宣言とNewを分けた書き方の方が早かった。
とはいえ、1000万回ループさせてやっと有意差が0.3~0.4秒なのでオーバーヘッドによる遅延は微々たるものだ。
変数宣言時にNewするとコンストラクターの発動タイミングが変わる。
実はDim C As New Collectionが実行されたタイミングではまだCにはCollectionが入っていない。
実際にCollectionが変数に格納されるのは、一度でもそのオブジェクト変数が利用されたときだ。
まぁ有名な話なので知ってる人は知ってると思うが、ここで今一度実験によってその挙動を明らかにしておきたい。
調査するためには、Collectionでは分かりにくいので自作のクラスにコンストラクターを実装して確認してみよう。
コンストラクターとは、オブジェクトが生成されたタイミングで自動的に発動される特殊なプロシージャである。
クラスモジュールClass1を作成し、次のコードを張り付ける。
Private Sub Class_Initialize() Debug.Print "オブジェクトが生まれました。" End Sub Sub hoge() Debug.Print "hoge" End Sub
次に標準モジュールのコード
まずはMicrosoftの推奨する方法で行儀よく。
Sub 宣言とNewを分けた場合() Dim c As Class1 Debug.Print "標準コード1" Set c = New Class1 Debug.Print "標準コード2" c.hoge End Sub
結果はこうなった。
標準コード1 オブジェクトが生まれました。 標準コード2 hoge
ちゃんとNew Class1とした瞬間にオブジェクトが生まれている。
では次に変数宣言時にNewした場合。
Sub 宣言時にNewした場合() Dim c As New Class1 Debug.Print "標準コード1" c.hoge End Sub
結果は以下のとおり。
標準コード1 オブジェクトが生まれました。 hoge
ほら、宣言時のNewではオブジェクトが生成されておらず、次の標準コード1が表示されている。
そしてc.hogeと命令を出した瞬間、命令よりも一瞬早くコンストラクターのコードが実行される。
次に、メソッドの実行などを行わずにオブジェクト変数の参照だけするケース。
変数のアドレスを調べるVarPtr関数を使ってみた。
ザ・参照!って感じの関数なのでちょうど良いかと思って。
Sub 宣言時にNewしたのち参照だけした場合() Dim c As New Class1 Debug.Print "標準コード1" Debug.Print VarPtr(c) Debug.Print "標準コード2" c.hoge End Sub
結果は次のとおり
標準コード1 オブジェクトが生まれました。 3141520 標準コード2 hoge
この場合も、初めて参照されたタイミングでコンストラクターが実行されている。
オブジェクト変数の宣言時にNewしたことで発生しうるバグ
まあ、変数宣言時にNewしたからといってそれが原因で起こるバグというのはあまり考えにくいのだけれど、そこはあえてバグが起きるコードを考えてみた。
※バグはエラーとイコールではない。作者の意図しない動作はすべてバグである。
次のようなクラスモジュールを作ってみた。
コンストラクターでSheet1のA1セルにHelloを書き込み、デストラクターでGood Bye!を書き込む。
Private Sub Class_Initialize() Sheet1.Range("A1").Value = "Hello" End Sub Sub hoge() Sheet1.Range("A1").Value = "hoge" End Sub Private Sub Class_Terminate() Sheet1.Range("A1").Value = "Good Bye!" End Sub
そして標準モジュール。
まずは宣言とNewを分けた場合。
Sub 宣言とNewを分ける() Sheet1.Range("A1").Value = "Start" Dim c As Class1 Set c = New Class1 Debug.Print Sheet1.Range("A1").Value c.hoge Debug.Print Sheet1.Range("A1").Value Set c = Nothing Debug.Print Sheet1.Range("A1").Value End Sub
結果はこうなった。
Hello hoge Good Bye!
最初にA1セルにStartを入れているが、cにNew Class1を代入した時点でコンストラクターによりA1にはHelloが入るため、Debug.Printで最初の出力はHelloとなっている。
次に宣言時にNewした場合。
Sub 宣言時にNewする() Sheet1.Range("A1").Value = "Start" Dim c As New Class1 Debug.Print Sheet1.Range("A1").Value c.hoge Debug.Print Sheet1.Range("A1").Value Set c = Nothing Debug.Print Sheet1.Range("A1").Value End Sub
結果はこうなった。
Start hoge Good Bye!
変数宣言時にNewしているためここではまだオブジェクトは生成されず、A1の値はStartとでる。
次にhogeが実行される瞬間、コンストラクターによりA1の値がHelloに書き換えられるが、直後にhogeが実行されてA1の値はhogeになってしまう。
折角コンストラクターを作ったのに無意味な動作になってしまったため、これはバグだと言える。
あるいは別のシチュエーションとして、すでにNothingを代入して破棄したオブジェクトを間違って参照してしまったとき、そのコードが動いてしまうことだ。
さらにまずいことに、内部ではオブジェクトが再作成されるため、再度コンストラクターが実行されることだ。
まあこれはプログラマーのミスなのであるが、変数宣言とNewを分けておけば、破棄したはずのオブジェクト変数に再アクセスしてしまった際にちゃんとエラーで知らせてくれる。