配列のコピーって使っていますか?
C#の配列のコピーには値のコピーと参照のコピーがあります。
また、値をコピーする方法についてもいくつか方法があります。
この記事では、配列のコピーについて
・値のコピーと参照のコピー
・Array.Copyで値をコピーする方法
という基本的な内容から、
・Buffer.BlockCopyを使う方法
・SkipとTakeを使う方法
・高速でコピーするための速度比較
・参照のみをコピーする方法
など応用的な内容についても解説していきます。
今回は配列のコピーについて、使い方をわかりやすく解説します!
値のコピーと参照のコピー
int型のようにクラス型などと違いメソッドを持たず、値として使用する型のことをプリミティブ型と呼びます。
これに対して配列はメソッドを持ち、クラス型と呼ばれています。
クラス型のオブジェクトを「=」記号を使ってコピーをしても、要素の値はコピーされずに、参照のみコピーされます。
これをシャローコピー(sharrow copy)といいます。
これに対して、要素の値も参照型の構造も含めてコピーすることをディープコピー(deep copy)と呼びます。
C#ではディープコピーを行うために、メソッドが用意されています。
そのメソッドの使い方について解説していきます!
Array.Copyで値をコピーする方法
配列をコピーする場合にはArray.Copyメソッドを使用します。
以下のように記述します。
public static void Copy( Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length )
引数sourceArrayは、コピー元のデータを格納しているArray型のオブジェクトを指定します。
引数sourceIndexは、コピーの操作の開始位置となるインデックス番号を指定します。
引数destinationArrayは、データを受け取るArray型のオブジェクトを指定します。
引数destinationIndexは、データの格納を開始するインデックス番号を指定します。
引数lengthは、コピーする要素の数を指定します。
それでは、詳しい使い方についてみていきましょう。
1次元配列をコピーする方法
1次元配列をコピーする方法について、サンプルコードで確認します。
using System; namespace ArrayCopy { class ArrayCopy { static void Main() { char[] src = {'a', 'b', 'c'}; char[] dst = new char[src.Length]; Array.Copy(src, dst, src.Length); Console.WriteLine("[{0}]", string.Join(", ", dst)); Console.ReadKey(); } } }
実行結果:
[a, b, c]
このサンプルコードでは、Lengthメソッドを使って配列の要素数を取得し、Array.Copyメソッドの引数に指定してコピーしています。
2次元配列をコピーする方法
2次元配列をコピーする方法について、サンプルコードで確認します。
using System; namespace ArrayCopy { class ArrayCopy { static void Main() { char[,] src = {{'a', 'b', 'c'}, {'d', 'e', 'f'}}; char[,] dst = new char[src.GetLength(0), src.GetLength(1)]; Array.Copy(src, dst, src.Length); Console.Write("["); for(int i = 0; i < 2; i++) { Console.Write("["); for(int j = 0; j < 3; j++) { Console.Write(dst[i,j]); Console.Write(", "); } Console.Write("], "); } Console.Write("]"); Console.ReadKey(); } } }
実行結果:
[[a, b, c, ], [d, e, f, ], ]
このサンプルコードでは、まずGetLengthメソッドを使ってそれぞれの1次元配列の要素数を取得し、2次元配列を宣言しています。
そして、Array.Copyメソッドを使ってコピーしています。
一部の範囲を指定してコピーする方法
Array.Copyメソッドは範囲を指定して配列の一部をコピーすることができます。
サンプルコードで確認しましょう。
using System; namespace ArrayCopy { class ArrayCopy { static void Main() { char[] src = {'a', 'b', 'c'}; char[] dst = new char[src.Length - 1]; Array.Copy(src, 1, dst, 0, src.Length - 1); Console.WriteLine("[{0}]", string.Join(", ", dst)); Console.ReadKey(); } } }
実行結果:
[b, c]
このサンプルコードでは、Array.Copyメソッドの第2引数に1を指定して、インデックス番号の1からあとをコピーするように指定しています。
第4引数でコピー先の位置を指定し、第5引数で要素の数を指定しています。
その他のコピー方法
配列をコピーするには、Array.Copyメソッド以外にもBuffer.BlockCopyメソッドを使う方法やSkipメソッドとTakeメソッドを使う方法があります。
Buffer.BlockCopyを使う方法
Buffer.BlockCopyメソッドは複数の配列を結合する場合に使います。
コピーの場合も使うことができます。
サンプルコードで確認しましょう。
using System; namespace ArrayCopy { class ArrayCopy { static void Main() { char[] src = {'a', 'b', 'c'}; char[] dst = new char[src.Length]; // 型のサイズを取得 int size = System.Runtime.InteropServices.Marshal.SizeOf(src.GetType().GetElementType()); // Buffer.BlockCopyでコピー // UTF-8では1文字当たり2バイト Buffer.BlockCopy(src, 0, dst, 0, src.Length * size * 2); Console.WriteLine("[{0}]", string.Join(", ", dst)); Console.ReadKey(); } } }
実行結果:
[a, b, c]
このサンプルコードでは、Buffer.BlockCopyメソッドを使って配列をコピーしています。
Buffer.BlockCopyメソッドの引数はインデックス番号や要素の数を指定するわけではありません。
位置はオフセットのバイト数で指定する必要があり、またコピーするバッファ数を指定する必要があります。
使用している文字コードによっては、1文字あたりのバイト数が変わってきますので、注意して指定するようにしましょう!
SkipとTakeを使う方法
LINQが使えるのであれば、Enumerable.SkipメソッドとEnumerable.Takeメソッドで配列をコピーすることができます。
Skipメソッドで指定した要素までスキップして、Takeメソッドで指定した数の要素を取得します。
サンプルコードで確認しましょう。
using System; using System.Linq; namespace ArrayCopy { class ArrayCopy { static void Main() { char[] src = {'a', 'b', 'c'}; char[] dst = src.Skip(0).Take(3).ToArray(); Console.WriteLine("[{0}]", string.Join(", ", dst)); Console.ReadKey(); } } }
実行結果:
[a, b, c]
高速でコピーするための速度比較
配列のコピーについて、ここまで3つの方法をお伝えしました。
それでは、どの方法が処理速度が速くて高速なのでしょうか?
サンプルコードで比較してみましょう!
速度の計測にはStopwatchクラスを使用します。
using System; using System.Diagnostics; using System.Linq; namespace ArrayCopy { class ArrayCopy { static void Main() { Stopwatch sw = new Stopwatch(); long nums = 1000000; char[] src = {'a', 'b', 'c'}; sw.Start(); for(int i = 0; i < nums; i++) { char[] dst = new char[src.Length]; Array.Copy(src, dst, src.Length); } sw.Stop(); Console.WriteLine("Array.Copyでは{0}ミリ秒", sw.Elapsed.TotalMilliseconds); int size = System.Runtime.InteropServices.Marshal.SizeOf(src.GetType().GetElementType()); sw.Start(); for(int i = 0; i < nums; i++) { char[] dst = new char[src.Length]; Buffer.BlockCopy(src, 0, dst, 0, src.Length * size * 2); } sw.Stop(); Console.WriteLine("Buffer.BlockCopyでは{0}ミリ秒", sw.Elapsed.TotalMilliseconds); sw.Start(); for(int i = 0; i < nums; i++) { char[] dst = src.Skip(0).Take(3).ToArray(); } sw.Stop(); Console.WriteLine("Skip.Takeでは{0}ミリ秒", sw.Elapsed.TotalMilliseconds); Console.ReadKey(); } } }
実行結果:
Array.Copyでは47.6585ミリ秒 Buffer.BlockCopyでは84.4946ミリ秒 Skip.Takeでは213.8613ミリ秒
このサンプルコードでは、それぞれの方法を100万回繰り返すのにかかった時間をDiagnostics.Stopwatchクラスを使って計測しています。
Array.CopyメソッドはBuffer.BlockCopyに比べて若干速いようです。
LINQのSkipメソッドとTakeメソッドは他の2つに比べて、明らかに遅い結果になっています。
参照のみをコピーする方法
「=」記号を使ってコピーすると参照のみのコピーとなります。
参照のみのコピーを行うと、コピー元の要素の値が変わった場合に、コピー先の要素の値も変わります。
これに対して、Array.Copyメソッドなどで値と参照のコピーを行うと、コピー先の要素の値が変わっても、コピー先の要素の値は変わりません。
サンプルコードで確認しましょう。
using System; namespace ArrayCopy { class ArrayCopy { static void Main() { char[] src = {'a', 'b', 'c'}; char[] dst = new char[src.Length]; // 値と参照のコピー Array.Copy(src, dst, src.Length); src[1] = 'B'; Console.WriteLine("値と参照のコピー: [{0}]", string.Join(", ", dst)); // 参照のみのコピー src[1] = 'b'; dst = src; src[1] = 'B'; Console.WriteLine("参照のみのコピー: [{0}]", string.Join(", ", dst)); Console.ReadKey(); } } }
実行結果:
値と参照のコピー: [a, b, c] 参照のみのコピー: [a, B, c]
このサンプルコードでは、Array.Copyメソッドでコピーした場合、元の要素の値が変わってもコピー先の値は変わっていません。
これに対して、「=」記号でコピーした場合は、コピー先の値まで変わっています。
注意しましょう!
まとめ
ここでは、配列のコピーについて説明しました。
配列をコピーする方法として、Array.Copyメソッドを使う方法、Buffer.BlockCopyを使う方法、SkipとTakeメソッドを使う方法の3つの方法がありました。
それぞれ使い方や処理性能が異なりますので、目的に合わせて使い分けるようにしましょう。
また、「=」記号でコピーすると、参照のみのコピーで値が意図せずに変わる可能性もあるので、注意しましょう!
使いこなすことができるように、この記事を何度も参考にして下さいね!