はじめに
VBAのオブジェクト変数について、スタックやヒープなどの概念を用いての説明は厳密にいうと正確ではないかもしれませんが、イメージがしやすいので、敢えてこれらの言葉を使ってまとめてみました。
スタックとヒープ
一般的なプログラミングのメモリ管理において、データ格納先のメモリ領域にはスタックとヒープがあります。
スタックは、組み込み型(Integer
、Double
、Boolean
など)の変数を格納するために使用され、後入れ先出し(LIFO)で管理されます。関数やプロシージャの実行が完了すると、そのスコープにある変数は自動的にスタックから削除されます。スタックで使用される記憶領域のサイズはデータ型ごとに決まっており、メモリを管理する上では単純な仕組みとなります。
一方、ヒープは動的なメモリ割り当てに使用され、プログラムの実行中にサイズが変動する可能性のあるオブジェクトなどのデータを格納します。オブジェクトを作成する際には、New
キーワードを使用して(あるいはCreateObject
関数を使用して)ヒープ上にオブジェクトを割り当て、そのオブジェクトへの参照を変数に格納します。
扱われるデータの性質によって、これらのメモリ領域の区別が必要なことは容易に理解できます。
オブジェクト変数はショートカット
上の説明で、オブジェクト変数に格納されているのはオブジェクトへの参照ということでした。つまり、オブジェクトが割り当てられたヒープメモリのアドレスということです。
関数やプロシージャの引数としてRange
、Worksheet
、Dictionary
などを渡したとしても単にアドレスを受け渡ししているだけで実体のコピーが生成されるわけではありません。
これはファイルシステムで言うところのショートカットに似ています。例えて言うと、オブジェクト変数はファイルそのものではなく、ファイルを開くためのショートカットです。
ByValとByRef
関数やプロシージャへの引数の渡し方としてByVal
とByRef
があります。
-
ByVal
(値渡し)は、関数やプロシージャに引数が渡されるとき、その値のコピーが作成され、関数またはプロシージャ内での変更は呼び出し元の変数に影響を与えません -
ByRef
(参照渡し)は、引数として変数が渡されるとき、その変数への参照が渡され、関数またはプロシージャ内での変更が呼び出し元の変数に影響を与えます
では、オブジェクトをByVal
の引数に渡した場合、オブジェクトのコピーが作成されるのでしょうか?そうではありません。オブジェクトの実体ではなくショーカットのコピーが渡されるだけです。また、ByVal
が呼び出し元に影響を与えないと言っても、ショートカットが指している先は同じヒープアドレスにあるオブジェクトの実体です。関数またはプロシージャ内のオブジェクト変数でそのオブジェクトのプロパティを書き換えれば、呼び出し元のプロパティも当然書き換わります。ByVal
により呼び出し元の変数とプロシージャ内の変数は別物になりましたが、中に持っているヒープアドレスは同じ実体を指しているからです。
Nothingの勘違い
では、この引数のオブジェクト変数にNothing
を代入するとどうなるでしょうか。ここで勘違いするかもしれませんが、ヒープにあるオブジェクトの実体が即座に解放されるわけではありません。代入の瞬間に消去されるのはオブジェクト変数が保持しているショートカット(ヒープ上のアドレス)だけです。また、呼び出し元のオブジェクト変数は影響を受けず、ショートカット機能を保持したままとなります(ByVal
の場合です)。
解放はいつか?
次に、呼び出し元のオブジェクト変数にNothing
を代入した場合を考えてみましょう。同様に、ショートカット(ヒープ上のアドレス)が消去されますが、もしこのオブジェクト変数が実行中のプロシージャ内で宣言されている場合、即座に解放されます。どこからも参照されなくなった時が解放される時となります。
以下はオブジェクトが解放されるタイミングを確認する実験です。クラスモジュールでClass1
を作り、解放時に呼ばれるTerminate
イベントプロシージャがいつ動作するかを確認します。
Private cName As String
Public Property Let Name(ByRef vl As String)
cName = vl
End Property
' クラス解放時に呼ばれる
Private Sub Class_Terminate()
Debug.Print cName & "クラス解放"
End Sub
以下のHoge
プロシージャを実行すると、イミディエイトウインドにプロシージャ終了時とクラス解放時にテキストメッセージが出力されます。
' グローバル変数
Public zClass As Class1
Public Sub Hoge()
Dim xClass As Class1: Set xClass = New Class1
xClass.Name = "x"
Dim yClass As Class1: Set yClass = New Class1
yClass.Name = "y"
' グローバル変数への代入
Set zClass = New Class1
zClass.Name = "z"
' xだけNothingしてみる
Set xClass = Nothing
Debug.Print "プロシージャ終了"
End Sub
出力結果は以下のとおりです。
xクラス解放
プロシージャ終了
yクラス解放
-
xClass
は、Nothing
を代入した直後にTerminate
が呼ばれます -
yClass
は、Nothing
を代入していませんが、Hoge
プロシージャ終了後にTerminate
が呼ばれます -
zClass
は、Terminate
が呼ばれず、オブジェクトは解放されません
オブジェクト変数がグローバル変数の場合、解放にはNothing
の代入が必須となります。また、Nothing
しなくても、変数が宣言されたプロシージャのスコープから抜けると自動的に解放されることがわかりました。
例外として、一部のオブジェクトにはClose
メソッドなどがあり、プログラマの責任でClose
して解放しなければならない場合もあります。
おわりに
以上の説明はVBAに限らず、他の言語でも共通する部分があります。
また、冒頭でも言っているとおり、この説明はVBAの文脈では正確なものではなく、イメージしやすいように表現したものであることをもう一度付け加えておきます