前回はオブジェクト変数についての自分の勘違いしていた内容をまとめてみました。
今回は色々な方にアドバイスを頂いて自分の中で理解できた内容をまとめてみようと思います。
検証用コード
前回も記載したコードですが再掲いたします。
StudentクラスにClass_Terminateを追加しています。
それ以外は前回と同じです。
Studentクラス
'### Studentクラス Private mName As String Public Property Let Name(ByVal vName As String) mName = vName End Property Public Property Get Name() As String Name = mName End Property Sub Class_Terminate() Debug.Print "破棄" End Sub
標準モジュール
Sub オブジェクト参照テスト() Dim 生徒A As Student Set 生徒A = New Student Dim 名簿Col As Collection Set 名簿Col = New Collection 名簿Col.Add 生徒A 生徒A.Name = "Kou" Debug.Print 名簿Col(1).Name Set 生徒A = Nothing Debug.Print 名簿Col(1).Name End Sub
上記のコードを実行すると下記のようにイミディエイトウィンドウに表示されます。
前回の勘違い編では、2回めのDebug.Printの直前でset 生徒A = nothingをしているにも関わらず、'Kou'と表示されるのが理解できていませんでした。
今回の見直し編では、なぜ2回目のDebug.Printが正常に動作するのか解明したいと思います。
オブジェクト型変数の生成
オブジェクト型変数はInteger型・String型とはデータの扱い方が異なっています。
メモリのスタックメモリとヒープメモリ
メモリはスタックメモリとヒープメモリに分けられます。
オブジェクト型以外の変数はスタックメモリに直接保存されます。
オブジェクト型はオブジェクト本体はヒープメモリに、本体への参照情報がスタックメモリに保存されます。
スタックメモリへの変数確保
VBAでは『Dim XXX as 変数のデータ型』と記載することでスタックメモリへ変数が確保されます。
この時に変数を確保するために使用されるメモリは下記のようになっています。
データ型 | 記憶領域サイズ | 範囲 |
---|---|---|
Boolean | 2 バイト | True または False |
Byte | 1 バイト | 0 ~ 255 |
Currency (スケーリングされた整数) | 8 バイト | -922,337,203,685,477.5808 ~ 922,337,203,685,477.5807 |
Date | 8 バイト | 100 年 1 月 1 日 から 9999 年 12 月 31 日 |
Double (倍精度浮動小数点数) | 8 バイト | -1.79769313486231E308 から -4.94065645841247E-324 (負の値) 4.94065645841247E-324 から 1.79769313486232E308 (正の値) |
Integer | 2 バイト | -32,768 〜 32,767 |
Long (Long 整数) | 4 バイト | -2,147, 483,648 〜 2,147, 483,647 |
Single (単精度浮動小数点数) | 4 バイト | -3.402823E38 から -1.401298E-45 (負の値) 1.401298E-45 から 3.402823E38 (正の値) |
String (可変長) | 10 バイト + 文字列の長さ | 0 〜 約 20 億 |
文字列型 (String) (固定長) | 文字列の長さ | 1 〜 約 65,400 |
バリアント型 (Variant) (文字) | 22 バイト + 文字列長 (64 ビット システムでは 24 バイト) | 可変長 文字列型 と同じ範囲 |
Object | 4 バイト | 任意の Object 参照 |
データ型の概要 | Microsoft Docs より抜粋 docs.microsoft.com
上記表でBoolean型からString型の範囲は具体的な変数へ代入できる数値・文字数が記載されています。
Object型に関しては『任意のObject参照』と記載されているだけです。
String型などの変数には値そのものが代入されています。 しかしObject型は代入されているのはオブジェクト本体そのものではなく、オブジェクト本体への参照情報が代入されています。
昔から大きなデータを扱うオブジェクトでも4バイトしか使用しないのはなぜなのかと思っていました。
Object型変数は参照情報しか持っていないため、扱っているデータ量に関わらず一定のメモリしか使用せずに済むのですね。
オブジェクト型変数への代入
Object型変数への代入は『set XXX = New クラス名』で行います。
まず『New クラス名』でクラスからオブジェクトを作成します。
クラスはオブジェクトの設計図。
クラスを元に実際にインスタンス(実体化・作成)されたものがオブジェクトです。
『set XXX』では作成されたオブジェクトへの参照情報を代入します。
Object型変数の場合、ヒープメモリに確保されているオブジェクトの本体と、スタックメモリに確保されているオブジェクト本体への参照情報の2つの情報が存在します。
表中のオレンジの線はオブジェクト変数がオブジェクト本体を参照している事を表します。
この参照設定が残っていることがオブジェクトの破棄に関わってきます。
余談ですが、with句を使用することもオブジェクト本体への参照を行っていることになります。
さらに言えば、With 句によっても
— 弁士 (@Benshi_Orator) 2021年9月24日
参照カウントは増加します。
(With 句を抜けた時に解放)
だから、こういう動作にもなるわけで。 pic.twitter.com/DxsGdi0jHt
同じように名簿Collectionも作成します。
ユーザー定義クラスでも既存のクラスでも同様に参照情報とオブジェクト本体の2つの情報が存在します。
オブジェクト変数を渡す
名簿Colに生徒Aを追加します。
この時に気をつけないといけないことは、ヒープメモリに確保されているオブジェクト本体ではなく、スタックメモリに確保されているオブジェクト本体への参照情報が追加されるということです。
オブジェクト変数のプロパティを実行
Collectionオブジェクト内にコピーされた「生徒A」は「生徒A2」と表現しています。
「生徒A.Name = 'Kou'」を実行すると、生徒Aが参照している変数本体に対して値が追加されます。
生徒Aは参照情報のみを保持しますので、それ以外の情報を直接保持しません。
この時点でStudentオブジェクト本体は2つのオブジェクト変数から参照されています(矢印2つ)。
この時点で「Debug.Print 名簿Col(1).Name」を実行する。
Collectionオブジェクト内の「生徒A2」の参照情報を通じてStudentオブジェクト本体からNameの情報を抽出します。
オブジェクト変数の破棄
オブジェクト変数は使用後に破棄をする必要があります。
「set 生徒A = Nothing」の部分がそうです。
しかしこのコードで破棄されるのは生徒Aというオブジェクト本体への参照部分のみになります。
この時点ではオブジェクト本体はヒープメモリ内に確保されたままになります。
オブジェクト本体はオブジェクト変数から参照されなくなった時に自動で削除されます。
Studentオブジェクト本体は「生徒A」からの参照が無くなっても、Collectionオブジェクト内の「生徒A2」から参照されているため削除されません。
そのため2回めの「Debug.Print 名簿Col(1).Name」は生徒Aが削除されても実行できます。
Collectionオブジェクト内の生徒A2を通じてStudentオブジェクトからNameの情報を抽出しています。
前回の勘違い変では生徒Aというオブジェクト変数自体がオブジェクト本体で、Nameの情報を持っていると思っていたため、動作を正しく理解することが出来ていませんでした。
最後にプロシージャを終了する時にCollectionオブジェクトが破棄されます。
その結果Collectionオブジェクト内の生徒A2も破棄され、Studentオブジェクトへの参照情報が無くなり、Class_Terminateが発動します。
まとめ
Object型変数は変数自体には参照情報しか持っていないということが、他の型の変数と違うところで大きく勘違いしていたことでした。
この知識を持って自作のコードを見直してみたいと思います。
※言うまでもないですが、自分なりの解釈ですので事実とは異なる可能性があります
参照情報
色々と教えてくださってありがとうございます。
Set x = New Student
— 弁士 (@Benshi_Orator) 2021年9月24日
⇒Newで新しい生徒が作成される
⇒その生徒をxという名札で管理(参照数1)
m.Add x
⇒名札xの生徒を名簿mに追加(参照数2)
With x
⇒名札xの生徒の情報を操作
Set x = Nothing
⇒名札xをクリアする(参照数1)
⇒生徒は名簿mにまだ記載されており、参照数0ではないので生存している
関数の仮引数の話しにも関係はありますが、概念は別のものです。VBAでは参照型変数とはオブジェクト型変数と同じ意味で使われています。①変数の実態と②それを参照する変数が「分離」していることを言います。検索するといろいろ情報ありますので実際に絵を書いて理解することをおすすめします。
— ほえほえ@DX塾 (@hoehoe1234) 2021年9月23日
・Set vStudent = Nothing はインスタンスの破棄ではなく、あくまで変数vStudentの参照を切っているだけ
— 和風スパゲティのレシピ (@wafu_spaghetti) 2021年9月23日
・生成されたStudentのインスタンスはmItemsから参照されたまま
・インスタンスは「参照が0になったときに破棄される」ので、参照が1残っているとまだ破棄されない。
って感じな気がします。 https://t.co/xp4xS6HrlJ