5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Visual BasicAdvent Calendar 2024

Day 15

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

Last updated at Posted at 2024-12-15

はじめに

これは、Visual Basic Advent Calendar 2024 の15日目の記事です。

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

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

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

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

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

  • 組み込み型
    BooleanLongStringDateVariantTypeなど
  • オブジェクト型
    CollectionDictionaryFileSystemObjectなど
    (クラスモジュールで自作したクラスも含まれる)

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

引数の渡し方、ByValとByRef

FunctionSub(以後、関数という)への引数の渡し方としてByValByRefがあります。引数が必要な関数を使う際、通常は引数に変数を渡します。

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

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

引数が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を代入するとどうなるでしょうか。ByValByRef、それぞれで試してみました。

まずはByVal(値渡し)から。

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

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

次に、ByRef(参照渡し)です。

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 ブロック変数が設定されていません。

関数内でdicNothingを代入すると呼び出し元のdic1Nothingになりました。
当然ながら"SHIBA"キーは参照できず、ここでエラーとなります。

もう一例見てみましょう。呼び出し元のSubにもう一つのディクショナリ変数dic2を追記した上でdic1を参照渡ししてみます。
引数として渡す前にdic2dic1を代入しているのがポイントです。

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:

ゾンビ変数

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

Test02
'    Dim dic1 As Dictionary: Set dic1 = New Dictionary
    Dim dic1 As New Dictionary                          
実行結果
dic1 Is Nothing = False
dic1("PEPE") = 
dic1("SHIB") = 

この場合、dic1Nothingにはなりません。キーは残ったまま値だけが消去されました。

image.png

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

ちなみにVB.NETではこの現象は起こりません。

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

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する必要があります。

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

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

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

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を使ってみました。

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を実行すると、イミディエイトウインドに関数終了時クラス解放時にテキストメッセージが出力されます。

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関数内のnewInstNothingを代入しても、外側のnewInstに影響はありません
  • Hoge関数内のnewInstは、Nothingを代入していませんが、yInstと同様にHoge終了後にTerminateが呼ばれます

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

出力結果
?zInst.Name
z

Nameプロパティに値が残っていることがわかります。

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

おわりに

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

5
1
0

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
5
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?