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

.NET TIPS:配列の一部だけをコピーするには?[C#/VB]

ArrayクラスのCopyメソッド、ArraySegmentクラスなどを使い、配列の一部の要素だけをコピーする方法を説明。また、2つの配列をマージする方法も紹介する。

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

連載目次

 配列の一部だけを別の配列にコピーしたいことがある。もちろんforループやforeachループを記述してもよいのだが、Arrayクラス(System名前空間)などの機能を使えば簡単に書ける。

 さらに本稿では、その応用として、複数の配列を1つにマージする方法も紹介する。

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

配列を一部だけをコピーするには?

 ArrayクラスのCopyメソッドを使うと、1行でコピーできる(次のコード)。また、ArrayクラスのCopyメソッドは、同じ配列内でコピーしたり、多次元配列間でコピーしたりもできる(後述)。

// コピーする個数
int n = ……省略……;

// コピー元の配列
string[] a = { ……省略…… };
int i = ……省略……; // 配列a内での読み取り開始インデックス

// コピー先の配列
int j = ……省略……; // 配列b内での書き込み開始インデックス
string[] b = new string[n+j];

// Array.Copyメソッドでコピー
Array.Copy(a, i, b, j, n);
// 配列aの(i+1)番目の要素を配列bの(j+1)番目の位置へと、順にn個をコピーする。

' コピーする個数
Dim n As Integer = ……省略……

' コピー元の配列
Dim a() As String = { ……省略…… }
Dim i As Integer = ……省略…… ' 配列a内での読み取り開始インデックス

' コピー先の配列
Dim j As Integer = ……省略…… ' 配列b内での書き込み開始インデックス
Dim b(n + j - 1) As String

' Array.Copyメソッドでコピー
Array.Copy(a, i, b, j, n)
' 配列aの(i+1)番目の要素を配列bの(j+1)番目の位置へと、順にn個をコピーする。

ArrayクラスのCopyメソッドで配列の一部を別の配列へコピーする(上:C#、下:VB)

 なお、元の配列の指定範囲の要素だけを含む配列を新しく作りたいとき(=上のコードでjが0の場合)、.NET Framework 4.5(またはそれ以降)であれば、ArraySegment構造体(System名前空間)も使える(次のコード)。

// コピー元の配列
string[] a = { ……省略…… };

int i = ……省略……; // 配列a内での読み取り開始インデックス
int n = ……省略……; // コピーする個数

string[] c = (new ArraySegment<string>(a, i, n)).ToArray();
// 配列aの(i+1)番目からのn個を参照するArraySegment構造体を作り、
// それを配列に変換する。

' コピー元の配列
Dim a() As String = { ……省略…… }

Dim i As Integer = ……省略…… ' 配列a内での読み取り開始インデックス
Dim n As Integer = ……省略…… 'コピーする個数

Dim c() As String = (New ArraySegment(Of String)(a, i, n)).ToArray()
' 配列aの(i+1)番目からのn個を参照するArraySegment構造体を作り、
' それを配列に変換する。

ArraySegment構造体を使い、指定範囲を取り出して新しい配列を作る(上:C#、下:VB)
このようにしてJavaScriptのsliceメソッドと同様な結果を得ることができる。

ただし、ArraySegment構造体は.NET Framework 2.0からあるが、ここで使っているToArray拡張メソッドが利用できるのは.NET Framework 4.5からである。また、ArraySegment構造体が扱える配列は1次元のものだけだ。


実際の例

 上記2つの方法を実際のサンプルコードで示すと、例えば次のコンソールアプリのようになる。

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

class Program
{
  static void Main(string[] args)
  {
    // コピー元の配列
    string[] a = { "a", "b", "c", "d", "e", "f", };

    // 配列の2番目から3個を、別の配列にコピーする例
    // Array.Copyメソッドを使う
    string[] b = new string[3];
    Array.Copy(a, 1, b, 0, 3);
    WriteLine($"b[] = {string.Join(", ", b)}");
    // 出力:b[] = b, c, d

    // ArraySegment構造体を使う(.NET Framework 4.5以上/1次元配列のみ)
    string[] c = (new ArraySegment<string>(a, 1, 3)).ToArray();
    WriteLine($"c[] = {string.Join(", ", c)}");
    // 出力:c[] = b, c, d

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

Imports System.Console

Module Module1
  Sub Main()
    ' コピー元の配列
    Dim a() As String = {"a", "b", "c", "d", "e", "f"}

    ' 配列の2番目から3個を、別の配列にコピーする例
    ' Array.Copyメソッドを使う
    Dim b(2) As String
    Array.Copy(a, 1, b, 0, 3)
    WriteLine($"b[] = {String.Join(", ", b)}")
    ' 出力:b[] = b, c, d

    ' ArraySegment構造体を使う(.NET Framework 4.5以上/1次元配列のみ)
    Dim c() As String = (New ArraySegment(Of String)(a, 1, 3)).ToArray()
    WriteLine($"c[] = {String.Join(", ", c)}")
    ' 出力:c[] = b, c, d

#If DEBUG Then
    ' コンソールが閉じてしまうのを防ぐ
    ReadKey()
#End If
  End Sub
End Module

2通りの方法で配列の一部をコピーするコンソールアプリの例(上:C#、下:VB)

同じ配列内にコピーするとどうなるか?

 ArrayクラスのCopyメソッドは、コピー先としてコピー元と同じ配列も指定できる。そのとき、コピー元の範囲とコピー先の範囲が重なっていたらどうなるだろうか?  実際に試してみれば分かるが(次のコード)、コピー元の範囲の全ての要素がいったんバッファーにコピーされてからそれがコピー先の範囲へコピーし直されるような動作になる。

// コピー元の配列
string[] a = { "a", "b", "c", "d", "e", "f", };

// 配列の2番目から3個を、同じ配列の4番目以降にコピーする
Array.Copy(a, 1, a, 3, 3);
WriteLine($"a[] = {string.Join(", ", a)}");
// 出力:a[] = a, b, c, b, c, d

' コピー元の配列
Dim a() As String = {"a", "b", "c", "d", "e", "f"}

' 配列の2番目から3個を、同じ配列の4番目以降にコピーする
Array.Copy(a, 1, a, 3, 3)
WriteLine($"a[] = {String.Join(", ", a)}")
' 出力:a[] = a, b, c, b, c, d

同じ配列内の重なった領域にコピーする例(上:C#、下:VB)
配列の2番目から3個(={"b", "c", "d"})が取り出され、それが4番目(="d"のところ)からの3個に上書きされるような動作になる。

次のような動作ではない。
  (1) 配列の2番目から"b"を取り出して4番目(="d"のところ)に書き込む
  (2) 配列の3番目から"c"を取り出して5番目(="e"のところ)に書き込む
  (3) 配列の4番目((1)で"b"に変わっている)から"b"を取り出して6番目(="f"のところ)に書き込む
  もしもこの(1)(3)のような動作だったならば、結果が「a[] = a, b, c, b, c, b」となってしまうところだ。


多次元配列をコピーする

 ArrayクラスのCopyメソッドは、多次元配列でもコピーできる。ただし、コピー元とコピー先の次元は同じでなければならない。このとき、コピー先の配列のどの要素から書き込み(コピー)が開始されるかは、書き込み開始インデックスとして指定した値、配列の次元数、各次元の要素数により決まる。書き込みを行っている次元の配列から溢れた要素は次に書き込みを行う次元の配列の先頭から順に書き込まれる。次の例では、配列は2次元で、各次元の配列の要素数は3、書き込み開始インデックスは「0」、コピーする要素数は4個となっている。そのため、コピー先となる配列では、1次元目の配列の先頭から順に要素が書き込まれ(3個)、そこから溢れた1個の要素が2次元目の配列の先頭に書き込まれる(出力結果の最後が「, ,」となっているのは2次元目の配列における2個目と3個目の要素にはコピーされないから)。

 2次元配列の一部をコピーする例を次のコードに示す。

// コピー元の配列
string[,] d = { { "a", "b", "c", }, { "1", "2", "3", } };

// 2次元配列の2番目から4個を、別の配列にコピーする

// コピー先の配列
//string[] e = new string[d.Length]; // コピー元と次元が違うと、例外になる
string[,] e = new string[d.GetLength(0), d.GetLength(1)];

Array.Copy(d, 1, e, 0, 4);
WriteLine($"e[] = {string.Join(", ", e.Cast<string>())}");
// 出力:e[] = b, c, 1, 2, ,

' コピー元の配列
Dim d(,) As String = {{"a", "b", "c"}, {"1", "2", "3"}}

' 2次元配列の2番目から4個を、別の配列にコピーする

' コピー先の配列
'Dim e(d.Length - 1) As String  ' コピー元と次元が違うと、例外になる
Dim e(d.GetLength(0) - 1, d.GetLength(1) - 1) As String

Array.Copy(d, 1, e, 0, 4)
WriteLine($"e[] = {String.Join(", ", e.Cast(Of String)())}")
' 出力:e[] = b, c, 1, 2, ,

2次元配列の一部をコピーする例(上:C#、下:VB)

応用:複数の配列をマージした配列を作る

 ArrayクラスのCopyメソッドを使って、複数の配列を結合した配列を作れる。次のコードは、2つの配列を結合させた1つの配列を作る例だ。1次元配列の場合には、配列のCopyToメソッドやLINQ拡張を使ってもできる。

// 元の配列
string[] a = { "a", "b", "c", };
string[] b = { "1", "2", "3", };

// 結合先の配列
string[] c = new string[a.Length + b.Length];

// 配列aを配列cの先頭にコピーする
Array.Copy(a, c, a.Length);

// その後ろに配列bをコピーする
Array.Copy(b, 0, c, a.Length, b.Length);

WriteLine($"c[] = {string.Join(", ", c)}");
// 出力:c[] = a, b, c, 1, 2, 3

// 1次元配列なら配列のCopyToメソッドを使ってもよい
// (引数が少ないので、分かりやすい)
string[] d = new string[a.Length + b.Length];
a.CopyTo(d, 0);
b.CopyTo(d, a.Length);
WriteLine($"d[] = {string.Join(", ", d)}");
// 出力:d[] = a, b, c, 1, 2, 3

// また、1次元配列ではLINQのConcat拡張メソッドとToArray拡張メソッドでも可能
string[] e = a.Concat(b).ToArray();
WriteLine($"e[] = {string.Join(", ", e)}");
// 出力:e[] = a, b, c, 1, 2, 3

' 元の配列
Dim a() As String = {"a", "b", "c"}
Dim b() As String = {"1", "2", "3"}

' 結合先の配列
Dim c(a.Length + b.Length - 1) As String

' 配列aを配列cの先頭にコピーする
Array.Copy(a, c, a.Length)

' その後ろに配列bをコピーする
Array.Copy(b, 0, c, a.Length, b.Length)

WriteLine($"c[] = {String.Join(", ", c)}")
' 出力:c[] = a, b, c, 1, 2, 3

' 1次元配列なら配列のCopyToメソッドを使ってもよい
' (引数が少ないので、分かりやすい)
Dim d(a.Length + b.Length - 1) As String
a.CopyTo(d, 0)
b.CopyTo(d, a.Length)
WriteLine($"d[] = {String.Join(", ", d)}")
' 出力:d[] = a, b, c, 1, 2, 3

' また、1次元配列ではLINQのConcat拡張メソッドとToArray拡張メソッドでも可能
Dim e() As String = a.Concat(b).ToArray()
WriteLine($"e[] = {String.Join(", ", e)}")
' 出力:e[] = a, b, c, 1, 2, 3

2つの配列を1つにマージする例(上:C#、下:VB)

まとめ

 配列の一部をコピーするには、ArrayクラスのCopyメソッドを使う。1次元配列の場合には、ArraySegment構造体(.NET 4.5以降)やLINQ拡張も活用できる。

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

.NET TIPS

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

@IT Special

- PR -

TechTargetジャパン

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

RSSについて

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

メールマガジン登録

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