ArraySegment<T>構造体を使うと、オリジナルの配列の一部分(区間)を参照して部分配列を構成することができます。 例えば、配列arr
の要素10番目から5つ分を切り出して使いたいといった場合に、ArraySegment構造体を用いることができます。
ArraySegmentを使うと、配列の一部分をコピーして別の配列に格納する、といった操作を省略あるいは簡略化することができます。 ArraySegment構造体はジェネリック構造体であるため、任意の型の配列に対して用いることができます。 ArraySegment構造体はオリジナルの配列を切り出したコピーを作成するものではなく、オリジナルの配列に対するビューとして動作します。
.NET Frameworkでは、Array.Sliceといった配列の一部分をコピーして部分配列を作成するようなメソッドは用意されていないため、Array.Copyメソッドを使って実装するなどする必要があります。
ここでは、オリジナルの配列のビューとして機能する部分配列を構成するArraySegment構造体についてと、オリジナルの配列をコピーして部分配列を作成する方法について解説します。
ArraySegment構造体
概略
PerlやRubyなどの言語では、配列から部分配列を切り出すための以下のような構文が用意されています。
@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
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]
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構造体を用いるとこれに似た操作を行うことができます。
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));
}
}
Imports System
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
' 配列arrのインデックス2から3つ分の要素を参照する部分配列を作成する
Dim segment As New ArraySegment(Of Integer)(arr, 2, 3) ' from 2 length 3
' VBでは以下のような構文は用意されていない
'Dim segment() As Integer = arr(2..4)
Console.WriteLine(String.Join(", ", arr))
Console.WriteLine(String.Join(", ", segment))
End Sub
End Class
0, 1, 2, 3, 4, 5 2, 3, 4
部分配列の操作
ArraySegment構造体では、配列と同様にforeach
による要素の列挙を行うことができます。
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();
}
}
Imports System
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
Dim segment As New ArraySegment(Of Integer)(arr, 2, 3)
' For Eachによる列挙
For Each e As Integer In segment
Console.Write("{0}, ", e)
Next
Console.WriteLine()
End Sub
End Class
2, 3, 4,
ArraySegment構造体はインデクサを直接サポートしないので、インデックスを使ったアクセスはできません。 ArraySegmentでインデックスを使った要素へのアクセスを行いたい場合は、ArraySegmentをIReadOnlyList<T>インターフェイス(System.Collections.Generic名前空間)にキャストして用います。 IReadOnlyList<T>にキャストした場合も、foreach
によって列挙することができます。
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();
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
' ArraySegmentをIReadOnlyList<T>にキャストして用いる
Dim segment As IReadOnlyList(Of Integer) = New ArraySegment(Of Integer)(arr, 2, 3)
' インデクサによる要素へのアクセス
Console.WriteLine(segment(0))
Console.WriteLine(segment(1))
Console.WriteLine(segment(2))
Console.WriteLine()
For Each e As Integer In segment
Console.Write("{0}, ", e)
Next
Console.WriteLine()
End Sub
End Class
2 3 4 2, 3, 4,
インデクサの仕組みについてはプロパティ §.インデクサを参照してください。
このほかにも、ArraySegment構造体はIEnumerable<T>やIReadOnlyList<T>をはじめとしてコレクションとしてのインターフェイスを複数実装しているので、配列や他のコレクションと同様に扱うことができます。
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));
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
Dim segment As New ArraySegment(Of Integer)(arr, 2, 3)
Dim list As New List(Of Integer)()
list.Add(0)
list.Add(1)
list.AddRange(segment) ' ListにArraySegmentの内容を追加する
Console.WriteLine(String.Join(", ", list))
End Sub
End Class
0, 1, 2, 3, 4
また、LINQのメソッドも配列同様に用いることができます。
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());
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
Dim segment As New ArraySegment(Of Integer)(arr, 2, 3)
' 部分配列内の最大の要素を取得する
Console.WriteLine("Max() = {0}", segment.Max())
' 部分配列内の値の合計を求める
Console.WriteLine("Sum() = {0}", segment.Sum())
End Sub
End Class
Max() = 4 Sum() = 9
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));
}
}
Imports System
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
Dim segment As New ArraySegment(Of Integer)(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))
End Sub
End Class
2, 3, 4 9, 8, 7
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));
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
Dim segment As IList(Of Integer) = New ArraySegment(Of Integer)(arr, 2, 3)
Console.WriteLine(String.Join(", ", arr))
' ArraySegmentのIListインターフェイスを介して
' オリジナルの配列に変更を加える
segment(0) = 9
segment(1) = 8
segment(2) = 7
Console.WriteLine(String.Join(", ", arr))
End Sub
End Class
0, 1, 2, 3, 4, 5 0, 1, 9, 8, 7, 5
このようにオリジナルの配列を参照する部分配列ではなく、オリジナルをコピーして互いに独立した部分配列を作成したい場合は、§.コピーによる部分配列の作成で後述するような方法を使用します。
.NET Framework 4以前
.NET Framework 4以前のArraySegmentは、IEnumerable<T>やIList<T>などのインターフェイスが一切実装されておらず、配列の区間を定義する程度のごく限定された機能しか持ちません。 (これらのインターフェイスが実装されるのは.NET Framework 4.5以降です。)
したがって、直接列挙したり、インデックスを指定して参照したり、ここまでで述べたような操作を行うことはできません。 .NET Framework 4以前では、以下のプロパティを参照して部分配列を扱うことになります。
- Arrayプロパティ
- 部分配列として参照される配列(オリジナルの配列)。
- Offsetプロパティ
- 部分配列の最初のインデックス。
- Countプロパティ
- 部分配列内の要素の数。
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();
}
}
Imports System
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
Dim segment As New ArraySegment(Of Integer)(arr, 2, 3)
' ArraySegmentで表される部分配列内の要素を表示する
For index As Integer = 0 To segment.Count - 1
Console.Write("{0}, ", segment.Array(segment.Offset + index))
Next
Console.WriteLine()
End Sub
End Class
2, 3, 4,
ArraySegmentが用いられるクラス
.NET Frameworkにおいては、引数などでArraySegmentを用いるクラスはそう多くありません。
- Socket.Sendメソッド・Socket.Receiveメソッドでは、送受信バッファを格納する目的でArraySegment<byte>を用いることができます。
- WebSocket.SendAsyncメソッド・WebSocket.ReceiveAsyncメソッドも同様に、送受信バッファを格納する目的で用いられます。
- .NET Framework 4.6以降では、MemoryStream.TryGetBufferメソッドが取得した内部バッファを格納する目的でArraySegment<byte>を要求します。
Stream.ReadメソッドやStream.Writeメソッドなど、引数でarray, offset, count
のように配列の一部分を表すオフセットと長さを指定するメソッドの方が現状では一般的です。
このような引数を要求するメソッドを作成する場合、オーバーロードのひとつとしてArraySegmentを引数にとるバージョンも用意しておくことにより、利便性を向上させることができます。
コピーによる部分配列の作成
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 ]
Array.Slice
.NET FrameworkのArrayクラスでは、Array.Sliceのようなメソッドが用意されていないため、次の例のようにArray.Copyメソッドを使って独自に実装する必要があります。
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));
}
}
Imports System
' 拡張メソッドを定義するモジュール
Module ArrayExtensions
' 配列から部分配列をコピーして切り出す拡張メソッド
<System.Runtime.CompilerServices.Extension> _
Public Function Slice(Of T)(ByVal arr As T(), ByVal offset As Integer, ByVal count As Integer) As T()
Dim subarr(count - 1) As T
' arrのインデックスoffsetからcount個分の内容をsliceにコピーする
Array.Copy(arr, offset, subarr, 0, count)
Return subarr
End Function
End Module
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
' 実装した拡張メソッドを使って部分配列を作成する
Dim subarr() As Integer = arr.Slice(2, 3)
Console.WriteLine(string.Join(", ", arr))
Console.WriteLine(string.Join(", ", subarr))
End Sub
End Class
0, 1, 2, 3, 4, 5 2, 3, 4
この例では、配列型にSlice
という拡張メソッドを追加することにより、配列から直接呼び出せるようなメソッドを作成しています。 拡張メソッドについての詳細は拡張メソッドを参照してください。
この例で用いているArray.Copyでは、各要素の簡易コピーが作成されます。 これは、参照型では参照のみがコピーされ、インスタンスのコピーが作成されるわけではないことを意味します。 簡易コピーの動作や詳細コピーの方法などについてはオブジェクトの複製を参照してください。
Listクラスでは、List.GetRangeメソッドによってListの一部分をコピーして取得することができます。
Skipメソッド・Takeメソッド
配列として取得する必要がなく、単なる列挙や他のコレクションへの格納、LINQでの操作を目的とする場合は、LINQのメソッド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);
}
}
}
Imports System
Imports System.Linq
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
' arrの最初の2つの要素をスキップし、続く3つの要素を取得して列挙する
For Each e As Integer In arr.Skip(2).Take(3)
Console.WriteLine(e)
Next
End Sub
End Class
2 3 4
さらに、ToArrayメソッドを続けて呼びだせば、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));
}
}
Imports System
Imports System.Linq
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
' arrの最初の2つの要素をスキップし、続く3つの要素を配列として取得する
Dim subarr() As Integer = arr.Skip(2).Take(3).ToArray()
Console.WriteLine(String.Join(", ", subarr))
End Sub
End Class
2, 3, 4
上記の例において、ToArrayメソッドの代わりにToListメソッドを用いれば、結果をList<T>として取得することができます。