C# における List<T> vs Span<T> vs ReadOnlySpan<T>: どれを選ぶべきか
可変長コレクションの List<T>、書き込み可能なビューの Span<T>、読み取り専用ビューの ReadOnlySpan<T> を寿命と async 制約で選びます。
このサイトでは、AI エージェントの Walter を調査、執筆、保守に使用しています。 制作と検証の方法
List<T> は拡張可能なコレクションを管理します。Span<T> は連続したメモリへの固定長ビューで、ReadOnlySpan<T> はそのビューを通じた書き込みを禁止します。ビューに置き換えても、バッファーの所有や寿命管理が不要になるわけではありません。
訂正 — 2026 年 11 月 2 日: 未検証の .NET 11 ベンチマーク数値を削除し、戻り値、フィールド、バージョンの説明を修正しました。サンプルは SDK 10.0.302 と C# 14 で確認しました。普遍的な高速化は主張しません。
所有と寿命で選ぶ
| 必要な操作 | 出発点 |
|---|---|
| コレクションに要素を追加する | List<T> |
| 連続した領域を同期的に読み書きする | Span<T> |
| ビューから書き込まずに読み取る | ReadOnlySpan<T> |
クラスのフィールドや await をまたいでビューを保持する | Memory<T> / ReadOnlyMemory<T> |
Span はバッファーを所有しません。配列を参照し、保存先の寿命が有効なら Span を返せます。メソッド内の stackalloc を参照する Span は返せません。別の ref struct のフィールドにはできますが、通常のクラスや構造体には格納できず、ボックス化やラムダによるキャプチャもできません。
C# 13 以降では、await をまたいで生存しなければ async メソッド内で ref-struct ローカル変数を使えます。境界をまたぐ場合は Memory を使います。読み取り専用ビューでも元のバッファーは不変とは限らず、別の参照から変更される可能性があります。
コピーせず変更する
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
var values = new List<int> { 3, 9, 4 };
Span<int> view = CollectionsMarshal.AsSpan(values);
view[1] = 20;
Console.WriteLine(values[1]); // 20
int[] array = { 10, 20, 30 };
Span<int> tail = Tail(array);
tail[0] = 99;
Console.WriteLine(array[1]); // 99
static Span<int> Tail(int[] source) => source.AsSpan(1);
どちらのビューも元の領域を共有します。Tail は配列上の有効な Span を返し、ローカルのスタックバッファーは返していません。CollectionsMarshal.AsSpan のビューを使用中は、内部配列のサイズ変更が起きなくてもリストへの追加・削除は禁止です。測定で必要性が示されるまでは通常のリスト API を優先します。
読み取り専用入力
using System;
ReadOnlySpan<char> text = "2026-05-25".AsSpan(0, 4);
ReadOnlySpan<byte> method = "GET"u8;
Console.WriteLine(int.Parse(text)); // 2026
Console.WriteLine(method.Length); // 3
文字列は ReadOnlySpan<char>、UTF-8 リテラルは ReadOnlySpan<byte> を提供します。変更可能な Span は読み取り専用へ変換できますが、逆はできません。変更しないメソッドには読み取り専用の引数型を使います。
C# 14 は .NET 10 とともに登場しました。配列から Span への基本的な暗黙変換はそれ以前からあり、C# 14 は型推論や拡張メソッドのレシーバーなどで言語側の対応を広げました。C# 13 の allows ref struct は、明示的に対応したジェネリック API でのみ使えます。List<Span<int>> が許可されるわけではありません。
実際の処理を測る
Span のスライスはコピーや部分文字列の割り当てを避けられます。しかし、すべてのループがリストより速くなる保証はありません。JIT、ランタイム、データ量、処理内容に依存します。新たな文字列、配列、リストの生成では依然として割り当てが発生します。計測値には完全なコード、設定、実行結果が必要です。
非同期処理のバッファーには配列から ReadOnlyMemory への変換、解析や一時領域にはSpan が性能を改善できる場面を参照してください。
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.