.NET9, C#13 の新機能ピックアップ

[Applibot Advent Calendar 2024](https://qiita.com/advent-calendar/2024/applibot) 6日目の記事です。

# 概要

2024/11/13(日本時間) に .NET 9, C# 13, F# 9 が正式リリースされました。
この記事では、今回追加された新機能のうち、個人的に影響度が高いものをピックアップして紹介します。

※ 2024/12/4 現在は**公式の日本語記事が不完全**なので、英語版を参照しました

# .NET 9 ランタイムの新機能

パフォーマンスに影響がありそうな改善のみピックアップします。

* [ガベージコレクションの改善](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/runtime#garbage-collection)
* [パフォーマンス改善](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/runtime#performance-improvements)
  * [Exceptionが2〜4倍高速化](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/runtime#faster-exceptions)
  * [ボックス化オブジェクトのスタック割り当て](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/runtime#object-stack-allocation-for-boxes)
  * 他、ループ、PGO、Arm 系の最適化

Exceptionが高速化したと言っても、Exceptionを使わないコードと比べれば相当遅いことには変わりないので過信は禁物です。
「ボックス化オブジェクトのスタック割り当て」は詳しく調べる必要がありますが、ボックス化しそうなコードが特定条件下で事実上ボックス化を回避できるようになる機能のようです。

# .NET 9 ライブラリの新機能

## LINQ 拡張メソッド `CountBy` `AggregateBy` の追加

元記事 : [LINQ](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/libraries#linq)

`CountBy` はコレクションに含まれる各キーの個数を数える拡張メソッドです。
`c.CountBy(x => x.Id)` は、 `c.GroupBy(x => x.Id).Select(g => new KeyValuePair.Create(g.Key, g.Count()))` と同じ結果になります。

## Debug.Assertがアサート条件を表示するように改善

元記事 : [Debug.Assert reports assert condition by default](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/libraries#debugassert-reports-assert-condition-by-default)

Debug.Assert の引数に判定式を渡すと、falseだった場合にデフォルトでその**判定式を出力**してくれるようになりました。
実装としては、省略可能な第2パラメータ message に `CallerArgumentExpressionAttribute` をつけ、message なしのオーバーライドの優先度を下げたようです。

```csharp
Debug.Assert(a > 0 && b > 0);
// false である時の出力
// Process terminated. Assertion failed.
// a > 0 && b > 0
//   at Program.SomeMethod(Int32 a, Int32 b)
```

## GetAlternateLookupによるSpanを使ったコレクション検索

元記事 : [Collection lookups with spans](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/libraries#collection-lookups-with-spans)

高度なパフォーマンスチューニングのための機能です。
`Hash<string>` や `Dictionary<string, T>` に対して `GetAlternateLookup` メソッドを呼び出すことで、 `string` の代わりに `ReadOnlySpan<char>` をキーとして使えるラッパー的な構造体が取得できるようになりました。
この機能を使うためには `IAlternateEqualityComparer<TAlternate, T>` の実装が必要ですが、デフォルトで用意されているのは `ReadOnlySpan<char>` → `string` だけのようです。

## OrderedDictionary&lt;TKey, TValue&gt;の追加

元記事 : [OrderedDictionary&lt;TKey, TValue&gt;](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/libraries#ordereddictionarytkey-tvalue)

辞書構造であると同時に、追加順が維持されるリストでもあるコレクションです。キー順にソートされるわけではないので注意(キー順でソートされるのは `SortedDictionary<TKey, TValue>` のほう)。

## ReadOnlySet&lt;T&gt;の追加

元記事 : [ReadOnlySet&lt;T&gt;](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/libraries#readonlysett)

HashSetなどの読み取り専用ラッパーです。

## JSONシリアライザーの機能追加

Jsonのシリアライズ・デシリアライズが便利になります。

元記事 : [Serialization (System.Text.Json)](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/libraries#serialization-systemtextjson)

### 参照型のnull許容性を使う

`JsonSerializerOptions.RespectNullableAnnotations` を true にすることで、デシリアライズする際に参照型のnull許容性が尊重されるようになります。
デシリアライズの過程で null 非許容のプロパティに null を代入しそうになった場合、 `JsonException` が投げられます。

### コンストラクタ・パラメータを必須にする

従来では、コンストラクタベースのデシリアライズ(主にレコード型)では全てのパラメータが省略可能として扱われていました。 `JsonSerializerOptions.RespectRequiredConstructorParameters` を true にすることで、デフォルト値を指定していないコンストラクタ・パラメータが必須扱いになります。

### Enumメンバー名のカスタマイズ

型に `JsonConverter` 、対象の値に `JsonStringEnumMemberName` をつけることで、Jsonにシリアライズする際のメンバー名をカスタマイズできるようになります。

```csharp:C#
using System.Text.Json;

[JsonConverter(typeof(JsonStringEnumConverter))]
enum MyEnum
{
    Value1 = 1,
    [JsonStringEnumMemberName("Custom enum value")]
    Value2 = 2,
}
```

### インデント文字・サイズがカスタマイズ可能に

`JsonSerializerOptions.IndentCharacter` と `JsonSerializerOptions.IndentSize` を指定することで、表示に適したJSONが出力できるようになりました。
なお、 `IndentCharacter` に指定できるのは半角スペースとタブだけのようです。

### 規定のWeb向けオプションの追加

Web向けのデフォルトオプションとして `JsonSerializerOptions.Web` が追加されました。以下の定義と同じです。

```csharp
new JsonSerializerOptions {
    // デシリアライズ時に大文字と小文字を区別しない
    PropertyNameCaseInsensitive = true,
    // シリアライズ時に camelCase を使う
    JsonPropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    // シリアライズ時、数値は数値として書き込まれる
    // デシリアライズ時、文字列から数値への自動変換を許可する(数値からも読み込み可能)
    NumberHandling = JsonNumberHandling.AllowReadingFromString,
}
```

# C# 13 の新機能

最近のC#の傾向として、一般開発者ではなくライブラリ開発者に向けた改善が多いです。

## `params` 任意のコレクション

元記事 : [params ReadOnlySpan&lt;T&gt; overloads](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/libraries#params-readonlyspant-overloads)

従来は `params` をつけられるのは配列だけでしたが、コレクション式で構築できる任意のコレクション型にもつけられるようになりました。基本的には `params ReadOnlySpan<T>` の形でパフォーマンス改善のために使われることでしょう。
標準ライブラリ内でもこのオーバーロードが追加されたメソッドがあるため、既存コードを .NET9 で再コンパイルするだけでもオーバーロード解決先が変わってパフォーマンス改善が見込めそうです。

## partialプロパティ、partialインデクサー

元記事 : [More partial members](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#more-partial-members)

主に SourceGenerator で使う partial メソッドが、プロパティとインデクサーでも使えるようになりました。自動実装プロパティと見た目が紛らわしい…。

```csharp
public partial class C
{
    // 宣言側
    public partial string Name { get; set; }
}

public partial class C
{
    private string _name;
    // 実装側
    public partial string Name
    {
        get => _name;
        set => _name = value;
    }
}
```

## Lock オブジェクトの追加

元記事 : [New lock object](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#new-lock-object)

ロック用に確保している object 型のインスタンスを Lock 型に変えるだけでパフォーマンス改善が見込めます。

```csharp:C#
using System.Threading;

public class Example
{
    // 以前のロックオブジェクトコード
    // private readonly object _lockObj = new();
    private readonly Lock _lockObj = new();

    public void Method()
    {
        lock (_lockObj)
        {
            // クリティカルセクション
        }
    }
}
```

## オーバーロード解決優先度

元記事 : [Overload resolution priority](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#overload-resolution-priority)

メソッドに `OverloadResolutionPriorityAttribute` をつけることで、オーバーロードの解決優先度を明示的に制御できるようになりました。
「Debug.Assertがアサート条件を表示するように改善」でさっそく使われています。

## イテレータメソッド・ `async` メソッド内での `ref` と `unsafe`

元記事 : [ref and unsafe in iterators and async methods](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#ref-and-unsafe-in-iterators-and-async-methods)

従来は、イテレータメソッドや `async` メソッドの中では全面的に `unsafe` ブロックや `ref` 変数( `Span<T>` なども含む)を使えませんでしたが、 `await` や `yield` をまたがない場合に限って使えるようになりました。
パフォーマンスのために `Span<T>` を使う機会が多くなってきているのでありがたい変更です。

## `allows ref struct`

元記事 : [allows ref struct](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#allows-ref-struct)

従来はジェネリック型に `ref` 構造体を指定することはできませんでしたが、ジェネリック型制約に `allows ref struct` をつけることで `ref` 構造体を型引数に使えるようになりました。
標準のデリゲート `Action` や `Func` に `allows ref struct` 制約が追加されたため、 `Span<T>` などをやり取りするデリゲートが簡単に定義できるようになりました。

```csharp
// C#13以降で定義できるようになった
void M(Func<ReadOnlySpan<char>, char> selector)
{
    // ...
}
```

## `ref struct` に対するインターフェイス実装

元記事 : [ref struct interfaces](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#ref-struct-interfaces)

`ref` 構造体にインターフェイスを実装できるようになりました。ひとつ上の `allows ref struct` と合わせて、特定の実装を持つ `ref` 構造体に対するジェネリックなメソッドが作れるようになります。
ただし `ref` 構造体がボックス化できない制約が消えるわけではないため、インターフェイス型の変数に代入することはできないなど、使用には一定の制限がかかります。

## その他

* エスケープシーケンス `\e` ( `U+001B` )の追加 (元記事 : [New escape sequence](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#new-escape-sequence))
* メソッドグループの自然型 (元記事 : [Method group natural type](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#method-group-natural-type))
* オブジェクト初期化子における末尾からインデックス `^i` (元記事 : [Implicit index access](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13#implicit-index-access))

# 参考文献

https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-9/overview

https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13

https://source.dot.net/