C# ArraySegmentとは?配列コピーを減らす使い方・メリット・注意点を初心者向けに解説
はじめに
C#で配列の一部分だけを扱いたいとき、よく使われる方法として Array.Copy や LINQ の Skip・Take・ToArray があります。
たとえば、配列の途中から5個だけ取り出したい場合、次のように新しい配列を作るコードを書いたことがあるかもしれません。
C#int[] source = { 10, 20, 30, 40, 50, 60 };
int[] copied = source.Skip(2).Take(3).ToArray();
このコードはわかりやすい一方で、ToArray() を呼んでいるため、元の配列とは別の新しい配列が作られます。小さな配列なら問題になりにくいですが、大きな配列や大量のデータを何度も処理する場面では、メモリ使用量やコピー処理のコストが気になることがあります。
そこで役立つのが ArraySegment<T> です。
ArraySegment<T> を使うと、配列そのものをコピーせずに「配列のこの範囲だけを扱う」という表現ができます。特に、byte配列、通信バッファ、ファイル処理、大量データの分割処理などで便利です。
この記事では、C#の ArraySegment とは何か、どのように使うのか、配列コピーとの違い、メリット、注意点、Span<T> や Memory<T> との違いまで、初心者向けにわかりやすく解説します。
1. C#のArraySegmentとは?配列の一部分を表す「ビュー」を理解しよう
1-1. ArraySegment<T>は配列の一部をoffsetとcountで参照する構造体
ArraySegment<T> は、配列の一部分を表すための構造体です。
名前のとおり、Array の Segment、つまり「配列の区間」を表します。
たとえば、次のような配列があるとします。
C#int[] numbers = { 10, 20, 30, 40, 50, 60 };
この配列のうち、30, 40, 50 の部分だけを扱いたい場合、ArraySegment<int> を使って次のように表現できます。
C#ArraySegment<int> segment = new ArraySegment<int>(numbers, 2, 3);
第1引数の numbers は元の配列です。
第2引数の 2 は開始位置です。C#の配列インデックスは0から始まるため、numbers[2] は 30 です。
第3引数の 3 は要素数です。つまり、インデックス2から3個分を扱うという意味になります。
この segment は、元配列 numbers の中の 30, 40, 50 という範囲を表します。
1-2. 配列をコピーせずに範囲だけを扱えるのが最大の特徴
ArraySegment<T> の最大の特徴は、配列をコピーしないことです。
ArraySegment<T> は、元の配列、開始位置、要素数を持っているだけです。新しい配列を作るわけではありません。
イメージとしては、次のような情報を持っているだけです。
C#元の配列: numbers
開始位置: 2
要素数: 3
そのため、ArraySegment<T> は「配列の一部分を切り出した新しい配列」ではなく、「元配列の一部を見るためのビュー」と考えると理解しやすいです。
コピーをしないため、巨大な配列の一部をメソッドに渡したいときや、バッファの一部だけを処理したいときに便利です。
1-3. 初心者がつまずきやすい「新しい配列ではない」という考え方
ArraySegment<T> を初めて使うときに特につまずきやすいのが、「配列の一部を取り出す」と聞くと、新しい配列が作られるように感じてしまう点です。
しかし、ArraySegment<T> は新しい配列ではありません。
次のコードを見てみましょう。
C#int[] numbers = { 10, 20, 30, 40, 50, 60 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 2, 3);
この時点で、30, 40, 50 だけが入った新しい配列が作られているわけではありません。
segment は、あくまで numbers のインデックス2から3個分を参照しているだけです。
そのため、元の配列を変更すると、segment から見える値も変わります。逆に、segment 側から値を変更すると、元の配列にも反映されます。
この「元配列を共有している」という性質は、メリットにもなりますが、注意点にもなります。
1-4. ArraySegmentが使われる主な場面:大きな配列・バッファ・分割処理
ArraySegment<T> は、特に次のような場面で使われます。
大きな配列の一部だけをメソッドに渡したいとき。
通信処理で受信バッファの一部だけを処理したいとき。
ファイル読み込みで、実際に読み込まれたバイト数だけを扱いたいとき。
配列を複数の範囲に分割して順番に処理したいとき。
配列コピーによるメモリ使用量を抑えたいとき。
たとえば、ネットワーク通信では、大きな byte[] バッファを用意して、その中に受信データを書き込むことがあります。このとき、バッファ全体ではなく「今回受信した部分だけ」を処理したい場合に ArraySegment<byte> が役立ちます。
2. ArraySegmentを使うメリット:配列コピーを減らせる理由
2-1. Array.CopyやToArrayを多用するとメモリ使用量が増える
配列の一部だけを取り出す方法として、Array.Copy を使う方法があります。
C#int[] source = { 10, 20, 30, 40, 50, 60 };
int[] copied = new int[3];
Array.Copy(source, 2, copied, 0, 3);
このコードでは、source のインデックス2から3個分を copied にコピーしています。
結果として、copied には 30, 40, 50 が入ります。
この方法は明確で安全ですが、新しい配列を作成し、要素をコピーしています。
LINQを使って次のように書くこともできます。
C#int[] copied = source.Skip(2).Take(3).ToArray();
こちらも見た目は簡潔ですが、最後に ToArray() を呼んでいるため、新しい配列が作られます。
配列が小さく、処理回数も少ない場合は問題になりにくいです。しかし、大きな配列を何度も分割したり、通信データやファイルデータを頻繁に処理したりする場合は、コピーによってメモリ使用量が増えやすくなります。
2-2. ArraySegmentなら元配列を共有して一部だけを渡せる
ArraySegment<T> を使うと、新しい配列を作らずに元配列の一部だけを表現できます。
C#int[] source = { 10, 20, 30, 40, 50, 60 };
ArraySegment<int> segment = new ArraySegment<int>(source, 2, 3);
このコードでは、source のインデックス2から3個分を表す segment を作成しています。
重要なのは、30, 40, 50 の新しい配列は作られていないという点です。
segment は、元の配列 source と、開始位置 2、要素数 3 を保持しているだけです。
つまり、メソッドに配列の一部を渡したい場合でも、次のように ArraySegment<T> を渡せます。
C#void PrintSegment(ArraySegment<int> segment)
{
foreach (int value in segment)
{
Console.WriteLine(value);
}
}
int[] source = { 10, 20, 30, 40, 50, 60 };
ArraySegment<int> segment = new ArraySegment<int>(source, 2, 3);
PrintSegment(segment);
このようにすれば、配列をコピーせずに、必要な範囲だけをメソッドに渡せます。
2-3. 大量データ処理でGC負荷を抑えやすい
C#では、配列などのオブジェクトを新しく作成すると、ヒープにメモリが確保されます。不要になったオブジェクトは、ガベージコレクション、つまりGCによって回収されます。
小さなプログラムではあまり気にならないかもしれませんが、大量の配列を何度も作成すると、GCの負荷が増えやすくなります。
たとえば、次のような処理を大量に行う場合です。
C#for (int i = 0; i < 10000; i++)
{
int[] copied = source.Skip(i).Take(100).ToArray();
// copiedを使った処理
}
このコードでは、ループのたびに新しい配列が作られます。
一方、ArraySegment<T> を使えば、元配列を共有しながら範囲だけを変えて処理できます。
C#for (int i = 0; i < 10000; i++)
{
ArraySegment<int> segment = new ArraySegment<int>(source, i, 100);
// segmentを使った処理
}
もちろん、ArraySegment<T> も値として作られますが、新しい配列を大量に作るよりもメモリ確保を抑えやすくなります。
そのため、大量データ処理では ArraySegment<T> を使うことで、コピー処理やGC負荷を減らせる可能性があります。
2-4. byte配列・通信バッファ・ファイル処理で効果を発揮しやすい
ArraySegment<T> は、特に byte[] と相性が良いです。
通信処理やファイル処理では、データを byte[] バッファに読み込むことがよくあります。
たとえば、ファイルから最大1024バイトずつ読み込む場合でも、毎回必ず1024バイト読み込めるとは限りません。最後の読み込みでは、200バイトだけ読み込まれることもあります。
このようなとき、バッファ全体ではなく、実際に読み込まれた範囲だけを表すために ArraySegment<byte> を使えます。
C#byte[] buffer = new byte[1024];
int bytesRead = 200;
ArraySegment<byte> data = new ArraySegment<byte>(buffer, 0, bytesRead);
この data は、buffer の先頭から200バイト分だけを表します。
バッファ全体をコピーする必要がないため、効率的にデータを扱えます。
3. C# ArraySegmentの基本的な使い方
3-1. ArraySegment<T>の基本構文
ArraySegment<T> の基本構文は次のとおりです。
C#ArraySegment<T> segment = new ArraySegment<T>(array, offset, count);
T には配列の要素型を指定します。
int[] を扱うなら ArraySegment<int>、byte[] を扱うなら ArraySegment<byte>、string[] を扱うなら ArraySegment<string> です。
たとえば、int 配列の一部を扱う場合は次のようになります。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
この場合、segment は 20, 30, 40 の範囲を表します。
3-2. コンストラクタで配列・開始位置・要素数を指定する
ArraySegment<T> のコンストラクタには、主に次の2つの使い方があります。
C#new ArraySegment<T>(array)
C#new ArraySegment<T>(array, offset, count)
配列全体を表したい場合は、配列だけを指定します。
C#int[] numbers = { 10, 20, 30 };
ArraySegment<int> all = new ArraySegment<int>(numbers);
この場合、all は配列全体を表します。
一部分だけを表したい場合は、配列、開始位置、要素数を指定します。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> part = new ArraySegment<int>(numbers, 1, 3);
この場合、part は 20, 30, 40 の範囲を表します。
3-3. Array・Offset・Countプロパティの意味
ArraySegment<T> には、主に次のプロパティがあります。
Array は、元になっている配列を表します。
Offset は、元配列のどの位置から始まるかを表します。
Count は、何個の要素を範囲として扱うかを表します。
コードで確認してみましょう。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
Console.WriteLine(segment.Offset); // 1
Console.WriteLine(segment.Count); // 3
Console.WriteLine(segment.Array[segment.Offset]); // 20
この例では、segment は numbers のインデックス1から3個分を表しています。
そのため、Offset は 1、Count は 3 になります。
3-4. foreachでArraySegmentの中身を順番に処理する
ArraySegment<T> は foreach で処理できます。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
foreach (int value in segment)
{
Console.WriteLine(value);
}
出力結果は次のようになります。
20
30
40
foreach を使うと、Offset や Count を意識せずに、セグメントの範囲内だけを順番に処理できます。
ただし、手動でインデックスを使って処理する場合は、Offset を考慮する必要があります。
C#for (int i = 0; i < segment.Count; i++)
{
Console.WriteLine(segment.Array[segment.Offset + i]);
}
このように、元配列にアクセスする場合は segment.Offset + i のように書きます。
3-5. サンプルコードで配列の一部だけを取り出す
実際に、配列の一部だけを ArraySegment<T> で扱うサンプルを見てみましょう。
C#using System;
class Program
{
static void Main()
{
string[] fruits =
{
"Apple",
"Banana",
"Orange",
"Grape",
"Melon"
};
ArraySegment<string> segment = new ArraySegment<string>(fruits, 1, 3);
foreach (string fruit in segment)
{
Console.WriteLine(fruit);
}
}
}
出力結果は次のとおりです。
Banana
Orange
Grape
このコードでは、fruits のインデックス1から3個分を ArraySegment<string> として扱っています。
新しい配列を作ることなく、Banana, Orange, Grape の範囲だけを処理できます。
4. ArraySegmentで元の配列を書き換えるとどうなる?
4-1. ArraySegmentは元配列への参照を保持している
ArraySegment<T> は、元配列への参照を保持しています。
つまり、ArraySegment<T> 自体が要素を持っているわけではありません。要素の実体は、あくまで元の配列にあります。
次のコードを見てください。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
このとき、segment は 20, 30, 40 という値をコピーして保持しているのではありません。
元配列 numbers のインデックス1から3個分を参照しているだけです。
4-2. Segment側の変更は元配列にも反映される
ArraySegment<T> の範囲内の要素を書き換えると、元配列にも反映されます。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
segment[0] = 999;
Console.WriteLine(numbers[1]); // 999
この例では、segment[0] を 999 に変更しています。
segment[0] は、元配列でいうと numbers[1] に対応します。
そのため、numbers[1] の値も 999 に変わります。
ArraySegment<T> はコピーではなくビューなので、セグメント側の変更は元配列に影響します。
4-3. 元配列を変更するとArraySegmentの見え方も変わる
逆に、元配列を変更すると、ArraySegment<T> から見える値も変わります。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
numbers[2] = 888;
foreach (int value in segment)
{
Console.WriteLine(value);
}
出力結果は次のようになります。
20
888
40
segment は numbers のインデックス1から3個分を見ているため、numbers[2] を変更すると、segment から見える2番目の値も変わります。
4-4. 読み取り専用として扱いたい場合の注意点
ArraySegment<T> は、元配列を共有する仕組みです。
そのため、「この範囲は読み取り専用として扱いたい」と思っていても、元配列やセグメント経由で値が変更される可能性があります。
たとえば、メソッドに ArraySegment<T> を渡した場合、そのメソッド内で値を書き換えることができます。
C#static void ChangeFirst(ArraySegment<int> segment)
{
segment[0] = 999;
}
呼び出し側が変更を期待していない場合、これはバグの原因になります。
読み取り専用として扱いたい場合は、次のような対策を検討します。
メソッド名やコメントで、変更しない前提を明確にする。
変更されたくない場合は、必要に応じてコピーを渡す。
読み取り専用のAPI設計にしたい場合は、ReadOnlyMemory<T> や ReadOnlySpan<T> も検討する。
ArraySegment<T> は便利ですが、元配列を共有する以上、「誰がその配列を変更できるのか」を意識することが大切です。
5. ArraySegmentと配列コピーの違いをコードで比較
5-1. Array.Copyで一部をコピーする例
まずは、Array.Copy を使って配列の一部をコピーする例です。
C#int[] source = { 10, 20, 30, 40, 50, 60 };
int[] copied = new int[3];
Array.Copy(source, 2, copied, 0, 3);
foreach (int value in copied)
{
Console.WriteLine(value);
}
出力結果は次のとおりです。
30
40
50
この方法では、copied という新しい配列が作られます。
元配列とは別の配列なので、copied を変更しても source には影響しません。
C#copied[0] = 999;
Console.WriteLine(source[2]); // 30
コピー後に独立した配列として扱いたい場合は、Array.Copy が適しています。
5-2. LINQのSkip・Take・ToArrayでコピーする例
LINQを使うと、より簡潔に書けます。
C#using System;
using System.Linq;
class Program
{
static void Main()
{
int[] source = { 10, 20, 30, 40, 50, 60 };
int[] copied = source.Skip(2).Take(3).ToArray();
foreach (int value in copied)
{
Console.WriteLine(value);
}
}
}
出力結果は次のとおりです。
30
40
50
Skip(2) で先頭2個を飛ばし、Take(3) で3個取り出し、ToArray() で配列に変換しています。
読みやすいコードですが、ToArray() によって新しい配列が作られます。
そのため、処理のわかりやすさを重視する場面では便利ですが、コピーコストを避けたい場面では注意が必要です。
5-3. ArraySegmentでコピーせずに範囲を扱う例
次に、ArraySegment<T> を使う例です。
C#int[] source = { 10, 20, 30, 40, 50, 60 };
ArraySegment<int> segment = new ArraySegment<int>(source, 2, 3);
foreach (int value in segment)
{
Console.WriteLine(value);
}
出力結果は次のとおりです。
30
40
50
結果だけを見ると、Array.Copy や LINQ の ToArray() と似ています。
しかし、ArraySegment<T> は新しい配列を作っていません。
元配列 source のインデックス2から3個分を参照しているだけです。
そのため、segment 経由で値を変更すると、元配列も変わります。
C#segment[0] = 999;
Console.WriteLine(source[2]); // 999
この違いを理解することが、ArraySegment<T> を使いこなすうえで重要です。
5-4. どの方法を選ぶべきかの判断基準
配列の一部を扱う方法は、目的によって選び分けるのが大切です。
元配列とは独立した新しい配列が必要なら、Array.Copy や ToArray() を使うのが安全です。
コピーを避けたい場合や、大きな配列の一部を効率よく渡したい場合は、ArraySegment<T> が向いています。
処理の読みやすさを重視し、データ量が小さい場合は、LINQの Skip・Take・ToArray でも問題ないことが多いです。
高性能な範囲操作や新しいC#コードでは、Span<T> や Memory<T> も候補になります。
つまり、単純に「ArraySegmentが常に良い」というわけではありません。
コピーして安全に扱いたいのか、コピーせず効率を重視したいのかによって選ぶべき方法が変わります。
6. ArraySegmentを使うときの注意点
6-1. offsetとcountの指定ミスに注意する
ArraySegment<T> を使うときは、offset と count の指定ミスに注意が必要です。
C#ArraySegment<int> segment = new ArraySegment<int>(array, offset, count);
offset は開始位置です。
count は要素数です。
初心者が間違えやすいのは、count を「終了位置」と勘違いすることです。
たとえば、次のコードを見てください。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
これは、インデックス1からインデックス3までという意味ではありません。
インデックス1から3個分という意味です。
つまり、対象になるのは 20, 30, 40 です。
6-2. 範囲外を指定すると例外が発生する
offset や count に不正な値を指定すると、例外が発生します。
C#int[] numbers = { 10, 20, 30 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 2, 5);
この例では、インデックス2から5個分を指定しています。
しかし、元配列には3個しか要素がありません。
そのため、範囲外となり例外が発生します。
安全に使うには、事前に範囲を確認するとよいです。
C#int offset = 2;
int count = 1;
if (offset >= 0 && count >= 0 && offset + count <= numbers.Length)
{
ArraySegment<int> segment = new ArraySegment<int>(numbers, offset, count);
}
特に、外部入力や計算結果をもとに offset と count を決める場合は、範囲チェックを忘れないようにしましょう。
6-3. default(ArraySegment<T>)の扱いに注意する
ArraySegment<T> は構造体なので、default(ArraySegment<T>) を作ることができます。
C#ArraySegment<int> segment = default;
この場合、segment.Array は null になります。
そのため、何も考えずに segment.Array にアクセスすると、NullReferenceException の原因になることがあります。
C#ArraySegment<int> segment = default;
Console.WriteLine(segment.Array.Length); // 例外の可能性
ArraySegment<T> を受け取るメソッドでは、必要に応じて segment.Array が null でないか確認すると安全です。
C#static void PrintSegment(ArraySegment<int> segment)
{
if (segment.Array == null)
{
return;
}
foreach (int value in segment)
{
Console.WriteLine(value);
}
}
ただし、通常のコンストラクタで作成した ArraySegment<T> であれば、元配列が設定されています。
6-4. ToArrayを呼ぶと結局コピーが発生する
ArraySegment<T> はコピーを避けられるのがメリットです。
しかし、ToArray() を呼ぶと、その時点で新しい配列が作られます。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
int[] copied = segment.ToArray();
この場合、copied には 20, 30, 40 が入ります。
ただし、これは元配列とは別の新しい配列です。
ToArray() が悪いわけではありません。元配列から独立した配列が必要な場合には便利です。
しかし、「コピーを減らすためにArraySegmentを使っている」のに最後に毎回 ToArray() を呼んでしまうと、メリットが薄れてしまいます。
6-5. 元配列を共有するため意図しない変更に注意する
ArraySegment<T> は元配列を共有します。
そのため、複数の処理が同じ配列を参照している場合、どこかで値が変更されると、別の場所にも影響します。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> a = new ArraySegment<int>(numbers, 1, 3);
ArraySegment<int> b = new ArraySegment<int>(numbers, 2, 2);
a[1] = 999;
Console.WriteLine(b[0]); // 999
この例では、a[1] と b[0] がどちらも元配列の numbers[2] を参照しています。
そのため、片方から変更すると、もう片方から見える値も変わります。
このような共有は効率面ではメリットですが、意図しない変更が起こる可能性もあります。
6-6. マルチスレッド処理では同じ配列の共有範囲に注意する
複数のスレッドやタスクで同じ配列を扱う場合も注意が必要です。
たとえば、配列を複数の ArraySegment<T> に分けて並列処理する場合、範囲が重ならなければ比較的扱いやすいです。
C#int[] numbers = new int[1000];
ArraySegment<int> firstHalf = new ArraySegment<int>(numbers, 0, 500);
ArraySegment<int> secondHalf = new ArraySegment<int>(numbers, 500, 500);
このように、完全に別々の範囲を処理するなら、意図しない競合は起きにくくなります。
しかし、複数のセグメントが同じ範囲を参照していたり、元配列全体を別の処理が変更していたりすると、データ競合の原因になります。
マルチスレッド処理では、次の点を意識しましょう。
範囲が重なっていないか確認する。
同じ要素を複数のスレッドから同時に書き換えない。
必要に応じてロックや同期処理を使う。
読み取り専用にできるなら、書き換えない設計にする。
ArraySegment<T> は配列を共有する仕組みなので、並列処理では特に共有範囲を慎重に管理する必要があります。
7. ArraySegment・Span<T>・Memory<T>の違い
7-1. ArraySegmentは配列専用の範囲参照
ArraySegment<T> は、配列の一部分を表すための型です。
対象は配列です。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
このように、元になるのは T[] です。
ArraySegment<T> は昔から使われている仕組みで、既存のAPIやライブラリでも見かけることがあります。
特徴はシンプルです。
元配列を参照する。
開始位置と要素数を持つ。
配列コピーを避けられる。
元配列の変更が反映される。
配列専用なので、まず「範囲参照」の考え方を理解するにはわかりやすい型です。
7-2. Span<T>はより高速で柔軟な範囲操作に向いている
Span<T> は、配列やメモリ上の連続した領域の一部を扱うための型です。
ArraySegment<T> よりも新しいC#コードでよく使われます。
C#int[] numbers = { 10, 20, 30, 40, 50 };
Span<int> span = numbers.AsSpan(1, 3);
span[0] = 999;
Console.WriteLine(numbers[1]); // 999
Span<T> もコピーではなく、元のデータを参照します。
ArraySegment<T> と比べると、より柔軟で高性能な範囲操作に向いています。
ただし、Span<T> はスタック上で安全に扱うための制約があり、フィールドに保持できない場合がある、非同期メソッドをまたいで使いにくい、といった特徴があります。
そのため、どこでも万能に使えるわけではありません。
7-3. Memory<T>は非同期処理でも扱いやすい
Memory<T> は、Span<T> と似ていますが、非同期処理やフィールド保持にも使いやすい型です。
C#int[] numbers = { 10, 20, 30, 40, 50 };
Memory<int> memory = numbers.AsMemory(1, 3);
Memory<T> は、必要に応じて Span<T> として取り出して処理できます。
C#Span<int> span = memory.Span;
span[0] = 999;
非同期処理でバッファを渡したい場合や、クラスのフィールドとして範囲情報を保持したい場合は、Memory<T> が候補になります。
読み取り専用にしたい場合は、ReadOnlyMemory<T> もあります。
7-4. 初心者はArraySegmentから理解すると範囲参照の考え方がわかりやすい
初心者がいきなり Span<T> や Memory<T> を学ぶと、少し難しく感じるかもしれません。
特に Span<T> には、スタック、ヒープ、ref struct、非同期処理での制約など、理解すべき要素が多くあります。
一方で、ArraySegment<T> は考え方がシンプルです。
配列がある。
開始位置がある。
要素数がある。
その範囲をコピーせずに見る。
この4点を理解すれば、基本的な使い方はつかめます。
そのため、まず ArraySegment<T> で「配列の一部をコピーせずに参照する」という考え方を理解し、その後に Span<T> や Memory<T> に進むとスムーズです。
7-5. 現代のC#ではどれを使うべきか
現代のC#では、用途によって使い分けるのが基本です。
既存APIが ArraySegment<T> を受け取る場合や、配列の一部をシンプルに表したい場合は、ArraySegment<T> が使えます。
メソッド内で高速に範囲操作したい場合は、Span<T> が有力です。
非同期処理やフィールド保持が必要な場合は、Memory<T> や ReadOnlyMemory<T> が向いています。
読み取り専用の範囲を表したい場合は、ReadOnlySpan<T> や ReadOnlyMemory<T> も検討できます。
新しくAPIを設計する場合は、用途に応じて Span<T> や Memory<T> を選ぶことが増えています。
ただし、ArraySegment<T> は今でも意味のある型です。特に、配列ベースの既存コードやバッファ処理では十分に役立ちます。
8. ArraySegmentの実践的な使用例
8-1. byte配列の一部をメソッドに渡す
ArraySegment<T> の実践的な使い方として、byte[] の一部をメソッドに渡す例を見てみましょう。
C#static void ProcessData(ArraySegment<byte> data)
{
foreach (byte b in data)
{
Console.WriteLine(b);
}
}
byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8 };
ArraySegment<byte> segment = new ArraySegment<byte>(buffer, 2, 4);
ProcessData(segment);
出力結果は次のようになります。
3
4
5
6
このコードでは、buffer のインデックス2から4個分だけを ProcessData メソッドに渡しています。
新しい byte[] を作らずに、必要な範囲だけを処理できます。
通信処理やバイナリデータ処理では、このような使い方がよくあります。
8-2. 大きな配列を分割して処理する
大きな配列を一定サイズごとに分割して処理したい場合にも、ArraySegment<T> は便利です。
C#int[] numbers = Enumerable.Range(1, 25).ToArray();
int chunkSize = 10;
for (int offset = 0; offset < numbers.Length; offset += chunkSize)
{
int count = Math.Min(chunkSize, numbers.Length - offset);
ArraySegment<int> chunk = new ArraySegment<int>(numbers, offset, count);
Console.WriteLine($"offset={chunk.Offset}, count={chunk.Count}");
foreach (int value in chunk)
{
Console.Write(value + " ");
}
Console.WriteLine();
}
このコードでは、25個の要素を10個ずつに分割しています。
最後の範囲は10個に満たないため、Math.Min を使って残りの要素数を計算しています。
このようにすれば、大きな配列をコピーせずに、範囲ごとに処理できます。
8-3. ネットワーク受信バッファの一部だけを扱う
ネットワーク通信では、受信バッファ全体ではなく、実際に受信したデータ部分だけを扱いたい場面があります。
C#byte[] receiveBuffer = new byte[1024];
// 例として、実際には200バイト受信したとする
int receivedBytes = 200;
ArraySegment<byte> receivedData =
new ArraySegment<byte>(receiveBuffer, 0, receivedBytes);
HandleReceivedData(receivedData);
C#static void HandleReceivedData(ArraySegment<byte> data)
{
Console.WriteLine($"受信データサイズ: {data.Count} bytes");
// data.Count の範囲だけを処理する
foreach (byte b in data)
{
// 受信データの処理
}
}
receiveBuffer のサイズは1024バイトですが、実際に有効なデータは200バイトだけです。
このようなとき、ArraySegment<byte> を使えば、有効な範囲だけを明確に表現できます。
8-4. ファイル読み込み時のバッファ処理に使う
ファイルを少しずつ読み込む処理でも、ArraySegment<byte> が使えます。
C#using System;
using System.IO;
class Program
{
static void Main()
{
byte[] buffer = new byte[4096];
using FileStream stream = File.OpenRead("sample.dat");
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
ArraySegment<byte> data =
new ArraySegment<byte>(buffer, 0, bytesRead);
ProcessFileData(data);
}
}
static void ProcessFileData(ArraySegment<byte> data)
{
Console.WriteLine($"読み込んだサイズ: {data.Count} bytes");
// dataの範囲だけを処理する
}
}
FileStream.Read は、実際に読み込んだバイト数を返します。
そのため、バッファ全体ではなく、bytesRead の範囲だけが有効なデータです。
ArraySegment<byte> を使うことで、「このバッファの先頭から bytesRead バイト分だけを処理する」という意図を明確にできます。
8-5. 複数タスクで配列の別々の範囲を処理する
配列を複数の範囲に分けて、別々のタスクで処理することもできます。
C#int[] numbers = Enumerable.Range(1, 1000).ToArray();
ArraySegment<int> first =
new ArraySegment<int>(numbers, 0, 500);
ArraySegment<int> second =
new ArraySegment<int>(numbers, 500, 500);
Task task1 = Task.Run(() => ProcessRange(first));
Task task2 = Task.Run(() => ProcessRange(second));
Task.WaitAll(task1, task2);
C#static void ProcessRange(ArraySegment<int> segment)
{
long sum = 0;
foreach (int value in segment)
{
sum += value;
}
Console.WriteLine(sum);
}
この例では、配列の前半と後半を別々のタスクで処理しています。
範囲が重なっておらず、読み取りだけであれば比較的安全に扱えます。
ただし、複数のタスクで同じ配列を書き換える場合は注意が必要です。範囲が重なっていると、意図しない結果になる可能性があります。
9. ArraySegmentを使うべきケース・使わない方がよいケース
9-1. コピーコストを減らしたいときはArraySegmentが有効
ArraySegment<T> を使うべき代表的なケースは、配列コピーのコストを減らしたいときです。
たとえば、次のような場面です。
大きな配列の一部だけを処理したい。
byte配列のバッファを何度も使い回したい。
配列を分割して処理したい。
メソッドに範囲情報だけを渡したい。
新しい配列を大量に作りたくない。
このような場面では、Array.Copy や ToArray() で毎回コピーするよりも、ArraySegment<T> を使うことでメモリ使用量を抑えやすくなります。
特に、通信処理、ファイル処理、バイナリデータ処理などでは効果を感じやすいです。
9-2. 元配列を変更されたくない場合はコピーも検討する
一方で、元配列を変更されたくない場合は、ArraySegment<T> が常に最適とは限りません。
ArraySegment<T> は元配列を共有するため、セグメント経由で値を変更される可能性があります。
C#static void Update(ArraySegment<int> segment)
{
segment[0] = 999;
}
このようなメソッドに渡すと、呼び出し元の配列も変更されます。
変更されたくない場合は、あえてコピーを作る方が安全です。
C#int[] copied = segment.ToArray();
コピーにはコストがありますが、元配列から独立したデータとして扱えるというメリットがあります。
安全性を優先するなら、コピーを選ぶことも重要です。
9-3. 新しいC#コードではSpan<T>やMemory<T>も候補にする
新しいC#コードを書く場合は、ArraySegment<T> だけでなく、Span<T> や Memory<T> も候補にするとよいです。
メソッド内だけで一時的に範囲を扱うなら、Span<T> が便利です。
C#Span<int> span = numbers.AsSpan(1, 3);
非同期処理やフィールド保持が必要なら、Memory<T> が向いています。
C#Memory<int> memory = numbers.AsMemory(1, 3);
読み取り専用で扱いたい場合は、ReadOnlySpan<T> や ReadOnlyMemory<T> を使うこともできます。
ただし、既存のAPIが ArraySegment<T> を受け取る場合や、配列専用の範囲参照をシンプルに扱いたい場合は、ArraySegment<T> で十分です。
9-4. APIの引数として受け渡すときの設計ポイント
自分でメソッドやAPIを設計するときは、どの型で範囲を受け取るかを考える必要があります。
配列の一部だけを受け取りたいなら、次のように ArraySegment<T> を引数にできます。
C#static void Process(ArraySegment<byte> data)
{
// dataの範囲だけを処理する
}
この設計にすると、呼び出し側は配列全体ではなく、必要な範囲だけを渡せます。
ただし、現代のC#では、用途によって次のような設計も考えられます。
読み取りだけなら ReadOnlySpan<T>。
一時的な高速処理なら Span<T>。
非同期処理なら Memory<T> や ReadOnlyMemory<T>。
既存互換や配列専用なら ArraySegment<T>。
API設計では、変更してよいのか、読み取り専用なのか、非同期で使うのか、配列以外も受け取りたいのかを考えると選びやすくなります。
10. C# ArraySegmentに関するよくある質問
10-1. ArraySegmentは配列をコピーしますか?
いいえ、ArraySegment<T> は配列をコピーしません。
ArraySegment<T> は、元配列、開始位置、要素数を保持しているだけです。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
このコードでは、20, 30, 40 だけを含む新しい配列が作られるわけではありません。
元配列 numbers の一部を参照しているだけです。
ただし、segment.ToArray() を呼ぶと、新しい配列が作られます。
10-2. ArraySegmentの中身を変更できますか?
はい、変更できます。
ArraySegment<T> の要素を変更すると、元配列も変更されます。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
segment[0] = 999;
Console.WriteLine(numbers[1]); // 999
segment[0] は、元配列の numbers[1] に対応しています。
そのため、セグメント側で変更した値は、元配列にも反映されます。
10-3. ArraySegmentとList<T>は何が違いますか?
ArraySegment<T> は、配列の一部を表すための型です。
一方、List<T> は、要素を追加・削除できる可変長のコレクションです。
ArraySegment<T> は元配列を共有しており、新しい要素を追加したり削除したりする用途には向いていません。
C#ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
これは、配列の一部を参照しているだけです。
一方、List<T> は次のように要素を追加できます。
C#List<int> list = new List<int>();
list.Add(10);
list.Add(20);
配列の一部をコピーせずに扱いたいなら ArraySegment<T>。
要素を追加・削除しながら管理したいなら List<T>。
このように使い分けるとよいです。
10-4. ArraySegmentを配列に戻すにはどうすればよいですか?
ArraySegment<T> を新しい配列にしたい場合は、ToArray() を使います。
C#int[] numbers = { 10, 20, 30, 40, 50 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
int[] array = segment.ToArray();
この場合、array には 20, 30, 40 が入ります。
ただし、ToArray() を呼ぶとコピーが発生します。
コピーを避けたい場合は、ArraySegment<T> のまま処理するか、元配列と Offset・Count を使って処理しましょう。
10-5. ArraySegmentは今でも使うべきですか?
はい、用途によっては今でも使う価値があります。
特に、配列の一部をコピーせずに扱いたい場合や、既存APIが ArraySegment<T> を使っている場合には有効です。
一方で、新しいC#コードでは、Span<T>、ReadOnlySpan<T>、Memory<T>、ReadOnlyMemory<T> を使う方が適している場面も増えています。
メソッド内の一時的な範囲操作なら Span<T>。
非同期処理なら Memory<T>。
読み取り専用なら ReadOnlySpan<T> や ReadOnlyMemory<T>。
配列専用でシンプルに範囲を表したいなら ArraySegment<T>。
このように、目的に応じて使い分けるのがおすすめです。
まとめ
ArraySegment<T> は、C#で配列の一部分をコピーせずに扱うための便利な型です。
元配列、開始位置、要素数を持つことで、「配列のこの範囲だけを使う」という表現ができます。
Array.Copy や LINQ の Skip・Take・ToArray を使うと、新しい配列が作られます。一方、ArraySegment<T> は元配列を共有するため、コピーを減らし、メモリ使用量やGC負荷を抑えやすくなります。
特に、byte配列、通信バッファ、ファイル処理、大量データの分割処理では効果を発揮しやすいです。
ただし、ArraySegment<T> は新しい配列ではありません。元配列を共有しているため、セグメント側の変更は元配列にも反映されます。逆に、元配列の変更もセグメントから見える値に影響します。
そのため、使うときは次の点に注意しましょう。
offset と count の指定ミスに注意する。
範囲外を指定しないようにする。
default(ArraySegment<T>) では Array が null になる可能性を考慮する。
ToArray() を呼ぶとコピーが発生することを理解する。
元配列の共有による意図しない変更に注意する。
新しいC#コードでは Span<T> や Memory<T> も候補にする。
初心者の方は、まず ArraySegment<T> を通じて「配列の一部をコピーせずに参照する」という考え方を理解すると、Span<T> や Memory<T> の理解にもつながります。
C#で配列やバッファを効率よく扱いたい場合は、ArraySegment<T> の特徴と注意点を押さえて、適切な場面で活用しましょう。

