まずはじめに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インターフェイスは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フィールドの参照しているインスタンス自体は異なるものの、そのインスタンスのフィールドの値は全く同じものになります。
最後に、今まで説明してきませんでしたが、これらの方法でコピーされるのはインスタンスのフィールドであって、静的なフィールドは当然コピー対象ではありません。
これまではオブジェクトのコピーについて見てきましたが、ここからはオブジェクトの破棄と消滅に関することを見ていきたいと思います。 まず、.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クラスはプログラムの実行中であれば解放されていません。
このように、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()を呼び出すことを忘れても解放されないままになるなどの深刻な事態にはならなくなります。