【VBA】オブジェクト変数はショートカット

# はじめに
これは、[Visual Basic Advent Calendar 2024](https://qiita.com/advent-calendar/2024/vb) の15日目の記事です。

この記事は、今年の春先に投稿したVBAのオブジェクト変数についてまとめた同名の記事の書き直しです。前の記事を書き直す気になれず、削除して今回新たに書き直しました。前の記事にいいねしていただい方々、すみません:pray:

## オブジェクト変数はショートカット

VBAのオブジェクト変数とは何でしょうか:question:
詳しいことは以下をご参照ください。ここでは自分がオブジェクト変数の挙動確認のために試したことを書きます。

https://learn.microsoft.com/ja-jp/office/vba/language/concepts/getting-started/creating-object-variables

### 組み込み型とオブジェクト型

VBAのデータ型には大きく分けて**組み込み型**と**オブジェクト型**があります。

+ **組み込み型**
`Boolean`、`Long`、`String`、`Date`、`Variant`、`Type`など
+ **オブジェクト型**
`Collection`、`Dictionary`、`FileSystemObject`など
（クラスモジュールで自作したクラスも含まれる）

この二つはデータの扱われ方が違います。違いがはっきりわかるのは、これらを`Function`や`Sub`の引数として使うときです。

### 引数の渡し方、ByValとByRef

`Function`や`Sub`（以後、関数という）への引数の渡し方として`ByVal`と`ByRef`があります。引数が必要な関数を使う際、通常は引数に変数を渡します。

+ **ByVal（By Value: 値渡し）**
変数の値のコピーが作成され、関数内での引数の変更は呼び出し元の変数に影響を与えません
+ **ByRef（By Reference: 参照渡し）**
変数への参照（メモリ上の値のアドレス）が渡され、関数内での変更が呼び出し元の変数に影響を与えます

渡し方を書かない引数は、デフォルトで`ByRef`になります。渡し方を`ByVal`にすることで引数は変数の値のコピーとなり、呼び出し元の変数が書き換わることはありません。

```vb:引数がByValの場合
' 値渡し
Sub ProcHage(ByVal txt As String)
    txt = "Hage"
End Sub

Sub Main()
    Dim txt As String: txt = "Hoge"
    Call ProcHage(txt)
    Debug.Print "txt = " & txt   ' <--ホゲのまま、ハゲにならない
End Sub
```

呼び出し元の変数を書き換えたくないとき、組み込み型の場合はこれで安心できますが、オブジェクト型だったらどうでしょうか。

オブジェクト型変数を`ByVal`の引数に渡した場合、単に参照先オブジェクトのアドレスを受け渡ししているだけでオブジェクト自体のコピーが生成されるわけではありません。
参照先オブジェクトを指し示すアドレスをコピーしているだけなので、関数内の書き換えは呼び出し元の変数が参照するオブジェクトを書き換えることになります。

これはファイルシステムで言うところのショートカットに似ています。例えて言うと、オブジェクト変数はファイルそのものではなく、ファイルを開くためのショートカットです。
+ オブジェクト変数はファイルそのものではなく、ファイルを開くためのショートカット
+ `ByVal`であっても関数内の書き換えは呼び出し元のオブジェクトを書き換えることになる


Nothingの勘違い
---
では、オブジェクト変数を渡された引数に関数内で`Nothing`を代入するとどうなるでしょうか。`ByVal`と`ByRef`、それぞれで試してみました。

まずは`ByVal`（値渡し）から。
```vb:ByVal - 値渡し
' 値渡しでオブジェクト変数を受ける
Sub ProcVal(ByVal dic As Dictionary)
    dic.Add "SHIB", 0.0042
    Set dic = Nothing
End Sub

' 実行する関数
Sub Test01()
    On Error GoTo Catch
    
    Dim dic1 As Dictionary: Set dic1 = New Dictionary
    dic1.Add "PEPE", 0.0035
    
    ' ここで値渡し
    Call ProcVal(dic1)
    
    Debug.Print "dic1 Is Nothing = " & (dic1 Is Nothing)
    Debug.Print "dic1(""PEPE"") = " & dic1("PEPE")
    Debug.Print "dic1(""SHIB"") = " & dic1("SHIB")

    Exit Sub
Catch:   
    Debug.Print Err.Description
End Sub
```

 ```:実行結果
dic1 Is Nothing = False
dic1("PEPE") = 0.0035
dic1("SHIB") = 0.0042
 ```

関数内で`dic`に`Nothing`を代入しても呼び出し元の`dic1`は`Nothing`にならず、使い続けられます。
また、関数内で追加した `"SHIBA"`キーが呼び出し元の`dic1`に追加されていることがわかります。

次に、`ByRef`（参照渡し）です。

```vb:ByRef - 参照渡し
' 参照渡しでオブジェクト変数を受ける
Sub ProcRef(ByRef dic As Dictionary)
    dic.Add "SHIB", 0.0042
    Set dic = Nothing
End Sub

' 実行する関数
Sub Test02()
    On Error GoTo Catch
    
    Dim dic1 As Dictionary: Set dic1 = New Dictionary
    dic1.Add "PEPE", 0.0035
    
    ' ここで参照渡し
    Call ProcRef(dic1)
    
    Debug.Print "dic1 Is Nothing = " & (dic1 Is Nothing)
    Debug.Print "dic1(""PEPE"") = " & dic1("PEPE")        ' <--ここでエラー
    Debug.Print "dic1(""SHIB"") = " & dic1("SHIB")

    Exit Sub
Catch:   
    Debug.Print Err.Description
End Sub
```
```:実行結果
dic1 Is Nothing = True
オブジェクト変数または With ブロック変数が設定されていません。
```
関数内で`dic`に`Nothing`を代入すると呼び出し元の`dic1`も`Nothing`になりました。
当然ながら`"SHIBA"`キーは参照できず、ここでエラーとなります。

もう一例見てみましょう。呼び出し元の`Sub`にもう一つのディクショナリ変数`dic2`を追記した上で`dic1`を参照渡ししてみます。
引数として渡す前に`dic2`に`dic1`を代入しているのがポイントです。
```vb:ByRef - 参照渡し
Sub Test03()
    On Error GoTo Catch
    
    Dim dic1 As Dictionary: Set dic1 = New Dictionary
    dic1.Add "PEPE", 0.0035
    
    ' 二つ目のディクショナリ
    Dim dic2 As Dictionary: Set dic2 = dic1              ' <--ここがポイント
    
    ' ここで参照渡し
    Call ProcRef(dic1)
    
    Debug.Print "dic2 Is Nothing = " & (dic2 Is Nothing)
    Debug.Print "dic2(""PEPE"") = " & dic2("PEPE")
    Debug.Print "dic2(""SHIB"") = " & dic2("SHIB")
    
    Debug.Print "dic1 Is Nothing = " & (dic1 Is Nothing)
    Debug.Print "dic1(""PEPE"") = " & dic1("PEPE")        ' <--ここでエラー
    Debug.Print "dic1(""SHIB"") = " & dic1("SHIB")

    Exit Sub
Catch:
    Debug.Print Err.Description
End Sub
```
```:実行結果
dic2 Is Nothing = False
dic2("PEPE") = 0.0035
dic2("SHIB") = 0.0042
dic1 Is Nothing = True
オブジェクト変数または With ブロック変数が設定されていません。
```

`ByRef`の場合、通常はショートカットが消えると共に参照先オブジェクトも解放されますが、参照先オブジェクトのもう一つのショートカットである変数`dic2`が呼び出し元の`Sub`に残っている場合、ショートカットとしての変数`dic1`が消えるだけで参照先オブジェクトは残り続けます。
+ オブジェクト変数はオブジェクトのショートカットであり、オブジェクトの解放スイッチでもある
+ オブジェクトが同じスコープで複数の変数に参照されていたら、全てを`Nothing`しないと解放されない

この様な仕組みを**参照カウンタ**と言うんですね:thinking:

https://yougo.ascii.jp/caltar/%E5%8F%82%E7%85%A7%E3%82%AB%E3%82%A6%E3%83%B3%E3%82%BF

## ゾンビ変数

わき道にそれますが、上述のテスト関数`Test02`を1行書き換えて実行してみました。

```vb:Test02
'    Dim dic1 As Dictionary: Set dic1 = New Dictionary
    Dim dic1 As New Dictionary                          
```
```:実行結果
dic1 Is Nothing = False
dic1("PEPE") = 
dic1("SHIB") = 
```
この場合、`dic1`は`Nothing`にはなりません。キーは残ったまま値だけが消去されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294209/a70d4ee3-1b53-7be7-041a-ff964accd73d.png)

この書き方だと`Nothing`を代入されたにもかかわらず、即座に新しいインスタンスを生成するようです。しかも純粋な空のオブジェクトではなく、元の変数の抜け殻のような状態で生き返ります。まるでゾンビのようですね:scream:
これはちょっと闇仕様だと思います。この書き方を結構多用していましたが、今回詳しく調べてみて考え直さなければならないかなと思いました。


<details><summary>ちなみにVB.NETではこの現象は起こりません。</summary>

Visual Studio 2022なら、プロジェクトで**Microsoft Scripting Runtime**を参照した上で、

```vb:VB.NET
Imports Scripting

Module Program

    ' 参照渡し
    Sub ProcRef(ByRef dic As Dictionary)
        dic.Add("SHIB", 0.0042)
        dic = Nothing
    End Sub

    Sub Main(args As String())

        Dim dic1 As New Dictionary

        Try
            Try
                dic1.Add("PEPE", 0.0035)

                'ここで参照渡し
                ProcRef(dic1)

                Console.WriteLine($"dic1 Is Nothing = {dic1 Is Nothing}")
                Console.WriteLine($"dic1(""PEPE"") = {dic1("PEPE")}")       ' <-ここでエラー

            Catch ex As Exception
                Console.WriteLine($"dic1(""PEPE""): {ex.Message}")
            End Try

            ' 自動的にNewされることを期待...
            dic1.Add("SHIB", 0.0042)                                        ' <-ここでエラー

        Catch ex As Exception
            Console.WriteLine($"dic1.Add(""SHIB"", 0.0042): {ex.Message}")
        End Try

        ' Newする必要がある
        dic1 = New Dictionary

        dic1.Add("DOGE", 60)
        Console.WriteLine($"dic1(""DOGE"") = {dic1("DOGE")}")

    End Sub

End Module
```
```:実行結果
dic1 Is Nothing = True
dic1("PEPE") : Object reference not set to an instance of an object.
dic1.Add("SHIB", 0.0042) : Object reference not set to an instance of an object.
dic1("DOGE") = 60
```

`As New`による自動的なインスタンス生成がされません。新たなインスタンスが必要なときは`New`する必要があります。
</details>

## オブジェクトの解放はいつか？

わき道にそれましが、気を取り直して以下、最後のテストです。

これはオブジェクトが解放されるタイミングを確認する実験です。クラスモジュールで`Class1`を作り、解放時に呼ばれる`Terminate`イベントプロシージャがいつ動作するかを確認します。

```vb:Class1.cls
Private mName As String

Public Property Let Name(ByRef vl As String)
    mName = vl
End Property

Public Property Get Name() As String
    Name = mName
End Property

' クラス解放時に呼ばれる
Private Sub Class_Terminate()
    Debug.Print mName & "インスタンス解放"
End Sub
```
また、`Class1`を生成する`CreateClass1`関数を作り、その中で`Nothing`を使ってみました。
```vb:Module1.bas
Private Function CreateClass1() As Class1
    Dim newInst As Class1
    Set newInst = New Class1
    newInst.Name = "new"

    ' 生成したClass1のインスタンスを返す
    Set CreateClass1 = newInst
    ' Nothingしてみる
    Set newInst = Nothing
    
    Debug.Print "CreateClass1関数終了"
End Function
```

以下の`Hoge`を実行すると、イミディエイトウインドに**関数終了時**と**クラス解放時**にテキストメッセージが出力されます。

```vb:Module1.bas
' グローバル変数
Public zInst As Class1

Public Sub Hoge()
    Dim xInst As Class1: Set xInst = New Class1
    xInst.Name = "x"
    
    Dim yInst As Class1: Set yInst = New Class1
    yInst.Name = "y"
   
    ' グローバル変数への代入    
    Set zInst = New Class1
    zInst.Name = "z"

    ' CreateClass1関数でClass1を生成
    Dim newInst As Class1: Set newInst = CreateClass1
    Debug.Print "newInstのName: " & newInst.Name
   
    ' xだけNothingしてみる
    Set xInst = Nothing
    
    Debug.Print "Hoge関数終了"
End Sub

```
出力結果は以下のとおりです。

```:出力結果
CreateClass1関数終了
newInstのName: new
xインスタンス解放
Hoge関数終了
newインスタンス解放
yインスタンス解放
```
+ `xInst`は、`Nothing`を代入した直後に`Terminate`が呼ばれます
+ `yInst`は、`Nothing`を代入していませんが、`Hoge`終了後に`Terminate`が呼ばれます
+ `zInst`は、`Terminate`が呼ばれず、オブジェクトは解放されません
+ `CreateClass1`関数内の`newInst`に`Nothing`を代入しても、外側の`newInst`に影響はありません
+ `Hoge`関数内の`newInst`は、`Nothing`を代入していませんが、`yInst`と同様に`Hoge`終了後に`Terminate`が呼ばれます


オブジェクト変数がグローバル変数の場合、解放には`Nothing`の代入が必須となります。`Hoge`関数を実行した後で`zInst`の`Name`プロパティをイミディエイトウインドウで確認してみましょう。

```:出力結果
?zInst.Name
z
```
`Name`プロパティに値が残っていることがわかります。

また、`Nothing`しなくても、変数が宣言されたプロシージャのスコープから抜けると自動的に解放されることがわかりました。
例外として、一部のオブジェクトには`Close`メソッドなどがあり、プログラマの責任で`Close`して解放しなければならない場合もあります。

https://learn.microsoft.com/ja-jp/office/vba/language/concepts/getting-started/passing-arguments-efficiently

https://learn.microsoft.com/en-us/office/client-developer/excel/memory-management-in-excel



# おわりに

以上、カオスなVBAの世界のほんの一部をまとめてみました。
ChatGPTでプログラムや記事を簡単に書ける昨今ですが、自分の書き方や自分の言葉でまとめたほうが納得いくということを今回つくづく感じました:laughing: