bucket-sort logo bucket-sort

プログラミングとインフラエンジニアリングの覚え書き

  • Posts
  • About
  • Contact
  1. Home
  2. All Posts
  3. [C#] ICloneable インターフェース — オブジェクトのコピーと浅いコピー・深いコピー

[C#] ICloneable インターフェース — オブジェクトのコピーと浅いコピー・深いコピー

May 8, 2026 C# , .NET bucket-sort

オブジェクトの「コピー」が必要になる場面は少なくありません。同じ状態から始まる複数のオブジェクトを作りたい、オブジェクトを変更前に退避しておきたい、といった用途です。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・独自インターフェース・コピーコンストラクタなどの代替手段も積極的に検討する
C# .NET ICloneable Clone 浅いコピー 深いコピー MemberwiseClone オブジェクト指向
← [C#] IEnumerable と IEnumerator — イテレータの仕組みと yield を使った実装 [C#] IComparable と IComparer — オブジェクトの順序比較と複数ソート戦略 →

Related Posts

  • [C#] IComparable と IComparer — オブジェクトの順序比較と複数ソート戦略 May 9, 2026
  • [C#] インターフェースの活用パターン — 参照取得・パラメータ・配列・既定実装・階層設計 May 6, 2026
  • [C#] インターフェース(Interface)を体系的に理解する May 5, 2026
  • [C#] Finalizable & Disposable パターン実践 — Dispose パターンの完全形 May 13, 2026

Table of Contents

  • ICloneable とは
  • 浅いコピー と 深いコピー
    • 浅いコピー(Shallow Copy)
    • 深いコピー(Deep Copy)
  • MemberwiseClone() — 浅いコピーの組み込み手段
    • 参照型フィールドを含む場合の浅いコピーの問題
  • 深いコピーの実装
    • 手動で各フィールドをコピーする
    • ネストしたオブジェクトの深いコピー
    • コピーコンストラクタパターン
  • 型安全なクローンメソッドを追加する
  • ICloneable の設計上の問題点
    • 問題 1:浅いコピーか深いコピーかが不明
    • 問題 2:戻り値が object で型安全でない
    • 問題 3:サブクラスでの正確なコピーが難しい
  • 現代的な代替手段
    • record の with 式(C# 9.0 以降)
    • 型パラメータ付きの独自インターフェース
    • System.Text.Json によるシリアライズ/デシリアライズ(簡易深いコピー)
  • 使いどころの整理
  • まとめ

Recent Posts

  • [C#] Finalizable & Disposable パターン実践 — Dispose パターンの完全形 May 13, 2026
  • [C#] Disposable Objects — IDisposable / Dispose() と using 構文 May 12, 2026
  • [C#] Finalizable Objects — Finalize() の役割と使いどころ May 11, 2026
  • [C#] System.GC クラスを整理する — ガベージコレクションを制御するための API May 10, 2026
  • [C#] IComparable と IComparer — オブジェクトの順序比較と複数ソート戦略 May 9, 2026

Categories

  • C#63
  • .NET62
  • AWS27
  • Laravel16
  • Linux15
  • MySQL9
  • Apache8
  • PHP8
  • DynamoDB6
  • セキュリティ6
  • Nginx5
  • WordPress4
  • インフラ4
  • Hugo3
  • .NET Framework1
  • Aurora1
  • Filament1
  • Git1
  • SQS1

Tags

  • C#
  • .NET
  • AWS
  • Laravel
  • PHP
  • セキュリティ
  • MySQL
  • Linux
  • Apache
  • Code Snippet
  • DynamoDB
  • NoSQL
  • PHP-FPM
  • RDS
  • パフォーマンス
  • DoS
  • Nginx
  • Windows
  • WordPress
  • メモリ管理
  • 監視
  • 設計
  • Amazon Linux 2023
  • Docker
  • IDisposable
  • Ipset
  • Iptables
  • OPCache
  • Webサーバー
  • オブジェクト指向
  • クラス設計
  • コレクション
  • デザインパターン
  • パターンマッチング
  • 継承
  • 認可
  • Aurora
  • Blade
  • Grafana
  • Hugo
  • InfluxDB
  • Policy
  • Record
  • SSG
  • インターフェース
  • エラーハンドリング
  • カプセル化
  • ガベージコレクション
  • モニタリング
  • 例外
Powered by Hugo & Explore Theme.