ref 構造体のインターフェイス実装 / 型引数での使用
ref 構造体で説明しているように、
Span<T> 型など一部の型は「スタック上にないといけない」という強い制約があります。
この制約を守るため、これまで、ref 構造体は
- インターフェイスを実装できなかった
- ジェネリック型引数に使えなかった
という制限が掛かっていました。
C# 13 では、この制限を緩和するため、
ジェネリック型引数に「allows ref struct」という「アンチ制約」を追加する予定です。
こういう案自体は ref フィールドが追加された C# 11 (2022年)の頃から温められてはいたんですが、 いよいよ C# 13 で本格的に取り組むみたいです。 .NET 8/C# 12 がリリースされた後くらいからちらほら提案ドキュメントの更新あり。
- Add draft for demonstrating ref-struct-constraint soundness
- Update ref struct interfaces based on LDM discussions
- ref struct interfaces updates
ちなみに、ランタイム側はその2022年頃に対応すでに入っているみたいです。
ref 構造体の制限緩和の要求
わかりやすい例でいうと、Span<T> は IEnumerable<T> であってほしいというものです。
C# 12 時点だと、以下のような2重実装を余儀なくされています。
List<int> list = [1, 2, 3, 4, 5];
ReadOnlySpan<int> span = [1, 2, 3, 4, 5];
Console.WriteLine(MyMath.Sum(list));
Console.WriteLine(MyMath.Sum(span));
static class MyMath
{
public static int Sum(IEnumerable<int> numbers)
{
var sum = 0;
foreach (var x in numbers) sum+= x;
return sum;
}
// メソッドの中身全く同じ。
// Span/ReadOnlySpan が IEnumerable じゃないので別メソッドでの実装が必須。
public static int Sum(ReadOnlySpan<int> numbers)
{
// 実装的に、numbers をボックス化したり、ref フィールドを外に漏らしたりもしてない。
// IEnumerable に対する実装をそのまま使って何も問題ない。
var sum = 0;
foreach (var x in numbers) sum += x;
return sum;
}
}
ref 構造体にインターフェイス実装を持たせること自体はそこまで問題ではありません。 問題は、以下のように、「インターフェイス型の変数に直接代入してしまうとボックス化を起こしてまずい」という点です。
Span<int> span = [1, 2, 3, 4, 5];
// たとえ、Span が IEnumerable<T> を実装していたとしても、
// 以下のようなコードを書くとこの時点でボックス化が起きる。
// span がヒープに漏れてしまうのでまずい。
IEnumerable<int> e = span;
じゃあどうすべきかというと、ジェネリクスを介します。
Span<int> span = [1, 2, 3, 4, 5];
// ジェネリクスを介すれば、ボックス化を起こさずにインターフェイスのメンバーを呼べる。
// (前述の問題はクリア。)
static T Sum<T, TEnumerable>(TEnumerable list)
where TEnumerable : IEnumerable<T>
{
// 省略
return default!; // 仮
}
// なので残る問題はこっち。
// ref 構造体を型引数に渡したい。
Sum<int, Span<int>>(span);
ということで次節で説明する「アンチ制約」が必要になります。
アンチ制約
ジェネリック型制約(where T : みたいなやつ)は、普通、制限を掛けることで、
- メソッド内で
Tに対して できること(呼べるメソッドとか)が増える - その代わり、呼び出し側で
Tに対して渡せる型が減る
というものになります。
// 制限なし。
static void M1<T>() { }
// 何の型でも渡せる。
M1<int>();
M1<string>();
M1<object>();
// 制限あり。
static void M2<T>() where T:ISpanParsable<T>
{
// 呼べるメソッドが増える。
T value = T.Parse("123", null);
}
// 渡せる型が減る。
M2<int>();
M2<string>();
M2<object>(); // コンパイルエラー。
ところが今回、「ref 構造体を渡せるようにしたい」という逆の要件なので、「制約」ではなく「アンチ制約(制約の撤回)」が必要になります。
2年くらい前のブログでちょこっと触れていますが、
逆のことをするのに where T : ref struct とは書かせたくないようで、ちょっと別文法を模索していました。
当初案だと allow T : ref struct とかも検討されていたんですが、
結局は where T : allows ref struct (where はそのまま。制約の前に allows)になりそうです。
// allows で制限を緩める。
static void M3<T>(T x)
where T : allows ref struct // アンチ制約。
{
// メソッド内でできることが減る。
object obj = x; // box 化ダメ。エラーにする予定。
}
// 渡せる型が増える。
M3<int>();
M3<string>();
M3<object>();
M3<Span<int>>(); // allows ref struct がないと呼べない。
ちなみに、where T : IDisposable, allows ref struct みたいに、制約とアンチ制約は並べて書けます。
