tagCANDY CGI VBレスキュー(花ちゃん) の Visual Basic 2010 用 掲示板(VB.NET 掲示板) [ツリー表示へ]   [Home]
一括表示(VB.NET VB2005)
タイトルCollectionオブジェクトのコピーについて
記事No11798
投稿日: 2017/02/08(Wed) 15:07
投稿者OrientalMelody
お世話になっております。

Collectionオブジェクトをその時点のデータ付きで別インスタンスとしてコピーしたいと思っています。

Class1クラスとForm1フォームを作成し、Button1ボタンを貼り付けて、
以下のコードを貼り付けます。

Public Class Class1
    Private mintTestNo As String
    Private mstrTestData As String
    Public Property TestNo() As Integer
        Get
            TestNo = mintTestNo
        End Get
        Set(ByVal Value As Integer)
            mintTestNo = Value
        End Set
    End Property
    Public Property TestData() As String
        Get
            TestData = mstrTestData
        End Get
        Set(ByVal Value As String)
            mstrTestData = Value
        End Set
    End Property

End Class

Public Class Form1
    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
        Dim TestCollection1 As New Collection
        Dim TestCollection2 As New Collection
        Dim TestItemA As New Class1
        Dim TestItemB As New Class1
        TestItemA.TestData = "AAA"
        TestItemA.TestNo = "1"
        TestCollection1.Add(TestItemA)
        'TestCollection2.Add(TestItemA)     '---@
        TestCollection2 = TestCollection1   '---A
        TestItemB.TestData = "BBB"
        TestItemB.TestNo = "2"
        TestCollection2.Add(TestItemB)

        Dim objItem As Class1
        For Each objItem In TestCollection1
           Debug.WriteLine("TestCollection1 " & objItem.TestNo & " " & objItem.TestData)
        Next objItem

        For Each objItem In TestCollection2
           Debug.WriteLine("TestCollection2 " & objItem.TestNo & " " & objItem.TestData)
        Next objItem

    End Sub
End Class


TestCollection2 = TestCollection1   '---A
でその時点までのデータと構造をコピーしたつもりなのですが、
結果は、

TestCollection1 1 AAA
TestCollection1 2 BBB
TestCollection2 1 AAA
TestCollection2 2 BBB

と参照先が共有されてしまうようです。

TestCollection2 = TestCollection1   '---A
をコメントアウトし、
TestCollection2.Add(TestItemA)      '---@
のコメントを外すともちろん所望の結果の

TestCollection1 1 AAA
TestCollection2 1 AAA
TestCollection2 2 BBB

になります。

CollectionオブジェクトはCloneメソッドもないようですし、
データと構造を別インスタンスとしてコピーするにはどのようにしたら良いのでしょうか?

もし何かお分かりの方がいらっしゃいましたら、ご教示の程、宜しくお願いいたします。

使用OS、言語:Win7 64bit、VB2005

[ツリー表示へ]
タイトルRe: Collectionオブジェクトのコピーについて
記事No11799
投稿日: 2017/02/08(Wed) 20:03
投稿者魔界の仮面弁士
> Private mintTestNo As String
> Public Property TestNo() As Integer

接頭辞が「mint」なのに、As String なのですか? (^^;


> Collectionオブジェクト

そもそも Collection を使うのではなく、System.Collections.Generic 名前空間の
ジェネリックなクラス (List や HashSet や Dictionary など)を使うことをお奨めします。

ついでに VB のバージョンアップもお奨めしておきます。
2008 以降であれば、"LINQ" を使うことができるため、
List や Dictionary や一次元配列の二次加工も容易になりますよ。



> CollectionオブジェクトはCloneメソッドもないようですし、

無ければ作りましょう。
シャローコピーにするかディープコピーにするかも貴方次第。


> データと構造を別インスタンスとしてコピーするにはどのようにしたら良いのでしょうか?

たとえば、Collection に Add されていた TestItemA のインスタンス が、
Dim TestItemA As New Class1() ではなく、
Dim TestItemA As New System.IO.FileInfo("C:\Folder\File1.txt")
だった場合を想像してみて下さい。

この場合、コレクションはどのようにコピーされるべきでしょうか?

複製された Collection に入っているべき FileInfo を考えてみると
 (A案) 元のコレクションに Add されていたのと同一の FileInfo インスタンス
 (B案) 同じファイル C:\Folder\File1.txt を参照した、別の FileInfo インスタンス
 (C案) ファイルコピーした C:\AnotherFolder\File1.txt へのFileInfo インスタンス
のように、要件によっていろいろな考え方がありそうですよね。


しかし Collection は、特定の型向けに専用に用意されたコレクション型ではなく、
何でも入る汎用のコレクションです。

それぞれの要素をどのように複製するべきかを、Collection クラス側では分かりません。
ですから、Collection に Clone を持たせるというのも、やや酷な話と言えます。


なので、データをどのように複製するべきかという指示は、
あらかじめデータ側で提供しておく必要がある、というわけです。

言い換えれば、この場合に複製するのは Collection ではなく、Class1 の方だということです。


ここでは、複製に使える二種類の方法を記しておきます。

==========================================================================
【案1】Class1 に、複製するためのメソッドを用意する方法
--------------------------------------------------------------------------
まずは Class1 に
  Public Function Clone() As Class1
    Return CType(MemberwiseClone(), Class1)
  End Function
を実装した上で、利用する際に
  TestCollection2.Add(TestItemA.Clone())
のようにします。

この場合、TestCollection1(1) と TestCollection2(1) は
コピーされた別のオブジェクトとなりますので、
 TestCollection1(1).TestData = "XYZ"
と書き換えても、TestCollection2(1) は "AAA" のままになります。


なお、TestItemA だけを複製するのではなく、コレクション全体に対して適用したいのなら、
  For Each o As Class1 In TestCollection1
    TestCollection2.Add(o.Clone())
  Next
のように、個別に複製していけば OK です。


より丁寧に実装する場合、そのクラスが複製可能であることが明確となるよう、
ICloneable インターフェイスも Implements しておきましょう。


というのも、今回は Clone メソッドの実装を MemberwiseClone に任せていますが、
MemberwiseClone は、あくまでの簡易コピー(shallow copy)だからです。

値型の場合(もしくは値型のように振舞うクラス)、具体的には
Integer や String などであれば、簡易コピーでも十分なのですが、
クラス(すなわち参照型)が相手の場合、参照がコピーされるだけであり、
参照先オブジェクトまではコピーされません。

たとえば「Class2 クラスのインスタンスを返すプロパティ」や
配列を返すメンバーがあった場合、Class2 や配列への『参照』が
そのまま複写されてしまうことになるため、Class2 も同様に複製したり、
配列の各要素を複製したりと、再帰的に複製していく必要があります。

ただし、参照先オブジェクト(この場合は Class2)が System.ICloneable インターフェイスを
実装していた場合は、System.Object.MemberwiseClone メソッドを呼び出したときに、
自動的に ICloneable.Clone メソッドが利用されます。この場合は参照ではなく
参照先オブジェクトがコピーされる仕様です(deep copy)。

https://msdn.microsoft.com/ja-jp/library/system.object.memberwiseclone%28vs.90%29.aspx
http://smdn.jp/programming/netfx/cloning/



==========================================================================
【案2】シリアライズを用いる方法
--------------------------------------------------------------------------

まずは準備として、下記の名前空間をインポートしておきます。

Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Binary


次に、Class1 に Serializable 属性を付与します。
 <Serializable> Public Class Class1

これにより、Class1 がシリアライズ可能な(永続化可能な)クラスであると
マークされることになります。
(ちなみに、Collection 自身にも Serializable 属性が付与されています)


あとは、シリアライザを用いてこんな感じです。
今回は、BinaryFormatter というシリアライザを利用してみます。

 '元データ
 Dim TestCollection1 As New Collection
 Dim TestItemA As New Class1()
 TestItemA.TestData = "AAA"
 TestItemA.TestNo = "1"
 TestCollection1.Add(TestItemA)

 '複製結果を入れるためのコレクションです
 'これから複製結果を受け取るので、New しておく必要はありません
 Dim TestCollection2 As Collection

 Using stream As New MemoryStream() 'バイト配列をストリームとして扱うクラス
  Dim f As New BinaryFormatter() 'バイナリデータ形式でシリアライズするクラス

  'コレクションをストリームオブジェクトにシリアル化します
  f.Serialize(stream, TestCollection1)

  '一度、読み取り位置を先頭に戻しておきます
  stream.Position = 0L

  'シリアル化を解除します
  'Deserialize メソッドにより、バイナリデータの入ったストリームから
  '元データを復元し、それを TestCollection2 変数に渡します
  TestCollection2 = CType(f.Deserialize(stream), Collection)
 End Using


シリアライズは、フォーマット変換を伴うため、案1に比べると比較的低速です。
特に BinaryFormatter は、Deserialize に時間がかかることがあり、
データ量によっては、分単位で待たされることすらあります。

速度面が問題になる場合は、その他のシリアライザも試してみると良いかもしれません。

http://d.hatena.ne.jp/matarillo/20101207/p1
http://wannabe-note.com/1863
http://neue.cc/2010/05/29_261.html


また、複製のために追加の処理が必要になるようなケースでは、さらに
OnDeserialized / OnDeserializing / OnSerialized / OnSerializing 属性を
併用することも出来ます。
http://devlights.hatenablog.com/entry/20100330/p7

その他、複製したくないメンバーがある場合(たとえば TextBox なら
Text の値はコピーしたいけど Handle はコピーしないなど)や
循環参照があった場合にどうするかなどは、個別に考慮する必要がありますが、
シリアライザによって特性が異なりますので、興味があれば調べてみてください。

[ツリー表示へ]
タイトルRe^2: Collectionオブジェクトのコピーについて
記事No11804
投稿日: 2017/02/24(Fri) 00:13
投稿者OrientalMelody
魔界の仮面弁士様

もの凄い遅レスになってしまい、申し訳ございません。

丁寧なご回答、大変ありがとうございます。

実はCollectionオブジェクトは、業務プログラムでは、
あるクラスのメンバーとして存在していまして、
そのクラスをコピーしても、他のメンバーは実体がきちんとコピーされるのに、
CollectionオブジェクトのみどうしてもShallowコピーになってしまうので、
単純化してCollectionオブジェクトのみで行った場合、どのようになるか
試してみたところ、やはり参照部のみコピーされることを確認しましたので、
Collectionの問題と限定して投稿させて頂いたものです。

最終的には【案1】の方を採用させていただきます。

回数をこなさなかったり等、速度が気になるものではなければ、
【案2】のシリアライズを用いる方法も、汎用性があって、魅力的ですね。

状況に応じて使い分けたいと思います。

ありがとうございました。

[ツリー表示へ]