忍者ブログ

VB.NET-TIPS などプログラミングについて

VB.NETのTIPS(小技集)を中心に、その他のプログラミングについて少し役に立つ情報を発信します。いわゆる個人的な忘備録ですが、みなさんのお役に立てれば幸いです。

関数の引数がクラスオブジェクトの場合の注意点について

関数の引数が値渡しのクラス変数である場合は注意が必要です。 関数内でクラスの変数を介して、クラス内のデータの書き変えが行われた場合、呼び出した側でそれを参照した時に書き変った値を扱います。

これは、クラス変数は参照型として扱われ、関数にはクラス変数の参照そのものの値が渡されて、 その参照値を使ってクラスの実体(インスタンス)にアクセスを行う為、関数呼び出し側と、関数内で同じインスタンスを処理対象とする為です。

実際のプログラムを見れば一目瞭然なのですが以下の様になります。

関数の引数が値型のクラス変数である場合

Module mdlClassPrm
    ' テスト用クラス
    Class clsPrmTest
        ' 内部変数を1個持つ
        Public intData1 As Integer
    End Class

    ' 引数がテストクラスの値渡し
    Sub PrmTestByVal(ByVal clsP As clsPrmTest)
        ' クラス内部の変数を1加算
        clsP.intData1 += 1
    End Sub

    ' 引数がテストクラスの参照渡し
    Sub PrmTestByRef(ByRef clsP As clsPrmTest)
        ' クラス内部の変数を1加算
        clsP.intData1 += 1
    End Sub

    Public Sub Main()
        ' テストクラス生成
        Dim clsPrm As New clsPrmTest
        ' テストクラス変数初期化
        clsPrm.intData1 = 0
        Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1)

        ' 引数がテストクラスの値渡しを呼出す
        Call PrmTestByVal(clsPrm)
        Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1)

        ' 引数がテストクラスの参照渡しを呼出す
        Call PrmTestByRef(clsPrm)
        Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1)
    End Sub
End Module

クラス clsPrmTest は内部にパブリックな1個の変数を持つだけの簡単なものです。 関数 PrmTestByVal は引数に ByVal (値渡し)としての引数を持ち、内部の処理はクラス内の変数を+1しています。

このプログラムを実行すると、 PrmTestByVal を実行後の表示が「clsPrm.intData1=1」と1加算されたものになります。 関数内でアクセスされた変数と、関数の呼出し側でアクセスされた変数が同じものを扱っています。

引数が ByVal 指定なので値型変数(Integer, Long 等)の様に値そのものが渡されるので、 クラスの場合もクラスそのものが値として渡されると勘違いしがちです。
しかし、関数に渡される値は、クラスの参照型データ、つまりインスタンスを参照している参照データが渡されて、それを元にインスタンスにアクセスされてしまうからです。 参照値はクラスインスタンスのアドレスが渡されると考えた方が分かりやすいかもしれません。

PrmTestByVal の値渡し引数を参照渡しにした PrmTestByRef を宣言しましたが、 この場合、引数で渡されるのは、クラスインスタンスの参照データの参照が渡されます。
参照の参照を使ってクラスインスタンスにアクセスすると、インスタンスの実体にアクセスできるので、PrmTestByVal と同様の動作となります。 この動きがどうも解せないのですが、参照の参照でも、その内部の変数にアクセスが可能な様です。 (原理的に上手く説明ができませんが...)

この参照の参照を理解する為に、以下の様にプログラムを変更します。

参照データの引数を関数内で書き変える例

Module mdlClassPrm
    ' テスト用クラス
    Class clsPrmTest
        Public intData1 As String
    End Class

    ' 引数がテストクラスの値渡し
    Sub PrmTestByVal(ByVal clsP As clsPrmTest)
        clsP.intData1 += 1
        ' 内部で新しくインスタンス生成
        Dim clsPnew As New clsPrmTest
        clsPnew.intData1 = 100
        clsP = clsPnew
    End Sub

    ' 引数がテストクラスの参照渡し
    Sub PrmTestByRef(ByRef clsP As clsPrmTest)
        clsP.intData1 += 1
        ' 内部で新しくインスタンス生成
        Dim clsPnew As New clsPrmTest
        clsPnew.intData1 = 200
        clsP = clsPnew
    End Sub

    Public Sub Main()
        ' テストクラス生成
        Dim clsPrm As New clsPrmTest
        ' テストクラス変数初期化
        clsPrm.intData1 = 0
        Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1)

        ' 引数がテストクラスの値渡しを呼出す
        Call PrmTestByVal(clsPrm)
        Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1)

        ' 引数がテストクラスの参照渡しを呼出す
        Call PrmTestByRef(clsPrm)
        Console.WriteLine("clsPrm.intData1={0}", clsPrm.intData1)
    End Sub
End Module

これを実行すると、以下の様な表示になります。

clsPrm.intData1=0
clsPrm.intData1=1
clsPrm.intData1=200

PrmTestByRef の中では新しくクラスのインスタンスを生成し、参照渡し引数に代入していますので 変数の中身が新しいインスタンスへの参照に置き換わってしまいます。 そのため、関数呼び出し側に戻って表示を行うと、関数内で行った初期値の「200」となります。

では、メイン関数で最初に生成されたクラスのインスタンスはどうなったのでしょうか?
インスタンス自体はVB.NETが管理するメモリ上に存在するのですが、どこからも利用できない状態になります。
このプログラムでは直ぐに実行が終わるので、問題はありません。 もし直ぐに実行が終わらなくても、まあ、心配しなくても、どこからも参照されなくなったインスタンスは、そのうちシステムの方で片づけてくれます。
(この仕組みを「ガベージコレクション」いわゆる「ゴミ集め」というそうです。)
だからと言って、どんどん参照を書き変えていくことをしない方が良いと思います。

関連する記事

関数の戻り値がクラス(オブジェクト)の場合について
クラスにイベントを実装する方法について(Event, RaiseEvent, WithEvents)

おすすめ本

PR

コメント

コメントを書く