QA@IT

VB.NetのArrayList型を複写すると、複写元/先の内容が連動してしまう

10258 PV

うまく説明できるか不安ですが、VB.Net2010sp1の環境でArrayListを使用しています
ArrayList内には構造体を格納していて、この構造体にもArrayListがあります
この状態で、ArrayListに1件構造体を追加して、次に追加した1件を複写すると、構造体内のArrayListの値が
全て連動してしまい(0)=複写元に代入した値が(1)移行の複写先にも反映してしまいます。
文字型(構造体内のString)は問題なく使用できるのですが。

'#- StringとArrayListを持つ構造体を用意
Public Structure Items
    Public Item1 As String
    Public Item2 As ArrayList
End Structure

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    Dim AR As New ArrayList()
    Dim wArray As New ArrayList
    Dim wItem1 As New Items
    Dim wItem2 As New Items
    Dim wItem3 As New Items

    '構造体のインスタンスを生成
    wItem1.Item1 = "hoge"
    wArray.Add("111初期値")
    wItem1.Item2 = wArray

    '構造体のインスタンスをArrayListに追加する
    AR.Add(wItem1)

    'ArrayListを複写して追加 - パターン1
    AR.Add(AR(0))
    'ArrayListを複写して追加(Cloneを使用してみる) - パターン2
    AR.Add(AR.Clone(0))
    'ArrayListを複写して追加(一度構造体を介して複写) - パターン3
    wItem2 = AR(0)
    AR.Add(wItem2)

    'ArrayList (0)のItem1を変更してみる
    wItem3 = AR(0)
    wItem3.Item1 = "hoge2"
    wItem3.Item2(0) = "222変更後"
    AR(0) = wItem3

    '#- Item1の内容表示 → これは(0)だけ値が変わっている -> OK
    MsgBox(AR(0).item1 & vbCrLf & AR(1).item1 & vbCrLf & AR(2).item1 & vbCrLf & AR(3).item1)
    '#- Item2の内容表示 → これは(0)以外も値が変わってしまっている -? NG 下記全てが同値となってしまう
    MsgBox(AR(0).item2(0) & vbCrLf & AR(1).item2(0) & vbCrLf & AR(2).item2(0) & vbCrLf & AR(3).item2(0))

End Sub

何か使用方法が間違っている気がしますが、何か対策/指摘があれば教えて下さい。

  • 詳しく見てませんのでハズレかもしれませんが、Item2 が全て同じ値になってしまうのは AR(0) ~ AR(3) は全て同じオブジェクトを参照しているから、Item1 は .NET の String 型の「作成時点以降に値を変更できない」というところから来ている現象だと思います。 -

回答

気になった点はソースコードのコメントに「ArrayListを複写して」とあるのにArrayListを複写していないことです。

そこでコメントに合わせて実際に複写してみたところ、myname1-nanaさんがおそらく意図しているであろう動作になるようです。つまりItem2も(0)だけ値が変わります。

ArrayListの複写にはシャローコピーとディープコピーがありますが、これはシャローコピーのほうです。

元のものとの差分を示します。

        'ArrayListを複写して追加 - パターン1
        wItem1.Item1 = AR(0).Item1
        wItem1.Item2 = AR(0).Item2.Clone()  'ArrayListの簡易(シャロー)コピー
        AR.Add(wItem1)

        'ArrayListを複写して追加(Cloneを使用してみる) - パターン2
        Dim AR1 = AR.Clone()                'ArrayListの簡易(シャロー)コピー
        wItem1.Item1 = AR1(0).Item1
        wItem1.Item2 = AR1(0).Item2.Clone() 'ArrayListの簡易(シャロー)コピー
        AR.Add(wItem1)

        'ArrayListを複写して追加(一度構造体を介して複写) - パターン3
        wItem2 = AR(0)
        wItem1.Item1 = wItem2.Item1
        wItem1.Item2 = wItem2.Item2.Clone()  'ArrayListの簡易(シャロー)コピー
        AR.Add(wItem1)

というわけで「VB.NetのArrayList型を複写すると、複写元/先の内容が連動してしまう」わけではないと思います。

追記

質問者さんのやり方での複写は参照の複写で、実体は複写していないので、1個の実体(StringやArrayListのインスタンス)に対して参照が複数個作られます。それらの参照はみな1個の実体を指しています。つまり1個の実体を「共有」します。

その状態で下記のように質問者さんのやり方での変更を行ないます。

    'ArrayList (0)のItem1を変更してみる
    wItem3 = AR(0)
    wItem3.Item1 = "hoge2"
    wItem3.Item2(0) = "222変更後"
    AR(0) = wItem3

するとItem1のほうは別の実体("hoge2")への参照を代入したので、別の実体を指すようになります。しかしこれはAR(0)のItem1(参照)を書きかえただけで、AR(1)等のItem1(参照)はいじっておらず、AR(1)等はもとの実体("hoge")を指したままなので変化はしません。これは意図する結果ということになります。

Item2のほうは、ArrayListの先頭要素に"222変更後"を代入したので、Item2(参照)が指す先の実体を更新します。しかしItem2(参照)自体は元のままであり、AR(0)のItem2(参照)もAR(1)等のItem2(参照)も同じ1個のArrayListの実体を指しています。その結果AR(1)等も先頭要素が"222変更後"に変化して一見「連動」しているように見えますが、実際は1個の実体の「共有」なので、AR(0)だけの更新のつもりが、そうはうまくいかなかっただけで、これは予期せぬ結果ということになります。

ArrayListの複写時に、実体(ArrayListの器の部分およびその中に入っている全要素)を複写し、AR(0)、AR(1)等のItem2(参照)が別々の実体を指すようにしておけば、予期せぬ結果は避けられます。なのでそういう例を出しました。

ただし実装方法ですが、例えば変更がほとんどないのであれば、実際に変更があるまでは実体を複写する必要はないかもしれませんし、また別の話として、要件によってはシャローコピーではNGでディープコピーが必要とされるかもしれませんし、その辺は要件次第でしょうか。どう実装するかは要件次第だと思いますが、他人のコードをいじったときに思わぬ副作用が生じるとあまり嬉しくはないので、なるべく副作用が生じないようになっているといいのかなと思います。

編集 履歴 (3)
  • 今の質問者さんのやり方で、質問者さんが意図する「複写」はできているのかもしれません。問題は、質問者さんのやり方で「複写」した場合、質問者さんのやり方で「変更」すると、結果が質問者さんの期待したものにならないということでしょうか? -
  • なので、質問者さんが期待した結果を得るためには、blunder3 さんの書かれたように「複写」のやり方を変えるか、私のレスに書いたように「変更」のやり方を変えるか、いずれかの対応を取るということになりますが、どちらが良いかは質問者さんしだいだと思います。 -
  • SurferOnWwwさん、コメントありがとうございます。少し説明を追加してみました。
    -
  • 重箱の隅的なことを言ってすみません。Items は構造体(値型)なのでボックス化 / ボックス化解除という操作が行われ、質問者さんが行った「複写」の結果、AR(0) ~ AR(2) と AR(3) は別のオブジェクトを指すことになると思われます。詳細は解答欄に追記しますので、よろしければ見てください。 -
  • すいません。表現がまずかったので修正した後、SurferOnWwwさんのコメントに気づきました。ご指摘ありがとうございます。
    -
  • SurferOnWwwさん、回答欄の追記の部分を読みました。ボックス化のことは考えていなかったので、とても参考になりました。ありがとうございます。
    -

少し詳しく見てみましたが、先のコメントで書いたことは合っていたようです。

まず、値型と参照型の違いは理解されているでしょうか。もし、理解してない、もしくはあやふやと言うことでしたら、ググって調べるなどして理解した上で、以下を読んでください。

ArrayListの値が全て連動してしまい(0)=複写元に代入した値が(1)移行
の複写先にも反映してしまいます。

AR(0).Item2 ~ AR(3).Item2 は全く同じ ArrayList のオブジェクトを指していることは理解できるでしょうか?

その ArrayList オブジェクトの最初の要素を "111初期値" から "222変更後" に変更したのですから、AR(0).Item2(0) ~ AR(3).Item2(0) は全て "222変更後" になりますよね。

文字型(構造体内のString)は問題なく使用できるのですが。

それは「問題なく使用できる」と言うわけではなくて、先にコメントしましたように、String 型の「作成時点以降に値を変更できない」というところから来ています。

以下の MSDN ライブラリの「文字列オブジェクトの不変性」のセクションの 2 つめのコードを見てください(C# ですが VB.NET でも同じことです)

文字列 (C# プログラミング ガイド)
https://msdn.microsoft.com/ja-jp/library/ms228362(v=vs.120).aspx

質問者さんのケースで言うと、AR(0).Item1 は新たに作成した "hoge2" というオブジェクトを指すようになったが、AR(1).Item1 ~ AR(3).Item1 は元のオブジェクト "hoge" を指したままになっているということです。

なので、一般的には、こちらの方が想定外の結果ということで(むしろ、Item2 の方が理解しやすい動き)、ハマる人が多いようです。

【2015/6/9 追記】

質問者さんのコードで「ArrayList (0)のItem1を変更してみる」のところですが、

wItem3 = AR(0)
wItem3.Item1 = "hoge2" '------------(1)
wItem3.Item2(0) = "222変更後"  '----(2)
AR(0) = wItem3

これがどのような操作を行っているかと言うと、

(1) String 型のオブジェクト "hoge2" を新たに作ってその参照(ポインタ)を wItem3.Item1 に代入。

(2) wItem3.Item2 が指す ArrayList 型のオブジェクトのインデックス 0 の要素に、String 型のオブジェクト "222変更後" を新たに作ってその参照(ポインタ)を代入。

という違いがあります。

なので、AR(0).Item1 が指す String 型のオブジェクトは上記の操作で "hoge2" に変更されましたが、AR(1).Item1 ~ AR(3).Item1 は依然として "hoge" を指しています。

一方、AR(0).Item2 ~ AR(3).Item2 が指す ArrayList 型のオブジェクトは変わっておらず、ArrayList の中のインデックス 0 の要素が変わっただけです。AR(0).Item2 ~ AR(3).Item2 は依然として同一オブジェクトを指しているので、当然そのインデックス 0 の要素が指す String 型オブジェクト("222変更後")は同じです。

(2) を (1) と同じようにするには、(2) のコードを以下のようにすればいいはずです。お試しください。

wItem3.Item2 = New ArrayList(New String() {"222変更後"})

【2015/6/12 追記】

blunder3 さんの回答へのコメントで「詳細は解答欄に追記します」と書きましたが、以下に書きます。

Option Strict On
'On にして遅延バインディングを禁止(そうしないと何が起こっているか分かり難いので)

Module Module1
    '構造体は値型
    Public Structure Items
        Public Item1 As String
        Public Item2 As ArrayList
    End Structure

    Sub Main()
        Dim AR As ArrayList = New ArrayList()
        Dim wArray As ArrayList = New ArrayList() 
        Dim wItem1 As Items 'Items は値型なのでオリジナルコードにあった New は不用
        Dim wItem2 As Items
        Dim wItem3 As Items

        '上でメモリを確保した wItem1 の Item1, Item2 に値(オブジェクトへのポインタ)を代入
        '(代入する前は Nothing)
        wItem1.Item1 = "hoge"
        wArray.Add("111初期値")
        wItem1.Item2 = wArray

        'wItem1 がボックス化(Object 内部にラップされヒープに格納)され、その Object への
        'ポインタが AR の最初の要素として追加される。
        AR.Add(wItem1)

        'AR(0) は wItem1 がボックス化された Object 型ラッパーを指すポインタ。
        'それを AR の次の要素として追加。結果、AR(1) と AR(0) は同一のオブジェクトを指す(はず)。
        AR.Add(AR(0))

        'クローンを作っているが、実質的にやっていることは上の操作と全く同じ。
        '結果、AR(0), AR(1), AR(2) は同一オブジェクトを指す(はず)。
        AR.Add(CType(AR.Clone(), ArrayList)(0))

        'AR(0) の指すオブジェクトのボックス化を解除(オブジェクトの中の値を wItem2 にコピー)。
        wItem2 = CType(AR(0), Items)

        'wItem2 をボックス化し、ボックス化されたオブジェクトのポインタを AR に追加。
        AR.Add(wItem2)

        '以上の結果、AR(0), AR(1), AR(2) は同一オブジェクト、AR(3) は新たにボックス化した
        '別オブジェクトを指す(はず)。
        'ただし、指しているオブジェクトの中の値はコピーしているのですべて同じ。つまり、
        'AR(0).Item2 ~ AR(3).Item2 は全く同じ ArrayList オブジェクトを指すポインタ。

        Console.WriteLine("初期値")
        WriteResults(AR)

        'AR(0) を変更(質問者さんがやったとおり)

        'AR(0) が指すオブジェクト(wItem1 の値がコピーされている)のボックス化を解除。
        '結果、wItem1 の値と同じ値が wItem3 にコピーされる。つまり、wItem1.Item2 と 
        'wItem3.Item2 が指す ArrayList 型オブジェクトは同一。
        wItem3 = CType(AR(0), Items)

        'wItem3.Item1 が指す String 型オブジェクトが差し替えられる
        wItem3.Item1 = "hoge2"

        'wItem3.Item2 が指す ArrayList 型オブジェクトの最初の要素が差し替えられる。
        'ArrayList 型オブジェクト本体は変わってないことに注意(変わったのは中身だけ)
        wItem3.Item2(0) = "222変更後"

        'wItem3 をボックス化し、そのオブジェクトへのポインタを代入。
        AR(0) = wItem3

        '以上、いろいろやった結局、AR(0).Item2 ~ AR(3).Item2 は全く同じ ArrayList 型オブジェクト
        'を指していることに注意。
        '上の操作で、その ArrayList の最初の要素が "222変更後" になったので、AR(0).Item2(0) ~ 
        'AR(3).Item2(0) はすべて同じ "222変更後" になる。

        Console.WriteLine(vbCrLf + "質問者さんのコードの実行結果")
        WriteResults(AR)

    End Sub

    Sub WriteResults(ByRef AR As ArrayList)
        Console.WriteLine(CType(AR(0), Items).Item1 + " " + CType(AR(1), Items).Item1 + " " + _
                          CType(AR(2), Items).Item1 + " " + CType(AR(3), Items).Item1)
        Console.WriteLine(CType((CType(AR(0), Items).Item2(0)), String) + " " + _
                          CType((CType(AR(1), Items).Item2(0)), String) + " " + _
                          CType((CType(AR(2), Items).Item2(0)), String) + " " + _
                          CType((CType(AR(3), Items).Item2(0)), String))
    End Sub

End Module

実行結果は下記の通り:

初期値
hoge hoge hoge hoge
111初期値 111初期値 111初期値 111初期値

質問者さんのコードの実行結果
hoge2 hoge hoge hoge
222変更後 222変更後 222変更後 222変更後

編集 履歴 (3)
  • これはちょっと誤解を招く表現の気がします(普通に言葉通り受け取ると間違って理解します。文字列が仮に可変だとしても同じ事が起こりますし)。
    →それは「問題なく使用できる」と言うわけではなくて、先にコメントしましたように、String 型の「作成時点以降に値を変更できない」というところから来ています。
    -
  • そう思うなら、あなたが誤解を招く余地がない完璧な回答を質問者さんに対してしてあげてください。その方が質問者さんや、ここの一般閲覧者さんの役に立ちます。 -
  • ありがとうございます
    Stringクラスの不変性については今まで知りませんでした。

    参照型のせいなのかな?、とは思っていましたのでおおよそ納得いたしましたが、
    それならば別の参照型として複写したい時にはどうすれば良いのかが
    判りませんでした。

    今回はシステム改修の為の修正なので、ArrayListから変更する事はできませんが、
    今後はList型も検討させて頂きます。
    -
  • 理解されているでしょうか? ArrayList を List(Of T) に変更しても本質的なところは何も変わらないのですが・・・ コメント欄にはうまくかけないので解答欄に追記します。 -

まず ArrayListではなくList(Of T)を使った方がよいです。その方がパフォーマンス的にも可読性も向上します。

構造体Itemsの複写を行うには
下記のようにList部分を明示的に作成しないとインスタンスが
変わらないので提示されたような状態になります。

Dim wItem1 as Items
Dim wItem2 as Items

wItem2 = wItem1
wItem2.Item2 = wItem1.Item2.ToList()
編集 履歴 (0)
ウォッチ

この質問への回答やコメントをメールでお知らせします。