概要
Ver. 12
.NET 8 で、
InlineArray
属性 (System.Runtime.CompilerServices
名前空間) というものが入りました。
基本的には .NET ランタイム側の機能ですが、
いくつか、C# 側にもこの InlineArray
向けの特殊対応が入っています。
ちなみに、この機能は現状、 コレクション式の内部実装にこそ使っていますが、 本稿で書いているようなコードを直接書く必要はほぼありません。 (実質、本稿はコレクション式の内部実装(の一部)の説明みたいなものです。)
InlineArray 属性
.NET 8 から、 以下のように、構造体に属性を付けると構造体のサイズが変わります。
using System.Runtime.CompilerServices; // この属性を付けると、 .NET ランタイムが特別扱いして、構造体のサイズを拡大する。 // (コンストラクター引数で Length 指定。) [InlineArray(3)] struct FixedBuffer<T> { // フィールドを1個だけ書く。 // (2個以上書くとコンパイル エラーになる。) // 構造体のサイズが sizeof(T) × Length になる。 private T _value; }
inline array という名前通り、「埋め込み配列」として使います。 (長さ N の配列代わりに、長さ N 個分のサイズを持った構造体を作ります。 C# の配列はヒープに割り当てられるのに対して、この inline array であればスタック上に値を持てます。)
要は、以下のような「N 個のフィールドを並べる」みたいな構造体を、ランタイム側で自動的に作ってくれる機能です。
using System.Runtime.InteropServices; // これまでの .NET/C# で同じことをやろうとすると… // 長さごとに専用の構造体を書いて、 struct FixedBuffer3<T> { // 所望の個数フィールドを書く。 // (3要素くらいならいいけども、数十とか数百になるときつい。) private T _value0; private T _value1; private T _value2; // 変換とかも自前で書く。 public static implicit operator Span<T>(FixedBuffer3<T> x) => MemoryMarshal.CreateSpan(ref x._value0, 3); public ref T this[int index] => ref ((Span<T>)this)[index]; }
ちなみに、InlineArrayAttribte
クラスには [EditorBrowsable(Never)]
属性がついています
(この属性が付いていると、Visual Studio などのコード補完の候補から外れます)。
要するに、開発者が InlineArray
属性を直接使うことは想定していなくて、隠してあります。
stackalloc との違い
これまでも stackalloc
という機能を使えば、
一応、スタック上に配列上のデータを置くことはできました。
ただ、stackalloc
には結構強い制限があって使いづらいです。
一番きつい制限は、参照型、もしくは、参照を含む型に対して使えないことです
(これを認めようとするとガベコレの負担が上がって、パフォーマンス的にかえって不利になるそうです)。
例えば以下のコードでは、string
以下の型に対してコンパイル エラーになります。
// 構造体に対しては使える。 Span<int> i = stackalloc int[100]; Span<DateTimeOffset> d = stackalloc DateTimeOffset[100]; // クラスに対しては使えない。 // (コンパイル エラーになる。) Span<string> s = stackalloc string[100]; // クラスや参照を含む構造体に対しても使えない。 // (コンパイル エラーになる。) Span<ContainsRefType> r1 = stackalloc ContainsRefType[100]; Span<ContainsRefField> r2 = stackalloc ContainsRefField[100]; struct ContainsRefType { public string String; } ref struct ContainsRefField { public ref int Ref; }
また、stackalloc
で確保したスタック領域は、メソッドを抜けるまで解放されません。
このせいで、ループの内側で間違って stackalloc
を使ってしまうと簡単にスタック オーバーフロー(要はメモリ不足)を引き起こします
(一般に、スタックはヒープよりもだいぶサイズが小さいです。Windows の場合は 1MB 程度)。
例えば以下のコードを Windows で実行するとスタック オーバーフローします
(1000 とか 200 とか、そこまで大きくない数字ですら簡単にスタック オーバーフローになります)。
for (int i = 0; i < 1000; i++) { _ = stackalloc long[200]; }
C# 側特殊対応
一応、C# 側にもこの InlineArray に対する特殊対応が入っています。 (一応、C# 12 の新機能。)
まず、属性を付けた型に対するチェックが働いています。
すでに前述の例でも書いていますが、
InlineArray
属性を付けた型にフィールドが2つ以上あるとコンパイル エラーになります。
using System.Runtime.CompilerServices; [InlineArray(3)] struct FixedBuffer<T> { // フィールドを2個以上書くとコンパイル エラーになるのは一応「C# の新機能」。 private T _value; }
また、この型を使う側に、以下のような特殊対応が入っています。
- インデクサーを直接書ける
Span<T>
/ReadOnlySpan<T>
に暗黙的に変換できるforeach
で列挙できる
FixedBuffer<string> buffer = new(); // InlineArray に対して直接インデクサーを書ける。 buffer[0] = "zero"; buffer[1] = "one"; // Span/ReadOnlySpan に暗黙的に変換できる。 Span<string> span = buffer; span[2] = "two"; // foreach で列挙できる。 foreach (var x in buffer) { Console.WriteLine(x); }
コレクション式と InlineArray
前述の通り、
InlineArray
属性には [EditorBrowsable(Never)]
が付いていて、
開発者が直接使う想定はあまりありません。
ただ、この機能は C# 12 時点で、コレクション式の最適化のために使われています。
Span<T>
や ReadOnlySpan<T>
型に対してコレクション式を使うと、
InlineArray
に展開されます。
例えば以下のようなコードの場合、
Span<int> i = [1, 2, 3, 4, 5]; ReadOnlySpan<string> s = ["a", "abc", ""];
以下のようなコードとほぼ同じ挙動になります。
using System.Runtime.CompilerServices; var i0 = new FixedArray5<int>(); i0[0] = 1; i0[1] = 2; i0[2] = 3; i0[3] = 4; i0[4] = 5; Span<int> i = i0; var s0 = new FixedArray3<string>(); s0[0] = "a"; s0[1] = "abc"; s0[2] = ""; ReadOnlySpan<string> s = s0; [InlineArray(3)] struct FixedArray3<T> { private T _value; } [InlineArray(5)] struct FixedArray5<T> { private T _value; }
将来展望
現状では、先ほどの例でいうと FixedArray3<T>
と FixedArray5<T>
があるように、
長さごとに別の型を用意せざるを得ない状態です。
「N 個のフィールドを並べる」コードを手書きするよりはマシですが、
まだ一時しのぎ的な実装になっていることは否めません。
根本的に大工事して型システムを改善するなら、
例えば、以下のように「整数型引数」を導入して、これを使って InlineArray
を作りたいという話もなくはないです。
// ※仮定の文法 namespace System; public struct InlineArray<T, int N>;
こういう「public にできる(一時しのぎではないちゃんとした) InlineArray
型」があるのなら、
C# 側でももう少し踏み込んだ文法を導入したかったみたいです。
候補として挙がっていたのは、int[N]
という書き方で「長さ N の InlineArray
」を書けるようにするというものです。
// ※仮定の文法 var c = new C(); int[3] values = c.Values; class C { private int[3] _values; public int[3] Values => _values; }
前述の InlineArray<T, int N>
みたいな書き方をできるようにするのは結構大変で、
短期的には実現しそうになく、
それに依存しそうな int[N]
という書き方も残念ながらしばらく実現の見込みはありません。