配列の複製を作るには?(シャローコピー編)[C#/VB].NET TIPS

ArrayクラスのCloneメソッドを使い、配列をコピー(シャローコピー)する方法を説明する。また、拡張メソッドを使いキャストを抑制する方法も取り上げる。

» 2017年05月17日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 配列の複製を作るのは、Arrayクラス(System名前空間)のCopyメソッドや配列のCopyToメソッドを使ってもよいが、配列のCloneメソッドを使えば簡単に書ける。

 ただし、上記のいずれにしてもシャローコピーであるので、注意してほしい(後述)。

 なお、配列は.NET Frameworkの最初からあるものだが、本稿はそれ以降の内容も含んでいる。サンプルコードをそのまま試すには、Visual Studio 2015(またはそれ以降)が必要である。

配列の複製を作るには?

 配列のCloneメソッドを使い、その結果をキャストすればよい。

 コード量は増えるが、ArrayクラスのCopyメソッドでも可能である。両方を記述したコンソールアプリの例を次のコードに示す。Cloneメソッドでは複製後にキャストが必要となる一方で、Copyメソッドではあらかじめ複製先の配列の用意が必要という違いがある。

using System;
using static System.Console;

class Program
{
  // 配列の内容をコンソールに出力するメソッド
  static void Display2dArray(string title, string[,] array)
  {
    for (int i = 0; i < array.GetLength(0); i++)
    {
      Write($"{title}[{i},*] = ");
      for (int j = 0; j < array.GetLength(1); j++)
        Write($"{array[i, j]}, ");
      WriteLine();
    }
  }

  static void Main(string[] args)
  {
    // 複製元の2次元配列
    string[,] src = {
      {"abc","def","ghi", },
      {"123","456","789", },
    };
    Display2dArray("src", src);
    // 出力:
    // src[0,*] = abc, def, ghi,
    // src[1,*] = 123, 456, 789,

    // ArrayクラスのCopyメソッドを使う
    string[,] clone1 = new string[src.GetLength(0), src.GetLength(1)];
    Array.Copy(src, clone1, src.Length);
    Display2dArray("clone1", clone1);
    // 出力:
    // clone1[0,*] = abc, def, ghi,
    // clone1[1,*] = 123, 456, 789,

    // 配列のCloneメソッドなら1行で書ける
    string[,] clone2 = src.Clone() as string[,];
    Display2dArray("clone2", clone2);
    // 出力:
    // clone2[0,*] = abc, def, ghi,
    // clone2[1,*] = 123, 456, 789,

#if DEBUG
    ReadKey();
#endif
  }
}

Imports System.Console

Module Module1
  ' 配列の内容をコンソールに出力するメソッド
  Sub Display2dArray(title As String, array(,) As String)
    For i As Integer = 0 To (array.GetLength(0) - 1)
      Write($"{title}({i},*) = ")
      For j As Integer = 0 To (array.GetLength(1) - 1)
        Write($"{array(i, j)}, ")
      Next
      WriteLine()
    Next
  End Sub

  Sub Main()
    ' 複製元の2次元配列
    Dim src(,) As String = {
      {"abc", "def", "ghi"},
      {"123", "456", "789"}
    }
    Display2dArray("src", src)
    ' 出力:
    ' src(0,*) = abc, def, ghi,
    ' src(1,*) = 123, 456, 789,

    ' ArrayクラスのCopyメソッドを使う
    Dim clone1(src.GetLength(0) - 1, src.GetLength(1) - 1) As String
    Array.Copy(src, clone1, src.Length)
    Display2dArray("clone1", clone1)
    ' 出力:
    ' clone1(0,*) = abc, def, ghi,
    ' clone1(1,*) = 123, 456, 789,

    ' 配列のCloneメソッドなら1行で書ける
    Dim clone2(,) As String = CType(src.Clone(), String(,))
    Display2dArray("clone2", clone2)
    ' 出力:
    ' clone2(0,*) = abc, def, ghi,
    ' clone2(1,*) = 123, 456, 789,

#If DEBUG Then
    ReadKey()
#End If
  End Sub
End Module

2通りの方法で配列を複製するコンソールアプリの例(上:C#、下:VB)
多次元配列でも複製できると示すために、2次元配列を用いた。1次元配列でも同様である。また、配列のCopyToメソッドを使っても複製できるが、さらに煩雑になるだけなので割愛した。

拡張メソッドでキャストを不要に

 Cloneメソッドで複製するときにはキャストしなければならない。複製するコードをたくさん書くときは、次のコードのような拡張メソッドを作っておくとキャストをいちいち書かずに済むので便利である。

static class CloneableExtension
{
  public static T CloneEx<T>(this T src) where T: class, ICloneable
    => src.Clone() as T;
}

……省略……

// 拡張メソッドを作れば、クローンするところではキャスト不要
string[,] clone3 = src.CloneEx();
Display2dArray("clone3", clone3);
// 出力:
// clone3[0,*] = abc, def, ghi,
// clone3[1,*] = 123, 456, 789,

Public Module CloneableExtension
  <Runtime.CompilerServices.Extension()>
  Public Function CloneEx(Of T As {Class, ICloneable})(src As T) As T
    Return src.Clone()
  End Function
End Module

……省略……

' 拡張メソッドを作れば、クローンするところではキャスト不要
Dim clone3(,) As String = src.CloneEx()
Display2dArray("clone3", clone3)
' 出力:
' clone3(0,*) = abc, def, ghi,
' clone3(1,*) = 123, 456, 789,

キャスト不要なCloneメソッドを拡張メソッドとして実装する例(上:C#、下:VB)
拡張メソッドの名前を「CloneEx」としている。「Clone」にすると、既存のCloneメソッドと名前が重複するため、呼び出し時に型引数の指定が必須になってしまうからだ。型引数を指定するのでは、キャストするのと打鍵数が大して変わらない。

なお、この拡張メソッドは、配列だけでなく、ICloneableインタフェースを実装しているクラスならどれでも使える。


シャローコピーであることを確かめる

 シャローコピーとは、参照だけを複製するという意味だ。参照先のオブジェクトも複製する場合は、ディープコピーという。

 ArrayクラスのCopyメソッドや配列のCloneメソッドは、シャローコピーを行う。配列の内容にオブジェクトへの参照が入っている場合は、オブジェクトそのものの複製は作られないのである。すなわち、複製を作った後で元の配列の参照先のオブジェクトに変更を加えると、複製側も変わるのだ。

 Cloneメソッドがそのようなシャローコピーを行うことは、次のコードで確かめられる。複製後に複製元の参照するオブジェクトに変更を加えると、複製した配列の方も変わっている(コード末尾に確認として載せたが、複製元のオブジェクトを入れ替えた場合には複製先に影響しない)。

// 配列に格納するクラスの例
public class SampleData
{
  public int Index { get; set; }
  public string Name { get; set; }
  public override string ToString() => $"{Index:00}:{Name}";
}

……省略……

// 複製元の配列
SampleData[] src = {
  new SampleData {Index=1, Name="aaa" },
  new SampleData {Index=2, Name="bbb" },
  new SampleData {Index=3, Name="ccc" },
};

// 複製を作る
var clone = src.Clone() as SampleData[];
// ソースコード冒頭にusing System.Linq;が必要
WriteLine($"{string.Join(", ", clone.Select(s => s.ToString()))}");
// 出力:01:aaa, 02:bbb, 03:ccc

// srcの2番目のNameプロパティを変更する
src[1].Name = "ZZZ";

// すると、cloneの方も変わっている
WriteLine($"{string.Join(", ", clone.Select(s => s.ToString()))}");
// 出力:01:aaa, 02:ZZZ, 03:ccc

// 確認:srcの3番目に別のオブジェクトを代入=参照そのものを入れ替える
src[2] = new SampleData { Index = 4, Name = "DDD" };
// この場合は、cloneの参照先は変わらない
WriteLine($"{string.Join(", ", clone.Select(s => s.ToString()))}");
// 出力:01:aaa, 02:ZZZ, 03:ccc

' 配列に格納するクラスの例
Public Class SampleData
  Public Property Index As Integer
  Public Property Name As String
  Public Overrides Function ToString() As String
    Return $"{Index:00}:{Name}"
  End Function
End Class

……省略……

' 複製元の配列
Dim src() As SampleData = {
  New SampleData With {.Index = 1, .Name = "aaa"},
  New SampleData With {.Index = 2, .Name = "bbb"},
  New SampleData With {.Index = 3, .Name = "ccc"}
}

' 複製を作る
Dim clone = CType(src.Clone(), SampleData())
WriteLine($"{String.Join(", ", clone.Select(Function(s) s.ToString()))}")
' 出力:01:aaa, 02:bbb, 03:ccc

' srcの2番目のNameプロパティを変更する
src(1).Name = "ZZZ"

' すると、cloneの方も変わっている
WriteLine($"{String.Join(", ", clone.Select(Function(s) s.ToString()))}")
' 出力:01:aaa, 02:ZZZ, 03:ccc

' 確認:srcの3番目に別のオブジェクトを代入=参照そのものを入れ替える
src(2) = New SampleData With {.Index = 4, .Name = "DDD"}
' この場合は、cloneの参照先は変わらない
WriteLine($"{String.Join(", ", clone.Select(Function(s) s.ToString()))}")
' 出力:01:aaa, 02:ZZZ, 03:ccc

Cloneメソッドがシャローコピーを行うことを確かめるコードの例(上:C#、下:VB)

まとめ

 配列の複製を作るには、配列のCloneメソッドを使うと1行で済む。Cloneメソッドによる複製はキャストが必要だが、拡張メソッドを作ればいちいちキャストを書かずに済む。

 Cloneメソッドによる複製はシャローコピーである。複製した後で元の配列のオブジェクトに加えた変更は、複製にも反映されるので気を付けよう。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。