目次

Ver. 12.0

※ 2024/7/6 現在、プレビュー版です。

リリース時期 2024/11
同世代技術
  • .NET 9.0

執筆予定: C# 13.0 トラッキング issue

params コレクション

コレクション式で使える型であれば何でも params にできるようになりました。

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

M1(1, 2);
M2(1, 2);
M3(1, 2);
M4(1, 2);

需要が高いのは ReadOnlySpan で、 params T[]params ReadOnlySpan<T> に変更すればそれだけでパフォーマンスの改善が見込めます。

実際、 .NET 9 では、string.JoinTask.WhenAll などのメソッドに params ReadOnlySpan<T> なオーバーロードが増えています。

// .NET 8 以前なら Join(string, string[])
// .NET 9 以降なら Join(string, ReadOnlySpan<string>)
var joiend = string.Join(",", "a", "b", "c");

このため、自分で params を使わない場合でも、 「.NET 9 にアップグレードして再コンパイルするだけでアプリのパフォーマンスがちょっと改善する」という間接的なメリットがあります。

詳しくは「params コレクション」で説明しています。

部分プロパティ

プロパティとインデクサーも partial にできるようになりました。

例えば、C# 13 と同世代の .NET 9 では、GeneratedRegex をプロパティにできるようになりました。

using System.Text.RegularExpressions;

partial class MyPatterns
{
    [GeneratedRegex(@"\d{4}")]
    public static partial Regex FourDigits { get; } // プロパティになった。
}

詳しくは「部分プロパティ」で説明します。

ref 構造体のインターフェイス実装

ref 構造体にインターフェイスを実装できるようになりました。 また、このインターフェイスのメンバーを呼び出すために、 ジェネリック型引数に ref 構造体を渡せるようにする仕組みとして allows ref struct アンチ制約が追加されました。

S x = new(); // S は IFormattable を実装してる。

// これはボックス化を起こすから C# 13 でもエラーになる。
IFormattable f = x;
f.ToString("X", null);

// allows ref struct なジェネリックメソッドを介して、
static void M<T>(T f) where T : IFormattable, allows ref struct
    => f.ToString("X", null);

// こうやって IFormattable.ToString を呼べば大丈夫になった。
M(x);

ref struct S : IFormattable
{
    public string ToString(string? format, IFormatProvider? formatProvider) => "";
}

詳しくは「ref 構造体のインターフェイス実装」で説明します。 また、「アンチ制約」という言葉については「アンチ制約」で説明しています。

Lock クラスに対する lock

.NET 9 で Lock クラス(System.Threading 名前空間)という新しい lock 用の型が追加されたことに伴って、 lock ステートメントでこの Lock クラスを特別扱いするようになりました。 既存の lock (Monitor.Enter に展開される)と異なり、以下のようなコードに展開されます。

var syncObject = new Lock();

// lock (syncObject)
using (syncObject.EnterScope())
{
}

詳しくは「Lock クラス」で説明しています。

ref/unsafe をイテレーター/非同期メソッド中に書けるように

ref ローカル変数ref 構造体の変数、 unsafe ブロックを、 イテレーター非同期メソッド内で使えるようになりました。

イテレーターと非同期メソッドは内部の仕組み的に非常に似ているにも関わらず、 この2者で微妙に制限のかかり方が違ったんですが、 それも C# 13 でそろいました。

以下のコードで、行末コメントで ⭕ を付けている部分が C# 13 で新たにコンパイルできるようになったコードです。

IEnumerable<object?> Enumerate()
{
    unsafe { } // ⭕

    yield return null;

    Span<byte> data = [];

    yield return null;

    int x = 123;
    ref int r = ref x; // ⭕
}

async Task GetAsync()
{
    unsafe { }

    await Task.Yield();

    Span<byte> data = []; // ⭕

    await Task.Yield();

    int x = 123;
    ref int r = ref x; // ⭕
}

async IAsyncEnumerable<object?> EnumerateAsync()
{
    unsafe { } // ⭕

    await Task.Yield(); yield return null;

    Span<byte> data = []; // ⭕

    await Task.Yield(); yield return null;

    int x = 123;
    ref int r = ref x; // ⭕
}

元々、原理的にはこう書いても問題ないことはわかっていたんですが、 正しく判定するのにコストがかかる割に、需要は低いだろうということでエラーにしていました。 C# 13 で書けるようになったのは、前述のLock クラスに対する lock のついでだそうです。 (Lock クラスの EnterScope が ref 構造体を使っています。)

ただし、これは yieldawait をまたがない場合に限って許されます。 例えば以下のコードは C# 13 でもコンパイル エラーを起こします。

IEnumerable<object?> Enumerate()
{
    int x = 123;
    ref int r = ref x;
    yield return null;
    r = 456;
}

async Task GetAsync()
{
    int x = 123;
    ref int r = ref x;
    await Task.Yield();
    r = 456;
}

\e (エスケープ文字のエスケープ シーケンス)

文字・文字列リテラル中のエスケープ シーケンス\e (U+001B、エスケープ文字)が追加されました。

例えば、コンソール アプリで以下のように書くことで、文字列の色を変えたり装飾したりできます。

Console.WriteLine("\e[31mred text");
Console.WriteLine("\e[4munderlined text");
Console.WriteLine("\e[0mreset style");

\e エスケープ シーケンス

機能追加の背景などについてはブログ記事「\e (エスケープ文字のエスケープ シーケンス)」で説明しています。

その他

その他、ほぼバグ修正レベルの機能がいくつかあります。

オブジェクト初期化子中の ^ 演算子

以下のように、オブジェクト初期化子中の [] の中でインデックスの ^ 演算子を使えるようになりました。

// これが C# 12 以前はコンパイル エラーを起こしてた。
var c = new C { [^1] = 1 };

// これなら昔からコンパイルできる。
// (オブジェクト初期化子はこれと同じコードに展開されるはずなのに。)
c[^1] = 1;

class C
{
    // インデクサーと Length さえ持っていれば c[^i] と書けるようになる。
    // c[c.Length - i] 扱い。
    public int Length => 1;
    public int this[int i] { get => i; set { } }
}

デリゲートの自然な型の改善

デリゲートの自然な型の決定の際、 メソッド グループに対する型決定がちょっと賢くなったそうです。 同名のインスタンス メソッドと拡張メソッドがあるとき、インスタンス メソッドを優先的に見るようになりました。

例えば以下のようなクラスがあったとします。

public class C
{
    public void M() { } // インスタンス メソッド M と、
}

public static class E
{
    public static void M(this C c, object o) { } // 同名の拡張メソッド。
}

この C 型のインスタンス x に対して x.M と書いたとき、 C# 12 までは自然な型を決定できなかったのに対して、 C# 13 ではインスタンスメソッドを優先的に見ます。

var x = new C();

// オーバーロード解決ではインスタンスメソッド優先。
x.M();      // C.M()
x.M(""); // E.M(C, object)

// 型の明示があると昔から大丈夫だった。
Action a = x.M;         // C.M()
Action<object> b = x.M; // E.M(C, object)

// var を使う。
// これが C# 13 から行けるように。
// インスタンス メソッド優先で、Action 型になる。
var z = x.M;

コレクション式の改善

コレクション式にも微妙な修正が2つ入っています。

1つは、Add メソッドが拡張メソッドでも大丈夫になりました。 (こちらは最新のコンパイラーにすると LangVersion 12 にしても元の挙動(= コンパイル エラー)にはなりません。)

using System.Collections;

C c = ['a'];

class C : IEnumerable
{
    public IEnumerator GetEnumerator() => throw new NotImplementedException();
}

static class Extensions
{
    // C# 12 の頃はこの拡張メソッドを見てくれずエラーになっていた。
    public static void Add(this C a, char _) { }
}

もう1つは、params コレクションとの兼ね合いで、オーバーロード解決ルールが変わっています。 以下のように、要素の型違いのオーバーロードがあるとき、要素の自然な型を見るようになりました。 (この変更は言語バージョンを見て分岐しているようで、 最新のコンパイラーでも LangVersion を12以前に戻すと古い挙動になります。)

// C# 12 では以下の2つとも解決不能(コンパイル エラー)になってた。

// C# 13 では int の方になる。
C.M([1]);

// C# 13 では string の方になる。
C.M([$""]);

class C
{
    public static void M(List<int> _) { }
    public static void M(List<byte> _) { }

    public static void M(List<string> _) { }
    public static void M(List<IFormattable> _) { }
}

ただ、この結果、ちょっとした破壊的変更も起きています。 C# 12 から C# 13 にアップデートすると、以下のような場合にオーバーロード解決先が変わります。

C.M([1, 2]);

class C
{
    // C# 12 だとこっちが呼ばれる。
    // (ReadOnlySpan 優先。)
    public static void M(ReadOnlySpan<byte> data) => Console.WriteLine("ReadOnlySpan<byte>");

    // C# 13 だとこっちが呼ばれる。
    // (中身の自然な型(整数リテラルは int になる)優先。)
    public static void M(Span<int> data) => Console.WriteLine("Span<int>");
}

更新履歴

ブログ