C# Tips | コレクション・LINQ:ディープコピー

C# C#
スポンサーリンク

はじめに:「ディープコピー」は“元データを絶対に汚したくないときの保険”

業務コードを書いていると、こういうことが起きがちです。

「編集用にコピーしたつもりが、元のデータまで変わってしまった」
「画面でキャンセルしたのに、なぜか元のオブジェクトが書き変わっている」

原因のほとんどは、「コピーしたつもりで参照を共有している」ことです。
これを防ぐための考え方・テクニックが ディープコピー(深いコピー) です。

ここでは、

浅いコピーと深いコピーの違い
オブジェクト 1 個のディープコピー
コレクション+LINQでのディープコピー
実務で使いやすいディープコピー用ユーティリティの形

を、初心者向けにかみ砕いて説明していきます。


浅いコピーと深いコピーの違いをまず腹落ちさせる

浅いコピー:入れ物だけ別、 中身は同じものを指している

クラスのインスタンスをコピーするとき、
「プロパティの値(参照)だけをそのままコピーする」のが 浅いコピー です。

public class Address
{
    public string City { get; set; } = "";
}

public class User
{
    public string Name { get; set; } = "";
    public Address Address { get; set; } = new Address();
}
C#
var u1 = new User
{
    Name = "Alice",
    Address = new Address { City = "Tokyo" }
};

var u2 = u1; // これはただの参照コピー(同じものを見る)
C#

u2.Address.City = "Osaka"; とすると、u1.Address.City"Osaka" になります。
「コピーしたつもりで、同じインスタンスを触っている」状態です。

MemberwiseClone() を使ったコピーも、基本は「浅いコピー」です。

深いコピー:中身(子オブジェクト)まで丸ごと別インスタンスにする

ディープコピー(深いコピー) は、

User も別インスタンス
User.Address も別インスタンス
その中の値だけ同じ

という状態を作ることです。

var u1 = new User
{
    Name = "Alice",
    Address = new Address { City = "Tokyo" }
};

var u2 = new User
{
    Name = u1.Name,
    Address = new Address { City = u1.Address.City }
};
C#

この場合、u2.Address.City = "Osaka"; としても、u1.Address.City"Tokyo" のままです。

ここでの重要ポイントは、「“参照型プロパティをどう扱うか”が浅いコピーと深いコピーの分かれ目」ということです。
ディープコピーでは、参照型も“中身ごとコピー”します。


オブジェクト 1 個のディープコピーをどう設計するか

IDeepCloneable のようなインターフェースを用意する

ディープコピーをきれいに扱うために、
「自分自身をディープコピーできる」ことを表すインターフェースを用意するのが定番です。

public interface IDeepCloneable<T>
{
    T DeepClone();
}
C#

User に実装してみます。

public class User : IDeepCloneable<User>
{
    public string Name { get; set; } = "";
    public Address Address { get; set; } = new Address();

    public User DeepClone()
    {
        return new User
        {
            Name = this.Name,
            Address = new Address
            {
                City = this.Address.City
            }
        };
    }
}
C#

Address も必要なら同じように DeepClone を持たせます。

ここでの重要ポイントは、「“ディープコピーの責任”をクラス自身に持たせると、外から安全にコピーできる」ということです。
外側のコードは「DeepClone を呼ぶだけ」で済みます。


コレクション+LINQ でディープコピーする

「1 要素の DeepClone × LINQ の Select」で一気にコピー

IDeepCloneable<T> を実装したオブジェクトのコレクションがあるとします。

var users = new List<User>
{
    new User { Name = "Alice", Address = new Address { City = "Tokyo" } },
    new User { Name = "Bob",   Address = new Address { City = "Osaka" } },
};
C#

これを「ディープコピーした新しいリスト」にしたいとき、LINQ を使うととても素直に書けます。

var clonedUsers = users
    .Select(u => u.DeepClone())
    .ToList();
C#

clonedUsers の中身は、users の各要素をディープコピーしたものです。
clonedUsers[0].Address.City を変えても、users[0].Address.City には影響しません。

ここでの重要ポイントは、「“1 要素の DeepClone”さえ定義しておけば、コレクションのディープコピーは LINQ の Select で一発」ということです。


ディープコピー用の拡張メソッドを用意する

DeepCloneAll のようなユーティリティにまとめる

毎回 Select(x => x.DeepClone()) と書くのが面倒なら、
拡張メソッドにしてしまうと読みやすくなります。

using System.Collections.Generic;
using System.Linq;

public static class DeepCloneExtensions
{
    public static List<T> DeepCloneAll<T>(this IEnumerable<IDeepCloneable<T>> source)
        where T : IDeepCloneable<T>
    {
        return source
            .Select(x => x.DeepClone())
            .ToList();
    }
}
C#

使い方はこうです。

List<User> clonedUsers = users.DeepCloneAll();
C#

ここでの重要ポイントは、「メソッド名に “Deep” を入れておくことで、“これは参照を共有しないコピーだ”と一目で分かる」ということです。
ToList との違いが明確になります。


実務での使いどころ 1:編集前のスナップショットを取る

画面編集用に「元データのコピー」を持ちたい

例えば、ユーザー情報を編集する画面で、

読み込んだ元データ
画面で編集中のデータ

を分けて持ちたいケースがあります。

User original = _userRepository.GetById(id);
User editing  = original.DeepClone();
C#

画面では editing をいじり、
「保存」ボタンで DB に反映し、
「キャンセル」なら捨てる、という流れです。

コレクションでも同じです。

List<User> originalList = _userRepository.GetAll().ToList();
List<User> editingList  = originalList.DeepCloneAll();
C#

ここでの重要ポイントは、「“キャンセルしても元に戻る”を保証したいときは、必ずディープコピーを取る」ということです。
浅いコピーだと、キャンセルしても元が汚れている、という悲劇が起きます。


実務での使いどころ 2:一時的な変形・ソート・フィルタ

元のコレクションを壊さずに、別バージョンを作りたい

例えば、「画面 A では ID 順、画面 B では名前順で表示したい」ようなケース。

var baseList = _userRepository.GetAll().ToList();

var byId   = baseList.DeepCloneAll().OrderBy(x => x.Id).ToList();
var byName = baseList.DeepCloneAll().OrderBy(x => x.Name).ToList();
C#

元の baseList はそのまま、
byIdbyName はそれぞれ独立したディープコピーです。

ここでの重要ポイントは、「“元のコレクションを基準に、複数のバリエーションを作る”ときは、ディープコピーしてから加工する」ということです。
そうしないと、「片方の画面での変更が、もう片方に影響する」事故が起きます。


ディープコピーの注意点と割り切り

なんでもかんでもディープコピーすると重い

ディープコピーは、「オブジェクトの数 × プロパティの数」だけコピー処理が走ります。
ネストが深いオブジェクトや、大量のデータに対して無闇にディープコピーすると、
メモリも CPU もそれなりに食います。

なので、

本当に元を守りたいところだけディープコピーする
読み取り専用でいいところは Immutable や ReadOnlyCollection で守る
そもそも設計として「書き換えない」方向に寄せる

といったバランス感覚が大事です。

ここでの重要ポイントは、「ディープコピーは“最後の保険”であって、乱発するものではない」ということです。
「ここだけは絶対に元を汚したくない」という場所に絞って使うと、効果的です。


まとめ:「ディープコピー・ユーティリティ」は“元データを守るための最後の盾”

ディープコピーの本質は、

参照を共有してしまうと危険な場面で、
オブジェクトやコレクションを“中身ごと別物”として複製し、
元データを絶対に汚さないようにする

ことです。

押さえておきたいポイントを整理すると、

浅いコピーは「参照をコピーするだけ」、深いコピーは「子オブジェクトまで別インスタンス」
1 要素の DeepClone をクラス自身に実装しておくと、LINQ の Select でコレクションを一気にディープコピーできる
DeepCloneAll のような拡張メソッドにすると、意図がコードに乗って読みやすい
編集前スナップショットや、複数バリエーションのリストを作るときに特に有効
コストもかかるので、「ここだけは守りたい」という場所に絞って使う

ここまで理解できていれば、
「なんとなく new して代入しているだけ」の段階から抜け出して、
“どこで参照を共有し、どこでディープコピーするか”を意識して設計できるようになります。

タイトルとURLをコピーしました