C# Tips | コレクション・LINQ:シャローコピー

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

はじめに:「シャローコピー」は“速いけど、油断すると元を汚すコピー”

ディープコピーの逆側にあるのが シャローコピー(浅いコピー) です。
一言でいうと、

「入れ物は別だけど、中身(参照先)はそのまま共有するコピー」

です。

だからこそ軽くて速い一方で、
「コピーしたつもりが、元のオブジェクトまで一緒に変わってしまう」
という事故が起きやすいコピーでもあります。

ここでは、まず「シャローコピーとは何か」をしっかり腹落ちさせてから、
コレクションや 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#

この User をコピーしたいとします。
シャローコピーは、ざっくり言うとこういう状態です。

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

// シャローコピーのイメージ
var u2 = new User
{
    Name = u1.Name,          // 値型や string はそのままコピーで問題になりにくい
    Address = u1.Address     // ここがポイント:同じ Address インスタンスを指す
};
C#

u2.Address は、u1.Address と同じインスタンスです。
つまり、

u2.Address.City = "Osaka";
Console.WriteLine(u1.Address.City); // "Osaka" と表示される
C#

となります。

ここでの重要ポイントは、「シャローコピーは“参照型プロパティを新しく作らない”」ということです。
“どこまで別物にしたいか”を意識していないと、簡単に元を汚します。


C# の代表的なシャローコピー手段

代入(=)は“コピー”ではなく“同じものを見るだけ”

まず一番やりがちなのが、単なる代入です。

var u1 = new User { Name = "Alice" };
var u2 = u1; // これはコピーではなく、同じインスタンスを指す別名
C#

u2.Name = "Bob"; とすると、u1.Name"Bob" になります。
これは「シャローコピー」というより、「参照のコピー(別名)」です。

「コピーしたつもりで同じものを触っている」典型パターンなので、
“代入はコピーではない”という感覚をしっかり持っておくことが大事です。

MemberwiseClone は“典型的なシャローコピー”

object.MemberwiseClone() は、
「フィールドをそのままコピーする」シャローコピーです。

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

    public User ShallowCopy()
    {
        return (User)this.MemberwiseClone();
    }
}
C#

この ShallowCopy は、

User 自体は別インスタンス
Name(string)は値としてコピー
Address は同じインスタンスを共有

という状態になります。

ここでの重要ポイントは、「MemberwiseClone は“参照型の中身まではコピーしてくれない”」ということです。
“速いけど危ないコピー”だと理解しておきましょう。


コレクションとシャローコピー

List の ToList は「要素の参照をコピーするだけ」

LINQ でよく使う ToList() も、実はシャローコピーです。

var users = new List<User>
{
    new User { Name = "Alice" },
    new User { Name = "Bob" }
};

var copied = users.ToList();
C#

userscopied は別の List<User> ですが、
中に入っている User インスタンスは同じものです。

copied[0].Name = "Changed";

Console.WriteLine(users[0].Name);  // "Changed" と表示される
C#

「リストの入れ物だけ増えたが、中身は共有している」という状態です。
これがまさに「コレクションに対するシャローコピー」です。

ここでの重要ポイントは、「ToList() は“要素のディープコピー”は一切していない」ということです。
“リストをコピーしたから安全”と思っていると、普通に元が汚れます。

Select(x => x) もシャローコピー

LINQ でこう書くことがあります。

var copied = users
    .Select(x => x)  // そのまま返しているだけ
    .ToList();
C#

これは ToList() と同じく、「入れ物だけ別、中身は同じ」なシャローコピーです。
Select(x => x.DeepClone()) のようにしない限り、ディープコピーにはなりません。


シャローコピーを“あえて使う”場面

読み取り専用で使うときは、シャローコピーでも問題にならない

例えば、「DB から取ってきたユーザー一覧を、画面 A と画面 B で別々にソートして表示したい」だけなら、
中身の User を書き換えない前提で、シャローコピーでも十分です。

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

var byId = baseList
    .OrderBy(x => x.Id)
    .ToList(); // シャローコピー

var byName = baseList
    .OrderBy(x => x.Name)
    .ToList(); // シャローコピー
C#

ここで byIdbyName の中身の User を変更しないなら、
「同じインスタンスを共有している」ことは問題になりません。

ここでの重要ポイントは、「“読み取り専用で使う”と割り切れるなら、シャローコピーは軽くて速い」ということです。
ディープコピーよりもメモリも CPU も安く済みます。

一時的な並べ替え・フィルタだけならシャローコピーで十分

例えば、「一時的にフィルタした結果を画面に出すだけ」のケース。

var filtered = users
    .Where(x => x.IsActive)
    .ToList(); // シャローコピー
C#

この filtered の中身を変更しないなら、
元の usersUser インスタンスを共有していても問題ありません。

「元のコレクションを壊さない」ことと
「要素の中身を壊さない」ことは別物です。

ここでの重要ポイントは、「“要素の中身を変更しない”前提なら、シャローコピーは十分安全」ということです。


シャローコピーを“使ってはいけない”典型パターン

編集用コピーをシャローコピーで作ってしまう

一番危険なのは、「編集用にコピーしたつもりでシャローコピーしてしまう」パターンです。

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

// 編集用にコピーしたつもり
var editing = original.ToList(); // シャローコピー
C#

ここで editing[0].Name = "変更"; とすると、
original[0].Name も一緒に変わります。

「キャンセルしたのに元が変わっている」
「別画面で見ているデータも変わっている」

といったバグの原因になります。

ここでの重要ポイントは、「“編集用”“一時的に書き換える”という用途では、シャローコピーは絶対に使ってはいけない」ということです。
その場合はディープコピーが必要です。


シャローコピーとディープコピーをどう使い分けるか

判断の軸は「要素の中身を変えるかどうか」

シンプルに言うと、次のように考えると整理しやすいです。

要素の中身を一切変えない
→ シャローコピー(ToList など)で十分

要素の中身を変える可能性がある
→ ディープコピーが必要(DeepClone など)

LINQ の ToList()Select(x => x) は、
「シャローコピーである」と意識して使うことが大事です。

ここでの重要ポイントは、「“コピーの目的”をはっきりさせてから、シャローかディープかを選ぶ」ということです。
なんとなく ToList() していると、いつか痛い目を見ます。


まとめ:「シャローコピー・ユーティリティ」は“軽いけど刃物みたいな道具”

シャローコピーの本質は、

入れ物だけ別にして、
中身(参照先)はそのまま共有することで、
軽く・速くコピーを作る

ことです。

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

シャローコピーは「参照型プロパティを新しく作らないコピー」
ToList()Select(x => x) は、要素のシャローコピーでしかない
読み取り専用で使うなら、シャローコピーは軽くて十分実用的
編集用・一時変更用には絶対に使ってはいけない(ディープコピーが必要)
「要素の中身を変えるかどうか」が、シャローかディープかを選ぶ判断軸になる

ここまで腹落ちしていれば、
「なんとなくコピーしている」段階から抜け出して、
“このコピーはシャローか?ディープか?それで本当にいいのか?”と自分に問いながら、
意図を持ってコレクションと LINQ を使い分けられるようになります。

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