ArraySegment<T>構造体を使うと、オリジナルの配列の一部分(区間)を参照して部分配列を構成することができます。 例えば、配列arrの要素10番目から5つ分を切り出して使いたいといった場合に、ArraySegment構造体を用いることができます。

ArraySegmentを使うと、配列の一部分をコピーして別の配列に格納する、といった操作を省略あるいは簡略化することができます。 ArraySegment構造体はジェネリック構造体であるため、任意の型の配列に対して用いることができます。 ArraySegment構造体はオリジナルの配列を切り出したコピーを作成するものではなく、オリジナルの配列に対するビューとして動作します。

.NET Frameworkでは、Array.Sliceといった配列の一部分をコピーして部分配列を作成するようなメソッドは用意されていないため、Array.Copyメソッドを使って実装するなどする必要があります。

ここでは、オリジナルの配列のビューとして機能する部分配列を構成するArraySegment構造体についてと、オリジナルの配列をコピーして部分配列を作成する方法について解説します。

§1 ArraySegment構造体

§1.1 概略

PerlやRubyなどの言語では、配列から部分配列を切り出すための以下のような構文が用意されています。

部分配列の切り出し(Perl)
@arr = (0, 1, 2, 3, 4, 5);
@segment = @arr[2..4]; # from 2 to 4

print "@arr\n";
print "@segment\n";
実行結果
0 1 2 3 4 5
2 3 4
部分配列の切り出し(Ruby)
arr = [0, 1, 2, 3, 4, 5]
segment = arr[2..4] # from 2 to 4

p array
p segment
実行結果
[0, 1, 2, 3, 4, 5]
[2, 3, 4]
部分配列の切り出し(Python)
lst = [0, 1, 2, 3, 4, 5]
segment = lst[2:5] # from 2 until 5

print lst
print segment
実行結果
[0, 1, 2, 3, 4, 5]
[2, 3, 4]

C#やVBなどの言語では部分配列を切り出すような構文は用意されていませんが、ArraySegment構造体を用いるとこれに似た操作を行うことができます。

ArraySegment構造体を使って部分配列を構成する
using System;

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};

    // 配列arrのインデックス2から3つ分の要素を参照する部分配列を作成する
    var segment = new ArraySegment<int>(arr, 2, 3); // from 2 length 3

    // C#では以下のような構文は用意されていない
    //var segment = arr[2..4];

    Console.WriteLine(string.Join(", ", arr));
    Console.WriteLine(string.Join(", ", segment));
  }
}
実行結果
0, 1, 2, 3, 4, 5
2, 3, 4

§1.2 部分配列の操作

ArraySegment構造体では、配列と同様にforeachによる要素の列挙を行うことができます。

foreachを使ってArraySegment内の要素を列挙する
using System;

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};
    var segment = new ArraySegment<int>(arr, 2, 3);

    // foreachによる列挙
    foreach (var e in segment) {
      Console.Write("{0}, ", e);
    }
    Console.WriteLine();
  }
}
実行結果
2, 3, 4, 

ArraySegment構造体はインデクサを直接サポートしないので、インデックスを使ったアクセスはできません。 ArraySegmentでインデックスを使った要素へのアクセスを行いたい場合は、ArraySegmentをIReadOnlyList<T>インターフェイス(System.Collections.Generic名前空間)にキャストして用います。 IReadOnlyList<T>にキャストした場合も、foreachによって列挙することができます。

IReadOnlyListインターフェイスを介して、インデックスによるArraySegment内の要素へのアクセスを行う
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};

    // ArraySegmentをIReadOnlyList<T>にキャストして用いる
    IReadOnlyList<int> segment = new ArraySegment<int>(arr, 2, 3);

    // インデクサによる要素へのアクセス
    Console.WriteLine(segment[0]);
    Console.WriteLine(segment[1]);
    Console.WriteLine(segment[2]);
    Console.WriteLine();

    // IReadOnlyList<T>もforeachによる列挙ができる
    foreach (var e in segment) {
      Console.Write("{0}, ", e);
    }
    Console.WriteLine();
  }
}
実行結果
2
3
4

2, 3, 4, 

インデクサの仕組みについてはプロパティ §.インデクサを参照してください。


このほかにも、ArraySegment構造体はIEnumerable<T>やIReadOnlyList<T>をはじめとしてコレクションとしてのインターフェイスを複数実装しているので、配列や他のコレクションと同様に扱うことができます。

ArraySegmentを使って配列の一部分をListに追加する
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};
    var segment = new ArraySegment<int>(arr, 2, 3);

    var list = new List<int>();

    list.Add(0);
    list.Add(1);
    list.AddRange(segment); // ListにArraySegmentの内容を追加する

    Console.WriteLine(string.Join(", ", list));
  }
}
実行結果
0, 1, 2, 3, 4

また、LINQのメソッドも配列同様に用いることができます。

ArraySegmentを使って配列の一部分をListに追加する
using System;
using System.Collections.Generic;
using System.Linq;

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};
    var segment = new ArraySegment<int>(arr, 2, 3);

    // 部分配列内の最大の要素を取得する
    Console.WriteLine("Max() = {0}", segment.Max());

    // 部分配列内の値の合計を求める
    Console.WriteLine("Sum() = {0}", segment.Sum());
  }
}
実行結果
Max() = 4
Sum() = 9

ArraySegmentは配列の一部分をコピーして切り出すものではなく、配列の一部分を参照するビューとして動作します。 したがって、オリジナルの配列に対する変更は、ArraySegmentにも影響します。 また、その逆も同様に影響します。

ArraySegmentが参照するオリジナルの配列に変更を加える
using System;

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};
    var segment = new ArraySegment<int>(arr, 2, 3);

    Console.WriteLine(string.Join(", ", segment));

    // segmentが参照しているオリジナルの配列に変更を加える
    arr[2] = 9;
    arr[3] = 8;
    arr[4] = 7;

    // segmentはarrを'参照'しているので、変更はsegmentにも反映される
    Console.WriteLine(string.Join(", ", segment));
  }
}
実行結果
2, 3, 4
9, 8, 7
ArraySegmentからオリジナルの配列に変更を加える
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};
    IList<int> segment = new ArraySegment<int>(arr, 2, 3);

    Console.WriteLine(string.Join(", ", arr));

    // ArraySegmentのIListインターフェイスを介して
    // オリジナルの配列に変更を加える
    segment[0] = 9;
    segment[1] = 8;
    segment[2] = 7;

    Console.WriteLine(string.Join(", ", arr));
  }
}
実行結果
0, 1, 2, 3, 4, 5
0, 1, 9, 8, 7, 5

このようにオリジナルの配列を参照する部分配列ではなく、オリジナルをコピーして互いに独立した部分配列を作成したい場合は、§.コピーによる部分配列の作成で後述するような方法を使用します。

§1.3 .NET Framework 4以前

.NET Framework 4以前のArraySegmentは、IEnumerable<T>やIList<T>などのインターフェイスが一切実装されておらず、配列の区間を定義する程度のごく限定された機能しか持ちません。 (これらのインターフェイスが実装されるのは.NET Framework 4.5以降です。)

したがって、直接列挙したり、インデックスを指定して参照したり、ここまでで述べたような操作を行うことはできません。 .NET Framework 4以前では、以下のプロパティを参照して部分配列を扱うことになります。

Arrayプロパティ
部分配列として参照される配列(オリジナルの配列)。
Offsetプロパティ
部分配列の最初のインデックス。
Countプロパティ
部分配列内の要素の数。
.NET Framework 4以前でのArraySegment構造体を使った部分配列の操作例
using System;

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};
    var segment = new ArraySegment<int>(arr, 2, 3);

    // ArraySegmentで表される部分配列内の要素を表示する
    for (var index = 0; index < segment.Count; index++) {
      Console.Write("{0}, ", segment.Array[segment.Offset + index]);
    }
    Console.WriteLine();
  }
}
実行結果
2, 3, 4, 

§1.4 ArraySegmentが用いられるクラス

.NET Frameworkにおいては、引数などでArraySegmentを用いるクラスはそう多くありません。

Stream.ReadメソッドStream.Writeメソッドなど、引数でarray, offset, countのように配列の一部分を表すオフセットと長さを指定するメソッドの方が現状では一般的です。

このような引数を要求するメソッドを作成する場合、オーバーロードのひとつとしてArraySegmentを引数にとるバージョンも用意しておくことにより、利便性を向上させることができます。



§2 コピーによる部分配列の作成

JavaScriptなどの言語では、配列の一部分をコピーして部分配列を切り出すための以下のようなメソッドが用意されています。

部分配列の切り出し(JavaScript)
var arr = [0, 1, 2, 3, 4, 5];
var subarr = arr.slice(2, 5); // from 2 until 5

console.log(arr);
console.log(subarr);
実行結果
Array [ 0, 1, 2, 3, 4, 5 ]
Array [ 2, 3, 4 ]

§2.1 Array.Slice

.NET FrameworkのArrayクラスでは、Array.Sliceのようなメソッドが用意されていないため、次の例のようにArray.Copyメソッドを使って独自に実装する必要があります。

Array.Copyメソッドを使ってSliceメソッドを実装する
using System;

// 拡張メソッドを定義するクラス
static class ArrayExtensions {
  // 配列から部分配列をコピーして切り出す拡張メソッド
  public static T[] Slice<T>(this T[] arr, int offset, int count)
  {
    var subarr = new T[count];

    // arrのインデックスoffsetからcount個分の内容をsliceにコピーする
    Array.Copy(arr, offset, subarr, 0, count);

    return subarr;
  }
}

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};

    // 実装した拡張メソッドを使って部分配列を作成する
    var subarr = arr.Slice(2, 3);

    Console.WriteLine(string.Join(", ", arr));
    Console.WriteLine(string.Join(", ", subarr));
  }
}
実行結果
0, 1, 2, 3, 4, 5
2, 3, 4

この例では、配列型にSliceという拡張メソッドを追加することにより、配列から直接呼び出せるようなメソッドを作成しています。 拡張メソッドについての詳細は拡張メソッドを参照してください。

この例で用いているArray.Copyでは、各要素の簡易コピーが作成されます。 これは、参照型では参照のみがコピーされ、インスタンスのコピーが作成されるわけではないことを意味します。 簡易コピーの動作や詳細コピーの方法などについてはオブジェクトの複製を参照してください。

Listクラスでは、List.GetRangeメソッドによってListの一部分をコピーして取得することができます。

§2.2 Skipメソッド・Takeメソッド

配列として取得する必要がなく、単なる列挙や他のコレクションへの格納、LINQでの操作を目的とする場合は、LINQのメソッドSkipTakeを使うことでも部分配列を切り出すことができます。

Skipメソッド・Takeメソッドを使って部分配列を切り出す
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};

    // arrの最初の2つの要素をスキップし、続く3つの要素を取得して列挙する
    foreach (var e in arr.Skip(2).Take(3)) {
      Console.WriteLine(e);
    }
  }
}
実行結果
2
3
4

さらに、ToArrayメソッドを続けて呼びだせば、Skipメソッド・Takeメソッドの結果を配列として取得することもできます。

Skipメソッド・Takeメソッドを使って部分配列を取得する
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};

    // arrの最初の2つの要素をスキップし、続く3つの要素を配列として取得する
    var subarr = arr.Skip(2).Take(3).ToArray();

    Console.WriteLine(string.Join(", ", subarr));
  }
}
実行結果
2, 3, 4

上記の例において、ToArrayメソッドの代わりにToListメソッドを用いれば、結果をList<T>として取得することができます。