11
7

1. 結論

最初に結論を述べると以下です。

  • 基本的には不要だが、必要な場合もある。
  • 必要/不要を都度判断して記述しても良いが、都度判断するのも思考リソースを消費するため、以下のようにルール化して記述すると良いと考える。
    1. オブジェクト変数を使用しなくなったタイミングでNothingを代入する。
    2. 循環参照がある場合は、それを解消してから上記1のルールでNothingを代入する。
    3. 外部参照のオブジェクトやWorkbookを使用した場合は、それ自身が持つクローズメソッドによりオブジェクトを破棄してから、上記1のルールでNothingを代入する。

以降、調査・検証した内容含め詳細について述べていきます。

2. そもそもオブジェクト変数やNothingとは

2-1. オブジェクト変数

オブジェクト変数には、オブジェクトへの参照、つまりオブジェクトのメモリアドレスが代入されています。1 2
オブジェクト自体が変数に代入されているわけではありません。
そのため、オブジェクト変数は参照型に分類されます。

2-2. Nothing

Nothingとは、オブジェクト変数にオブジェクトへの参照が代入されていない、つまり何も参照していない状態です。
正確に言うと、オブジェクトのメモリアドレスを返す関数である、ObjPtr関数の戻り値が「0」の状態がNothingです。
image.png

3. 「Set オブジェクト変数=Nothing」はどういった意味か

Set オブジェクト変数=Nothingは、そのオブジェクト変数からオブジェクトへの参照を無くす、という意味です。
オブジェクトそのものを破棄(メモリ解放)するという意味ではありません。
オブジェクトへの参照が無くなり、参照カウント(後述します)が0になることで、結果としてそのオブジェクトは破棄されメモリ解放されます。

4. VBAのメモリ管理について

VBAではメモリを参照カウント方式で管理しています。
これはオブジェクトへの参照が追加されるたびにカウントが増加し、参照が解除されるたびにカウントが減少する、というものです。

4-1. 参照カウント

1つのオブジェクトが何か所から参照されているかの数を参照カウントといいます。
参照カウントについては以下です。

  1. 参照カウントが0になった(=オブジェクトがどこからも参照されなくなった)時点で、そのオブジェクトは破棄されメモリ解放されます。
    また、参照カウントが残っていたとしても、プロシージャが終了するタイミングでオブジェクト変数からオブジェクトへの参照は無くなります。
  2. Endステートメントを呼び出した場合は、参照カウントが残っていたとしても、全てのオブジェクト変数からオブジェクトへの参照は無くなります。3
  3. 参照カウント方式は他のガーベジコレクションとは違い、循環参照がある場合に参照カウントが0にならないため、オブジェクトは破棄されずメモリ解放されません。4

4-2. オブジェクトがクラスのインスタンスの場合について

オブジェクトがクラスのインスタンスの場合、デストラクタを用意して実行させる事が出来ます。

デストラクタとは、クラスのインスタンスが破棄される時に実行されるメソッドです。
VBAでは、デストラクタをイベントプロシージャClass_Terminateで定義します。
Class_Terminateにより、クラスのインスタンスが破棄されるタイミングで実行したい処理を行わせることが出来ます。

4-1の1~3について、オブジェクトがクラスのインスタンスで、デストラクタを用意した場合は、それぞれ以下のように挙動が異なります。

  1. オブジェクトが破棄される前に、クラスのインスタンスに対してデストラクタが実行されます。
  2. Endステートメントはプログラムそのものを強制終了させるため、デストラクタは実行されません。
  3. 循環参照がある場合、オブジェクトが破棄されないため、デストラクタは実行されません。

4-3. プロシージャが終了するタイミングでオブジェクト変数からオブジェクトへの参照が無くなる順番について

プロシージャが終了するタイミングでオブジェクト変数からオブジェクトへの参照が無くなる順番は、オブジェクト変数にオブジェクトへの参照が代入された順番となります。
デストラクタを用意して確認してみます。

image.png
image.png

クラスのインスタンスが生成された順番で解放されました。

5. オブジェクト変数へNothingを代入する必要がある場合について

オブジェクト変数へNothingを代入する必要がある場合としては以下です。

  • プロシージャ外で宣言したオブジェクト変数やPublic/Staticで宣言したオブジェクト変数からオブジェクトへの参照を無くしたい時

もし上記の他に必要な場合があるようでしたら、ご存知の方はコメント等で教えてください。

また、必要がある場合ではありませんが、そこでオブジェクトへの参照が無くなるということを明示的に記しておきたい場合に記述しても良いです。

5-1. プロシージャ外で宣言したオブジェクト変数やPublic/Staticで宣言したオブジェクト変数からオブジェクトへの参照を無くしたい時について

これらのオブジェクト変数は他のローカルのオブジェクト変数とは違い、プロシージャが終了するタイミングでオブジェクトへの参照は無くなりません。
そのため、これらのオブジェクト変数からオブジェクトへの参照を無くしたい場合は、そのオブジェクト変数へNothingを代入する必要があります。

実際に確認してみます。

image.png
image.png
1回目と2回目の実行でメモリアドレスが変わっておらず、2回目の実行時にオブジェクト変数へNothingを代入する前まではオブジェクトへの参照が無くなっていませんでしたが、オブジェクト変数へNothingを代入したことで、オブジェクトへの参照が無くなりました。

Endステートメントを呼び出した場合でも確認してみます。

image.png
image.png
image.png
Endステートメントを呼び出した場合でも同様に、オブジェクトへの参照が無くなりました。

6. 基本的にオブジェクト変数へNothingは代入しなくて良いか

5-1のケースを除けば、プロシージャが終了するタイミングでオブジェクト変数からオブジェクトへの参照は無くなるため、基本的にオブジェクト変数へNothingは代入せずとも問題ありません。

ですが、オブジェクト変数を使用しなくなったタイミングでNothingを代入することを推奨します。5

理由としては、使用しなくなったオブジェクト変数をそのままにしておくと、オブジェクト変数はそのオブジェクトを参照し続けており、その分メモリが専有され、効率が落ちる可能性があるためです。
そのため、オブジェクト変数を使用しなくなったタイミングでNothingを代入し、早期段階でメモリ解放することで、効率化に繋げると良いです。
また、オブジェクト変数を使用しなくなったタイミングでNothingを代入することで、それ以降の処理でそのオブジェクト変数を使用しないことを明示することにもなるため、保守性の向上にも繋がります。

7. オブジェクト変数へNothingを代入してはいけない場合はあるか

オブジェクト変数へNothingを代入してはいけない場合としては以下です。

  • 循環参照がある場合に、それを解消しないままNothingを代入する
  • 外部参照のオブジェクトやWorkbookがある場合に、それを破棄しないままNothingを代入する

もし上記の他に記述してはいけない場合があるようでしたら、ご存知の方はコメント等で教えてください。

7-1. 循環参照がある場合について

循環参照がある場合、その対象のオブジェクト変数にNothingを代入しただけでは、対象のオブジェクトは破棄されずメモリ解放されません。

Sub loopTest1()
    Dim i As Long
    Dim c As Collection
    Dim d As Collection
    
    For i = 1 To 100000
        Set c = New Collection
        Set d = New Collection
        
        c.Add d
        d.Add c
        '↑循環参照
        
        Debug.Print ObjPtr(c)
        Debug.Print ObjPtr(d)
        '↑それぞれのメモリアドレスが返る
        
        Set d = Nothing
        Set c = Nothing
        '↑Nothingを代入しても対象のオブジェクトは破棄されずメモリ解放されない
        
        Debug.Print ObjPtr(c) '0
        Debug.Print ObjPtr(d) '0
    Next
End Sub

上記コードの実行前後でメモリ使用量を比較してみると、実行後はメモリ使用量がかなり増加していることが確認できます。

  • 実行前
    image.png
  • 実行後
    image.png

またこの場合、プロシージャが終了するタイミングやEndステートメントを呼び出した場合でも、対象のオブジェクトは破棄されずメモリ解放されません。

Sub loopTest2()
    Dim i As Long
    Dim c As Collection
    Dim d As Collection
    
    For i = 1 To 100000
        Set c = New Collection
        Set d = New Collection
        
        c.Add d
        d.Add c
        '↑循環参照
        
        Debug.Print ObjPtr(c)
        Debug.Print ObjPtr(d)
        '↑それぞれのメモリアドレスが返る
    Next
    
'プロシージャが終了するタイミングでも、対象のオブジェクトは破棄されずメモリ解放されない
End Sub
Sub loopTest3()
    Dim i As Long
    Dim c As Collection
    Dim d As Collection
    
    For i = 1 To 100000
        Set c = New Collection
        Set d = New Collection
        
        c.Add d
        d.Add c
        '↑循環参照
        
        Debug.Print ObjPtr(c)
        Debug.Print ObjPtr(d)
        '↑それぞれのメモリアドレスが返る
    Next
    
    End 'Endステートメントを呼び出しても対象のオブジェクトは破棄されずメモリ解放されない
End Sub

※上記2つのコードの実行結果は先ほどと同様なため省略します。

これはオブジェクト変数から対象のオブジェクトへの参照が無くなるのであって、オブジェクトそのもの同士に持たせた参照は無くならないためです。
そのため、循環参照がある場合はそれを解消する必要があります。
循環参照を解消したのち、対象のオブジェクト変数へNothingを代入し、オブジェクトへの参照を無くすことで、オブジェクトを破棄しメモリ解放すると良いです。

Sub loopTest4()
    Dim i As Long
    Dim c As Collection
    Dim d As Collection
    
    For i = 1 To 100000
        Set c = New Collection
        Set d = New Collection
        
        c.Add d
        d.Add c
        '↑循環参照
        
        Debug.Print ObjPtr(c)
        Debug.Print ObjPtr(d)
        '↑それぞれのメモリアドレスが返る
        
        c.Remove 1 'cからdをRemoveすることで循環参照を解消する
        
        Debug.Print ObjPtr(c)
        Debug.Print ObjPtr(d)
        '↑それぞれ先ほどと同じメモリアドレスが返る
        
        Set d = Nothing
        Set c = Nothing
        
        Debug.Print ObjPtr(c) '0
        Debug.Print ObjPtr(d) '0
    Next
End Sub

上記コードの実行前後でメモリ使用量を比較してみると、実行前後でメモリ使用量はあまり変わらないことが確認できます。

  • 実行前
    image.png
  • 実行後
    image.png

7-2. 外部参照のオブジェクトがある場合について

IE等の外部参照のオブジェクトは、オブジェクト変数からオブジェクトへの参照は無くなっても、オブジェクトは破棄されずメモリ解放されない場合があります。

例えばVBAからIEを起動し、プロシージャが終了してオブジェクト変数からオブジェクト(IE)への参照は無くなっても、IEのアプリケーション自体(=オブジェクト)は閉じられません。
つまり、オブジェクトは破棄されません。

Sub OpenIE()
    Dim IE As Object
    Set IE = CreateObject("InternetExplorer.Application")
    
    Debug.Print ObjPtr(IE)
    Debug.Print TypeName(IE)
    
    With IE
        .Visible = True 'TrueにしてIEを表示
        .navigate "https://qiita.com/" '任意のURLに変更
    End With
End Sub 'IEは閉じられない

この際、オブジェクト変数にNothingを代入したり、Endステートメントを呼び出しても結果は変わりません。

Sub OpenIE()
    Dim IE As Object
    Set IE = CreateObject("InternetExplorer.Application")
    
    Debug.Print ObjPtr(IE)
    Debug.Print TypeName(IE) 'オブジェクト名はIWebBrowser2
    
    With IE
        .Visible = True 'TrueにしてIEを表示
        .navigate "https://qiita.com/" '任意のURLに変更
    End With
    
    Set IE = Nothing
    Debug.Print ObjPtr(IE) '0
    Debug.Print TypeName(IE) 'Nothing
End Sub 'Nothingを代入してもIEは閉じられない
Sub OpenIE()
    Dim IE As Object
    Set IE = CreateObject("InternetExplorer.Application")
    
    Debug.Print ObjPtr(IE)
    Debug.Print TypeName(IE) 'オブジェクト名はIWebBrowser2
    
    With IE
        .Visible = True 'TrueにしてIEを表示
        .navigate "https://qiita.com/" '任意のURLに変更
    End With
    
    End 'Endステートメントを呼び出してもIEは閉じられない
End Sub

そのため、IE等の外部参照のオブジェクトを破棄するには、その外部参照のオブジェクト自身が持つクローズメソッドを実行する必要があります。
IEの場合では、Quitメソッドの実行が必要となります。

なお、クローズメソッドを実行してもオブジェクト変数からオブジェクトへの参照は残っているので、クローズメソッドを実行するだけではオブジェクトは破棄されません。
クローズメソッドを実行したうえで、プロシージャが終了するか、オブジェクト変数にNothingを代入し、オブジェクトへの参照がなくなることで、オブジェクトは破棄されメモリ解放されます。

Sub OpenIE()
    Dim IE As Object
    Set IE = CreateObject("InternetExplorer.Application")
    
    Debug.Print ObjPtr(IE)
    Debug.Print TypeName(IE) 'オブジェクト名はIWebBrowser2
    
    With IE
        .Visible = True 'TrueにしてIEを表示
        .navigate "https://qiita.com/" '任意のURLに変更
    End With
    
    IE.Quit 'IEを閉じる
    Debug.Print ObjPtr(IE) 'IEを閉じてもオブジェクトへの参照は残っている
    Debug.Print TypeName(IE) 'オブジェクト名はObject
    
    Set IE = Nothing
    Debug.Print ObjPtr(IE) '0
    Debug.Print TypeName(IE) 'Nothing
End Sub

7-3. Workbookがある場合について

7-3-1. オブジェクトの親子関係について

まずオブジェクトの親子関係について説明します。
オブジェクトは以下のように親子関係(階層構造)になっています。
Application
└Workbook
 └Worksheet
  └Range

オブジェクトの親子関係はオブジェクトそのものに存在しています。
親/子オブジェクトのオブジェクト変数には、それらへの参照が入っているだけであり、どちらへ先にNothingを代入しても最終的にそれらのオブジェクト変数からオブジェクトへの参照は無くなります。
そのため、Nothingを代入する順番は気にする必要はありません。

オブジェクトの親子関係はオブジェクトそのものに存在しているかを実際に確認してみます。

Workbook/WorksheetオブジェクトそれぞれをWB/WS変数へ代入した場合で考えます。
この場合、WorkbookオブジェクトとWB変数、WorksheetオブジェクトとWS変数のメモリアドレスは同じです。
そのため、Workbookオブジェクトを使用してWS変数を作成するのと、WB変数を使用してWS変数を作成するのは同義となります。
image.png
image.png

WB/WS変数の親オブジェクト名をParentプロパティで確認してみると、WB変数はApplication、WS変数はWorkbookが返ります。
これはWB/WS変数それぞれの参照先オブジェクトの親オブジェクト名が返っています。

変数宣言直後のWB/WS変数のオブジェクト名を確認してみると、それぞれNothingとなっています。
ObjPtr関数でWB/WS変数のメモリアドレスを確認すると0が返ります。
これはまだ何も参照していないためです。

また、Workbookそのものを閉じてからWB/WS変数のオブジェクト名を確認してみると、それぞれObjectとなっています。6
ObjPtr関数でWB/WS変数のメモリアドレスを確認するとWorkbookを閉じる前後で同一であることが確認できます。
つまり、Workbookそのものを閉じてもWB/WS変数からオブジェクトへの参照は残っています。
image.png

変数宣言直後とWorkbookそのものを閉じた後にWB/WS変数へParentプロパティを実行すると、どちらの場合でもエラー発生するため、NothingObjectには親子関係は存在しないことが確認できます。
そのため、オブジェクトの親子関係はオブジェクトそのものに存在しているということが確認できます。

Sub wbTest()
    Dim wb As Workbook
    Dim ws As Worksheet
    
    Debug.Print TypeName(wb.Parent) 'エラー発生
    Debug.Print TypeName(ws.Parent) 'エラー発生
    
    Set wb = Workbooks.Open(Filename:="C:\test\test.xlsx")
    Set ws = wb.Sheets("Sheet1")
    
    wb.Close
    
    Debug.Print TypeName(wb.Parent) 'オートメーションエラー発生
    Debug.Print TypeName(ws.Parent) 'オートメーションエラー発生
End Sub

7-3-2. Workbook/Worksheetオブジェクトの場合について

Workbookそのものを閉じてもWB/WS変数からオブジェクトへの参照は残っているということは、Workbookを閉じるだけではオブジェクトは破棄されません。
プロシージャが終了するか、WB/WS変数にNothingを代入し、オブジェクトへの参照がなくなることで、オブジェクトは破棄されメモリ解放されます。7

また、Workbookを閉じずにプロシージャが終了しても、Workbookを閉じずにWB/WS変数へNothingを代入しても、どちらの場合でもWorkbookそのものが残っているため、オブジェクトは破棄されません。これはEndステートメントを呼び出した場合でも同様です。
つまり、Workbook/Worksheetオブジェクトの場合、オブジェクトへの参照がなくなり、Workbookそのものが閉じられることで、オブジェクトが破棄されメモリ解放されます。8 9

ちなみに、Workbookを一度開いてその後開きっぱなしの場合、プロシージャを何度実行してもWorkbook/Worksheetのメモリアドレスは毎回同じです。10
image.png

そのためWB/WS変数①にNothingを代入後、WB/WS変数②にWorkbook/Worksheetオブジェクトを代入してみると、WB/WS変数①と②のメモリアドレスはどちらも同じであることが確認できます。
image.png

8. おわりに

このように、Set オブジェクト変数=Nothingは必要かという話は、状況によるうえ複雑であり、全て理解したうえで都度判断するというのはなかなか大変だと思います。
そのため理解しているに越したことはありませんが、冒頭で書いた結論の通りルール化すると良いと思います。

9. おまけ

以下は関連する内容ではありますが、本記事の趣旨から少しずれるため、おまけとして記載します。

9-1. オブジェクト変数にオブジェクト変数を代入した場合について

オブジェクト変数にオブジェクト変数を代入した場合、オブジェクトのメモリアドレスのコピーとなります。
そのため、代入元/代入先のオブジェクト変数の参照先のオブジェクトは結果的に同じものとなります。

また、オブジェクト変数にオブジェクト変数を代入した後に、片方のオブジェクト変数にNothingを代入しても、Nothingを代入したオブジェクト変数からオブジェクトへの参照が無くなるだけで、もう片方のオブジェクト変数からオブジェクトへの参照は残ったままとなります。
そのため対象のオブジェクトを破棄したい場合、もう片方のオブジェクト変数にもNothingを代入する必要があります。
なお、この場合でもプロシージャが終了するタイミングでそれらのオブジェクト変数からオブジェクトへの参照は無くなります。
image.png

9-2. Withステートメントでのオブジェクトの扱いについて

Withステートメントは、指定したオブジェクトへの参照をWithブロック内で保持します。
例えば、オブジェクト変数を作成し、そのオブジェクト変数をWithステートメントで指定したとします。
その後Withブロック内でそのオブジェクト変数へNothingを代入すると、オブジェクト変数からオブジェクトへの参照は無くなりますが、Withブロック内でオブジェクトへの参照は続いているため、Withブロックを抜けるまでWithステートメントからオブジェクトへの参照は無くなりません。
image.png
image.png

ちなみにWithステートメントで保持するのはオブジェクトへの参照(メモリアドレス)なので、オブジェクトとオブジェクト変数のどちらを指定しても同じです。
image.png

10. 参考記事

  1. ここでいうメモリアドレスとは、物理メモリアドレスではなく、OSによって管理されたプロセスごとの仮想メモリ空間のアドレスです。

  2. オブジェクトはメモリ内のヒープ領域に保存されるようです。ちなみに、ローカル変数はメモリ内のスタック領域を使用しているようです。

  3. End SubEnd Functionなどとは別物です。
    それらはSubやFunctionといったプロシージャレベルのものを終了させますが、Endはプログラムそのものを強制終了させます。

  4. 私はガーベジコレクション派です(英語でGarbage Collectionのため)。

  5. 念のため、オブジェクト変数へオブジェクトを再代入する前にも、オブジェクト変数へNothingを代入することを推奨します。

  6. このObjectは、Workbook/Worksheetオブジェクトのために確保されたメモリ領域が、Workbookそのものを閉じた後もオブジェクト変数から参照され続けていることで、保持されているものと考えられます。

  7. 外部参照のオブジェクトがある場合と似ています。

  8. Workbook/Worksheetオブジェクトへの参照がなくなるのと、Workbookそのものを閉じるのはどちらが先でも問題ありません。

  9. 今回記載していませんが、Rangeオブジェクトの場合もWorkbook/Worksheetオブジェクトの場合と同様です。

  10. Workbook/WorksheetオブジェクトはWorkbook/Worksheetそのものなため。

11
7
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7