オブジェクトの「コピー」が必要になる場面は少なくありません。同じ状態から始まる複数のオブジェクトを作りたい、オブジェクトを変更前に退避しておきたい、といった用途です。C# では ICloneable インターフェースがこの操作の標準的な契約として用意されています。
この記事では ICloneable の役割と使い方を解説し、浅いコピー(shallow copy) と 深いコピー(deep copy) の違い、MemberwiseClone() の活用、そして ICloneable の設計上の問題点と現代的な代替手段までを整理します。
ICloneable とは
ICloneable は System 名前空間に定義されたインターフェースで、メンバは 1 つだけです。
public interface ICloneable
{
object Clone();
}
「このオブジェクトはコピーを作れる」という能力を型に付与する契約です。Clone() を実装することで、その型のコピーを生成するロジックをクラス自身に閉じ込めることができます。
.NET の標準ライブラリでも Array、ArrayList、Queue、Stack など多くのコレクションが ICloneable を実装しています。
int[] original = { 1, 2, 3 };
int[] copy = (int[])original.Clone();
copy[0] = 99;
Console.WriteLine(original[0]); // 1 — 元は変わらない
Console.WriteLine(copy[0]); // 99
浅いコピー と 深いコピー
ICloneable を理解するうえで、浅いコピーと深いコピーの違いを明確にしておく必要があります。
浅いコピー(Shallow Copy)
オブジェクトのフィールドを一対一でコピーします。値型フィールドはそのまま複製されますが、参照型フィールドは参照先のアドレスがコピーされます。つまり、コピー元とコピー先が同じオブジェクトを指し続けます。
original ─→ [ name: "Alice" | scores ─→ [10, 20, 30] ]
copy ─→ [ name: "Alice" | scores ─→ [10, 20, 30] ] ← 同じリストを指す!
深いコピー(Deep Copy)
オブジェクトが参照しているオブジェクトも再帰的にコピーします。コピー元と完全に独立したオブジェクトグラフが生成されます。
original ─→ [ name: "Alice" | scores ─→ [10, 20, 30] ]
copy ─→ [ name: "Alice" | scores ─→ [10, 20, 30] ] ← 別のリストを指す
ICloneable の Clone() が浅いコピーを返すのか深いコピーを返すのかはインターフェースの仕様として定まっていません。これが後述する設計上の問題につながります。
MemberwiseClone() — 浅いコピーの組み込み手段
Object クラスには protected なメソッド MemberwiseClone() が用意されており、現在のオブジェクトの浅いコピーを簡単に作れます。Clone() の実装でよく使われます。
public class Point : ICloneable
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y) => (X, Y) = (x, y);
public object Clone() => MemberwiseClone(); // 浅いコピー
}
Point p1 = new Point(3, 7);
Point p2 = (Point)p1.Clone();
p2.X = 100;
Console.WriteLine(p1.X); // 3 — 元は変わらない
Console.WriteLine(p2.X); // 100
X と Y はどちらも値型(int)なので、浅いコピーで十分です。コピー先を変更してもコピー元に影響しません。
参照型フィールドを含む場合の浅いコピーの問題
フィールドに参照型が含まれると、浅いコピーでは問題が起きます。
public class Student : ICloneable
{
public string Name { get; set; }
public List<int> Scores { get; set; }
public Student(string name, List<int> scores)
{
Name = name;
Scores = scores;
}
public object Clone() => MemberwiseClone(); // 浅いコピー
}
Student s1 = new Student("Alice", new List<int> { 80, 90, 100 });
Student s2 = (Student)s1.Clone();
s2.Name = "Bob"; // string は不変なので問題なし
s2.Scores.Add(70); // Scores は同じリストを指している!
Console.WriteLine(s1.Name); // "Alice"
Console.WriteLine(s1.Scores.Count); // 4 — s1 の Scores も変わってしまった!
string は不変(immutable)なので Name は問題ありませんが、List<int> はミュータブルな参照型であるため、コピー先で変更するとコピー元にも影響します。
深いコピーの実装
参照型フィールドを含む場合は、深いコピーが必要です。
手動で各フィールドをコピーする
最も明示的で確実な方法です。
public class Student : ICloneable
{
public string Name { get; set; }
public List<int> Scores { get; set; }
public Student(string name, List<int> scores)
{
Name = name;
Scores = scores;
}
public object Clone()
{
// Name は string(不変)なので参照コピーで問題なし
// Scores は新しいリストを作成して深くコピー
return new Student(Name, new List<int>(Scores));
}
}
Student s1 = new Student("Alice", new List<int> { 80, 90, 100 });
Student s2 = (Student)s1.Clone();
s2.Name = "Bob";
s2.Scores.Add(70);
Console.WriteLine(s1.Name); // "Alice"
Console.WriteLine(s1.Scores.Count); // 3 — 元は変わらない
Console.WriteLine(s2.Name); // "Bob"
Console.WriteLine(s2.Scores.Count); // 4
ネストしたオブジェクトの深いコピー
オブジェクトグラフが深い場合は、各クラスが ICloneable を実装し、再帰的にコピーします。
public class Address : ICloneable
{
public string City { get; set; }
public string Street { get; set; }
public Address(string city, string street)
{
City = city;
Street = street;
}
public object Clone() => new Address(City, Street);
}
public class Employee : ICloneable
{
public string Name { get; set; }
public Address HomeAddress { get; set; }
public List<string> Skills { get; set; }
public Employee(string name, Address address, List<string> skills)
{
Name = name;
HomeAddress = address;
Skills = skills;
}
public object Clone()
{
return new Employee(
Name,
(Address)HomeAddress.Clone(), // Address も深くコピー
new List<string>(Skills) // Skills も新しいリストで
);
}
}
var original = new Employee(
"田中",
new Address("東京", "渋谷 1-1"),
new List<string> { "C#", "Azure" }
);
var copy = (Employee)original.Clone();
copy.Name = "鈴木";
copy.HomeAddress.City = "大阪";
copy.Skills.Add("Docker");
Console.WriteLine(original.Name); // "田中"
Console.WriteLine(original.HomeAddress.City); // "東京" — 変わらない
Console.WriteLine(original.Skills.Count); // 2 — 変わらない
コピーコンストラクタパターン
ICloneable の代わりに、同じ型を受け取るコンストラクタ(コピーコンストラクタ)でコピーを実現するパターンも一般的です。
public class Configuration
{
public string Host { get; set; }
public int Port { get; set; }
public List<string> AllowedIPs { get; set; }
public Configuration(string host, int port, List<string> allowedIPs)
{
Host = host;
Port = port;
AllowedIPs = allowedIPs;
}
// コピーコンストラクタ
public Configuration(Configuration other)
{
Host = other.Host;
Port = other.Port;
AllowedIPs = new List<string>(other.AllowedIPs);
}
}
var prod = new Configuration("prod.example.com", 443, new List<string> { "10.0.0.1" });
var staging = new Configuration(prod); // コピーコンストラクタでコピー
staging.Host = "staging.example.com";
staging.AllowedIPs.Add("192.168.0.1");
Console.WriteLine(prod.Host); // "prod.example.com"
Console.WriteLine(prod.AllowedIPs.Count); // 1 — 変わらない
型安全なクローンメソッドを追加する
ICloneable.Clone() は object を返すため、呼び出し側で毎回キャストが必要になります。型安全なメソッドを別途追加するのが実用的です。
public class Point : ICloneable
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y) => (X, Y) = (x, y);
// ICloneable の実装(object を返す)
object ICloneable.Clone() => Clone();
// 型安全な公開メソッド
public Point Clone() => new Point(X, Y);
}
Point p1 = new Point(5, 10);
Point p2 = p1.Clone(); // キャスト不要
p2.X = 99;
Console.WriteLine(p1.X); // 5
Console.WriteLine(p2.X); // 99
ICloneable.Clone() を明示的実装にして隠し、型安全な Clone() をパブリックに公開するのがよく使われるパターンです。
ICloneable の設計上の問題点
ICloneable は .NET 1.0 から存在しますが、現在では 使用を避けるべき場面が多いとされています。Microsoft 自身のガイドラインでも、新しいライブラリへの実装は推奨されていません。
問題 1:浅いコピーか深いコピーかが不明
Clone() がどちらを返すかはドキュメントを見なければわかりません。インターフェース自体はどちらでも許容しているため、呼び出し側は安全に動作するかどうかを確認できません。
ICloneable obj = GetSomeCloneable();
var copy = obj.Clone(); // 浅いコピー? 深いコピー? 実装次第で異なる
問題 2:戻り値が object で型安全でない
ジェネリクス以前の設計のため、呼び出し元で常にキャストが必要です。
Student s2 = (Student)s1.Clone(); // キャストが必要
問題 3:サブクラスでの正確なコピーが難しい
継承がある場合、基底クラスの Clone() がサブクラスの追加フィールドを適切にコピーできるか保証するのが難しくなります。
現代的な代替手段
record の with 式(C# 9.0 以降)
record 型は with 式で一部のプロパティを変更しながら非破壊的コピーを作れます。
public record Point(int X, int Y);
Point p1 = new Point(3, 7);
Point p2 = p1 with { X = 100 }; // X だけ変えたコピー
Console.WriteLine(p1); // Point { X = 3, Y = 7 }
Console.WriteLine(p2); // Point { X = 100, Y = 7 }
record のプロパティが参照型を含む場合は浅いコピーになる点に注意が必要です。
型パラメータ付きの独自インターフェース
プロジェクト内で型安全なクローンが必要な場合は、独自インターフェースを定義する方法があります。
public interface IDeepCloneable<T>
{
T DeepClone();
}
public class Configuration : IDeepCloneable<Configuration>
{
public string Host { get; set; }
public List<string> Tags { get; set; }
public Configuration(string host, List<string> tags)
{
Host = host;
Tags = tags;
}
public Configuration DeepClone()
=> new Configuration(Host, new List<string>(Tags));
}
var c1 = new Configuration("prod", new List<string> { "web", "api" });
Configuration c2 = c1.DeepClone(); // キャスト不要、型安全
c2.Tags.Add("db");
Console.WriteLine(c1.Tags.Count); // 2 — 変わらない
System.Text.Json によるシリアライズ/デシリアライズ(簡易深いコピー)
複雑なオブジェクトグラフをまるごとコピーしたい場合、JSON シリアライズ経由でのコピーが手軽です。
using System.Text.Json;
T? DeepCopy<T>(T source) where T : notnull
{
string json = JsonSerializer.Serialize(source);
return JsonSerializer.Deserialize<T>(json);
}
var original = new Configuration("prod", new List<string> { "web" });
var copy = DeepCopy(original)!;
copy.Tags.Add("db");
Console.WriteLine(original.Tags.Count); // 1 — 変わらない
ただし、シリアライズできないフィールド(private、循環参照など)は欠落するため、シンプルな DTO 向けの手法です。
使いどころの整理
| 場面 | 推奨手段 |
|---|---|
| 値型のみのシンプルなオブジェクト | MemberwiseClone() + ICloneable |
| 一部プロパティを変えたコピーが主目的 | record + with 式 |
| 型安全で深いコピーが必要 | 独自の IDeepCloneable<T> |
| 複雑なグラフをまるごとコピー(DTO) | JSON シリアライズ/デシリアライズ |
| 既存の .NET API との互換性が必要 | ICloneable を実装(型安全メソッドも追加) |
まとめ
ICloneableは「このオブジェクトはコピーを作れる」という契約を表すMemberwiseClone()で浅いコピーを手軽に作れるが、参照型フィールドは共有される- 深いコピーには参照型フィールドを再帰的にコピーする処理が必要
Clone()の戻り値がobjectのため、型安全な追加メソッドを公開するのが実用的- 浅いコピーか深いコピーかが曖昧という設計上の問題から、新規コードでは
record with・独自インターフェース・コピーコンストラクタなどの代替手段も積極的に検討する