CodeGym /コース /C# SELF /イントロダクション: Sp...

イントロダクション: Span<T>ReadOnlySpan<T> の導入

C# SELF
レベル 65 , レッスン 2
使用可能

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.JsonSpan の 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> は独立した配列ではなく、データの一部を透かして見る“レンズ”です。

他のコレクションとの違い:比較表

データを保持する? 要素を変更できる? サイズを変更できる? スライスでコピーする? どこに存在する?
int[]
はい はい いいえ はい (.Take などで) Heap
List<int>
はい はい はい はい Heap
Span<int>
いいえ はい いいえ いいえ Stack
ReadOnlySpan<int>
いいえ いいえ いいえ いいえ Stack

9. Span/ReadOnlySpan を使うときの典型的なミス

ミス №1: Span をクラスのフィールドとして保存しようとする。コンパイラは “Span type may not be used in this context” というエラーを出します。これは安全性のための制約です。

ミス №2: Spanasync メソッドから返そうとする。async メソッドはヒープに逃げる可能性があるため禁止されています。代わりに配列など別の型を使ってください。

ミス №3: Span を通しての変更が元の配列に反映されることを忘れてしまう。これにより外側のデータが意図せず変わり、不具合につながることがあります。

2
タスク
C# SELF, レベル 65, レッスン 2
ロック未解除
`Span`での配列の変更
`Span`での配列の変更
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION