LoginSignup
3
0

今更だけどシャローコピーとディープコピーについて

Last updated at Posted at 2023-12-08

はじめに

これは、Visual Basic Advent Calendar 2023の8日目の記事となります。

現在、仕事で作成しているアプリケーションで、正常パターンでは問題ないのですが一旦エラー画面から戻って再度入力し直すと値が表示されなくなったり、照合値が変更されたりする不具合がありました。
調査をすると、Dictionary型で保持している値をエラー画面から戻った際に元に戻しているのですが、この時に値が書き換わってしまっていたのです。そう、シャローコピーの罠に嵌っていたわけです。

【2023/12/09追加】
C# を併記するようにしました。

環境

  • Windows 11 Home
  • Visual Studio 2022
  • Visual Basic
  • C#
  • .NET 8.0

値型と参照型

シャローコピーとディープコピーを理解する上で、型の違いを理解しておく必要があります。

代入はシャローコピーではない

【2024/01/17追加】

シャローコピー

シャローとは浅いという意味を持ちます。反対後として深いを意味するディープがあります。

参照型のオブジェクトの中身は参照先(メモリ上の位置やサイズなど)を表すデータ(アドレス)となります。参照型のシャローコピーは「参照先はどこか」という情報の複製になるので、複製元オブジェクトと複製先オブジェクトが同じ場所を参照している状態になります。

単純な代入

VB

Sub Main(args As String())
    Dim dict As Dictionary(Of String, String) = New Dictionary(Of String, String)
    dict.Add("A", "Foo")

    ' 代入
    Dim dictCopy = dict

    dict("A") = "Bar"
    Console.WriteLine("Original  = " + dict("A"))
    Console.WriteLine("dictCopy  = " + dictCopy("A"))
End Sub

C#

static void Main(string[] args)
{
    Dictionary<string, string> dict = new();
    dict.Add("A", "Foo");

    // 代入
    var dictCopy = dict;

    dict["A"] = "Bar";
    Console.WriteLine("Original  = " + dict["A"]);
    Console.WriteLine("dictCopy  = " + dictCopy["A"]);
}

結果

コピー先である dictCopy にコピーした後にコピー元を書き換えると、dictCopy の値も変更されてしまい両方とも Bar になってしまいます。

Original  = Bar
dictCopy  = Bar

コンストラクタを使用した代入

VB

Sub Main(args As String())
    Dim dict As Dictionary(Of String, String) = New Dictionary(Of String, String)
    dict.Add("A", "Foo")

    ' コピーコンストラクタ
    Dim dictCopy = New Dictionary(Of String, String)(dict)

    dict("A") = "Bar"
    Console.WriteLine("Original  = " + dict("A"))
    Console.WriteLine("dictCopy  = " + dictCopy("A"))
End Sub

C#

static void Main(string[] args)
{
    Dictionary<string, string> dict = new();
    dict.Add("A", "Foo");

    // コピーコンストラクタ
    var dictCopy = new Dictionary<string, string>(dict);

    dict["A"] = "Bar";
    Console.WriteLine("Original  = " + dict["A"]);
    Console.WriteLine("dictCopy  = " + dictCopy["A"]);
}

結果

今度はコピー先である dictCopy にコピーした後にコピー元を書き換えても、dictCopy の値は変更されません。
このことからシャローコピーでありながら、ディープコピーが実現されたことになります。

参照型である文字列が別インスタンスに変更されたことで、コピー元を書き換えても値が変更されなくなったのです。

Original  = Bar
dictCopy  = Foo

理由

string型は参照型なのですが、string型はコンストラクタによって、変数コピーの際は値型の挙動をとるような仕組みになっています。

Dictionary型の内容が、string型で構成されているなら、コンストラクタによるシャローコピーであってもディープコピーと同じ動きになるわけです。

ディープコピー

ディープとは深いという意味を持ちます。

クラスなどの参照型を使用した場合、コンストラクタを使用した代入でもシャローコピーとなってしまいます。

コンストラクタを使用した代入

VB

Sub Main(args As String())
    Dim dict As Dictionary(Of String, RefTest) = New Dictionary(Of String, RefTest)

    Dim t1 As New RefTest
    t1.Name = "Foo"
    dict.Add("A", t1)

    Dim dictCopy = New Dictionary(Of String, RefTest)(dict)
    t1.Name = "Bar"

    Console.WriteLine("Original  = " + dict("A").Name)
    Console.WriteLine("dictCopy  = " + dictCopy("A").Name)
End Sub

Public Class RefTest
    Public Property Name As String = ""
End Class

C#

static void Main(string[] args)
{
    Dictionary<string, RefTest> dict = new();

    RefTest t1 = new();
    t1.Name = "Foo";
    dict.Add("A", t1);

    Dictionary<string, RefTest> dictCopy = new(dict);
    t1.Name = "Bar";

    Console.WriteLine("Original  = " + dict["A"].Name);
    Console.WriteLine("dictCopy  = " + dictCopy["A"].Name);
}

public class RefTest
{
    public string Name { get; set; } = "";
}

結果

コピー先である dictCopy にコピーした後にコピー元を書き換えると、dictCopy の値も変更されてしまい両方とも Bar になってしまいます。

Original  = Bar
dictCopy  = Bar

JSONシリアライズを使用した代入

複製元オブジェクトと複製先オブジェクトが違う新しいオブジェクトを生成することで、ディープコピーを実現させます。

拡張メソッドにジェネリック関数のDeepCopy を追加しています。

VB

Sub Main(args As String())
    Dim dict As Dictionary(Of String, RefTest) = New Dictionary(Of String, RefTest)

    Dim t1 As New RefTest
    t1.Name = "Foo"
    dict.Add("A", t1)

    Dim dictCopy = New Dictionary(Of String, RefTest)(dict)
    t1.Name = "Bar"

    Console.WriteLine("Original  = " + dict("A").Name)
    Console.WriteLine("dictCopy  = " + dictCopy("A").Name)
End Sub

' 拡張メソッド
<Extension()>
Public Function DeepCopy(Of T)(ByVal src As T) As T
    Dim jsonSerializerOptions = New JsonSerializerOptions() With {
        .ReferenceHandler = ReferenceHandler.Preserve,
        .WriteIndented = True
    }
    Dim jsonData = JsonSerializer.Serialize(src, jsonSerializerOptions)
    Return JsonSerializer.Deserialize(Of T)(jsonData, jsonSerializerOptions)
End Function

Public Class RefTest
    Public Property Name As String = ""
End Class

C#

static void Main(string[] args)
{
    Dictionary<string, RefTest> dict = new();

    RefTest t1 = new();
    t1.Name = "Foo";
    dict.Add("A", t1);

    Dictionary<string, RefTest> dictCopy = dict.DeepCopy();
    t1.Name = "Bar";

    Console.WriteLine("Original  = " + dict["A"].Name);
    Console.WriteLine("dictCopy  = " + dictCopy["A"].Name);
}

public static class Utiliry
{
    // 拡張メソッド
    public static T DeepCopy<T>(this T src)
    {
        var jsonSerializerOptions = new JsonSerializerOptions()
        {
            ReferenceHandler = ReferenceHandler.Preserve,
            WriteIndented = true
        };

        var jsonData = JsonSerializer.Serialize(src, jsonSerializerOptions);

#pragma warning disable CS8603 // Null 参照戻り値である可能性があります。
        return JsonSerializer.Deserialize<T>(jsonData, jsonSerializerOptions);
#pragma warning restore CS8603 // Null 参照戻り値である可能性があります。
    }
}

public class RefTest
{
    public string Name { get; set; } = "";
}

結果

今度はコピー先である dictCopy にコピーした後にコピー元を書き換えても、dictCopy の値は変更されません。
このことからディープコピーが実現されたことになります。

Original  = Bar
dictCopy  = Foo

但し、JSONシリアライズしているだけあって遅いです。ま、遅いといっても何万回も繰り返すわけではないので気にする必要はないでしょう。

最後に

仕事で作成しているアプリケーションでは、Dictionary型の内容が string型のみで構成されているので「コンストラクタを使用した代入」で問題ないわけです。

アプリケーションは C#で作成しており、記事用に Visual Basic に書き換えてみました。

参照

3
0
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0