1. はじめに
配列、文字列、バイトバッファを扱うとき、データの一部を“覗く”必要がよく出てきます。例えばサブストリングを取り出したり、配列のスライスを取ったり、受信バッファの一部を処理したり。古い .NET ではこれをやるにはデータをコピーして新しい配列やサブストリングを作るか、必要な開始と終了を指定して自分でループを書くしかなく、パフォーマンスも可読性もイマイチでした。
古いアプローチの例:大きな配列の一部だけをメソッドに渡したいとき、
// 古いやり方 — 配列の一部をコピーしている(非効率!)
int[] source = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
int[] subArray = source.Skip(2).Take(4).ToArray(); // 新しい配列が作られる
このように、配列や文字列の“スライス”を余計なオブジェクトを作らずに効率よく渡したい場合、従来のC#の手段は特に大きなデータ量だと不利になります。
ここで登場するのが今日の主役 — Span<T>です!
2. Span<T>とは? 基本アイデア
Span<T> は同じ型 T の連続したメモリ領域を表す型です。配列や文字列、構造体、あるいはアンマネージドメモリ(例えば.NETの管理領域外で確保したメモリ)などの一部を、速く、安全に、効率的に扱うために使います。
Span<T> の肝は「新しい配列を作らずにスライスできる」ことです。ルーラーで同じ配列の任意の区間を測るように、データをコピーせずに参照でき、インデックスのミスも最小限にできます。
要点:
- Span<T> はメモリの一部を扱う“ウィンドウ”や“ビュー”で、安全に操作できます。
- 新しいメモリ割り当てが不要 — リソース節約と GC の負荷軽減。
- 配列だけでなく文字列の一部、stackalloc で確保した領域、アンマネージドメモリにも使える。
- 普通のクラスのフィールドに保持できない:スタック専用の型(stack-only struct)です。
なぜ重要か?
ファイルのパース、大きなバッファ処理、暗号化、シリアライズなどの高パフォーマンスなタスクでは、配列のコピーをちょっと減らすだけで処理速度が大幅に向上し、GCの負荷も下がります。さらに、最新のC#/.NETの知識があることを同僚に示せます。
3. 基本的な使い方:最初のスライス
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 配列の一部に対してSpanを作る(例えば2番目から5番目まで)
Span<int> middle = new Span<int>(numbers, 2, 4); // インデックス: 2, 3, 4, 5
// 部分配列は 3, 4, 5, 6 が見える
Console.WriteLine(string.Join(", ", middle.ToArray())); // 3, 4, 5, 6
// Spanを変更すると元の配列も変わる!
middle[1] = 999;
Console.WriteLine(numbers[3]); // 999
重要! Span<T> はデータをコピーせず、配列の“区間”を指しているだけです。変更は元の配列にも反映されます。
4. Span<T> の主な作り方
配列から:
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> span = arr; // 全体
Span<int> slice = arr.AsSpan(1, 3); // 要素 20, 30, 40
配列の一部分から:
Span<int> part = new Span<int>(arr, 2, 2); // 要素 30, 40
stackalloc:スタック上にメモリを確保(超高速でヒープに乗らない):
Span<byte> buffer = stackalloc byte[128];
buffer[0] = 42;
.Slice() を使う:
Span<int> subSpan = span.Slice(1, 2); // 要素 20, 30
スライスの視覚的イメージ
元の配列: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
<--- Span: 3, 4, 5, 6 --->
5. Span<T> の制約と特徴
- Stack-only! 普通のクラスのフィールドに保存したり、クロージャの一部にしたりできません — スタック専用の型です。
- async メソッドから返すことはできません(コンパイラがエラーにします)。
- ラムダや匿名メソッドでキャプチャできません — 「ここで使ってすぐ終わらせる」使い方を想定します。
- 直接シリアライズしたりスレッド間で渡したりはできません。
これは Span が任意のメモリ領域を指せることと関係しています。もし Span がヒープに移動すると安全でない状態が発生する可能性があるためです。
6. 不変版:ReadOnlySpan<T>
メモリの一部を参照するけれど変更は許可したくない場合は、不変の ReadOnlySpan<T> を使います。
string text = "Hello, Span!";
ReadOnlySpan<char> letters = text.AsSpan(7, 4); // 'S', 'p', 'a', 'n'
Console.WriteLine(string.Join(", ", letters.ToArray())); // S, p, a, n
// letters[0] = 'Z'; // エラー: インデクサは読み取り専用!
典型的なシナリオは、文字列や配列の“切れ端”を安全に渡したいときです(変更させたくない場合)。
7. 実践例:配列と文字列の“スライス”
例として、長い文字列から部分文字列を取り出して、その中の数値を探して合計を返す分析器を考えます(最初のスライスで余計なコピーをしない)。
using System;
class Program
{
static void Main()
{
// ユーザーが空白で区切った長い数字列を入力したとする
string input = "12 34 56 78 90 123 456 789";
// たとえば「中央」だけ(56 78 90)を合計したい
// 部分文字列を取り出す(コピーしない)
ReadOnlySpan<char> center = input.AsSpan(6, 8); // インデックスは動的に計算できる
// Splitで分割してパース(ここでは一時的な配列が作られる)
string[] numbers = center.ToString().Split(' ');
int sum = 0;
foreach (var str in numbers)
{
if (int.TryParse(str, out int num))
sum += num;
}
Console.WriteLine($"中央の数の合計: {sum}");
}
}
現代の高速なCSV/JSONパーサーは大きな文字列を扱う際に Span を活用していて、その高速化の仕組みはこうした考え方に基づいています。
8. 便利なティップス
Span と配列コピーの比較
// 古い方法: 配列の一部をコピーする
int[] arr = Enumerable.Range(0, 1000000).ToArray();
int[] firstThousand = arr.Take(1000).ToArray(); // 1000要素の新しい配列が作られる
// 新しい方法: Span
Span<int> bestThousand = arr.AsSpan(0, 1000); // まったくコピーしない!
bestThousand[0] = 42; // arrにも反映される
特に大量のファイルをパースしたり、ネットワークバッファを処理したり、バイナリデータを扱う場面で違いが顕著です。
実践的な用途:なぜ Span を知っておくべきか
- 高速な文字列/バイナリパース。現代のシリアライズライブラリ(例:System.Text.Json、Span の Microsoft ドキュメント)は処理速度向上のために Span を使っています。
- バッファリングやファイル読み込み(大きなバッファをコピーせずに分割して扱える)。
- メモリ制約のある組み込み系や IoT 向けの処理(embedded、IoT) — Memory/Spans の公式ドキュメント を参照。
- 画像や音声処理など、速度と不要なアロケーション回避が重要なアルゴリズム。
- CSV、JSON、XML のパース高速化(特に .NET 8/9 以降で効果的)。
面接でも Span に関する質問は .NET Core 2.1 以降によく出るようになり、.NET 9 では知っていると好印象です。
視覚図:どこが Span でどこが配列か
+--------------------+
| int[] 配列 |
| 1 2 3 4 5 6 7 8 |
+--------------------+
^ ^
| |
[ 2, 3, 4, 5 ] <-- Span<int> の "メモリの窓" (slice)
Span<T> は独立した配列ではなく、データの一部を透かして見る“レンズ”です。
他のコレクションとの違い:比較表
| 型 | データを保持する? | 要素を変更できる? | サイズを変更できる? | スライスでコピーする? | どこに存在する? |
|---|---|---|---|---|---|
|
はい | はい | いいえ | はい (.Take などで) | Heap |
|
はい | はい | はい | はい | Heap |
|
いいえ | はい | いいえ | いいえ | Stack |
|
いいえ | いいえ | いいえ | いいえ | Stack |
9. Span/ReadOnlySpan を使うときの典型的なミス
ミス №1: Span をクラスのフィールドとして保存しようとする。コンパイラは “Span type may not be used in this context” というエラーを出します。これは安全性のための制約です。
ミス №2: Span を async メソッドから返そうとする。async メソッドはヒープに逃げる可能性があるため禁止されています。代わりに配列など別の型を使ってください。
ミス №3: Span を通しての変更が元の配列に反映されることを忘れてしまう。これにより外側のデータが意図せず変わり、不具合につながることがあります。
GO TO FULL VERSION