t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

VBA ウォッチウインドウとステップ実行でオブジェクト変数の仕組みを学ぶ

以前クラスモジュールの入門記事でオブジェクト変数はタグのようなものだと書いた。
https://thom.hateblo.jp/entry/2016/12/31/013555#オブジェクト変数は箱じゃなくてネームタグでイメージしよう

具体的な仕組みは以下の記事で書いた。
thom.hateblo.jp

とはいえメモリの動きなどは座学ではなかなかイメージしづらいので、今回はウォッチウインドウとステップ実行を使って具体的なメモリのアドレスを確認していこうと思う。

使用するのは、VarPtr関数とObjPtr関数だ。

ウォッチウインドウを使用する前に、基本的な使い方を押さえておこう。
まずVarPtrは、変数が指す実際のメモリアドレスを取得するための関数だ。

次のコードを実行してみてほしい。

Sub 基本型の変数()
    Dim L As Long
    L = 10
    Debug.Print "変数Lはアドレス" & VarPtr(L) & "を指している。"
    Debug.Print "アドレス" & VarPtr(L) & "に" & L & "が入っている。"
End Sub

私の環境ではイミディエイトウインドウに次のように表示された。

変数Lはアドレス1896880を指している。
アドレス1896880に10が入っている。

アドレスの数値は実行環境やタイミングによって変わるので皆さんの環境ではまた違った結果になると思う。

基本型の変数はこのように、「変数→メモリアドレス→中身」という仕組みになっている。
対してオブジェクト型の場合は、「変数→メモリアドレス→メモリアドレス→中身」という仕組みだ。
1つ目のメモリアドレスの中身に2つ目のメモリアドレスが入っていて、間接的に中身を指している。

この2つ目のメモリアドレスを取り出すための関数がObjPtrである。

次のコードで確かめてみよう。

Sub オブジェクト型の変数()
    Dim O As Collection
    Set O = New Collection
    O.Add 10
    Debug.Print "変数Oはアドレス" & VarPtr(O) & "を指している。"
    Debug.Print "アドレス" & VarPtr(O) & "にアドレス" & ObjPtr(O) & "が入っている。"
    Debug.Print "アドレス" & ObjPtr(O) & "に" & TypeName(O) & "が入っている。"
    Debug.Print TypeName(O) & "のItem(1)に" & O.Item(1) & "が入っている。"
End Sub

結果はこのようになった。

変数Oはアドレス1896880を指している。
アドレス1896880にアドレス88747728が入っている。
アドレス88747728にCollectionが入っている。
CollectionのItem(1)に10が入っている。

つまり「変数O→1896880→88747728→Collection」という構成。

さて間接参照になっているということを理解したところで、ウォッチウインドウでその動きを見ていこう。
対象のオブジェクトは今回は自作することにした。

まずクラスモジュールを挿入し、以下の1行を記入する。今回クラスモジュール名はClass1のまま変更しない。

Public Value As Long

これはValueというパブリック変数のみを持つクラスで実用性はまったくないが今回はオブジェクト変数の仕組みを知るためなのでこれで良い。

次に標準モジュールに以下のコードを書く。

Sub hoge()
    Dim a As Class1
    Set a = New Class1
    a.Value = 10
    
    Dim b As Class1
    Set b = New Class1
    b.Value = 20

    Debug.Print a.Value
    Debug.Print b.Value
End Sub

次に、表示メニューからウォッチウインドウを表示させ、右クリックメニューからウォッチ式の追加をクリックする。
f:id:t-hom:20170116001050p:plain

そして式欄に「VarPtr(a)」と入れ、OKを押す。
f:id:t-hom:20170116001406p:plain

するとこのように一行ウォッチ式が追加される。
f:id:t-hom:20170116001638p:plain

同じようにして「VarPtr(b)」「ObjPtr(a)」「ObjPtr(b)」も追加しよう。

このように表示されたら準備完了。
f:id:t-hom:20170116001842p:plain

F8キーでステップ実行を開始すると、早速VarPtr(a)とVarPtr(b)の値が確定する。
f:id:t-hom:20170116002014p:plain
どうやら変数のアドレスは宣言文より前、実行直後に決まるようだ。

ちなみにステップ実行では、黄色く表示されているところがこれから実行するコードである。すでに実行されたコードと勘違いしやすいので注意。

Set a = New Class1の実行がおわると、ObjPtr(a)にアドレスが入る。
f:id:t-hom:20170116002506p:plain
つまりアドレス88522536に実際のオブジェクトが生成されたということ。
こういう状態:「変数a→1896880→88522536→オブジェクト」

そしてSet b = New Class1の実行が終わると、ObjPtr(b)にもアドレスが入る。
f:id:t-hom:20170116002801p:plain

別のオブジェクトなのでアドレスも異なる。

では次に、コードを書き換えて実行してみる。

Sub hoge()
    Dim a As Class1
    Set a = New Class1
    a.Value = 10
    
    Dim b As Class1
    Set b = New Class1
    b.Value = 20
    
    Dim c As Class1
    Set c = a
    Set a = b
    Set b = c

    Debug.Print a.Value
    Debug.Print b.Value
End Sub

ウォッチ式にも「ObjPtr(c)、VarPtr(c)」をそれぞれ追加しよう。

ステップ実行で下図の位置まですすめる。
f:id:t-hom:20170116003308p:plain
※この時点でSet c = aはまだ未実行であることに注意。

現時点では、変数aが88521736にあるオブジェクトを指していて、変数bは88522536にあるオブジェクトを指している。

次にSet c = aを実行すると、変数cも88521736にあるオブジェクトを指すようになる。
f:id:t-hom:20170116003620p:plain

つまり、変数aと変数cは全く同一のオブジェクトを参照していることになる。

次にSet a = bを実行すると、変数aは88521736を参照するのをやめて、変数bと同じ88522536のオブジェクトを参照するようになった。
f:id:t-hom:20170116003752p:plain

最後にSet b = cが実行されると、変数bは88522536の参照をやめて、変数cと同じ(つまり当初変数aが参照していた)88521736を参照するようになる。
f:id:t-hom:20170116003908p:plain

これで変数aが指すオブジェクトと変数bが指すオブジェクトが入れ替わったことがわかる。

さて、次はコレクションを使って見ていこう。

コードは以下のとおり。

Sub hoge()
    Dim a As Class1
    Dim col As Collection
    Set col = New Collection
    For i = 1 To 3
        Set a = New Class1
        col.Add a
    Next
End Sub

ウォッチ式はいったんすべて削除し、新たに「ObjPtr(a)」「ObjPtr(col.Item(1))」「ObjPtr(col.Item(2))」「ObjPtr(col.Item(3))」「i」を追加しておく。

ステップ実行を1周目のFor分の終わりまですすめる。
f:id:t-hom:20170116004755p:plain
すると変数aとcol.Item(1)は同じオブジェクトを参照していることがわかる。

次に2周目のSet a = New Class1実行直後まですすめる。
f:id:t-hom:20170116004924p:plain
すると変数aが指すオブジェクトが変わったが、col.Item(1)は先ほど保持させたオブジェクトのままである。

次の行を実行するとcol.Item(2)に今aに入っているオブジェクトが保持された。
f:id:t-hom:20170116005043p:plain

以下、3周目の終わり。
f:id:t-hom:20170116005158p:plain

変数aは使い回ししてもコレクションのそれぞれのアイテムは別のオブジェクトを保持していることになる。

「コレクションは変数を保持する」という勘違いがよく見受けられるが、コレクションが保持するのは変数ではなくその変数の中身である。

「コレクション.Add 変数」という書き方が混乱の原因だと思う。

これは基本型の変数で試してみるとわかる。

Sub hoge()
    Dim a As Long
    a = 10
    
    Dim col As Collection
    Set col = New Collection
    
    col.Add a
    a = 20
    
    Debug.Print col.Item(1)
End Sub

もしコレクションが変数aそのものを保持するなら、イミディエイトウインドウには20が出力されるはずだが、実際には10が出力される。

以上

ウォッチウインドウについて

ウォッチウインドウはローカルウインドウほど簡単ではないけれど、ローカルウインドウと比べて、

  • 指定したものだけをウォッチできる
  • 任意の式をウォッチできる

という特徴がある。

ローカルウインドウが基本的に変数しか見られないのに対し、ウォッチウインドウで使える任意の式はかなり強力だ。

ウォッチウインドウをうまく使うには、「式」とは何なのかが分かってないと辛い。
thom.hateblo.jp

私は最初、式って何なのか分かってなかったので、数学風に「a + b =」とか、Excel風に「= i * 2」と入力してエラーがでて使用を諦めていた。分かってしまえば簡単なんだけれど、これらはVBAでいう式ではない。イコールは不要だ。イコールを使用するのは比較演算子としてBoolean型を返す式を作るときだけで、この場合は右辺・左辺が両方必要になる。

その他、具体的な使い方の例。
thom.hateblo.jp

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。