Sponsored Link

インスタンスのコピー

値型と参照型における代入の違い

まずはじめにIClonableインターフェイスを用いたオブジェクトの複製方法について検証してみようと思うのですが、その前に値型と参照型の代入における動作の違いについて軽くおさらいしておきたいと思います。

値型と参照型の違い
Module ClonableAndDisposable

    ' 参照型
    Class ReferencialTypeMusume

        Public Name As String

    End Class

    ' 値型
    Structure ValueTypeMusume

        Public Name As String

    End Structure

    ' アプリケーションのエントリーポイント
    Sub Main()

        ' 参照型の場合
        Dim r1 As New ReferencialTypeMusume(), r2 As New ReferencialTypeMusume()

        r1.Name = "加護亜依"
        r2.Name = "石川梨華"

        Console.WriteLine("r1.Name: {0}, r2.Name: {1}", r1.Name, r2.Name)

        ' 「参照」を代入
        r1 = r2

        r2.Name = "高橋愛"

        Console.WriteLine("r1.Name: {0}, r2.Name: {1}", r1.Name, r2.Name)


        ' 単なる改行
        Console.WriteLine()


        ' 値型の場合
        Dim v1 As New ValueTypeMusume(), v2 As New ValueTypeMusume()

        v1.Name = "加護亜依"
        v2.Name = "石川梨華"

        Console.WriteLine("v1.Name: {0}, v2.Name: {1}", v1.Name, v2.Name)

        ' 「値」を代入
        v1 = v2

        v2.Name = "高橋愛"

        Console.WriteLine("v1.Name: {0}, v2.Name: {1}", v1.Name, v2.Name)

    End Sub

End Module
実行結果
r1.Name: 加護亜依, r2.Name: 石川梨華
r1.Name: 高橋愛, r2.Name: 高橋愛

r1.Name: 加護亜依, r2.Name: 石川梨華
r1.Name: 石川梨華, r2.Name: 高橋愛
Press any key to continue

値型と参照型の根本的な違いについてをここで述べることはしませんが、代入だけについてみれば、値型ではその値すべてを代入するのに対して、参照型はその参照のみを代入します。

参照型インスタンスのコピー

それでは参照型インスタンスの参照の代入ではなく、インスタンスそのもののコピーを生成するにはどのような方法があるかを検討してみたいと思います。 まず、その最も簡単な方法はすべてのフィールドを直接コピーするという方法です。

Module ClonableAndDisposable

    ' 参照型
    Class ReferencialTypeMusume

        Public Name As String
        Public Age As Integer
        Public BloodType As String

    End Class

    ' アプリケーションのエントリーポイント
    Sub Main()

        ' インスタンスを生成
        Dim r1 As New ReferencialTypeMusume(), r2 As New ReferencialTypeMusume()

        ' インスタンスを初期化
        With r1

            .Name = "加護亜依"
            .Age = 15
            .BloodType = "AB"

        End With

        ' インスタンスのフィールドをコピー
        With r2

            .Name = r1.Name
            .Age = r1.Age
            .BloodType = r1.BloodType

        End With

    End Sub

End Module

これでもコピーは行えますが、重大な問題が二つあります。 このようなフィールドの少ないクラスであれば問題ないのですが、たくさんのフィールドを持つクラスや、少ないフィールドしか持たないクラスでも何度もコピーすることが必要な場合などには、コピーの度に大量のソースコードを書かなければならないと言う問題がその一つです。 また、この方法ではパブリックなフィールドしかコピーできません。 プライベートなフィールドについてはコピーする手段を持ち得ません。 そこで、これらの問題を解決する方法としてあげられるコピー方法がコンストラクタを用いる方法です。

Module ClonableAndDisposable

    ' 参照型
    Class ReferencialTypeMusume

        ' 様々なスコープを持つフィールド
        Public Name As String
        Protected Age As Integer
        Private BloodType As String

        ' コンストラクタ (初期化用)
        Public Sub New(ByVal name As String, ByVal age As Integer, ByVal bloodType As String)

            Me.Name = name
            Me.Age = age
            Me.BloodType = bloodType

        End Sub

        ' コンストラクタ (コピー用)
        Public Sub New(ByVal musume As ReferencialTypeMusume)

            Me.Name = musume.Name
            Me.Age = musume.Age
            Me.BloodType = musume.BloodType

        End Sub

        ' インスタンスの状態を表示するためのメソッド
        Public Overrides Function ToString() As String

            Return Me.Name + " " + Me.Age.ToString() + "歳, " + Me.BloodType + "型"

        End Function

    End Class

    ' アプリケーションのエントリーポイント
    Sub Main()

        ' 変数を宣言
        Dim r1, r2 As ReferencialTypeMusume

        ' インスタンスを生成
        r1 = New ReferencialTypeMusume("加護亜依", 15, "AB")

        ' インスタンスのコピーを生成
        r2 = New ReferencialTypeMusume(r1)

        ' 状態を表示
        Console.WriteLine("r1: {0}", r1)
        Console.WriteLine("r2: {0}", r2)

    End Sub

End Module
実行結果
r1: 加護亜依 15歳, AB型
r2: 加護亜依 15歳, AB型
Press any key to continue

これで様々なスコープのフィールドを持ったクラスのインスタンスでも、比較的簡単な方法ですべてのフィールドをコピーしてインスタンスのコピーを作ることができるようになります。 ただ、このような方法を用いてもフィールドが多数ある場合はコンストラクタにコピーのコードを記述するだけでも一苦労です。

IClonableインターフェイスとMemberwiseClone()メソッド

これまで検討した方法よりももっと簡単にコピーを生成する方法にIClonableインターフェイスとMemberwiseClone()メソッドを利用する方法があります。 IClonableインターフェイスはObject型を戻り値とするCloneメソッドだけを持ちます。 このインターフェイスを実装することでそのクラスはインスタンスのコピーを生成することができると言うことを示すことができます。

また、MemberwiseClone()メソッドはObjectクラスより継承されるメソッドなので、すべてのクラスがこのメソッドを暗黙的に継承しています。 このメソッドはProtectedで、インスタンスの簡易コピーを生成します。 参考までに、このメソッドは派生クラスでオーバーライドすることはできません。 簡易コピーとは何かを説明する前に、実際にこのインターフェイスとメソッドを利用したコピー方法の例を見てみます。

Option Strict On

Module ClonableAndDisposable

    ' 参照型
    Class Musume

        ' IClonableインターフェイスを実装
        Implements ICloneable

        ' 様々なスコープを持つフィールド
        Public Name As String
        Protected Age As Integer
        Private BloodType As String

        ' コンストラクタ (初期化用)
        Public Sub New(ByVal name As String, ByVal age As Integer, ByVal bloodType As String)

            Me.Name = name
            Me.Age = age
            Me.BloodType = bloodType

        End Sub

        ' Clone()メソッドの実装
        Public Function Clone() As Object Implements ICloneable.Clone

            ' MemberwiseClone()メソッドを利用して簡易コピーを生成
            Return Me.MemberwiseClone()

        End Function

        ' インスタンスの状態を表示するためのメソッド
        Public Overrides Function ToString() As String

            Return Me.Name + " " + Me.Age.ToString() + "歳, " + Me.BloodType + "型"

        End Function

    End Class

    ' アプリケーションのエントリーポイント
    Sub Main()

        ' 変数を宣言
        Dim r1, r2 As Musume

        ' インスタンスを生成
        r1 = New Musume("加護亜依", 15, "AB")

        ' インスタンスの簡易コピーを生成
        r2 = DirectCast(r1.Clone(), Musume)

        ' 状態を表示
        Console.WriteLine("r1: {0}", r1)
        Console.WriteLine("r2: {0}", r2)

    End Sub

End Module
実行結果
r1: 加護亜依 15歳, AB型
r2: 加護亜依 15歳, AB型
Press any key to continue

この実行結果を見てわかるとおり、コピーに関するコードを記述することなくインスタンスのコピーを生成できていることがわかると思います。 ただ、この例ではコピーするためのメソッドを用意すればよいだけなのでIClonableインターフェイスは必ずしも実装する必要はありませんが、IClonable インターフェイスを実装することによってこのクラスがインスタンスのコピーを生成する能力をもつということを明示的にすることはできます。

また、IClonableインターフェイスでは汎用性を高めるためにIClonable.Clone()メソッドの戻り値は Object型になっていますが、このままだとこのメソッドを呼び出すたびに52行目のようにキャストが必要になります(Option Strict が On の場合)。 そこで、この実装を隠蔽して次のように書き換えるのがよいと思われます。

Class Musume

    ' IClonableインターフェイスを実装
    Implements ICloneable


    ' 途中省略


    ' 戻り値が型指定された公開されるClone()メソッド
    Public Function Clone() As Musume

        Return DirectCast(Me.MemberwiseClone(), Musume)

    End Function

    ' 非公開のClone()メソッド
    Private Function CloneMyself() As Object Implements ICloneable.Clone

        Return Me.Clone()

    End Function

End Class

このように、公開されるClone()メソッドではそのクラス型を戻り値とし、IClonableインターフェイスのClone()メソッドでは厳密に型指定されたClone()メソッドの戻り値を利用することでインターフェイスの機能を保つこともできます。

簡易コピーと詳細コピー

先ほどMemberwiseClone()メソッドは「簡易コピー」であるとしました。 「簡易コピー」では、インスタンスの各フィールドがコピーされますが、参照型フィールドではその参照のみがコピーされ、値型フィールドでは通常の代入同様値がコピーされます。 反対に、「詳細コピー」では、値型フィールドは当然値がコピーされますが、参照型フィールドでも値型同様参照先のインスタンスのコピーが生成されます。

つまり、簡易コピーでは参照はコピーされるもののインスタンス全体がコピーされるのではなく、詳細コピーでは参照先のインスタンスを含めたすべてのフィールドのコピーが生成されます。 簡易コピーと詳細コピーのそれぞれを行った例を次に示します。

簡易コピーの例
Option Strict On

Module ClonableAndDisposable

    ' Musumeから参照される型
    Class Group

        ' IClonableインターフェイスを実装
        Implements ICloneable

        ' 値型フィールド
        Public Name As String

        ' コンストラクタ
        Public Sub New(ByVal name As String)

            Me.Name = name

        End Sub

        ' 戻り値が型指定された公開されるClone()メソッド
        Public Function Clone() As Group

            Return DirectCast(Me.MemberwiseClone(), Group)

        End Function

        ' 非公開のClone()メソッド
        Private Function CloneMyself() As Object Implements ICloneable.Clone

            Return Me.Clone()

        End Function

    End Class


    ' コピー対象となるクラス
    Class Musume

        ' IClonableインターフェイスを実装
        Implements ICloneable

        ' 値型フィールド
        Public Name As String

        ' 参照型フィールド
        Public Belonging As Group

        ' コンストラクタ
        Public Sub New(ByVal name As String, ByVal belonging As Group)

            Me.Name = name
            Me.Belonging = belonging

        End Sub

        ' 戻り値が型指定された公開されるClone()メソッド
        Public Function Clone() As Musume

            Return DirectCast(Me.MemberwiseClone(), Musume)

        End Function

        ' 非公開のClone()メソッド
        Private Function CloneMyself() As Object Implements ICloneable.Clone

            Return Me.Clone()

        End Function

        ' インスタンスの状態を表示するためのメソッド
        Public Overrides Function ToString() As String

            If Me.Belonging Is Nothing Then

                Return Me.Name

            Else

                Return Me.Name + " (" + Me.Belonging.Name + ")"

            End If

        End Function

    End Class


    ' アプリケーションのエントリーポイント
    Sub Main()

        ' 所属グループ
        Dim g As New Group("モーニング娘。")

        ' 変数を宣言
        Dim m1, m2 As Musume

        ' インスタンスを生成
        m1 = New Musume("加護亜依", g)

        ' インスタンスの簡易コピーを生成
        m2 = m1.Clone
        m2.Name = "紺野あさ美"

        ' 状態を表示
        Console.WriteLine("m1: {0}", m1)
        Console.WriteLine("m2: {0}", m2)

        ' Belongingメンバの値を比較
        Console.WriteLine("m1.Belonging Is m2.Belonging: {0}", m1.Belonging Is m2.Belonging)

    End Sub

End Module
実行結果
m1: 加護亜依 (モーニング娘。)
m2: 紺野あさ美 (モーニング娘。)
m1.Belonging Is m2.Belonging: True
Press any key to continue

このコードではGroupクラスにClone()メソッドが実装されていますが、使用されることはありません。 また、Musumeクラスで行われる MemberwiseClose()メソッドは、参照型フィールドが参照しているインスタンスのコピーまでは作成しないので、実行結果の通り、コピーにより生成されたインスタンスと元のインスタンスのBelongingメンバはともに同じインスタンスを参照しています。

では、Musumeクラスで行われるコピー生成のコードを次のように変えた場合、すなわち、詳細コピーがなされるようにしたコードでその実行結果を見てみることにします。

詳細コピーの例
Class Musume


    ' 途中省略


    ' 戻り値が型指定された公開されるClone()メソッド
    Public Function Clone() As Musume

        ' インスタンスの簡易コピーを作成
        Dim inst As Musume = DirectCast(Me.MemberwiseClone(), Musume)

        ' 参照型フィールドであるBelongingフィールドのコピーを生成
        If Not Me.Belonging Is Nothing Then

            inst.Belonging = Me.Belonging.Clone()

        End If

        ' 生成されたコピーを返す
        Return inst

    End Function


    ' 途中省略


End Class
実行結果
m1: 加護亜依 (モーニング娘。)
m2: 紺野あさ美 (モーニング娘。)
m1.Belonging Is m2.Belonging: False
Press any key to continue

このように、詳細コピーでは参照型のフィールドが参照しているインスタンスのコピーも作成します。 そのため、Belongingフィールドの参照しているインスタンス自体は異なるものの、そのインスタンスのフィールドの値は全く同じものになります。

最後に、今まで説明してきませんでしたが、これらの方法でコピーされるのはインスタンスのフィールドであって、静的なフィールドは当然コピー対象ではありません。

Finalize()メソッド(デストラクタ)

これまではオブジェクトのコピーについて見てきましたが、ここからはオブジェクトの破棄と消滅に関することを見ていきたいと思います。 まず、.NET Frameworkによって提供されるクラスには、コンストラクタと同様にデストラクタも存在します。 これは Finalize() メソッドとして実装されます。 しかし、これらがあまり用いられることがないのは、マネージ・リソースはガベージコレクタによって開放されるからです。 ですから、逆にこれらのものが必要になるのはアンマネージ・リソースを使う場合など、用途は限られます。 このFinalize()メソッドを実装した例とそれが呼び出されるタイミングを、コンストラクタが呼び出されるタイミングとともに見てみたいと思います。

Module ClonableAndDisposable

    Class SampleClass

        Public Sub New()

            OutputMessage("コンストラクタが呼び出されました。")

        End Sub

        Protected Overrides Sub Finalize()

            OutputMessage("デストラクタが呼び出されました。")

        End Sub

    End Class

    ' アプリケーションのエントリーポイント
    Sub Main()

        OutputMessage("アプリケーションが開始されました。")

        OutputMessage("インスタンスを生成します。")

        Dim inst As New SampleClass()

        OutputMessage("インスタンスが生成されました。")

        OutputMessage("アプリケーションが終了しました。")

    End Sub

    ' メッセージを表示するメソッド
    Sub OutputMessage(ByVal msg As String)

        Debug.WriteLine("Ticks " + DateTime.Now.Ticks.ToString() + ": " + msg)

    End Sub

End Module
実行結果
・一回目
Ticks 631849925642777328: アプリケーションが開始されました。
Ticks 631849925650588560: インスタンスを生成します。
Ticks 631849925650688704: コンストラクタが呼び出されました。
Ticks 631849925650688704: インスタンスが生成されました。
Ticks 631849925650688704: アプリケーションが終了しました。
Ticks 631849925650888992: デストラクタが呼び出されました。

・二回目
Ticks 631849925719988352: アプリケーションが開始されました。
Ticks 631849925727799584: インスタンスを生成します。
Ticks 631849925727799584: コンストラクタが呼び出されました。
Ticks 631849925727899728: インスタンスが生成されました。
Ticks 631849925727899728: アプリケーションが終了しました。
Ticks 631849925728100016: デストラクタが呼び出されました。

・三回目
Ticks 631849925852879440: アプリケーションが開始されました。
Ticks 631849925860490384: インスタンスを生成します。
Ticks 631849925860590528: コンストラクタが呼び出されました。
Ticks 631849925860790816: インスタンスが生成されました。
Ticks 631849925860790816: アプリケーションが終了しました。
Ticks 631849925860991104: デストラクタが呼び出されました。

C++などの言語では、ブロック内で使用された変数はブロックから出る時点で解放されますが、実行結果を見てわかるとおり、生成されたインスタンスはブロックから出た後も多少の間残っていることがわかります。 また、解放されるタイミングも必ずしもブロックから出た直後ではないようです。 何度か同じコードを実行しましたが、多くの場合ブロックから出てからデストラクタが呼び出されるまで多少のラグがあるようです。

ちなみに、このサンプルでメッセージを表示するのにConsoleクラスではなくDebugクラスを使用しているのは、アプリケーションの終了時にConsoleクラスが解放されるからです。 Debugクラスはプログラムの実行中であれば解放されていません。

IDisposableインターフェイス

このように、Finalize()メソッド (デストラクタ)では、どのタイミングで呼び出されるかがわからないので、動的にメモリ確保を行ったりする場合などには非常に不便です。 そこで、明示的にリソースを解放するための手段として、IDisposableインターフェイスを利用するという方法があります。 IDisposableインターフェイスは唯一のメソッドであるDispose()メソッドを提供します。 IDisposableインターフェイスを利用した例を次に示します。

Module ClonableAndDisposable

    Class SampleClass

        ' IDisposableインターフェイスを実装
        Implements IDisposable

        Public Sub New()

            OutputMessage("コンストラクタが呼び出されました。")

        End Sub

        Public Sub Dispose() Implements IDisposable.Dispose

            OutputMessage("Dispose()メソッドが呼び出されました")

        End Sub

    End Class

    ' アプリケーションのエントリーポイント
    Sub Main()

        OutputMessage("アプリケーションが開始されました。")

        OutputMessage("インスタンスを生成します。")

        Dim inst As New SampleClass()

        OutputMessage("インスタンスが生成されました。")

        OutputMessage("インスタンスを解放します。")

        inst.Dispose()

        OutputMessage("インスタンスを解放されました。")

        OutputMessage("アプリケーションが終了しました。")

    End Sub

    ' メッセージを表示するメソッド
    Sub OutputMessage(ByVal msg As String)

        Debug.WriteLine("Ticks " + DateTime.Now.Ticks.ToString() + ": " + msg)

    End Sub

End Module
実行結果
Ticks 631849931316335504: アプリケーションが開始されました。
Ticks 631849931324747600: インスタンスを生成します。
Ticks 631849931324747600: コンストラクタが呼び出されました。
Ticks 631849931324747600: インスタンスが生成されました。
Ticks 631849931324847744: インスタンスを解放します。
Ticks 631849931324847744: Dispose()メソッドが呼び出されました
Ticks 631849931324947888: インスタンスを解放されました。
Ticks 631849931325148176: アプリケーションが終了しました。

実行結果を見てわかるとおり、Dispose()メソッドを呼び出すことによって明示的にリソースの解放などを行えるようになります。 後はこのメソッドにリソース解放のコードを記述すればよいだけです。 また、IDisposableインターフェイスを実装する場合は、Dispose()メソッドが呼び出されなかった場合に備えて、Finalize()メソッドも実装している必要があります。 また、この場合のFinalize()メソッドは次の例のようにDispose()を呼び出す場合が多いと思います。

Imports System.IO

Module ClonableAndDisposable

    Class SampleClass

        ' IDisposableインターフェイスを実装
        Implements IDisposable

        ' すでにインスタンスが解放されたかを保存する
        Private disposed As Boolean

        ' コンストラクタ
        Public Sub New()

            disposed = False

            OutputMessage("コンストラクタが呼び出されました")

        End Sub

        ' デストラクタ
        Protected Overrides Sub Finalize()

            Dispose()

        End Sub

        ' Dispose()メソッド
        Public Sub Dispose() Implements IDisposable.Dispose

            If Not disposed Then

                OutputMessage("Dispose()メソッドが呼び出されました")

                disposed = True

            End If

        End Sub

    End Class

    ' アプリケーションのエントリーポイント
    Sub Main()

        OutputMessage("アプリケーションが開始されました。")

        OutputMessage("インスタンスが作成されます。")

        Dim inst As New SampleClass()

        OutputMessage("インスタンスが作成されました。")

        OutputMessage("アプリケーションが終了されます。")

    End Sub

    ' メッセージを表示するメソッド
    Sub OutputMessage(ByVal msg As String)

        Debug.WriteLine("Ticks " + DateTime.Now.Ticks.ToString() + ": " + msg)

    End Sub

End Module
実行結果
Ticks 631850037169421520: アプリケーションが開始されました。
Ticks 631850037181639088: インスタンスが作成されます。
Ticks 631850037181739232: コンストラクタが呼び出されました
Ticks 631850037181739232: インスタンスが作成されました。
Ticks 631850037181739232: アプリケーションが終了されます。
Ticks 631850037181839376: Dispose()メソッドが呼び出されました。

このコードのように実装しておけば、Dispose()を呼び出せば明示的にリソースを解放でき、また万が一Dispose()を呼び出すことを忘れても解放されないままになるなどの深刻な事態にはならなくなります。