C#プログラミングにおいて、配列は最も基本的かつ頻繁に使用されるデータ構造の一つです。

しかし、配列の「コピー」という操作一つをとっても、単なる代入から、「浅いコピー(Shallow Copy)」と「深いコピー(Deep Copy)」の違い、さらにはメモリ効率を意識した最新のSpan<T>の活用まで、多岐にわたる知識が求められます。

特に参照型の要素を持つ配列を扱う際、適切なコピー手法を選択しなければ、予期せぬデータの書き換えによるバグを引き起こす原因となります。

本記事では、C#で配列をコピーするさまざまな手法を網羅し、その仕組みと使い分けの基準をプロの視点で詳しく解説します。

配列コピーの基本概念:参照と実体の違い

C#において、配列は参照型(Reference Type)として扱われます。

そのため、単に変数間で代入を行うだけでは、配列のデータそのものが複製されるわけではありません。

まずは、初心者が陥りやすい「代入」と「コピー」の違いについて整理しておきましょう。

単純な代入による参照の共有

配列変数に対して、別の配列変数を代入した場合、コピーされるのは「メモリ上のアドレス(参照情報)」のみです。

C#
using System;

class Program
{
    static void Main()
    {
        int[] source = { 1, 2, 3 };
        // 参照の代入(コピーではない)
        int[] destination = source;

        // destinationの要素を変更するとsourceも変わる
        destination[0] = 99;

        Console.WriteLine($"source[0]: {source[0]}");
        Console.WriteLine($"destination[0]: {destination[0]}");
    }
}
実行結果
source[0]: 99
destination[0]: 99

このように、代入では同じ実体を二つの変数が指し示している状態になるため、一方を変更すると他方にも影響が及びます。

新しい配列として独立させたい場合は、明示的なコピー操作が必要です。

C#で配列をコピーする主要な手法

C#には、配列をコピーするための標準的なメソッドが複数用意されています。

それぞれの特徴とパフォーマンス特性を理解し、用途に応じて選択することが重要です。

Array.Cloneによる複製

Array.Cloneメソッドは、配列の浅いコピーを生成する最もシンプルな方法です。

戻り値がobject型であるため、元の型へのキャストが必要になります。

C#
using System;

class Program
{
    static void Main()
    {
        int[] source = { 10, 20, 30 };
        // Shallow Copyを生成し、キャストする
        int[] destination = (int[])source.Clone();

        destination[0] = 100;

        Console.WriteLine($"Source: {string.Join(", ", source)}");
        Console.WriteLine($"Destination: {string.Join(", ", destination)}");
    }
}
実行結果
Source: 10, 20, 30
Destination: 100, 20, 30

メリットは記述が簡潔であることですが、デメリットとしてボックス化のコストやキャストの手間が発生します。

Array.Copyによる柔軟なコピー

Array.Copyは、静的メソッドとして提供されており、コピー元の開始インデックスやコピーする要素数を細かく指定できます。

C#
using System;

class Program
{
    static void Main()
    {
        int[] source = { 1, 2, 3, 4, 5 };
        int[] destination = new int[3];

        // sourceのインデックス1から3つの要素をdestinationの0番目以降にコピー
        Array.Copy(source, 1, destination, 0, 3);

        Console.WriteLine($"Destination: {string.Join(", ", destination)}");
    }
}
実行結果
Destination: 2, 3, 4

このメソッドは内部的に最適化されており、大量のデータを高速にコピーする場合に非常に有効です。

CopyToメソッド

CopyToはインスタンスメソッドであり、既存の配列に対してデータを流し込む場合に使用します。

C#
int[] source = { 1, 2, 3 };
int[] destination = new int[5];
source.CopyTo(destination, 2); // destinationのインデックス2からコピー開始

コピー先の配列があらかじめ確保されている場合に便利ですが、コピー先のサイズが不足していると例外が発生するため注意が必要です。

LINQのToArrayによるコピー

LINQ(Language Integrated Query)のToArray()を使用すると、フィルタリングや加工を行いながら新しい配列を作成できます。

C#
using System;
using System.Linq;

int[] source = { 1, 2, 3, 4, 5 };
// 偶数だけを抽出して新しい配列を作成
int[] destination = source.Where(n => n % 2 == 0).ToArray();

非常に宣言的で読みやすいコードになりますが、内部で列挙子(Enumerator)を介するため、他の手法に比べてオーバーヘッドが大きいという側面があります。

浅いコピー(Shallow Copy)と深いコピー(Deep Copy)の違い

配列のコピーにおいて最も重要な概念が、参照型の要素を扱う際の挙動です。

浅いコピーの限界

前述のArray.CopyCloneなどはすべて「浅いコピー」です。

要素が値型(int, double, structなど)の場合は問題ありませんが、要素がクラスなどの参照型である場合、「配列内の各要素が指している参照先」までもが同じになってしまいます。

特徴浅いコピー (Shallow Copy)深いコピー (Deep Copy)
コピー対象配列の構造と値(参照型ならアドレス)配列の構造と、全要素のインスタンス
メモリ消費少ない多い
実行速度高速低速(再帰的な処理が必要)
独立性不完全(内部オブジェクトは共有)完全(完全に別物)

深いコピーが必要なケース

深いコピーとは、配列そのものだけでなく、配列に含まれる各オブジェクトのインスタンスもすべて新しく生成してコピーすることを指します。

手動ループによる深いコピーの例

C#
using System;

class Person
{
    public string Name { get; set; }
}

class Program
{
    static void Main()
    {
        Person[] source = { new Person { Name = "Alice" } };
        
        // 深いコピーの実施
        Person[] destination = new Person[source.Length];
        for (int i = 0; i < source.Length; i++)
        {
            destination[i] = new Person { Name = source[i].Name };
        }

        destination[0].Name = "Bob";
        
        Console.WriteLine($"Source[0]: {source[0].Name}"); // Aliceのまま
        Console.WriteLine($"Destination[0]: {destination[0].Name}"); // Bobに変更
    }
}

シリアライズを利用した深いコピー

オブジェクトが複雑な階層を持っている場合、手動でのコピーは困難です。

その場合、System.Text.Jsonなどを使用して一度シリアライズし、再度デシリアライズすることで深いコピーを実現する手法が一般的です。

C#
using System;
using System.Text.Json;

public static T DeepCopy<T>(T self)
{
    var json = JsonSerializer.Serialize(self);
    return JsonSerializer.Deserialize<T>(json);
}

ただし、シリアライズによるコピーは実行速度が遅いため、パフォーマンスが重視されるループ内などでの使用は避けるべきです。

モダンC#における配列コピー:Span<T>の活用

C# 7.2以降、メモリ効率を劇的に向上させるSpan<T>およびReadOnlySpan<T>が導入されました。

これらを使用すると、配列の一部をコピーせずに参照したり(スライス)、高速にメモリブロックを転送したりすることが可能です。

Spanによる高速コピー

Span<T>.CopyToは、低レベルなメモリコピー(memmoveに相当する最適化)を行うため、非常に高速です。

C#
using System;

class Program
{
    static void Main()
    {
        int[] sourceArray = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        int[] destArray = new int[5];

        // 配列からSpanを作成し、スライス(範囲指定)してコピー
        Span<int> sourceSpan = sourceArray.AsSpan();
        sourceSpan.Slice(2, 5).CopyTo(destArray);

        Console.WriteLine(string.Join(", ", destArray));
    }
}
実行結果
3, 4, 5, 6, 7

Spanを活用することで、不必要な配列の生成(アロケーション)を抑え、ガベージコレクション(GC)の負荷を軽減できます。

現代のC#開発において、高頻度で呼ばれる処理には必須のテクニックと言えます。

多次元配列とジャグ配列のコピー

C#には、多次元配列(int[,])とジャグ配列(int[][]、配列の配列)の二種類があります。

これらのコピーには注意が必要です。

多次元配列のコピー

多次元配列に対してArray.Copyを使用すると、線形的なメモリ配置として扱われ、全ての要素が一括でコピーされます。

ジャグ配列のコピー

ジャグ配列は「配列の配列」であるため、CloneArray.Copyを行っても、内側の配列の参照がコピーされるだけです。

内側の配列まで含めて複製したい場合は、必ずループを回して各階層ごとにコピーを行う必要があります。

C#
int[][] source = { new int[] { 1, 2 }, new int[] { 3, 4 } };
int[][] destination = new int[source.Length][];

for (int i = 0; i < source.Length; i++)
{
    destination[i] = (int[])source[i].Clone();
}

パフォーマンスと使い分けの指針

どのコピー手法を選択すべきかは、以下の優先順位で判断するのがベストプラクティスです。

  1. 高速かつ柔軟なコピーが必要な場合Array.Copy または Span<T>.CopyTo を使用してください。
  2. 単純な全要素コピー(値型)の場合Clone() が最も記述が楽ですが、パフォーマンスを極めるなら Span<T> です。
  3. 参照型を完全に独立させたい場合ループによる手動コピー、またはシリアライズによる深いコピーが必要です。
  4. 読みやすさとフィルタリングを優先する場合LINQ のToArray() を使用しますが、大規模データには向きません。

以下の表に、主要な手法の比較をまとめました。

メソッド種類キャスト柔軟性(範囲指定)速度
Array.Clone浅いコピー必要不可普通
Array.Copy浅いコピー不要可能高速
CopyTo浅いコピー不要開始位置のみ高速
LINQ ToArray浅いコピー不要条件指定可能低速
Span.CopyTo浅いコピー不要自由自在最高速

まとめ

C#で配列をコピーする際は、まず「値のコピー」なのか「参照のコピー」なのかを意識することが第一歩です。

単純な値型の配列であればArray.CopyやモダンなSpan<T>を用いることで、安全かつ高速にデータを複製できます。

一方で、オブジェクトを要素に持つ配列の場合は、浅いコピーでは不十分なケースが多いことを理解しておかなければなりません。

要件に応じて、手動のディープコピーやシリアライザを適切に組み合わせて実装しましょう。

最新のC#(.NET 8/9世代以降)では、メモリパフォーマンスの観点からSpan<T>の利用が推奨される場面が増えています。

レガシーな手法だけでなく、これらの新しいAPIを使いこなすことで、より堅牢で効率的なアプリケーション開発が可能になります。

この記事で紹介した手法を、ぜひ日々のコーディングに役立ててください。