連載
» 2017年04月26日 05時00分 UPDATE

.NET TIPS:配列のコピーを1行でするには?[C#/VB]

配列をコピーするには、for/foreachループを使う方法もあるが、ArrayクラスのCopyメソッドを使うのが一番簡単で速度の面でも有利である。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 配列をコピーするのにforループやforeachループを書くのが面倒だと思ったことはないだろうか? Arrayクラス(System名前空間)の機能を使えば、それがたった1行で書けるのである。

 List<T>クラスなどの便利なコレクションを使わずにわざわざ配列を使うのは、速度を重視している場合に多いだろう。そこで本稿では、配列をコピーする方法を解説するとともに、コピー時間を計測するコードの例も紹介する。

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

配列をコピーするには?

 ArrayクラスのCopyメソッドを使うと、1行でコピーできる。

 特に多次元配列ではforループ/foreachループでコピーするコードを書くのは面倒なものだ。2次元配列をforループ/foreachループ/ArrayクラスのCopyメソッドを使ってそれぞれコピーする例を次のコードに示す(コンソールアプリ)。

 なお、1次元配列の場合には、その配列のCopyToメソッドも利用できる。

using System;
using System.Linq;
using static System.Console;

class Program
{
  static void Main(string[] args)
  {
    // コピー元の2次元配列
    int[,] src = { { 1, 2, 3 }, { 4, 5, 6 }, };
    WriteLine($"コピー元の配列:{string.Join(", ", src.Cast<int>())}");
    // 出力: 1, 2, 3, 4, 5, 6
    // (以降の出力例は省略)

    // forループでコピー
    int[,] dest1 = new int[src.GetLength(0), src.GetLength(1)];
    for (int i = 0; i < src.GetLength(0); i++)
      for (int j = 0; j < src.GetLength(1); j++)
        dest1[i,j] = src[i,j];

    WriteLine($"forループでコピー:{string.Join(", ", dest1.Cast<int>())}");

    // foreachループでコピー
    int[,] dest2 = new int[src.GetLength(0), src.GetLength(1)];
    int index0 = 0, index1 = 0;
    foreach (var n in src)
    {
      dest2[index0, index1++] = n;
      if (index1 >= src.GetLength(1))
      {
        index0++; index1 = 0;
      }
    }

    WriteLine($"foreachループでコピー:{string.Join(", ", dest2.Cast<int>())}");

    // Array.Copyでコピー
    int[,] dest3 = new int[src.GetLength(0), src.GetLength(1)];
    Array.Copy(src, dest3, src.Length);

    WriteLine($"Array.Copyでコピー:{string.Join(", ", dest3.Cast<int>())}");
    ReadKey();
  }
}

Imports System.Console

Module Module1
  Sub Main()
    ' コピー元の2次元配列
    Dim src As Integer(,) = {{1, 2, 3}, {4, 5, 6}}
    WriteLine($"コピー元の配列:{String.Join(", ", src.Cast(Of Integer)())}")
    ' 出力: 1, 2, 3, 4, 5, 6
    ' (以降の出力例は省略)

    ' Forループでコピー
    Dim dest1(src.GetLength(0) - 1, src.GetLength(1) - 1) As Integer
    For i As Integer = 0 To (src.GetLength(0) - 1)
      For j As Integer = 0 To (src.GetLength(1) - 1)
        dest1(i, j) = src(i, j)
      Next
    Next
    WriteLine($"Forループでコピー:{String.Join(", ", dest1.Cast(Of Integer)())}")

    ' For Eachループでコピー
    Dim dest2(src.GetLength(0) - 1, src.GetLength(1) - 1) As Integer
    Dim Index0 As Integer = 0, index1 As Integer = 0
    For Each n In src
      dest2(Index0, index1) = n
      index1 += 1
      If (index1 >= src.GetLength(1)) Then
        Index0 += 1 : index1 = 0
      End If
    Next
    WriteLine($"For Eachループでコピー:{String.Join(", ",
                                                 dest2.Cast(Of Integer)())}")

    ' Array.Copyでコピー
    Dim dest3(src.GetLength(0) - 1, src.GetLength(1) - 1) As Integer
    Array.Copy(src, dest3, src.Length)

    WriteLine($"Array.Copyでコピー:{String.Join(", ",
                                             dest3.Cast(Of Integer)())}")
    ReadKey()
  End Sub
End Module

3通りの方法で配列をコピーするコンソールアプリの例(上:C#、下:VB)
2次元配列をコピーする場合、forループでは2重ループになり、foreachループではインデックスの管理が面倒だ。最後に示したArrayクラスのCopyメソッドでは、それが1行で済むのだ。

配列をコピーする時間を計測する例

 ArrayクラスのCopyメソッドは、1行で書けるだけでなく、forループ/foreachループよりも少し速いという特徴がある(Releaseビルドの場合、筆者の実測では、1次元配列で2割くらい速かった)。

 List<T>クラスなどの便利なジェネリックコレクションを使わずに配列を使う理由は、主に処理速度を気にしているからだろう。そこで、先に示した3通りの配列コピー方法(ただし1次元配列)の速度を試してみるためのサンプルコードを次に示す。少々長いので、コードはC#のもののみとさせていただく。ポイントは、最初の数回は安定しないので、それを除外して処理時間の平均を求めることだ(この例では実行中のメモリ消費量は安定しているため、強制的なガベージコレクションはしていない)。

using System;
using System.Linq;
using static System.Console;

class Program
{
  static void Main(string[] args)
  {
    const int Length = 50000000; // テストに使う配列の長さ
    int[] a = new int[Length]; // コピー元の配列
    for (int i = 0; i < Length; i++)
      a[i] = i;
    int[] b = new int[Length]; // コピー先の配列

    // テスト用の定数と実行時間格納用の配列
    const int LoopCount = 23;
    long[] result1 = new long[LoopCount];
    long[] result2 = new long[LoopCount];
    long[] result3 = new long[LoopCount];

    // 経過時間計測に使うストップウォッチ
    var sw = new System.Diagnostics.Stopwatch();

    // テスト実施
    for (int i=0; i< LoopCount; i++)
    {
      WriteLine($"{i+1}回目");
      result1[i] = ArrayCopyTest1(a, b, sw);
      result2[i] = ArrayCopyTest2(a, b, sw);
      result3[i] = ArrayCopyTest3(a, b, sw);
      WriteLine();
    }

    // 結果を算出(最初の3回は除外して平均値を求める)
    double ave1 = result1.Skip(3).Average();
    double ave2 = result2.Skip(3).Average();
    double ave3 = result3.Skip(3).Average();
    WriteLine($"forループでコピー(平均):{ave1:#,##0.000}ミリ秒");
    WriteLine($"foreachループでコピー(平均):{ave2:#,##0.000}ミリ秒");
    WriteLine($"Array.Copyでコピー(平均):{ave3:#,##0.000}ミリ秒");

    // コンソールが閉じてしまうのを防ぐ
    ReadKey();
  }

  static long ArrayCopyTest1(int[] a, int[] b, System.Diagnostics.Stopwatch sw)
  {
    sw.Restart();
    for (int i = 0; i < a.Length; i++)
      b[i] = a[i];
    sw.Stop();
    WriteLine($"forループでコピー:{sw.ElapsedMilliseconds:#,##0}ミリ秒");

    Array.Clear(b, 0, b.Length);
    return sw.ElapsedMilliseconds;
  }

  static long ArrayCopyTest2(int[] a, int[] b, System.Diagnostics.Stopwatch sw)
  {
    sw.Restart();
    int index = 0;
    foreach (var n in a)
      b[index++] = n;
    sw.Stop();
    WriteLine($"foreachループでコピー:{sw.ElapsedMilliseconds:#,##0}ミリ秒");

    Array.Clear(b, 0, b.Length);
    return sw.ElapsedMilliseconds;
  }

  static long ArrayCopyTest3(int[] a, int[] b, System.Diagnostics.Stopwatch sw)
  {
    sw.Restart();
    Array.Copy(a, b, a.Length);
    sw.Stop();
    WriteLine($"Array.Copyでコピー:{sw.ElapsedMilliseconds:#,##0}ミリ秒");

    Array.Clear(b, 0, b.Length);
    return sw.ElapsedMilliseconds;
  }
}

配列のコピーにかかる時間を測定する例(C#)
整数5000万個の配列を、3通り×23回ずつコピーしている。どのくらいの時間がかかるかは、実際に試してみてほしい(恐らく1分もかからない)。試すときは、リリースビルドにして、(Visual Studioからではなく)エクスプローラーなどから直接起動する。
筆者の環境では、このコードの場合、ArrayクラスのCopyメソッドがforループよりも2割くらい速かった。forループとforeachループでは、foreachループの方がやや速かった(多次元配列の場合はインデックス管理のif文が入るので、逆転するだろう)。このような数割程度の速度差の場合、条件やちょっとしたコードの違いによって順位が入れ替わることもよくある。できるだけ実際に近い条件のテストとなるように心掛けてほしい。

まとめ

 ArrayクラスのCopyメソッドを使えば(1次元配列に限定するなら配列のCopyToメソッドでも)、配列のコピーが1行で書ける。速度的にも有利なので、コピーしながら何か他の処理もするというのでなければ、Copyメソッドを使おう。

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

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

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

メールマガジン登録

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