C# ArraySegmentとは?配列コピーを減らす使い方・メリット・注意点を初心者向けに解説

はじめに

C#で配列の一部分だけを扱いたいとき、よく使われる方法として Array.Copy や LINQ の SkipTakeToArray があります。

たとえば、配列の途中から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> は、配列の一部分を表すための構造体です。

名前のとおり、ArraySegment、つまり「配列の区間」を表します。

たとえば、次のような配列があるとします。

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);

この場合、segment20, 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);

この場合、part20, 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

この例では、segmentnumbers のインデックス1から3個分を表しています。

そのため、Offset1Count3 になります。

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 を使うと、OffsetCount を意識せずに、セグメントの範囲内だけを順番に処理できます。

ただし、手動でインデックスを使って処理する場合は、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);

このとき、segment20, 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

segmentnumbers のインデックス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.CopyToArray() を使うのが安全です。

コピーを避けたい場合や、大きな配列の一部を効率よく渡したい場合は、ArraySegment<T> が向いています。

処理の読みやすさを重視し、データ量が小さい場合は、LINQの SkipTakeToArray でも問題ないことが多いです。

高性能な範囲操作や新しいC#コードでは、Span<T>Memory<T> も候補になります。

つまり、単純に「ArraySegmentが常に良い」というわけではありません。

コピーして安全に扱いたいのか、コピーせず効率を重視したいのかによって選ぶべき方法が変わります。

6. ArraySegmentを使うときの注意点

6-1. offsetとcountの指定ミスに注意する

ArraySegment<T> を使うときは、offsetcount の指定ミスに注意が必要です。

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. 範囲外を指定すると例外が発生する

offsetcount に不正な値を指定すると、例外が発生します。

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);
}

特に、外部入力や計算結果をもとに offsetcount を決める場合は、範囲チェックを忘れないようにしましょう。

6-3. default(ArraySegment<T>)の扱いに注意する

ArraySegment<T> は構造体なので、default(ArraySegment<T>) を作ることができます。

C#
ArraySegment<int> segment = default;

この場合、segment.Arraynull になります。

そのため、何も考えずに segment.Array にアクセスすると、NullReferenceException の原因になることがあります。

C#
ArraySegment<int> segment = default;

Console.WriteLine(segment.Array.Length); // 例外の可能性

ArraySegment<T> を受け取るメソッドでは、必要に応じて segment.Arraynull でないか確認すると安全です。

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.CopyToArray() で毎回コピーするよりも、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> のまま処理するか、元配列と OffsetCount を使って処理しましょう。

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 の SkipTakeToArray を使うと、新しい配列が作られます。一方、ArraySegment<T> は元配列を共有するため、コピーを減らし、メモリ使用量やGC負荷を抑えやすくなります。

特に、byte配列、通信バッファ、ファイル処理、大量データの分割処理では効果を発揮しやすいです。

ただし、ArraySegment<T> は新しい配列ではありません。元配列を共有しているため、セグメント側の変更は元配列にも反映されます。逆に、元配列の変更もセグメントから見える値に影響します。

そのため、使うときは次の点に注意しましょう。

offsetcount の指定ミスに注意する。

範囲外を指定しないようにする。

default(ArraySegment<T>) では Arraynull になる可能性を考慮する。

ToArray() を呼ぶとコピーが発生することを理解する。

元配列の共有による意図しない変更に注意する。

新しいC#コードでは Span<T>Memory<T> も候補にする。

初心者の方は、まず ArraySegment<T> を通じて「配列の一部をコピーせずに参照する」という考え方を理解すると、Span<T>Memory<T> の理解にもつながります。

C#で配列やバッファを効率よく扱いたい場合は、ArraySegment<T> の特徴と注意点を押さえて、適切な場面で活用しましょう。