今回は params の話。

params の改善話は紆余曲折ありまして。 去年の時点では params Span<T> で検討されていました。 ちょこっとだけマイナーチェンジされまして、現在は params ReadOnlySpan<T> です。

いろんな型で params 案(没)

現在の C# の params (可変長引数)は、params T[] (引数の型は配列)しか書けません。 これに対して、任意のコレクション型を使って、params List<T> とか params IEnumerable<T> とか書きたいという要望が長らくありました。

// (あくまでも過去の案)
M1(1, 2, 3);
M2(1, 2, 3);
M3(1, 2, 3);

static void M1(params IEnumerable<int> items) { }
static void M2(params List<int> items) { }
static void M3(params Span<int> items) { }

「この類の何かを書きたい」という要望は今でもあるんですが、 ただ、ここにきてコレクション リテラルという提案が出ています。 コレクション リテラルがあれば、別に params がなくても以下のように書くことができます。

// 呼び出し側をコレクション リテラルにしてしまう。
// 元の params 案との差は [] の2文字だけ。
M1([1, 2, 3]);
M2([1, 2, 3]);
M3([1, 2, 3]);

// params で任意のコレクションを扱うのはやめちゃう。
static void M1(IEnumerable<int> items) { }
static void M2(List<int> items) { }
static void M3(Span<int> items) { }

[] の2文字程度ならさぼらず書いてもいいんじゃないかという感じがします。 なので、「params の汎用化」という目的においてはもう別にやらなくてもいいんじゃないかという雰囲気になっています。

params ReadOnlySpan

「params の汎用化」が没り気味な一方で、 「既存の params T[] 利用個所のパフォーマンスを改善したい」という要件は残っています。 そこで出てくるのが params ReadOnlySpan<T> になります。

すなわち、

  • params に使えそうな中で一番パフォーマンス的に有利な ReadOnlySpan だけを残す
  • 既存の params T[] よりも、params ReadOnlySpan<T> の方がオーバーロード解決優先順位を上にする
    • 既存のメソッドに params ReadOnlySpan<T> なオーバーロードを足せば、利用側は再コンパイルするだけでパフォーマンス改善になる

という方針で進めるようです。

ちなみに、params ReadOnlySpan<T> で定義した引数は常に scoped みたいです。 ReadOnly で受け取っているので書き換えできず、scoped なのでメソッドの外には漏らせません。 その結果、呼び出し側で M(a, b, c) みたいな書き方から「a, b, c を含む ReadOnlySpan」を作るときの最適化がしやすくなっています (DLL のデータ領域を直接参照したり、複数回呼び出されるときに同じバッファーを使いまわしたり)。

固定長バッファー

以下のようなコードを書いたとき、

M(1, 2, 3);
M("a", "b", "c");

static void M<T>(params ReadOnlySpan<T> items) { }

概念的には、以下のように「スタック割り当て」をしたいです。

// int の場合はこれで問題ない。
ReadOnlySpan<int> temp1 = stackalloc[] { 1, 2, 3 };
M(temp1);

// 現状、参照型の stackalloc はできないので、何らかの対処が必要。
ReadOnlySpan<string> temp2 = stackalloc[] { "a", "b", "c" };
M(temp2);

static void M<T>(params ReadOnlySpan<T> items) { }

そこで、参照型にも使えるスタック割り当て手段を必要とするわけですが。 去年の時点で、 「Experiment with 'Unsafe.StackAlloc'」とか「[hackathon] ValueArray」みたいなプロトタイプもあったんですが、あんまり筋はよくなかったようで没っています。

そして現状、「特殊な属性を1個用意して、それをつけると .NET ランタイムが特殊対応して固定長バッファーを生成する」みたいな案で進んでいるようです。

ちなみに、params ReadOnlySpan<T>ReadOnlySpan<T> に対するコレクション リテラルは同じ戦略をとるそうで (やることは同じなので2重実装は避ける)、 「params の改善」と「コレクション リテラル」は2つ合わせて同時に進めるということになりました。