C# Memoryとは?Spanとの違い・使い方・メモリ効率化を初心者にもわかりやすく解説
はじめに
C#で配列、文字列、ファイル、通信データなどを扱っていると、「一部のデータだけを処理したい」「大きなバッファを毎回コピーしたくない」「async/awaitでも効率よくメモリを扱いたい」と感じる場面があります。
そこで役立つのが、C#のMemory<T>です。
Memory<T>は、C# memory周りのパフォーマンス改善でよく登場する型で、配列やバッファなどの「連続したメモリ領域の一部」をコピーせずに扱うために使われます。似た型にSpan<T>がありますが、Memory<T>はSpan<T>より制約が少なく、非同期処理やクラスのフィールドでも使いやすいのが特徴です。
この記事では、Memory<T>とは何か、Span<T>との違い、基本的な使い方、実用コード、注意点、関連する型まで、初心者にもわかりやすく解説します。
1. C#のMemory<T>とは?まず押さえる基本概念
1-1. Memory<T>は「連続したメモリ領域」を扱うための型
Memory<T>は、配列やバッファのような「連続したデータ領域」を表すための構造体です。
たとえば、次のような配列があるとします。
C#int[] numbers = { 10, 20, 30, 40, 50 };
この配列全体、または一部をMemory<int>として扱えます。
C#Memory<int> memory = numbers;
Memory<int> part = numbers.AsMemory(1, 3); // 20, 30, 40
ここで重要なのは、partが新しい配列を作っているわけではない点です。numbersのうち「インデックス1から3個分」という範囲を表しているだけです。
Microsoftの公式ドキュメントでも、Memory<T>は連続したメモリ領域を表す型であり、Span<T>と似ている一方でref structではないため、クラスのフィールドやawait/yieldをまたぐ処理でも使えると説明されています。
1-2. 配列・文字列・バッファをコピーせずに参照できる仕組み
Memory<T>の大きな役割は、「データそのもの」ではなく「データの範囲」を表すことです。
たとえば、10万件のデータが入った配列から一部だけ処理したい場合、従来は次のように別の配列を作ることがありました。
C#int[] source = Enumerable.Range(1, 100000).ToArray();
int[] copied = source.Skip(1000).Take(500).ToArray();
この方法では、500個分の新しい配列が作られます。小さなデータなら問題になりませんが、大量データを何度も処理する場合は、不要なメモリ確保が増えます。
Memory<T>を使うと、次のようにコピーせず範囲だけを切り出せます。
C#Memory<int> memory = source.AsMemory(1000, 500);
このmemoryは、元のsource配列の一部を指しています。新しい500要素の配列を作っているわけではありません。
1-3. Memory<T>が登場した背景:メモリ効率とパフォーマンス改善
C#や.NETでは、開発者が安全にコードを書けるよう、ガベージコレクション、配列、文字列、コレクションなど便利な仕組みが提供されています。
一方で、次のような処理ではメモリ効率が重要になります。
大きなファイルを読み込む
ネットワーク通信でバイト列を扱う
画像や音声などのバイナリデータを処理する
高頻度で配列を作成・破棄する
Webサーバーや常駐サービスで大量リクエストを処理する
こうした場面で毎回データをコピーすると、メモリ使用量が増え、GCの負荷も高くなります。
Memory<T>は、「コピーせずに範囲を渡す」「必要な部分だけを処理する」「非同期処理でも安全に使いやすい」といった目的で使われます。
1-4. 初心者が混乱しやすい「メモリを持つ」のではなく「範囲を表す」という考え方
Memory<T>という名前を見ると、「メモリそのものを持っている型」と思うかもしれません。
しかし、多くの場合、Memory<T>はデータ本体を所有しているわけではありません。配列や文字列など、別の場所にあるデータの「範囲」を表しています。
イメージとしては、次のような関係です。
元の配列: [10, 20, 30, 40, 50]
↑ ↑
Memory<T>: ここから3個分
つまり、Memory<T>は「配列の一部を見るための窓」のようなものです。
そのため、元の配列を書き換えると、Memory<T>から見える内容も変わります。この性質は便利でもあり、注意が必要な点でもあります。
2. Memory<T>で解決できる悩みと使うメリット
2-1. 大きな配列やバッファのコピーを減らせる
Memory<T>の最大のメリットは、不要なコピーを減らせることです。
たとえば、配列の一部だけをメソッドに渡したい場合、従来は新しい配列を作ることがありました。
C#static int Sum(int[] values)
{
return values.Sum();
}
int[] source = { 1, 2, 3, 4, 5 };
int[] copied = source.Skip(1).Take(3).ToArray();
int result = Sum(copied);
これをMemory<T>で書くと、次のようにできます。
C#static int Sum(ReadOnlyMemory<int> values)
{
int total = 0;
foreach (int value in values.Span)
{
total += value;
}
return total;
}
int[] source = { 1, 2, 3, 4, 5 };
int result = Sum(source.AsMemory(1, 3));
source.AsMemory(1, 3)は、2, 3, 4の範囲を表します。新しい配列を作らずに処理できます。
2-2. Sliceで一部だけを安全に切り出せる
Memory<T>にはSliceメソッドがあります。これを使うと、現在のMemory<T>からさらに一部だけを切り出せます。
C#int[] numbers = { 10, 20, 30, 40, 50 };
Memory<int> memory = numbers;
Memory<int> sliced = memory.Slice(1, 3); // 20, 30, 40
Sliceは「開始位置」と「長さ」を指定します。
C#Memory<int> sliced = memory.Slice(start: 1, length: 3);
このように、配列全体を渡すのではなく、必要な範囲だけを明示できます。
Microsoftのドキュメントでも、Memory<T>.Sliceは現在のメモリ領域から指定した範囲のスライスを作るメソッドとして説明され、ToArrayはメモリ領域の内容を新しい配列へコピーすると説明されています。
2-3. 非同期処理でも使いやすい
Memory<T>は、async/awaitを使う非同期処理と相性がよい型です。
たとえば、ストリームからデータを非同期で読み込む処理では、Memory<byte>をバッファとして渡せます。
C#static async ValueTask<int> ReadAsync(
Stream stream,
Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
return await stream.ReadAsync(buffer, cancellationToken);
}
Span<T>は高速ですが、awaitをまたいで保持する使い方に制約があります。一方、Memory<T>はSpan<T>より制約が少なく、非同期APIの引数として使いやすいです。
2-4. GC負荷や不要なアロケーションを抑えやすい
アロケーションとは、メモリ上に新しいオブジェクトや配列を確保することです。
小さなアプリではあまり問題にならないこともありますが、次のような処理では影響が大きくなります。
1秒間に大量のリクエストを処理するWeb API
大きなログファイルを読み続けるバッチ処理
バイナリデータを分割・結合する通信処理
画像や動画の一部を繰り返し処理するアプリ
Memory<T>を使えば、既存の配列やバッファの一部を参照して処理できるため、不要な配列生成を減らせます。
ただし、Memory<T>を使えば必ず速くなるわけではありません。パフォーマンス改善を目的に導入する場合は、必ず計測することが大切です。
2-5. ファイル処理・通信処理・画像処理など大量データ処理と相性がよい
Memory<T>は、特にbyte配列を扱う処理でよく使われます。
たとえば、次のような場面です。
C#Memory<byte> buffer = new byte[4096];
ファイルやネットワーク通信では、データを一度にすべて読み込むのではなく、一定サイズのバッファに分けて処理することがよくあります。
C#byte[] buffer = new byte[4096];
int bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length));
読み込んだバイト数だけを処理したい場合は、Sliceを使えます。
C#Memory<byte> actualData = buffer.AsMemory(0, bytesRead);
このように、Memory<T>は「大きなデータの一部だけを効率よく扱う」場面に向いています。
3. Memory<T>とSpan<T>の違い
3-1. Memory<T>とSpan<T>の共通点
Memory<T>とSpan<T>は、どちらも連続したメモリ領域を扱うための型です。
共通点は次のとおりです。
| 共通点 | 内容 |
|---|---|
| 連続した領域を表す | 配列やバッファの一部を扱える |
| コピーを減らせる | 範囲を切り出しても基本的に新しい配列を作らない |
Sliceが使える | 必要な範囲だけを取り出せる |
| 型安全 | Tによって要素型が決まる |
| 高速な処理に向く | 大量データ処理で有効な場合がある |
たとえば、配列の一部を扱うという点では、どちらも似ています。
C#int[] numbers = { 10, 20, 30, 40, 50 };
Span<int> span = numbers.AsSpan(1, 3);
Memory<int> memory = numbers.AsMemory(1, 3);
どちらも20, 30, 40の範囲を表します。
3-2. 最大の違いは「ヒープに置けるか」「async/awaitで使えるか」
Memory<T>とSpan<T>の最大の違いは、扱える場所の制約です。
Span<T>はref structです。これは高速で安全なメモリアクセスを実現する一方、ヒープに置けないなどの制約があります。Memory<T>はref structではないため、クラスのフィールドに置いたり、awaitやyieldをまたいで使ったりできます。
簡単にいうと、次のような違いです。
Span<T> : 速いが制約が多い
Memory<T> : Span<T>より柔軟に保持・受け渡しできる
3-3. Span<T>は高速だが制約が多い
Span<T>は、メモリ領域を直接的に操作するための強力な型です。
たとえば、次のように配列の一部を書き換えられます。
C#int[] numbers = { 1, 2, 3, 4, 5 };
Span<int> span = numbers.AsSpan(1, 3);
span[0] = 99;
Console.WriteLine(numbers[1]); // 99
Span<T>は一時的な処理には非常に便利です。
しかし、次のような使い方には向きません。
C#class Sample
{
// Span<T>は通常、クラスのフィールドとして保持できない
// private Span<int> _span;
}
また、asyncメソッド内でawaitをまたいでSpan<T>を保持するような使い方にも制約があります。
3-4. Memory<T>は制約が少なく非同期処理やクラスのフィールドで使える
Memory<T>は、Span<T>より柔軟に使えます。
たとえば、クラスのフィールドに保持できます。
C#class BufferHolder
{
private Memory<byte> _buffer;
public BufferHolder(Memory<byte> buffer)
{
_buffer = buffer;
}
public Memory<byte> Buffer => _buffer;
}
非同期メソッドの引数としても扱いやすいです。
C#static async Task FillAsync(Stream stream, Memory<byte> buffer)
{
await stream.ReadAsync(buffer);
}
ただし、実際に要素を読み書きするときは、Memory<T>.SpanでSpan<T>を取り出して操作することが多いです。
C#Memory<int> memory = new int[3];
memory.Span[0] = 10;
memory.Span[1] = 20;
memory.Span[2] = 30;
3-5. Memory<T>とSpan<T>の使い分け早見表
| 比較項目 | Span<T> | Memory<T> |
|---|---|---|
| 主な用途 | 一時的・同期的な高速処理 | 保持・受け渡し・非同期処理 |
| クラスのフィールド | 基本的に不可 | 可能 |
async/awaitとの相性 | 制約が多い | 使いやすい |
| ヒープ上での保持 | 不可 | 可能 |
| 要素アクセス | 直接できる | .Span経由で行う |
| パフォーマンス | 非常に高い | 柔軟性が高い |
| API引数 | 同期処理向き | 非同期API向き |
| 代表例 | パース、検索、一時加工 | ストリーム、バッファ、非同期I/O |
3-6. 迷ったときの判断基準:同期処理ならSpan<T>、非同期や保持が必要ならMemory<T>
使い分けに迷ったら、次の基準で考えるとわかりやすいです。
同期処理の中だけで一時的に使うならSpan<T>。
C#static void Normalize(Span<int> values)
{
for (int i = 0; i < values.Length; i++)
{
values[i] = Math.Max(0, values[i]);
}
}
非同期処理に渡す、クラスに保持する、あとで使う可能性があるならMemory<T>。
C#static async Task<int> ReadDataAsync(Stream stream, Memory<byte> buffer)
{
return await stream.ReadAsync(buffer);
}
つまり、Span<T>は「今この場で処理する」ための型、Memory<T>は「あとで使える形で範囲を持ち回る」ための型と考えると理解しやすくなります。
4. Memory<T>の基本的な使い方
4-1. 配列からMemory<T>を作成する
配列は暗黙的にMemory<T>へ変換できます。
C#int[] numbers = { 1, 2, 3, 4, 5 };
Memory<int> memory = numbers;
このmemoryは、numbers配列全体を表します。
一部だけを対象にしたい場合は、コンストラクターも使えます。
C#Memory<int> part = new Memory<int>(numbers, 1, 3);
これは、インデックス1から3個分、つまり2, 3, 4を表します。
4-2. AsMemoryでMemory<T>に変換する
実務では、AsMemoryを使う書き方がよく使われます。
C#int[] numbers = { 10, 20, 30, 40, 50 };
Memory<int> all = numbers.AsMemory();
Memory<int> part = numbers.AsMemory(1, 3);
AsMemory()は配列全体、AsMemory(1, 3)は一部を表します。
配列に対する範囲インデクサーをそのまま使うとコピーが発生するケースがあるため、コピーを避けたい場合はAsMemoryやAsSpanを使うのが重要です。Microsoftのコード分析ルールでも、配列の範囲インデクサー結果をReadOnlySpan<T>やReadOnlyMemory<T>として使う場合、コピー回避のためにAsSpanまたはAsMemoryを使うことが説明されています。
注意したい例は次のとおりです。
C#int[] numbers = { 1, 2, 3, 4, 5 };
// 配列の範囲インデクサーは新しい配列を作る場合がある
int[] copied = numbers[1..4];
// コピーを避けたいならAsMemoryを使う
Memory<int> memory = numbers.AsMemory(1, 3);
4-3. Sliceで必要な範囲だけを切り出す
Memory<T>から一部だけを取り出すには、Sliceを使います。
C#int[] numbers = { 10, 20, 30, 40, 50 };
Memory<int> memory = numbers.AsMemory();
Memory<int> sliced = memory.Slice(2, 2); // 30, 40
Slice(2, 2)は、「インデックス2から2個分」という意味です。
開始位置だけ指定することもできます。
C#Memory<int> tail = memory.Slice(2); // 30, 40, 50
Sliceを使うと、データの一部を別メソッドへ渡すときに便利です。
C#Process(memory.Slice(0, 3));
static void Process(Memory<int> values)
{
Console.WriteLine(values.Length);
}
4-4. SpanプロパティでSpan<T>として操作する
Memory<T>自体には、配列のようなインデクサー操作はありません。
そのため、要素を読み書きしたいときは.Spanプロパティを使います。
C#int[] numbers = { 1, 2, 3 };
Memory<int> memory = numbers;
memory.Span[0] = 100;
Console.WriteLine(numbers[0]); // 100
Memory<T>は保持や受け渡しに使い、実際の読み書きはSpan<T>で行う、というイメージです。
読み取りだけならReadOnlyMemory<T>とReadOnlySpan<T>を使います。
C#ReadOnlyMemory<int> memory = numbers;
int first = memory.Span[0];
4-5. ToArrayで配列に戻すときの注意点
Memory<T>はToArrayで配列に変換できます。
C#int[] numbers = { 10, 20, 30, 40, 50 };
Memory<int> memory = numbers.AsMemory(1, 3);
int[] copied = memory.ToArray();
この場合、copiedには20, 30, 40が入ります。
ただし、ToArrayは新しい配列を作ります。つまりコピーが発生します。
C#copied[0] = 999;
Console.WriteLine(numbers[1]); // 20
copiedは元の配列とは別物なので、変更してもnumbersには影響しません。
コピーを避けるためにMemory<T>を使っているのに、最後に何度もToArrayを呼ぶと、メリットが小さくなります。
4-6. ReadOnlyMemory<T>で読み取り専用として扱う
データを書き換えたくない場合は、ReadOnlyMemory<T>を使います。
C#int[] numbers = { 1, 2, 3 };
ReadOnlyMemory<int> readOnly = numbers;
ReadOnlyMemory<T>は、読み取り専用のメモリ領域を表します。ReadOnlySpan<T>と似ていますが、ReadOnlySpan<T>と異なりbyref-likeな型ではないため、Memory<T>と同じように保持しやすい型です。
文字列を扱う場合も、ReadOnlyMemory<char>が使われます。
C#string text = "Hello Memory";
ReadOnlyMemory<char> memory = text.AsMemory(6, 6); // Memory
文字列は不変なので、Memory<char>ではなくReadOnlyMemory<char>として扱うのが自然です。
5. Memory<T>の実用コード例
5-1. 配列の一部をコピーせずに処理する例
配列の一部だけを合計する例を見てみましょう。
C#static int Sum(ReadOnlyMemory<int> values)
{
int total = 0;
foreach (int value in values.Span)
{
total += value;
}
return total;
}
int[] numbers = { 10, 20, 30, 40, 50 };
int result = Sum(numbers.AsMemory(1, 3));
Console.WriteLine(result); // 90
numbers.AsMemory(1, 3)は、20, 30, 40の範囲を表します。
このコードでは、新しい配列を作らずに一部だけを処理できます。
5-2. 文字列やchar配列をReadOnlyMemory<char>で扱う例
文字列の一部を処理したい場合、ReadOnlyMemory<char>が便利です。
C#static void Print(ReadOnlyMemory<char> text)
{
Console.WriteLine(text.ToString());
}
string message = "C# Memory<T> Guide";
ReadOnlyMemory<char> keyword = message.AsMemory(3, 9);
Print(keyword); // Memory<T>
ただし、ToString()で文字列化すると、新しい文字列が作られる場合があります。大量の文字列処理でコピーを避けたい場合は、ReadOnlySpan<char>として処理する設計も検討します。
C#static bool StartsWithCSharp(ReadOnlyMemory<char> text)
{
return text.Span.StartsWith("C#".AsSpan());
}
5-3. async/awaitを使った非同期メソッドでMemory<T>を扱う例
ファイルやネットワークからデータを読み込む処理では、Memory<byte>がよく使われます。
C#static async Task<int> ReadFromStreamAsync(Stream stream, Memory<byte> buffer)
{
int bytesRead = await stream.ReadAsync(buffer);
return bytesRead;
}
呼び出し側は、配列からMemory<byte>を作って渡せます。
C#byte[] buffer = new byte[4096];
using FileStream stream = File.OpenRead("sample.dat");
int bytesRead = await ReadFromStreamAsync(stream, buffer.AsMemory());
読み込んだ部分だけを処理したい場合は、Sliceを使います。
C#Memory<byte> actualData = buffer.AsMemory(0, bytesRead);
5-4. バッファ処理でMemory<byte>を活用する例
通信処理では、固定サイズのバッファを使い回すことがあります。
C#static async Task ReadLoopAsync(Stream stream, CancellationToken cancellationToken)
{
byte[] buffer = new byte[4096];
while (true)
{
int bytesRead = await stream.ReadAsync(
buffer.AsMemory(0, buffer.Length),
cancellationToken);
if (bytesRead == 0)
{
break;
}
ReadOnlyMemory<byte> received = buffer.AsMemory(0, bytesRead);
ProcessPacket(received);
}
}
static void ProcessPacket(ReadOnlyMemory<byte> packet)
{
Console.WriteLine($"Received: {packet.Length} bytes");
}
この例では、buffer自体は同じ配列を使い回し、実際に読み込まれた部分だけをReadOnlyMemory<byte>として処理しています。
5-5. Span<T>と組み合わせて一時的に高速処理する例
Memory<T>は保持や受け渡しに向いていますが、要素を実際に操作するときはSpan<T>と組み合わせることが多いです。
C#static void FillWithZero(Memory<int> memory)
{
Span<int> span = memory.Span;
for (int i = 0; i < span.Length; i++)
{
span[i] = 0;
}
}
int[] numbers = { 1, 2, 3, 4, 5 };
FillWithZero(numbers.AsMemory(1, 3));
Console.WriteLine(string.Join(", ", numbers));
// 1, 0, 0, 0, 5
このように、外側のAPIではMemory<T>を受け取り、メソッド内で一時的にSpan<T>へ変換して高速に処理する設計がよく使われます。
6. Memory<T>を使うときの注意点
6-1. Memory<T>自体はデータをコピーしない
Memory<T>を作っても、基本的には元データのコピーは作られません。
C#int[] numbers = { 1, 2, 3, 4, 5 };
Memory<int> memory = numbers.AsMemory(1, 3);
このmemoryは、numbersの一部を参照しているだけです。
そのため、「Memory<T>にしたから安全に別データとして独立した」と考えるのは間違いです。
独立した配列が必要な場合は、明示的にToArrayを使います。
C#int[] copied = memory.ToArray();
ただし、この場合はコピーが発生します。
6-2. 元の配列を書き換えるとMemory<T>側にも影響する
Memory<T>は元の配列を参照しているため、元配列を変更するとMemory<T>から見える内容も変わります。
C#int[] numbers = { 10, 20, 30, 40, 50 };
Memory<int> memory = numbers.AsMemory(1, 3);
numbers[1] = 999;
Console.WriteLine(memory.Span[0]); // 999
これは便利な場合もありますが、意図しない変更につながることもあります。
書き換えを防ぎたい場合は、APIの引数をReadOnlyMemory<T>にするのがおすすめです。
C#static void PrintValues(ReadOnlyMemory<int> values)
{
foreach (int value in values.Span)
{
Console.WriteLine(value);
}
}
6-3. Sliceの範囲外アクセスに注意する
Sliceでは、開始位置や長さが範囲外になると例外が発生します。
C#int[] numbers = { 1, 2, 3 };
Memory<int> memory = numbers;
// 範囲外なので例外
Memory<int> invalid = memory.Slice(2, 5);
安全に使うには、Lengthを確認しましょう。
C#if (memory.Length >= 3)
{
Memory<int> part = memory.Slice(0, 3);
}
外部から受け取ったデータを扱う場合は、範囲チェックを忘れないことが重要です。
6-4. ToArrayはコピーが発生するため多用しない
ToArrayは便利ですが、Memory<T>のメリットを打ち消すことがあります。
C#static void Process(Memory<byte> data)
{
byte[] copied = data.ToArray();
// copiedを使った処理
}
このコードでは、Memory<byte>として受け取っているにもかかわらず、すぐに配列へコピーしています。
もちろん、外部ライブラリが配列しか受け取れない場合や、独立したデータとして保持したい場合にはToArrayが必要です。
しかし、コピーを避ける目的でMemory<T>を導入しているなら、ToArrayの多用は避けましょう。
6-5. 長期間保持する場合は元データのライフタイムを意識する
Memory<T>をクラスのフィールドに保持できる点は便利ですが、元データの寿命に注意が必要です。
C#class Cache
{
private ReadOnlyMemory<byte> _data;
public void Set(ReadOnlyMemory<byte> data)
{
_data = data;
}
}
このようなコードでは、_dataがどのデータを参照しているかを意識する必要があります。
特に、プールから借りた配列や再利用されるバッファをMemory<T>として長期間保持すると、あとで別の内容に書き換わっている可能性があります。
長期間保持するなら、必要に応じてコピーを作る判断も必要です。
6-6. パフォーマンス改善目的で使う前に計測する
Memory<T>は便利ですが、使えば必ず速くなるわけではありません。
たとえば、次のような場合は、通常の配列やList<T>で十分なことも多いです。
データ量が小さい
処理回数が少ない
可読性のほうが重要
メモリコピーがボトルネックではない
パフォーマンス改善を目的にMemory<T>を使うなら、導入前後で計測しましょう。
見るべきポイントは次のとおりです。
| 観点 | 確認内容 |
|---|---|
| 実行時間 | 本当に速くなったか |
| メモリ割り当て | アロケーションが減ったか |
| GC回数 | GC負荷が下がったか |
| 可読性 | コードが複雑になりすぎていないか |
| 保守性 | チームメンバーが理解できるか |
7. Memory<T>と関連する型も理解しておこう
7-1. ReadOnlyMemory<T>とは
ReadOnlyMemory<T>は、読み取り専用のメモリ領域を表す型です。
C#ReadOnlyMemory<int> memory = new int[] { 1, 2, 3 };
要素を書き換える必要がない場合は、Memory<T>ではなくReadOnlyMemory<T>を使うと、意図が明確になります。
API設計でも、受け取ったデータを書き換えないならReadOnlyMemory<T>を選ぶのがよいでしょう。
C#static int CountPositive(ReadOnlyMemory<int> values)
{
int count = 0;
foreach (int value in values.Span)
{
if (value > 0)
{
count++;
}
}
return count;
}
7-2. Span<T>とは
Span<T>は、連続したメモリ領域を型安全に扱うための型です。
C#int[] numbers = { 1, 2, 3 };
Span<int> span = numbers.AsSpan();
span[0] = 100;
Span<T>は非常に高速で、配列、スタック上のメモリ、アンマネージメモリなどを扱える強力な型です。
ただし、ref structであるため、Memory<T>より制約があります。短い同期処理で一時的に使うのに向いています。
7-3. ReadOnlySpan<T>とは
ReadOnlySpan<T>は、読み取り専用のSpan<T>です。
C#static bool IsHello(ReadOnlySpan<char> text)
{
return text.SequenceEqual("Hello".AsSpan());
}
文字列処理でよく使われます。
C#string message = "Hello World";
ReadOnlySpan<char> span = message.AsSpan(0, 5);
ReadOnlySpan<T>もSpan<T>と同様に制約があるため、非同期処理や長期保持にはReadOnlyMemory<T>のほうが向いています。
7-4. ArraySegment<T>との違い
ArraySegment<T>は、配列の一部を表す古くからある型です。
C#int[] numbers = { 1, 2, 3, 4, 5 };
ArraySegment<int> segment = new ArraySegment<int>(numbers, 1, 3);
ArraySegment<T>も「配列の一部」を表せますが、対象は基本的に配列です。
一方、Memory<T>はより新しいAPIと相性がよく、Span<T>とも連携しやすいです。
| 型 | 主な特徴 |
|---|---|
ArraySegment<T> | 配列の一部を表す |
Memory<T> | 配列以外も含めた連続メモリ領域を扱いやすい |
Span<T> | 一時的な高速処理向き |
ReadOnlyMemory<T> | 読み取り専用で保持・非同期処理向き |
Microsoftのドキュメントでは、ArraySegment<T>は1次元配列の一部を区切る型として説明されています。
7-5. MemoryPool<T>・IMemoryOwner<T>とは
MemoryPool<T>は、メモリブロックをプールして再利用するための仕組みです。
C#using System.Buffers;
using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(4096);
Memory<byte> memory = owner.Memory.Slice(0, 4096);
MemoryPool<T>.Rentを使うと、指定サイズ以上のメモリブロックを借りられます。借りたメモリの所有者はIMemoryOwner<T>として表され、使い終わったらDisposeします。
C#using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(1024);
Memory<byte> buffer = owner.Memory.Slice(0, 1024);
// bufferを使った処理
usingを抜けるとDisposeされます。
7-6. ArrayPool<T>とMemory<T>を組み合わせる場面
ArrayPool<T>は、配列を借りて返すためのプールです。
C#using System.Buffers;
byte[] rented = ArrayPool<byte>.Shared.Rent(4096);
try
{
Memory<byte> memory = rented.AsMemory(0, 4096);
// memoryを使った処理
}
finally
{
ArrayPool<byte>.Shared.Return(rented, clearArray: true);
}
ArrayPool<T>は、配列を頻繁に作成・破棄することでGC負荷が高くなる場面でパフォーマンス改善に役立つ可能性があります。Microsoftのドキュメントでも、RentとReturnでバッファを再利用することで、配列を頻繁に作成・破棄する状況のGC負荷を下げられると説明されています。
ただし、ArrayPool<T>.Rentで借りた配列は要求サイズより大きい場合があります。そのため、必要な範囲だけをAsMemory(0, length)で切り出して使うのが安全です。
C#int requestedSize = 1024;
byte[] rented = ArrayPool<byte>.Shared.Rent(requestedSize);
try
{
Memory<byte> buffer = rented.AsMemory(0, requestedSize);
}
finally
{
ArrayPool<byte>.Shared.Return(rented);
}
8. Memory<T>はどんな場面で使うべきか
8-1. 非同期APIでバッファを受け渡ししたいとき
Memory<T>は、非同期APIでバッファを渡す場面に向いています。
C#static async Task<int> ReceiveAsync(Stream stream, Memory<byte> buffer)
{
return await stream.ReadAsync(buffer);
}
Span<T>はasync/awaitと組み合わせると制約が出やすいため、非同期I/OではMemory<T>が自然な選択になります。
ファイル、ネットワーク、パイプライン処理などで、Memory<byte>やReadOnlyMemory<byte>はよく使われます。
8-2. クラスのフィールドにメモリ範囲を保持したいとき
処理対象の一部をクラスに保持したい場合、Memory<T>が使えます。
C#class Message
{
public ReadOnlyMemory<byte> Payload { get; }
public Message(ReadOnlyMemory<byte> payload)
{
Payload = payload;
}
}
このように、メッセージ本文、受信データ、解析対象の一部などを保持したい場合に便利です。
ただし、元データがあとで書き換わったり、プールへ返却されたりしないかは必ず確認しましょう。
8-3. 大量データをコピーせずに部分処理したいとき
大きな配列の一部だけを処理したい場合、Memory<T>は非常に便利です。
C#static void ProcessBlock(ReadOnlyMemory<byte> block)
{
Console.WriteLine(block.Length);
}
byte[] largeData = new byte[1024 * 1024];
for (int offset = 0; offset < largeData.Length; offset += 4096)
{
int length = Math.Min(4096, largeData.Length - offset);
ProcessBlock(largeData.AsMemory(offset, length));
}
このコードでは、1MBの配列を4096バイトずつ処理しています。
各ブロックごとに新しい配列を作らず、範囲だけを渡しています。
8-4. ライブラリやAPIの引数設計で柔軟性を持たせたいとき
ライブラリや共通処理を作るとき、引数にReadOnlyMemory<T>を使うと柔軟性が高くなります。
C#public static int CalculateChecksum(ReadOnlyMemory<byte> data)
{
int checksum = 0;
foreach (byte value in data.Span)
{
checksum += value;
}
return checksum;
}
このAPIは、配列全体でも一部でも受け取れます。
C#byte[] data = { 1, 2, 3, 4, 5 };
int all = CalculateChecksum(data);
int part = CalculateChecksum(data.AsMemory(1, 3));
書き換えないAPIなら、Memory<T>ではなくReadOnlyMemory<T>にしておくと、呼び出し側にも意図が伝わりやすくなります。
8-5. 逆にMemory<T>を使わなくてよいケース
Memory<T>は便利ですが、すべての場面で必要なわけではありません。
次のようなケースでは、通常の配列やList<T>で十分です。
| ケース | 理由 |
|---|---|
| 小さなデータを扱うだけ | コピーコストが問題になりにくい |
| 要素を追加・削除したい | List<T>のほうが向いている |
| 単純な業務ロジック | 可読性を優先したほうがよい |
| パフォーマンス問題がない | 複雑化するメリットが少ない |
| 独立したデータが必要 | ToArrayなどでコピーしたほうが安全 |
初心者のうちは、まず配列やList<T>で書き、コピーやGC負荷が問題になったときにMemory<T>を検討するとよいでしょう。
9. 初心者がつまずきやすい疑問
9-1. Memory<T>を使うと必ず速くなる?
必ず速くなるわけではありません。
Memory<T>は、主に不要なコピーやアロケーションを減らすための型です。もともとコピーが少ない処理や、データ量が小さい処理では、効果がほとんどないこともあります。
また、Memory<T>やSpan<T>を使うことでコードが複雑になり、保守性が下がる場合もあります。
パフォーマンス改善のために使うなら、必ずベンチマークやプロファイラーで確認しましょう。
9-2. 配列とMemory<T>は何が違う?
配列はデータ本体です。
C#int[] numbers = { 1, 2, 3 };
一方、Memory<T>はデータの範囲を表す型です。
C#Memory<int> memory = numbers.AsMemory(1, 2);
このmemoryは、numbersのうち2, 3の範囲を表します。
つまり、配列は「実体」、Memory<T>は「その全部または一部を指すビュー」と考えるとわかりやすいです。
9-3. Memory<T>とList<T>は置き換えられる?
基本的には置き換えるものではありません。
List<T>は、要素を追加・削除できる可変長コレクションです。
C#List<int> list = new List<int>();
list.Add(1);
list.Add(2);
一方、Memory<T>は連続したメモリ領域の範囲を表す型であり、要素の追加・削除には向いていません。
| 型 | 向いている用途 |
|---|---|
List<T> | 要素数が増減するデータ |
Memory<T> | 既存データの範囲を効率よく扱う |
| 配列 | 固定長のデータ |
Span<T> | 一時的な高速処理 |
9-4. Span<T>からMemory<T>に変換できる?
基本的に、任意のSpan<T>から安全にMemory<T>へ変換することはできません。
理由は、Span<T>がスタック上のメモリや一時的な領域を指している可能性があるからです。
たとえば、次のようなSpan<T>はスタック上のメモリを指します。
C#Span<int> span = stackalloc int[3];
このようなデータをMemory<T>としてヒープ上に保持できてしまうと、安全性が壊れる可能性があります。
そのため、設計としては、長く保持したいなら最初からMemory<T>、一時的に処理したいならSpan<T>を使うと考えましょう。
9-5. stringにもMemory<T>は使える?
文字列には、ReadOnlyMemory<char>として使えます。
C#string text = "Hello C# Memory";
ReadOnlyMemory<char> memory = text.AsMemory(6, 2); // C#
stringは不変なので、書き換え可能なMemory<char>ではなく、読み取り専用のReadOnlyMemory<char>になります。
文字列の一部を頻繁に切り出す処理では、Substringで新しい文字列を作るより、ReadOnlyMemory<char>やReadOnlySpan<char>を使うほうが効率的な場合があります。
C#ReadOnlyMemory<char> part = text.AsMemory(0, 5);
ただし、最終的に文字列として保持したい場合は、どこかで文字列化が必要になります。
9-6. unsafeコードやポインタの知識は必要?
通常のMemory<T>利用に、unsafeコードやポインタの知識は必要ありません。
Memory<T>やSpan<T>は、メモリ効率のよい処理を、できるだけ安全に書くための仕組みです。
もちろん、低レベルな最適化やアンマネージメモリを扱う場合にはポインタの知識が役立つこともあります。
しかし、配列の一部をコピーせずに処理する、非同期I/Oでバッファを渡す、文字列の一部を扱う、といった一般的な用途では、unsafeコードを書かなくても十分に使えます。
まとめ
Memory<T>は、C#で連続したメモリ領域を効率よく扱うための型です。
配列やバッファの一部をコピーせずに参照できるため、大量データ処理、ファイル処理、通信処理、非同期I/Oなどで役立ちます。
特に重要なポイントは次のとおりです。
| ポイント | 内容 |
|---|---|
Memory<T>は範囲を表す | データ本体ではなく、配列やバッファの一部を指す |
| コピーを減らせる | AsMemoryやSliceで範囲を扱える |
Span<T>より制約が少ない | クラスのフィールドや非同期処理で使いやすい |
実際の読み書きは.Span経由 | Memory<T>.SpanでSpan<T>として操作する |
ToArrayはコピーする | 多用するとメリットが小さくなる |
読み取り専用ならReadOnlyMemory<T> | API設計では積極的に使いたい |
| 必ず速くなるわけではない | 導入前後で計測することが大切 |
使い分けの基本は、同期的で一時的な処理ならSpan<T>、非同期処理や保持が必要ならMemory<T>です。
初心者のうちは、まず次の3つだけ押さえれば十分です。
C#Memory<int> memory = array.AsMemory();
Memory<int> part = memory.Slice(1, 3);
Span<int> span = memory.Span;
Memory<T>は最初は少し難しく感じますが、「メモリそのものではなく、範囲を表す型」と理解すると一気に扱いやすくなります。配列コピーを減らしたい、非同期処理でバッファを効率よく扱いたい、C#のメモリ効率を改善したいときに、ぜひ活用してみてください。

