2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】ReadOnlySpan<T>とは何か?コピーなしでメモリを読む仕組みを整理した

2
Last updated at Posted at 2026-06-08

はじめに

文字列を扱っていると、こんなコードをよく書きます。

string text = "Hello, World!";
string sub = text.Substring(7, 5); // "World"

これで特に困ることはないのですが、あるとき ReadOnlySpan<char> という書き方を見かけました。

ReadOnlySpan<char> span = text.AsSpan(7, 5);

「なんだこれ?」と思いながらもスルーしていたのですが、パフォーマンス系の話題でよく出てくるので、ちゃんと調べてみました。

環境

  • .NET 10.0
  • C# 14
  • Visual Studio 2026

ReadOnlySpan<T> とは何か

一言で言うと、「コピーなしにメモリの一部を読み取り専用で参照できる型」です。ref struct という言語機能が C# 7.2 で導入され、ReadOnlySpan<T> という型そのものは .NET Core 2.1 で標準ライブラリに追加されました。

Substring との比較で考える

文字列の一部を取り出したいとき、Substring を使うのが一般的です。

string text = "Hello, World!";
string sub = text.Substring(7, 5); // "World" という新しい string が作られる

これは「World」という新しい string オブジェクトをヒープ上に確保します。1回2回ではたいして問題になりませんが、ループの中で大量に繰り返すと、GC(ガベージコレクター)への負荷が積み重なります。

ReadOnlySpan<char> を使うと、新しいオブジェクトを作りません。元の文字列のメモリを 「7文字目から5文字分」 と指し示すだけです。

string text = "Hello, World!";
ReadOnlySpan<char> span = text.AsSpan(7, 5); // コピーなし。元のメモリを参照するだけ

// 普通のインデックスアクセスや foreach が使える
Console.WriteLine(span[0]);  // 'W'
Console.WriteLine(span.Length);  // 5

foreach (char c in span)
{
    Console.Write(c); // "World"
}

処理の結果は同じですが、内部的には「元の文字列のどこからどこまで」という情報しか持っていないので、無駄なアロケーションが発生しません。

配列にも使える

文字列だけでなく、配列に対しても同様に使えます。

int[] numbers = { 1, 2, 3, 4, 5 };
ReadOnlySpan<int> span = numbers.AsSpan(1, 3); // { 2, 3, 4 } の範囲を参照

Console.WriteLine(span[0]);  // 2
Console.WriteLine(span.Length);  // 3

元の配列をそのまま参照しているので、配列の中身が変われば span から見える値も変わります。コピーではなく参照なので、当然の動作です。

Span<T> との違い

ReadOnlySpan<T> とセットで語られる Span<T> があります。違いは名前の通り、書き込みができるかどうかです。

int[] numbers = { 1, 2, 3, 4, 5 };

// Span<T> は書き込み可能
Span<int> span = numbers.AsSpan(1, 3);
span[0] = 99; // 元の配列が { 1, 99, 3, 4, 5 } に変わる

// ReadOnlySpan<T> は読み取り専用
ReadOnlySpan<int> readOnly = numbers.AsSpan(1, 3);
readOnly[0] = 99; // コンパイルエラー
読み取り 書き込み
Span<T>
ReadOnlySpan<T>

読み取るだけでよい場面では ReadOnlySpan<T> を使うのが安全です。メソッドの引数に使う場合も、ReadOnlySpan<T> にしておけば「このメソッドはデータを変更しない」という意図が伝わります。

制約がある

ReadOnlySpan<T>(および Span<T>)には、通常の型にはない制約があります。

これらは ref struct という特殊な構造体として定義されており、スタック上にしか置けないという設計になっています。

// ❌ クラスのフィールドにはできない
public class MyClass
{
    private ReadOnlySpan<char> _span; // コンパイルエラー
}

// ❌ ラムダにキャプチャできない
ReadOnlySpan<char> span = "hello".AsSpan();
Action action = () => Console.WriteLine(span[0]); // コンパイルエラー

なぜこういう制約があるかというと、スタック上のメモリを参照している可能性があるため、ヒープ上に持ち出すと参照先が消えてしまう(ダングリング参照)危険があるからです。コンパイラがそれを防いでいます。

async メソッドとの関係

async メソッドについては、C# のバージョンによって扱いが変わります。

C# 12 以前は async メソッド内で ref struct 変数を宣言すること自体がコンパイルエラーでした。

C# 13 以降は制約が緩和されており、await 境界をまたがない範囲であれば async メソッド内でも宣言できるようになっています。

// ❌ await と同じスコープにある場合はエラー(C# 13 以降も同様)
public async Task DoSomethingAsync()
{
    ReadOnlySpan<char> span = "hello".AsSpan();
    await Task.Delay(100); // span と await が同じスコープにあるのでNG
}

// ✅ C# 13 以降は、await をまたがない範囲なら使える
public async Task DoSomethingAsync()
{
    {
        ReadOnlySpan<char> span = "hello".AsSpan();
        Console.WriteLine(span[0]); // await の前に使い切る
    } // span のスコープはここで終わる

    await Task.Delay(100); // span はすでに存在していないのでOK
}

「async メソッドで一切使えない」のではなく、「await をまたいでアクセスすることはできない」というのが正確な制約です。理由は同じで、await の前後でスタックフレームが変わるため、スタック上の参照が無効になるリスクがあるからです。

どんな場面で使うのか

普段のアプリケーション開発でいきなり ReadOnlySpan<T> を書く場面は多くないかもしれませんが、使いどころとしては以下のような場面が考えられます。

文字列のパース処理

CSV や HTTP ヘッダーのような文字列を分割しながら読む処理は、Substring を多用するとアロケーションが多くなります。ReadOnlySpan<char>IndexOf を組み合わせると、コピーなしでパースできます。

ReadOnlySpan<char> line = "Alice,30,Engineer".AsSpan();

int comma1 = line.IndexOf(',');
ReadOnlySpan<char> name = line[..comma1]; // "Alice"

ReadOnlySpan<char> rest = line[(comma1 + 1)..];
int comma2 = rest.IndexOf(',');
ReadOnlySpan<char> age = rest[..comma2]; // "30"

バイナリデータの読み取り

バイト配列の特定範囲だけを解析したい場面でも、スライスを使って無駄なコピーを避けられます。

まとめ

  • ReadOnlySpan<T> は配列や文字列の一部をコピーなしに読み取り専用で参照できる型
  • Substring のように新しいオブジェクトを作らないため、アロケーションとGCの負荷を抑えられる
  • Span<T> との違いは書き込みができないこと(読み取り専用)
  • ref struct なのでクラスのフィールドやラムダへのキャプチャはできない
  • async メソッドについては、C# 13 以降は await をまたがない範囲なら使える(同じスコープに await がある場合はNG)
  • 普段のコードよりも、パース処理やバイナリ解析などパフォーマンスが気になる場面で特に有効

「なんとなく見かけるけどよくわからない型」という印象でしたが、調べると「なぜそういう制約があるのか」まで含めて納得できました。すぐに使う場面があるわけではないですが、パフォーマンス改善を考えるときの選択肢として頭に入れておきたいと思います。

参考になったら いいねストック をお願いします!
同じような疑問を持ったことがある方のコメントもお待ちしています。

参考

関連リンク

技術ブログでも学びや検証内容をまとめています。

nakamuuublog

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?