「Rosly の Language Feature Status に並んでいるもののうち、すでに preview 提供済みのものシリーズ第2段。

  • field キーワード
  • First-class Span ← 今日はこれ
  • nameof(T<>)

すでに今、LangVersionpreview を指定すれば利用可能です。

今日は 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とかでの対処を考えてみてください。