「Rosly の Language Feature Status に並んでいるもののうち、すでに preview 提供済みのものシリーズ第2段。
- field キーワード
- First-class Span ← 今日はこれ
- nameof(T<>)
すでに今、LangVersion に preview
を指定すれば利用可能です。
今日は First-class Span。 (これも昔1回取り上げてるんですが、案外書くことあり。)
First-class Span
C# 7.2 の頃に Span<T>
や ReadOnlySpan<T>
が導入されて以来、
これらの型を使った高パフォーマンスな API がたくさん提供されています。
また、C# 12 で入ったコレクション式や、
C# 13 で入った params
コレクションでは、
T[]
や IEnumerable<T>
よりも Span<T>
や ReadOnlySpan<T>
を優先的に選ぶように特別な処理が入っています。
この例からもわかるように、今や Span<T>
や ReadOnlySpan<T>
が重要な地位を占めています。
ところが、コレクション式などの一部の文脈を除いて、
Span<T>
や ReadOnlySpan<T>
は「ただの構造体」で、
配列からの型変換も「Span<T>
や ReadOnlySpan<T>
構造体に定義されたユーザー定義型変換」です。
C# 言語組み込みの型変換と比べて、ユーザー定義型変換は1段下扱いで、色々な不便があります。
そこで、Span<T>
や ReadOnlySpan<T>
を言語組み込み(= first-class、一級市民)にしたいという提案があって、
これもすでに実装があり、
Visual Studio 17.13.0 Preview 1 (.NET 9 の正式リリースと同時)で merge 済みです。
わかりやすいのは拡張メソッドの呼び出しで、 ユーザー定義型変換を挟む拡張メソッド呼び出しはできません。 例えば、以下のコードは C# 13 でコンパイル エラーだったものが、preview ではコンパイルできるようになっています。
// 拡張メソッドの呼び出しはユーザー定義の型変換を見ない。 // Span の特別扱いがないと拡張メソッドは呼べない。 new int[1].M(); static class Ex { public static void M<T>(this Span<T> _) { } }
また、
「Span<T>
や ReadOnlySpan<T>
引数を使った方がパフォーマンスがいいのでこちらを呼んでほしい」という要望があるんですが、
これまでは IEnumerable<T>
なオーバーロードがあるとそっちが優先されるという問題もありました。
// ユーザー定義の型変換よりも、「派生・実装クラスだから変換可能」の方が優先度が高い。 Ex.M(new int[1]); static class Ex { public static void M<T>(this IEnumerable<T> _) { } // C# 13 ではこっち。 public static void M<T>(this ReadOnlySpan<T> _) { } // preview ではこっち。Span の特別扱いがないとこっちは呼んでもらえない。 }
また、ユーザー定義の型変換では「型引数の共変性」を表現できないという問題があります。
ReadOnlySpan<string>
を ReadOnlySpan<object>
に代入できてもいいはずなのに、
これが C# 13 まではできませんでしたが、preview にすると受け付けます。
ReadOnlySpan<string> s = []; ReadOnlySpan<object> span = s; // C# 13 ではエラー。
ちなみに、Span<T>
と ReadOnlySpan<T>
の両方のオーバーロードがある場合、
ReadOnlySpan<T>
の方が優先されます。
string[] s = []; // ReadOnlySpan の方が優先。 s.M(); static class Ex { public static void M<T>(this Span<T> _) { } public static void M<T>(this ReadOnlySpan<T> _) { } }
target-typed で生成される型自体が変わるコレクション式と違って、
一度配列を作っちゃってるので ReadOnlySpan<T>
を優先しても別にパフォーマンス的なメリットは少ないんですけども。
じゃあどうしてこういう仕様にしたかというと…
こうしておかないとまた「配列の共変性の地雷を踏むから」とのこと。
string[] s = []; object[] o = s; // C# の配列は共変。 // Span を優先するとこれが例外を起こしちゃう。 // ReadOnlySpan<object> x = s; は合法。 // Span<object> x = s; は実行時例外。 o.M(); // ReadOnlySpan<object> を優先しないとここで例外が出る。 static class Ex { public static void M(this Span<object> _) { } public static void M(this ReadOnlySpan<object> _) { } }
以上、とりあえず、C# 14 (現状 LangVersion
preview)では Span<T>
、ReadOnlySpan<T>
が特別扱いされて、オーバーロードの解決順位が変わります。
おおむね便利な方向に変わるはずですが、もしかすると何らかの問題を起こす可能性もあります。
もしも「Span<T>
オーバーロードを呼ばれるとまずい」みたいなことがあれば、OverloadResolutionPriority
とかでの対処を考えてみてください。