1. はじめに
話はこう始まりました。 .NET の開発者たちは大量データ処理を高速に、安全かつ便利に行えるようにしたかったんです。最初に登場したのが Span<T>。これは連続したメモリ領域の「窓」で、配列や文字列、さらには P/Invoke 経由で確保されたネイティブメモリの一部を指し示すことができます。
ただし Span<T> には強力な制約があります: 常にスタック上にしか存在できません。クラスのフィールドに保持できないし、メソッドから返せないし、非同期メソッド間で渡すこともできません。理由は安全性です: 存在しないメモリを参照されるとアプリが確実にクラッシュします。
メソッドからスライスを返したり、コレクションやクラスのフィールドに保存したり、非同期 API で使いたいことがあります。そこで登場するのが Memory<T> — 要するに安全に「長生き」できる Span<T> です。ヒープに置けてスレッド間で渡せ、プロパティやオブジェクトに保持できます。
もう一つの「兄弟」があります。変更を許さない ReadOnlyMemory<T> です。読取専用で扱いたいときに使います。
2. Span<T> と Memory<T> の違い
わかりやすい比較表:
|
|
|
|---|---|---|
| どこに存在するか | スタックのみ (stack only) | スタックとヒープ (heap/stack) |
| フィールドに保持できるか | ❌ できない | ✅ できる |
| メソッドから返せるか | ❌ 返せない | ✅ 返せる |
| 非同期 / await-メソッドで使えるか | ❌ 使えない | ✅ 使える |
| 可変か | ✅ ReadOnlySpan<T> もある | ✅ ReadOnlyMemory<T> もある |
| スライス可能か | ✅ 可能 | ✅ 可能 |
メソッド内部で素早くデータを走査するなら Span<T>。結果を外に返したりクラスのフィールドに保存するなら Memory<T>。読み取り専用なら ReadOnlyMemory<T> を選びましょう。
3. Memory<T> のシグネチャと基本構造
普通どおり、Memory<T> はジェネリックです。Memory<int>、Memory<byte>、Memory<char>、あるいは Memory<MyType> を作れます。内部には配列や文字列、他のデータソースへの参照と、開始インデックスや長さを表す範囲情報が隠れています。
高速に処理したいときは、Memory<T> のプロパティ Span を使って瞬間的に Span<T> を得ます。これは同期メソッド内での処理に向いています。
4. Memory<T> の作り方: 実践
例 1. 配列から作成
int[] numbers = { 1, 2, 3, 4, 5, 6 };
Memory<int> memory = new Memory<int>(numbers); // 配列全体
// "スライス"、配列の一部を取る
Memory<int> slice = memory.Slice(2, 3); // インデックス2,3,4 の要素
例 2. 文字列から作る(Memory<char> 経由)
string text = "Privet, mir!";
Memory<char> charMemory = text.AsMemory(); // テキスト全体をメモリとして
Memory<char> subMemory = charMemory.Slice(7, 3); // 7文字目から3文字分 ("mir")
例 3. ReadOnlyMemory<T> の使用
同じようにできますが、変更はできません:
int[] data = { 10, 20, 30, 40 };
ReadOnlyMemory<int> readOnly = data; // このオブジェクト経由では変更できない
5. Memory<T> と Span<T> の相互変換
Memory<T> を Span<T> と同じ柔軟さで直接使うことはできませんが、素早く処理したいときは Memory<T> から瞬時に Span<T> を取り出せます(プロパティ Span を使う):
void ProcessData(Memory<int> memory)
{
Span<int> span = memory.Span;
for (int i = 0; i < span.Length; i++)
{
span[i] += 100;
}
}
注意: Span<T> はメソッド内でしか有効です。外に返そうとするとコンパイラエラーになります。
ReadOnlyMemory<T> でも同様で、得られるのは変更不可の ReadOnlySpan<T> です:
void PrintData(ReadOnlyMemory<int> memory)
{
ReadOnlySpan<int> roSpan = memory.Span;
foreach (var item in roSpan)
Console.WriteLine(item);
}
6. 実際のユースケースでの使い方
非同期データ処理
Memory<T> が本領を発揮するのはここです。非同期メソッドで使えます。例えばファイルの非同期読み取り:
using System.IO;
using System.Threading.Tasks;
public async Task ReadFileAsync(string path)
{
byte[] buffer = new byte[4096];
using var stream = File.OpenRead(path);
int bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length));
// ここで buffer を扱える
}
ここでは AsMemory がバッファを非同期メソッドに直接渡しており、Span<T> の場合にあるスコープや寿命の問題が発生しません。
スライスをプロパティやフィールドに保持する
大きな配列の一部を後で渡したいときにクラスで保持する例:
class DataChunk
{
public Memory<byte> Data { get; }
public DataChunk(Memory<byte> data)
{
Data = data;
}
}
7. コレクション、文字列、配列との併用
配列と一緒に
よくある例:
byte[] bytes = { 1, 2, 3, 4, 5 };
Memory<byte> mem = bytes; // 配列全体
Memory<byte> part = mem.Slice(2); // 3番目の要素から末尾まで
文字列と一緒に
AsMemory() 経由で:
string hello = "Hello, Memory!";
ReadOnlyMemory<char> mem = hello.AsMemory(6, 6); // "Memory"
コレクション(例えば List<T>)と一緒に
直接 List<T> から Memory<T> を作ることはできません。配列経由でのみ可能です:
List<int> list = new List<int> { 1, 2, 3 };
Memory<int> mem = list.ToArray(); // コピーされる。参照ではない!
コピーを避けたいなら、データを最初から配列で管理しましょう。
8. Memory<T> を扱うときの典型的なミス
ミス №1: クラスのフィールドに Span<T> を使おうとする。 Span<T> はスタックに束縛されるのでクラスフィールドに保持できません。コンパイラがエラーを出します。ヒープに保持したいなら Memory<T> を使ってください。
ミス №2: スライスするとコピーされると期待する。 Memory<T> はデータをコピーせず、既存配列の「窓」を作ります。ある Memory<T> 経由でデータを変更すると、同じメモリを参照している他のオブジェクトにも反映されます。
ミス №3: List<T> から直接 Memory<T> を作ろうとする。 Memory<T> は配列を前提としているため、List<T> は内部でデータの場所を移動する可能性があるので直接は作れません。ToArray() で配列に変換してください。
ミス №4: 変更不要なデータに Memory<T> を使うのを無視する。 データを変更しないなら ReadOnlyMemory<T> を使った方が安全です。
GO TO FULL VERSION