はじめに:「Span対応変換」は“速さを意識した LINQ の次の一歩”
LINQ は書きやすいけれど、
「配列や文字列をガッツリ触る処理」でパフォーマンスを突き詰めたいときには、
メモリ確保(アロケーション)が気になってきます。
そこで登場するのが Span<T> / ReadOnlySpan<T> です。
配列や文字列などの「連続したメモリ領域」を、
「追加の配列を作らずに」「スライスしながら」扱える型で、
.NET が用意している“低コストなビュー”だと思ってください。
ここでの「Span対応変換」というのは、ざっくり言うと、
普段 T[] や List<T>、string に対してやっている処理を、Span<T> / ReadOnlySpan<T> に“乗り換えられるようにしておく”ユーティリティ
のことです。
ここから、
Span / ReadOnlySpan のざっくりイメージ
配列・文字列からの Span 変換
LINQ っぽい処理を Span で書くとどう変わるか
業務で使いやすくするための「Span対応ユーティリティ」の考え方
を、初心者向けにかみ砕いて説明していきます。
Span / ReadOnlySpan の基本イメージ
「中身を持たない、“切り取りビュー”」だと思う
まず、Span<T> は「配列そのもの」ではありません。
「どこからどこまでを見るか」という“範囲情報”だけを持った構造体です。
var array = new int[] { 10, 20, 30, 40, 50 };
Span<int> span = array.AsSpan(); // 全体
Span<int> middle = array.AsSpan(1, 3); // 20, 30, 40 の部分だけ
C#Span<int> 自体は「メモリを所有していない」ので、new Span<int>(...) で自由に確保したりはしません。
既存の配列やスタック領域に対して「ビュー」を作るイメージです。
文字列の場合は ReadOnlySpan<char> を使います。
string s = "Hello, World";
ReadOnlySpan<char> span = s.AsSpan();
ReadOnlySpan<char> hello = s.AsSpan(0, 5); // "Hello"
C#ここでの重要ポイントは、
「Span は“軽量なビュー”であって、“新しい配列”ではない」
「だから、スライスしても追加の配列が増えない」
ということです。
配列・List・文字列から Span に変換する
配列 → Span
配列は AsSpan() でそのまま Span に変換できます。
var numbers = new int[] { 1, 2, 3, 4, 5 };
Span<int> span = numbers.AsSpan();
Span<int> sub = numbers.AsSpan(1, 3); // 2, 3, 4
C#sub[0] = 99; のように書くと、元の配列 numbers も書き換わります。
ビューなので、当然「同じメモリ」を見ています。
List → Span(配列経由)
List<T> は直接 AsSpan() を持っていませんが、
内部配列を ToArray() で取り出してから Span にできます。
var list = new List<int> { 1, 2, 3, 4, 5 };
var array = list.ToArray();
Span<int> span = array.AsSpan();
C#ここは「一度配列を作る」ので、アロケーションは発生します。
「その後の処理を Span でガッツリやりたい」ときに意味があります。
string → ReadOnlySpan<char>
文字列は AsSpan() で ReadOnlySpan<char> に変換できます。
string s = "ABCDE";
ReadOnlySpan<char> span = s.AsSpan();
ReadOnlySpan<char> bc = s.AsSpan(1, 2); // "BC"
C#ここでの重要ポイントは、
「配列・文字列は AsSpan で“追加の配列なしに切り取れる”」
「List は ToArray を挟む必要がある」
ということです。
LINQ っぽい処理を Span で書いてみる
例1:条件に合う最初の要素を探す(FirstOrDefault 相当)
LINQ ならこう書きます。
var firstEven = numbers.FirstOrDefault(x => x % 2 == 0);
C#Span で同じことをやると、こうなります。
Span<int> span = numbers.AsSpan();
int firstEven = -1;
foreach (var x in span)
{
if (x % 2 == 0)
{
firstEven = x;
break;
}
}
C#正直、書きやすさだけなら LINQ の勝ちです。
ただし、Span を使うと「余計な配列を作らずに」「範囲を絞りながら」処理できます。
例2:部分配列に対してだけ処理する
LINQ で「2 番目から 3 要素だけ」を処理したいとき、
素直に書くとこうなります。
var part = numbers
.Skip(1)
.Take(3)
.Where(x => x % 2 == 0)
.ToArray();
C#Skip や Take 自体は遅延実行ですが、ToArray() で新しい配列が確保されます。
Span なら、こう書けます。
Span<int> span = numbers.AsSpan(1, 3); // 2, 3, 4
foreach (var x in span)
{
if (x % 2 == 0)
{
Console.WriteLine(x);
}
}
C#ここでの重要ポイントは、
「“範囲を絞る”という操作を、LINQ の Skip/Take ではなく、
Span のスライス(AsSpan(offset, length))でやると、
追加の配列を作らずに済む」
ということです。
Span対応ユーティリティの考え方
「配列版」と「Span版」をペアで用意する
業務でよくあるのは、
「配列や文字列に対して、ある処理を何度も行うユーティリティメソッド」です。
例えば、「区切り文字で分割して、トリムして返す」処理を考えます。
public static string[] SplitAndTrim(string s, char separator)
{
return s
.Split(separator)
.Select(x => x.Trim())
.ToArray();
}
C#これを「Span対応」にするなら、
内部で ReadOnlySpan<char> を使う版を用意しておく、という発想です。
ざっくりイメージだけ示すと、こういう形になります。
public static List<ReadOnlySpan<char>> SplitAndTrim(ReadOnlySpan<char> span, char separator)
{
var result = new List<ReadOnlySpan<char>>();
while (true)
{
int index = span.IndexOf(separator);
if (index < 0)
{
var part = span.Trim();
if (!part.IsEmpty)
{
result.Add(part);
}
break;
}
var head = span.Slice(0, index).Trim();
if (!head.IsEmpty)
{
result.Add(head);
}
span = span.Slice(index + 1);
}
return result;
}
C#そして、文字列版は「Span版を呼ぶだけ」にします。
public static string[] SplitAndTrim(string s, char separator)
{
var spans = SplitAndTrim(s.AsSpan(), separator);
return spans.Select(x => x.ToString()).ToArray();
}
C#ここでの重要ポイントは、
「本体ロジックを Span 版に寄せておき、
必要に応じて string / 配列版から“ラップして呼ぶ”」
という設計です。
こうしておくと、
パフォーマンスが重要なところでは Span 版を直接使う
使いやすさ重視のところでは string / 配列版を使う
という選択ができるようになります。
業務での具体的な使いどころ
大きな配列・バッファを扱う処理
ログのバイナリ解析
ファイルフォーマットのパース
ネットワークパケットの解析
こういった「大きなバイト配列を何度もスライスする」処理は、
Span と相性がとても良いです。
void ParsePacket(ReadOnlySpan<byte> packet)
{
var header = packet.Slice(0, 4);
var body = packet.Slice(4);
// ここから先も、body をさらに Slice しながら解析していく
}
C#LINQ で Skip / Take / ToArray を繰り返すより、
Span でスライスしながら進めたほうが、メモリ効率も速度も良くなります。
文字列の部分解析・トークナイズ
ログ 1 行をパースする
CSV の 1 行を手書きで解析する
固定長ファイルの 1 行を切り出す
こういった処理も、ReadOnlySpan<char> で書くと「余計な string を増やさずに」済みます。
void ParseLine(ReadOnlySpan<char> line)
{
var dateSpan = line.Slice(0, 10);
var levelSpan = line.Slice(11, 5);
var messageSpan = line.Slice(17);
// 必要になったところだけ ToString() する
var level = levelSpan.ToString();
}
C#ここでの重要ポイントは、
「“全部 string にしてから処理する”のではなく、
“必要になったところだけ ToString する”」
という発想に切り替えられることです。
まとめ:「Span対応変換」は“LINQ の書きやすさと、低レベルの速さの橋渡し”
Span対応変換の本質は、
普段 LINQ や配列・文字列で書いている処理を、
必要に応じて Span<T> / ReadOnlySpan<T> ベースでも書けるようにしておき、
「速さが欲しいところだけ、そちらに乗り換えられるようにする」
ことです。
押さえておきたいポイントを整理すると、
Span / ReadOnlySpan は「配列や文字列の“ビュー”」であり、新しい配列を作らない
配列・文字列は AsSpan で簡単に Span 化できる
範囲を絞る処理は、Skip/Take ではなく AsSpan(offset, length) で書ける
本体ロジックを Span 版に寄せておき、string / 配列版はラッパーにすると設計がきれい
大きな配列・文字列をガッツリ触る処理(パース・解析)で特に効果が出やすい
ここまでイメージがつかめていれば、
「まずは LINQ で書いてみて、必要になったら Span 対応版を用意する」という、
一段階上の選択肢を持てるようになります。
