C#のコピー完全ガイド|代入・Clone・シャローコピー・ディープコピーの違いと安全な実装方法

はじめに

C#で開発していると、「このオブジェクトをコピーしたはずなのに、元のデータまで変わってしまった」「Listをコピーしたのに、中身のクラスが共有されていた」「Cloneを呼べば安全だと思っていたのに、実はシャローコピーだった」といった問題に遭遇することがあります。

C#のコピーは、単に「変数を別の変数に代入する」だけでは正しく理解できません。値型と参照型、代入、シャローコピー、ディープコピー、Clone、ICloneable、recordのwith式など、状況によって挙動が大きく変わります。

この記事では、「c# コピー」で迷いやすいポイントを、実装例を交えながら体系的に解説します。最終的に、どの場面で代入を使い、どの場面でシャローコピーやディープコピーを使うべきかを判断できるようになることを目指します。

1. C#の「コピー」で最初に理解すべき基本

C#のコピーを理解するうえで最初に押さえるべきことは、「変数の中に何が入っているのか」です。

値型の変数には値そのものが入り、参照型の変数にはオブジェクトそのものではなく、オブジェクトを指す参照が入ります。そのため、同じ「代入」という書き方でも、値型と参照型ではコピー後の動作が変わります。

C#
int a = 10;
int b = a;

b = 20;

Console.WriteLine(a); // 10
Console.WriteLine(b); // 20
C#
var user1 = new User { Name = "Alice" };
var user2 = user1;

user2.Name = "Bob";

Console.WriteLine(user1.Name); // Bob
Console.WriteLine(user2.Name); // Bob

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

上の例では、intは値そのものがコピーされるため、bを変更してもaは変わりません。一方、Userは参照型なので、user2 = user1では同じインスタンスを指す参照がコピーされます。その結果、user2.Nameを変更すると、user1.Nameも変わったように見えます。

1-1. C#でコピーが必要になる代表的な場面

C#でコピーが必要になる場面は多くあります。代表的なのは、元データを保持したまま編集用データを作るケースです。たとえば、画面上でユーザー情報を編集し、保存ボタンが押されるまでは元のデータを変更したくない場合、編集用のコピーを作る必要があります。

また、計算処理やシミュレーションで元データを壊したくない場合にもコピーが使われます。ゲーム開発では、キャラクターの状態、アイテム情報、マップデータなどをコピーして一時的に変更することがあります。業務システムでは、注文情報、顧客情報、設定情報などを安全に複製する場面があります。

そのほか、テストデータの作成、キャッシュの分離、履歴管理、Undo/Redo機能、マルチスレッド処理でのデータ分離などでもコピーは重要です。

1-2. 代入・複製・Clone・コピーの違い

「コピー」という言葉は広く使われますが、C#ではいくつかの意味に分けて考える必要があります。

代入は、変数に値や参照を入れる操作です。値型なら値そのものがコピーされ、参照型なら参照がコピーされます。

複製は、新しいインスタンスを作って、元のオブジェクトと同じようなデータを持たせる操作を指すことが多いです。

Cloneは、一般的には「複製するメソッド名」として使われます。ただし、Cloneがシャローコピーなのかディープコピーなのかは、実装によって異なります。ICloneable.Cloneについても、公式ドキュメントではシャローコピーにもディープコピーにもなり得るため、呼び出し側が予測しにくい点が注意点として説明されています。

コピーは、これらをまとめた広い表現です。C#で安全にコピーを扱うには、「このコピーは参照を共有するのか」「完全に独立したオブジェクトを作るのか」を常に意識する必要があります。

1-3. 値型と参照型でコピー結果が変わる理由

C#には大きく分けて値型と参照型があります。

値型には、intdoubleboolDateTimedecimalenumstructなどがあります。これらは代入時に値がコピーされます。

参照型には、classstring、配列、List<T>Dictionary<TKey, TValue>などがあります。これらは代入時にオブジェクト本体ではなく、オブジェクトへの参照がコピーされます。

C#
var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;

list2.Add(4);

Console.WriteLine(string.Join(", ", list1)); // 1, 2, 3, 4

list2 = list1によってListの中身が複製されたわけではありません。list1list2が同じListインスタンスを参照しているため、片方から追加した要素がもう片方にも見えます。

1-4. コピーを誤ると起きるバグの具体例

コピーの誤解によって起きる典型的なバグは、意図しないデータ変更です。

C#
var original = new Order
{
Id = 1,
Items = new List<string> { "Book", "Pen" }
};

var edited = original;
edited.Items.Add("Notebook");

Console.WriteLine(string.Join(", ", original.Items));
// Book, Pen, Notebook

class Order
{
public int Id { get; set; }
public List<string> Items { get; set; } = new();
}

編集用データを作ったつもりでも、実際には同じオブジェクトを参照しています。そのため、editedを変更するとoriginalも変更されます。

このようなバグは、画面編集、APIレスポンス加工、DB保存前の検証、設定ファイルの読み込み後の変更などで起きやすいです。特に、プロパティにListやDictionary、独自クラスが含まれている場合は注意が必要です。

2. 代入によるコピーの仕組み

C#のコピーを考えるうえで、最も基本になるのが代入です。

C#
var b = a;

この1行は非常に単純に見えますが、aの型によって意味が変わります。値型なら値がコピーされ、参照型なら参照がコピーされます。

2-1. 値型の代入は値そのものがコピーされる

値型では、代入すると値そのものがコピーされます。

C#
struct Point
{
public int X;
public int Y;
}

var p1 = new Point { X = 10, Y = 20 };
var p2 = p1;

p2.X = 99;

Console.WriteLine(p1.X); // 10
Console.WriteLine(p2.X); // 99

p2 = p1によって、p1の値がp2へコピーされます。その後、p2.Xを変更してもp1.Xは変わりません。

ただし、構造体の中に参照型フィールドがある場合は注意が必要です。

C#
struct Container
{
public List<int> Numbers;
}

var c1 = new Container { Numbers = new List<int> { 1, 2 } };
var c2 = c1;

c2.Numbers.Add(3);

Console.WriteLine(string.Join(", ", c1.Numbers)); // 1, 2, 3

構造体自体はコピーされていますが、Numbersに入っているListの参照はコピーされるため、List本体は共有されます。

2-2. 参照型の代入は参照先が共有される

クラスなどの参照型では、代入してもオブジェクト本体はコピーされません。

C#
var customer1 = new Customer { Name = "Tanaka" };
var customer2 = customer1;

customer2.Name = "Sato";

Console.WriteLine(customer1.Name); // Sato

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

この場合、customer1customer2は同じインスタンスを指しています。変数は2つありますが、オブジェクトは1つです。

参照型の代入は、コピーというより「同じものを別名でも参照する」と考えると理解しやすくなります。

2-3. stringは参照型でも安全に扱いやすい理由

stringは参照型ですが、他のクラスとは少し違った感覚で扱えます。理由は、stringが不変、つまり作成後に内容を変更できない型だからです。Microsoftのドキュメントでも、Stringオブジェクトは作成後に変更できず、変更しているように見える操作は新しい文字列を返すと説明されています。

C#
string s1 = "Hello";
string s2 = s1;

s2 = "World";

Console.WriteLine(s1); // Hello
Console.WriteLine(s2); // World

s2 = "World"は、元の文字列オブジェクトを書き換える操作ではありません。s2が別の文字列を参照するようになるだけです。そのため、stringは参照型でありながら、値のように安全に扱える場面が多くあります。

ただし、StringBuilderのような可変オブジェクトは別です。

C#
var sb1 = new System.Text.StringBuilder("Hello");
var sb2 = sb1;

sb2.Append(" World");

Console.WriteLine(sb1.ToString()); // Hello World

StringBuilderは内容を変更できるため、参照共有の影響を受けます。

2-4. 代入だけで十分なケース・不十分なケース

代入だけで十分なのは、コピー後に片方を変更しても問題がない場合です。たとえば、値型、string、不変オブジェクト、読み取り専用として扱う参照型などです。

一方で、コピー後に片方だけを変更したい場合、代入だけでは不十分です。

C#
var settings1 = new AppSettings { Theme = "Light" };
var settings2 = settings1;

settings2.Theme = "Dark";

このコードでは、settings1Darkになります。元データを守りたい場合は、新しいインスタンスを作るコピー処理が必要です。

3. シャローコピーとは

シャローコピーとは、オブジェクトの表面だけをコピーする方法です。

具体的には、コピー元とは別の新しいインスタンスを作りますが、その中にある参照型のプロパティやフィールドは、参照先のオブジェクトまでは複製せず、同じ参照を共有します。

3-1. シャローコピーの意味と動作イメージ

次のようなクラスを考えます。

C#
class Person
{
public string Name { get; set; } = "";
public Address Address { get; set; } = new();
}

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

Personをシャローコピーすると、Personインスタンス自体は新しく作られます。しかし、Addressプロパティが指すAddressインスタンスは共有されます。

C#
var p1 = new Person
{
Name = "Alice",
Address = new Address { City = "Tokyo" }
};

var p2 = p1.ShallowCopy();

p2.Name = "Bob";
p2.Address.City = "Osaka";

Console.WriteLine(p1.Name); // Alice
Console.WriteLine(p1.Address.City); // Osaka

Namestringなので実用上問題になりにくいですが、Addressはクラスなので共有されます。そのため、p2.Address.Cityを変更すると、p1.Address.Cityにも影響します。

3-2. MemberwiseCloneでシャローコピーを実装する方法

C#では、すべてのオブジェクトがMemberwiseCloneメソッドを持っています。ただし、このメソッドはprotectedなので、クラスの外部から直接呼び出すことはできません。

MemberwiseCloneは、新しいオブジェクトを作成し、現在のオブジェクトの非静的フィールドをコピーするシャローコピーを行います。値型フィールドは値がコピーされ、参照型フィールドは参照がコピーされるため、参照先オブジェクトは共有されます。

C#
class Person
{
public string Name { get; set; } = "";
public Address Address { get; set; } = new();

public Person ShallowCopy()
{
return (Person)MemberwiseClone();
}
}

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

使用例は次のとおりです。

C#
var original = new Person
{
Name = "Alice",
Address = new Address { City = "Tokyo" }
};

var copy = original.ShallowCopy();

copy.Name = "Bob";
copy.Address.City = "Osaka";

Console.WriteLine(original.Name); // Alice
Console.WriteLine(original.Address.City); // Osaka

Person自体は別インスタンスですが、Addressは共有されています。ここがシャローコピーの最大の注意点です。

3-3. 配列・List・Dictionaryのシャローコピー例

配列のコピーには、CloneArray.CopyToArrayなどがあります。

C#
var array1 = new[] { 1, 2, 3 };
var array2 = (int[])array1.Clone();

array2[0] = 99;

Console.WriteLine(array1[0]); // 1
Console.WriteLine(array2[0]); // 99

要素が値型の場合は、コピー後に片方の要素を変更してももう片方には影響しません。

しかし、要素が参照型の場合は注意が必要です。

C#
var users1 = new[]
{
new User { Name = "Alice" },
new User { Name = "Bob" }
};

var users2 = (User[])users1.Clone();

users2[0].Name = "Charlie";

Console.WriteLine(users1[0].Name); // Charlie

配列自体は別インスタンスですが、中のUserオブジェクトは共有されています。

Listも同様です。

C#
var list1 = new List<User>
{
new User { Name = "Alice" }
};

var list2 = new List<User>(list1);

list2[0].Name = "Bob";

Console.WriteLine(list1[0].Name); // Bob

new List<User>(list1)はListの入れ物をコピーしますが、要素であるUserまでは複製しません。

Dictionaryも同じ考え方です。

C#
var dict1 = new Dictionary<int, User>
{
[1] = new User { Name = "Alice" }
};

var dict2 = new Dictionary<int, User>(dict1);

dict2[1].Name = "Bob";

Console.WriteLine(dict1[1].Name); // Bob

Dictionaryのキーと値の組み合わせはコピーされますが、値が参照型の場合、その参照先は共有されます。

3-4. シャローコピーで参照プロパティが共有される注意点

シャローコピーの問題は、コードを見ただけでは共有されていることに気づきにくい点です。

C#
var copy = original.ShallowCopy();

この1行を見ると、完全に独立したコピーが作られたように見えます。しかし、実際には内側の参照型プロパティが共有されている可能性があります。

特に注意すべきプロパティは次のようなものです。

C#
public List<OrderItem> Items { get; set; } = new();
public Dictionary<string, string> Metadata { get; set; } = new();
public Address Address { get; set; } = new();
public Customer Customer { get; set; } = new();

これらを含むクラスをシャローコピーすると、コピー先で中身を変更したときにコピー元へ影響する可能性があります。

3-5. シャローコピーを使ってよいケース

シャローコピーを使ってよいのは、参照先を共有しても問題ないケースです。

たとえば、内部の参照型オブジェクトが不変である場合、読み取り専用として扱う場合、大きなデータをあえて共有してメモリ使用量を抑えたい場合などです。

また、トップレベルのプロパティだけを変更したい場合にもシャローコピーは有効です。

C#
var copy = original.ShallowCopy();
copy.Name = "New Name";

このように、ネストしたオブジェクトを変更しない前提なら、シャローコピーで十分な場合があります。

4. ディープコピーとは

ディープコピーとは、オブジェクトだけでなく、その内部で参照しているオブジェクトも含めて複製するコピー方法です。

シャローコピーでは共有される参照型プロパティも、ディープコピーでは新しいインスタンスとして作り直します。

4-1. ディープコピーの意味とシャローコピーとの違い

シャローコピーとディープコピーの違いは、ネストした参照型オブジェクトまでコピーするかどうかです。

C#
var original = new Person
{
Name = "Alice",
Address = new Address { City = "Tokyo" }
};

var copy = original.DeepCopy();

copy.Address.City = "Osaka";

Console.WriteLine(original.Address.City); // Tokyo
Console.WriteLine(copy.Address.City); // Osaka

ディープコピーでは、PersonだけでなくAddressも別インスタンスとして作られます。そのため、コピー先のAddress.Cityを変更しても、コピー元には影響しません。

4-2. ネストしたオブジェクトを完全に複製する考え方

ディープコピーでは、クラスが持つすべての参照型プロパティについて、「共有してよいか」「新しく作るべきか」を判断します。

C#
class Order
{
public int Id { get; set; }
public Customer Customer { get; set; } = new();
public List<OrderItem> Items { get; set; } = new();
}

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

class OrderItem
{
public string ProductName { get; set; } = "";
public int Quantity { get; set; }
}

このOrderを完全に独立させたいなら、CustomerItemsも、さらにItemsの中のOrderItemもコピーする必要があります。

4-3. 手動でディープコピーを実装する方法

最も安全で分かりやすいのは、手動でディープコピーを実装する方法です。

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

public Address DeepCopy()
{
return new Address
{
City = City
};
}
}

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

public Person DeepCopy()
{
return new Person
{
Name = Name,
Address = Address.DeepCopy()
};
}
}

この方法は、どのプロパティをどうコピーしているかが明確です。プロパティが増えたときにコピー処理を更新する必要はありますが、業務ロジックに合わせた安全なコピーを実装しやすいというメリットがあります。

nullを考慮する場合は、次のようにします。

C#
class Person
{
public string Name { get; set; } = "";
public Address? Address { get; set; }

public Person DeepCopy()
{
return new Person
{
Name = Name,
Address = Address?.DeepCopy()
};
}
}

Addressがnullの場合はnullのまま、値がある場合だけコピーします。

4-4. コレクションを含むクラスのディープコピー方法

Listを含むクラスでは、List自体を新しく作り、さらに要素もコピーします。

C#
class OrderItem
{
public string ProductName { get; set; } = "";
public int Quantity { get; set; }

public OrderItem DeepCopy()
{
return new OrderItem
{
ProductName = ProductName,
Quantity = Quantity
};
}
}

class Order
{
public int Id { get; set; }
public List<OrderItem> Items { get; set; } = new();

public Order DeepCopy()
{
return new Order
{
Id = Id,
Items = Items.Select(item => item.DeepCopy()).ToList()
};
}
}

Dictionaryの場合も、値が参照型であれば値をコピーします。

C#
class UserSetting
{
public string Value { get; set; } = "";

public UserSetting DeepCopy()
{
return new UserSetting
{
Value = Value
};
}
}

class SettingStore
{
public Dictionary<string, UserSetting> Settings { get; set; } = new();

public SettingStore DeepCopy()
{
return new SettingStore
{
Settings = Settings.ToDictionary(
pair => pair.Key,
pair => pair.Value.DeepCopy()
)
};
}
}

キーがstringintのように安全に扱える型なら、そのままコピーして問題ありません。キーも独自クラスの場合は、キーもコピーすべきか検討が必要です。

4-5. 循環参照・null・共有参照に注意する

ディープコピーで難しいのは、循環参照と共有参照です。

循環参照とは、オブジェクト同士が互いに参照し合っている状態です。

C#
class Node
{
public string Name { get; set; } = "";
public Node? Parent { get; set; }
public List<Node> Children { get; set; } = new();
}

親が子を参照し、子が親を参照するような構造では、単純に再帰でディープコピーすると無限ループになる可能性があります。

また、共有参照にも注意が必要です。

C#
var address = new Address { City = "Tokyo" };

var user1 = new User { Address = address };
var user2 = new User { Address = address };

このように、複数のオブジェクトが同じAddressを共有している場合、ディープコピー後も共有関係を維持すべきか、それぞれ別インスタンスにすべきかは要件によって変わります。

単純なクラスなら手動コピーで十分ですが、複雑なオブジェクトグラフでは、コピー済みオブジェクトを辞書で管理するなどの設計が必要になります。

5. CloneメソッドとICloneableの使い方

C#では、オブジェクトを複製するメソッド名としてCloneがよく使われます。また、標準インターフェースとしてICloneableも用意されています。

ただし、CloneICloneableを使えば常に安全というわけではありません。むしろ、設計を誤るとコピーの意味が曖昧になりやすいので注意が必要です。

5-1. Cloneメソッドの基本的な実装例

独自クラスにCloneメソッドを定義する例です。

C#
class Product
{
public string Name { get; set; } = "";
public decimal Price { get; set; }

public Product Clone()
{
return new Product
{
Name = Name,
Price = Price
};
}
}

このクラスは値に近いプロパティだけを持つため、上記の実装で問題になりにくいです。

参照型プロパティを含む場合は、Cloneの中でどこまでコピーするかを明確にする必要があります。

C#
class Product
{
public string Name { get; set; } = "";
public Category Category { get; set; } = new();

public Product Clone()
{
return new Product
{
Name = Name,
Category = Category.Clone()
};
}
}

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

public Category Clone()
{
return new Category
{
Name = Name
};
}
}

この例では、Categoryもコピーしているため、ディープコピーに近い動作になります。

5-2. ICloneableを使うメリットと注意点

ICloneableは、オブジェクトのコピーを作成するためのCloneメソッドを提供するインターフェースです。公式ドキュメントでは、ICloneableObject.MemberwiseCloneを超える複製サポートを提供するためのものと説明されています。

C#
class Product : ICloneable
{
public string Name { get; set; } = "";
public decimal Price { get; set; }

public object Clone()
{
return new Product
{
Name = Name,
Price = Price
};
}
}

ただし、ICloneable.Cloneの戻り値はobjectです。そのため、呼び出し側でキャストが必要になります。

C#
var product = new Product { Name = "Book", Price = 1000 };
var copy = (Product)product.Clone();

この点だけでも、現代的なC#では型安全な独自メソッドのほうが扱いやすい場面が多くあります。

5-3. Cloneがシャローコピーかディープコピーか曖昧になる問題

ICloneableの最大の問題は、Cloneがシャローコピーなのかディープコピーなのか、インターフェースからは分からないことです。

C#
var copy = (Order)order.Clone();

このコードを見ただけでは、Order.Itemsが共有されるのか、要素まで完全にコピーされるのか判断できません。

Microsoftのドキュメントでも、Cloneの実装はシャローコピーにもディープコピーにもなり得るため、呼び出し側が予測可能な複製処理に依存できないと説明され、公開APIではICloneableを実装しないことが推奨されています。

5-4. 安全なClone設計のポイント

Cloneを安全に設計するには、メソッド名やドキュメントでコピー範囲を明確にすることが重要です。

たとえば、次のように名前を分けると意図が伝わりやすくなります。

C#
public Product ShallowCopy()
{
return (Product)MemberwiseClone();
}

public Product DeepCopy()
{
return new Product
{
Name = Name,
Category = Category.DeepCopy()
};
}

また、戻り値はobjectではなく具体的な型にするほうが使いやすくなります。

C#
public Product Clone()
{
return new Product
{
Name = Name,
Price = Price
};
}

公開APIでは、Cloneという曖昧な名前よりも、CopyDeepCopyCreateSnapshotToEditableCopyのように目的が分かる名前を使うと安全です。

5-5. ICloneableを避けたほうがよいケース

ライブラリや公開APIでは、ICloneableは避けたほうがよいケースが多いです。理由は、シャローコピーかディープコピーかが呼び出し側に伝わりにくいからです。

また、ジェネリックではないため型安全性が低く、戻り値のキャストが必要になります。

社内の小さなコードや、コピー仕様が明確に共有されている範囲では使える場合もあります。しかし、保守性を重視するなら、独自の型付きコピーコンストラクタやDeepCopyメソッドを用意するほうが分かりやすいです。

6. C#で安全にコピーを実装する代表パターン

C#でコピーを実装する方法は複数あります。代表的なのは、コピーコンストラクタ、ファクトリメソッド、recordのwith式、JSONシリアライズ、ライブラリ利用です。

どれが最適かは、クラスの複雑さ、パフォーマンス要件、保守性、コピーの厳密さによって変わります。

6-1. コピーコンストラクタを使う方法

コピーコンストラクタとは、同じ型のインスタンスを受け取り、新しいインスタンスを作るコンストラクタです。

C#
class User
{
public string Name { get; set; }
public Address Address { get; set; }

public User(string name, Address address)
{
Name = name;
Address = address;
}

public User(User source)
{
Name = source.Name;
Address = new Address(source.Address);
}
}

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

public Address(string city)
{
City = city;
}

public Address(Address source)
{
City = source.City;
}
}

使用例は次のとおりです。

C#
var user1 = new User("Alice", new Address("Tokyo"));
var user2 = new User(user1);

user2.Address.City = "Osaka";

Console.WriteLine(user1.Address.City); // Tokyo

コピーコンストラクタは、どの値をコピーするかを明示しやすく、保守性に優れています。

6-2. ファクトリメソッドでコピーを作る方法

ファクトリメソッドを使うと、コピーの目的をメソッド名に込められます。

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

public static User CreateCopy(User source)
{
return new User
{
Name = source.Name,
Address = new Address
{
City = source.Address.City
}
};
}

public static User CreateEditableCopy(User source)
{
return CreateCopy(source);
}
}

CreateEditableCopyのように名前を付けると、「編集用に独立したコピーを作る」という意図が明確になります。

6-3. record型のwith式を使う方法

C#のrecord型では、with式を使って一部の値だけを変更したコピーを作れます。Microsoftのドキュメントでは、with式により、既存のレコードインスタンスのコピーである新しいレコードインスタンスを作成し、指定したプロパティやフィールドを変更できると説明されています。

C#
public record User(string Name, int Age);

var user1 = new User("Alice", 30);
var user2 = user1 with { Age = 31 };

Console.WriteLine(user1); // User { Name = Alice, Age = 30 }
Console.WriteLine(user2); // User { Name = Alice, Age = 31 }

recordは不変データを扱うときに便利です。ただし、参照型プロパティを持つ場合、with式は内部の参照オブジェクトまで自動でディープコピーするわけではありません。

C#
public record User(string Name, List<string> Tags);

var user1 = new User("Alice", new List<string> { "admin" });
var user2 = user1 with { Name = "Bob" };

user2.Tags.Add("editor");

Console.WriteLine(string.Join(", ", user1.Tags)); // admin, editor

Userレコード自体はコピーされていますが、TagsのListは共有されています。recordとwith式を使う場合も、可変な参照型プロパティには注意が必要です。

6-4. JSONシリアライズでディープコピーする方法

JSONシリアライズを使って、オブジェクトをJSON文字列に変換し、再度オブジェクトに戻すことでディープコピーのように扱う方法もあります。System.Text.Json.JsonSerializerは、オブジェクトや値をJSONへシリアライズし、JSONから指定型へデシリアライズする機能を提供します。

C#
using System.Text.Json;

static T DeepCopyByJson<T>(T source)
{
var json = JsonSerializer.Serialize(source);
return JsonSerializer.Deserialize<T>(json)!;
}

使用例です。

C#
var order1 = new Order
{
Id = 1,
Items = new List<OrderItem>
{
new OrderItem { ProductName = "Book", Quantity = 2 }
}
};

var order2 = DeepCopyByJson(order1);
order2.Items[0].Quantity = 5;

Console.WriteLine(order1.Items[0].Quantity); // 2

JSONシリアライズは手軽ですが、万能ではありません。既定では扱いに制限があるメンバー、循環参照、ポリモーフィズム、privateフィールド、コンストラクタ、パフォーマンスなどに注意が必要です。

6-5. AutoMapperなどライブラリを使う方法

AutoMapperなどのマッピングライブラリを使って、オブジェクトから別のオブジェクトへ値を移す方法もあります。

C#
// イメージ例
var copy = mapper.Map<User>(source);

ライブラリを使うメリットは、プロパティ数が多いクラスやDTO変換でコード量を減らせることです。一方で、設定が暗黙的になりやすく、どのプロパティがどうコピーされるかを追いにくくなることがあります。

複雑なドメインオブジェクトの完全なディープコピーには、ライブラリ任せにせず、コピー仕様を明確にすることが重要です。

6-6. 各実装方法のメリット・デメリット比較

方法メリットデメリット向いている場面
代入最も簡単参照型は共有される値型、不変オブジェクト
MemberwiseClone簡単にシャローコピーできる参照型プロパティが共有されるトップレベルだけコピーしたい場合
手動DeepCopy意図が明確で安全実装量が増える業務上重要なオブジェクト
コピーコンストラクタ保守性が高いプロパティ追加時に更新が必要クラス設計を明確にしたい場合
record with簡潔にコピーできる参照型プロパティは共有される不変データ、DTO
JSONシリアライズ手軽に深いコピーを作れる場合がある制約とパフォーマンスに注意簡易コピー、テスト用途
ライブラリ大量のマッピングに強い設定が見えにくいDTO変換、層間変換

7. 型別に見るC#のコピー方法

C#のコピー方法は、対象の型によって選び方が変わります。ここでは、クラス、構造体、配列、List、Dictionary、recordに分けて確認します。

7-1. クラスのコピー方法

クラスは参照型なので、代入だけでは同じインスタンスを共有します。

C#
var a = new User { Name = "Alice" };
var b = a;

b.Name = "Bob";

Console.WriteLine(a.Name); // Bob

独立したコピーを作りたい場合は、新しいインスタンスを作ります。

C#
var b = new User
{
Name = a.Name
};

参照型プロパティがある場合は、そのプロパティもコピーします。

C#
var b = new User
{
Name = a.Name,
Address = new Address
{
City = a.Address.City
}
};

クラスのコピーでは、「トップレベルだけでよいのか」「ネストしたオブジェクトまで必要なのか」を必ず確認します。

7-2. 構造体のコピー方法

構造体は値型なので、代入で値がコピーされます。

C#
var p1 = new Point { X = 1, Y = 2 };
var p2 = p1;

p2.X = 10;

Console.WriteLine(p1.X); // 1

ただし、構造体内に参照型フィールドがある場合、その参照先は共有されます。

C#
struct Data
{
public List<int> Values;
}

このような構造体は、値型であってもコピー時に注意が必要です。構造体はできるだけ小さく、不変に近い設計にすると扱いやすくなります。

7-3. 配列のコピー方法

配列をコピーする代表的な方法は次のとおりです。

C#
var array2 = (int[])array1.Clone();
var array3 = array1.ToArray();
Array.Copy(array1, array4, array1.Length);

値型配列であれば、要素の値がコピーされるため扱いやすいです。

C#
var nums1 = new[] { 1, 2, 3 };
var nums2 = nums1.ToArray();

nums2[0] = 99;

Console.WriteLine(nums1[0]); // 1

参照型配列では、配列の入れ物だけが別になり、要素の参照先は共有されます。

C#
var users2 = users1.ToArray();
users2[0].Name = "Bob";

この場合、users1[0].Nameも変わります。要素までコピーしたい場合は、次のようにします。

C#
var users2 = users1
.Select(user => user.DeepCopy())
.ToArray();

7-4. Listのコピー方法

Listの入れ物だけをコピーするなら、次のように書けます。

C#
var list2 = new List<int>(list1);

またはLINQで次のように書くこともできます。

C#
var list2 = list1.ToList();

値型やstringのListであれば、多くの場合これで十分です。

C#
var names1 = new List<string> { "Alice", "Bob" };
var names2 = names1.ToList();

names2[0] = "Charlie";

Console.WriteLine(names1[0]); // Alice

一方、要素がクラスの場合は注意が必要です。

C#
var users2 = users1.ToList();
users2[0].Name = "Charlie";

この場合、List自体は別ですが、要素のUserは共有されています。要素までコピーするには次のようにします。

C#
var users2 = users1
.Select(user => user.DeepCopy())
.ToList();

7-5. Dictionaryのコピー方法

Dictionaryの入れ物だけをコピーするなら、コンストラクタを使えます。

C#
var dict2 = new Dictionary<string, int>(dict1);

値が値型なら、この方法で十分な場合が多いです。

C#
var scores1 = new Dictionary<string, int>
{
["Alice"] = 90
};

var scores2 = new Dictionary<string, int>(scores1);
scores2["Alice"] = 100;

Console.WriteLine(scores1["Alice"]); // 90

値がクラスの場合は、値オブジェクトが共有されます。

C#
var dict2 = new Dictionary<string, User>(dict1);
dict2["admin"].Name = "Bob";

要素までコピーしたい場合は、次のようにします。

C#
var dict2 = dict1.ToDictionary(
pair => pair.Key,
pair => pair.Value.DeepCopy()
);

キーも参照型で、かつ可変である場合は、キーのコピーや等値比較の設計にも注意が必要です。

7-6. record・record structのコピー方法

record型では、with式を使うと簡潔にコピーできます。

C#
public record Product(string Name, decimal Price);

var p1 = new Product("Book", 1000);
var p2 = p1 with { Price = 1200 };

recordは、値オブジェクトやDTOに向いています。ただし、参照型プロパティを持つrecordでは、with式で参照先までディープコピーされるわけではありません。

C#
public record Cart(List<string> Items);

var cart1 = new Cart(new List<string> { "Book" });
var cart2 = cart1 with { };

cart2.Items.Add("Pen");

Console.WriteLine(string.Join(", ", cart1.Items)); // Book, Pen

record structは値型なので、代入で値がコピーされます。

C#
public readonly record struct Point(int X, int Y);

var p1 = new Point(1, 2);
var p2 = p1 with { X = 10 };

Console.WriteLine(p1.X); // 1
Console.WriteLine(p2.X); // 10

不変データとして扱うなら、recordやrecord structはコピーを安全に設計しやすい選択肢です。

8. 実装で失敗しやすいポイント

C#のコピーで失敗しやすい原因は、ほとんどが「どこまでコピーされているか」を誤解することです。

8-1. 参照共有による意図しないデータ変更

最も多い失敗は、参照型プロパティが共有されることです。

C#
var copy = new Order
{
Id = original.Id,
Items = original.Items
};

このコードでは、ItemsのListが共有されます。コピー先で要素を追加すると、コピー元にも影響します。

正しくは、少なくともList自体を新しく作る必要があります。

C#
var copy = new Order
{
Id = original.Id,
Items = original.Items.ToList()
};

さらに、Listの要素が参照型なら、要素もコピーします。

C#
var copy = new Order
{
Id = original.Id,
Items = original.Items.Select(item => item.DeepCopy()).ToList()
};

8-2. プロパティの一部だけコピー漏れする問題

手動コピーでは、プロパティを追加したときにコピー処理を更新し忘れることがあります。

C#
class User
{
public string Name { get; set; } = "";
public int Age { get; set; }
public string Email { get; set; } = "";

public User DeepCopy()
{
return new User
{
Name = Name,
Age = Age
// Emailのコピー漏れ
};
}
}

この問題を防ぐには、コピー処理のテストを書くことが有効です。また、コピーコンストラクタやファクトリメソッドをクラス内に集約し、コピー処理を複数箇所に分散させないことも重要です。

8-3. privateフィールドや読み取り専用プロパティの扱い

コピー対象がpublicプロパティだけとは限りません。privateフィールド、読み取り専用プロパティ、計算プロパティなどをどう扱うかも設計が必要です。

C#
class User
{
private readonly Guid _id;
public string Name { get; }

public User(string name)
{
_id = Guid.NewGuid();
Name = name;
}
}

このようなクラスをコピーするとき、_idも同じにするのか、新しいIDを発行するのかは要件次第です。

「コピー」といっても、技術的な複製と業務的な複製は違います。たとえば注文データをコピーして新規注文を作る場合、商品明細はコピーしても注文IDは新しくするべきかもしれません。

8-4. 継承クラスでコピー実装が複雑になる問題

継承があるクラスでは、コピー実装が複雑になります。

C#
class Animal
{
public string Name { get; set; } = "";
}

class Dog : Animal
{
public string Breed { get; set; } = "";
}

基底クラス側でCloneを定義した場合、派生クラスのプロパティまで正しくコピーする必要があります。

C#
Animal animal = new Dog { Name = "Pochi", Breed = "Shiba" };
var copy = animal.Clone();

このとき、戻り値がAnimalなのかDogなのか、Breedがコピーされるのかを明確にしなければなりません。

継承階層が深い場合は、コピー処理よりも、継承設計そのものを見直したほうがよいこともあります。コピーしやすい設計にするなら、record、コンポジション、不変オブジェクトを検討する価値があります。

8-5. パフォーマンスとメモリ使用量への影響

ディープコピーは安全ですが、コストが高くなる場合があります。大きなオブジェクトグラフを毎回コピーすると、メモリ使用量が増え、GCの負荷も高くなります。

特に、画像データ、大量のコレクション、ツリー構造、グラフ構造、キャッシュデータなどをコピーする場合は注意が必要です。

すべてをディープコピーするのではなく、共有してよい不変データは共有する、変更が必要な部分だけコピーする、差分だけ保持する、といった設計も検討できます。

9. コピー方法の選び方

C#でコピー方法を選ぶときは、「コピー後に何を変更するのか」「元データへの影響を許容できるのか」を基準に考えると判断しやすくなります。

9-1. 単純な値だけなら代入を使う

intdecimalboolDateTimeenumなどの単純な値型であれば、代入で十分です。

C#
decimal price1 = 1000m;
decimal price2 = price1;

price2 = 1200m;

Console.WriteLine(price1); // 1000

stringも不変なので、多くの場合は代入で問題ありません。

C#
string name1 = "Alice";
string name2 = name1;

name2 = "Bob";

Console.WriteLine(name1); // Alice

9-2. 参照先を共有してよいならシャローコピーを使う

トップレベルのオブジェクトだけを分けたい場合は、シャローコピーが使えます。

C#
var copy = original.ShallowCopy();

たとえば、参照型プロパティを読み取り専用として扱う場合や、内部のデータを共有する設計で問題ない場合は、シャローコピーで十分です。

ただし、ListやDictionaryなどの可変コレクションを共有してよいかは必ず確認します。

9-3. 完全に独立したオブジェクトが必要ならディープコピーを使う

コピー元とコピー先を完全に分離したい場合は、ディープコピーを使います。

C#
var copy = original.DeepCopy();

編集画面、試算処理、Undo/Redo、テストデータ生成などでは、ディープコピーが必要になることが多いです。

ただし、すべての参照を無条件に複製するのではなく、業務上共有すべきものと分離すべきものを区別することが重要です。

9-4. 保守性を重視するならコピーコンストラクタを使う

コピー仕様をクラス内に明確に閉じ込めたい場合は、コピーコンストラクタが有効です。

C#
public User(User source)
{
Name = source.Name;
Address = new Address(source.Address);
}

コピーコンストラクタは、コピー対象が一目で分かり、テストもしやすいです。プロパティが増えたときに更新が必要ですが、その分、意図しないコピーを防ぎやすくなります。

9-5. 不変オブジェクトならrecordやwith式を検討する

値オブジェクトやDTOのように、不変データとして扱いたい場合はrecordが便利です。

C#
public record User(string Name, int Age);

var user2 = user1 with { Age = 31 };

recordのwith式は簡潔で、変更前のインスタンスを残したまま新しいインスタンスを作れます。

ただし、recordに可変なListやDictionaryを持たせると、参照共有の問題が残ります。不変性を重視するなら、IReadOnlyList<T>や不変コレクションの利用も検討するとよいです。

10. C#のコピーに関するよくある質問

10-1. C#の代入はコピーですか?

値型の場合、代入は値のコピーです。

C#
int a = 1;
int b = a;

この場合、abは独立しています。

一方、参照型の場合、代入は参照のコピーです。

C#
var user2 = user1;

この場合、user1user2は同じオブジェクトを指します。オブジェクト本体が複製されるわけではありません。

10-2. Cloneとディープコピーは同じですか?

同じとは限りません。Cloneは単なるメソッド名であり、その中身がシャローコピーなのかディープコピーなのかは実装次第です。

ICloneable.Cloneも、シャローコピーとディープコピーのどちらを行うかがインターフェースから分かりません。そのため、公開APIではICloneableを避け、DeepCopyShallowCopyのように意味が明確なメソッド名を使うほうが安全です。

10-3. MemberwiseCloneは安全に使えますか?

MemberwiseCloneはシャローコピーとして使えます。ただし、参照型フィールドや参照型プロパティの参照先は共有されます。Microsoftのドキュメントでも、参照型フィールドでは参照がコピーされ、参照先オブジェクトはコピーされないと説明されています。

そのため、参照プロパティを変更しない前提なら使いやすいですが、完全に独立したコピーが必要な場合は追加でディープコピー処理が必要です。

10-4. Listをコピーしても中身のオブジェクトは共有されますか?

はい。new List<T>(source)source.ToList()はListの入れ物をコピーしますが、要素が参照型の場合、要素オブジェクトは共有されます。

C#
var list2 = list1.ToList();
list2[0].Name = "Bob";

この場合、list1[0].Nameも変わります。要素まで独立させたい場合は、次のようにします。

C#
var list2 = list1
.Select(x => x.DeepCopy())
.ToList();

10-5. ディープコピーはどの方法が一番おすすめですか?

業務ロジックで重要なオブジェクトなら、手動のDeepCopyメソッドやコピーコンストラクタがおすすめです。理由は、どのプロパティをどうコピーするかが明確で、仕様変更にも対応しやすいからです。

一方、テスト用途や単純なDTOでは、JSONシリアライズを使ったコピーが便利な場合もあります。ただし、循環参照、privateメンバー、パフォーマンス、シリアライズ対象外のプロパティには注意が必要です。

最も大切なのは、「どの方法が万能か」ではなく、「そのオブジェクトにとって正しいコピーの意味は何か」を明確にすることです。

まとめ

C#のコピーは、値型と参照型の違いを理解することから始まります。

値型の代入では値そのものがコピーされます。一方、クラス、配列、List、Dictionaryなどの参照型では、代入してもオブジェクト本体はコピーされず、参照が共有されます。

シャローコピーは、トップレベルのオブジェクトだけを複製し、内部の参照型オブジェクトは共有します。MemberwiseCloneはシャローコピーを行う代表的な方法ですが、参照プロパティが共有される点に注意が必要です。

ディープコピーは、内部の参照型オブジェクトまで含めて複製する方法です。完全に独立したオブジェクトが必要な場合に有効ですが、循環参照、共有参照、null、パフォーマンスに注意する必要があります。

CloneやICloneableは便利に見えますが、シャローコピーかディープコピーかが曖昧になりやすいため、公開APIでは慎重に扱うべきです。保守性を重視するなら、コピーコンストラクタ、DeepCopyメソッド、目的が分かるファクトリメソッドを使うと安全です。

recordのwith式は、不変データを扱う場面で非常に便利です。ただし、Listなどの可変参照型プロパティを持つ場合は、with式でも参照が共有される点を忘れてはいけません。

C#で安全にコピーを実装するコツは、「代入でよいのか」「シャローコピーでよいのか」「ディープコピーが必要なのか」を毎回明確にすることです。コピーの範囲を意識して実装すれば、意図しないデータ変更を防ぎ、保守しやすいコードを書くことができます。