【C#】.NET 10 Preview 1 ｷﾀ━━(ﾟ∀ﾟ)━━!!

# はじめに

2024/11 に .NET の最新バージョンである .NET9 がリリースされたところですが、次期バージョン候補の **.NET 10 Preview 1** が利用可能になりました。

<https://dotnet.microsoft.com/ja-jp/download/dotnet/10.0>

今回は、このデプロイしたて・**アチアチ**の SDK をかいつまんで見ていこうと思います。

## サンプルコード

<details>
<summary>テストコード</summary>

```cs
using Xunit;

public class _DotNet10Preview1Test
{
    [Fact]
    void ReadOnlySpanCovariance()
    {
        var stringArray = new string[] { "a", "b", "c" };
        // 共変性: ReadOnlySpan<string> は ReadOnlySpan<object> に代入できる
        ReadOnlySpan<object> objectSpan = stringArray;

        Assert.Equal("a", objectSpan[0]);
        Assert.Equal("b", objectSpan[1]);
        Assert.Equal("c", objectSpan[2]);
    }

    [Fact]
    void ReadOnlySpanContravariance()
    {
        var names = new List<string> { "Alice", "Bob", "Charlie" };
        Comparison<object> comparison = (a, b) =>
            a.GetHashCode().CompareTo(b.GetHashCode());
        // 反変性: Comparison<object> は Comparison<string> に代入できる
        names.Sort(comparison);
    }

    [Fact]
    void PersonTest()
    {
        var person = new Person();
        Assert.Equal("", person.Name);
        Assert.Equal(0, person.Age);

        person.Name = "Alice";
        person.Age = 20;
        Assert.Equal("Alice", person.Name);
        Assert.Equal(20, person.Age);

        person.Name = null!;
        person.Age = -1;
        Assert.Equal("", person.Name);
        Assert.Equal(0, person.Age);
    }

    [Fact]
    void LambdaExpression()
    {
        RefAction<int> act = (ref number) => number++;

        var value = 0;
        act(ref value);
        Assert.Equal(1, value);
    }
}

file class Person
{
    [field: System.Diagnostics.CodeAnalysis.MaybeNull]
    internal string Name { get => field ?? ""; set => field = value; }
    internal int Age { get => Math.Max(field, 0); set => field = value; }
}

file delegate void RefAction<T>(ref T arg);
```

</details>

## 変更点の概要

リリースのお知らせ <https://github.com/dotnet/core/discussions/9763>  
リリースノート <https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/libraries.md>

以下に個人的に面白そうに感じた新機能をあげてみます。

### TimeSpan.FromMilliseconds() オーバーロードの追加

[Adding TimeSpan.FromMilliseconds Overload with a Single Parameter](https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/libraries.md#adding-timespanfrommilliseconds-overload-with-a-single-parameter)

`TimeSpan` の生成メソッドにオーバーロードが追加されました。引数が `long` であり（`double` ではない）丸め誤差がないやつです。

```cs
public readonly struct TimeSpan
{
    // Second parameter is no longer optional
    public static TimeSpan FromMilliseconds(long milliseconds, long microseconds);
    // New overload
    public static TimeSpan FromMilliseconds(long milliseconds);
}
```

### ZipArchive のパフォーマンスとメモリ使用量の改善

[ZipArchive performance and memory improvements](https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/libraries.md#ziparchive-performance-and-memory-improvements)

以下の表は圧縮ファイルの読み取りの例です。読み取り時間・使用メモリ双方 20％ 程度改善しているようです。すごい。試しに手元のアプリを .NET10 にしてみたところ、圧縮ファイルの読み取り・書き込みが体感できるレベルで改善されています。

|Runtime|NumberOfFiles|Mean|Ratio|Allocated|Alloc Ratio|
|---|---|---|---|---|---|
|.NET 9.0|100|46,986.2 ns|1|58.42 KB|1|
|.NET 10.0|100|37,767.2 ns|0.81|47.38 KB|0.81|

### 値型の配列をスタックに作成する

[Stack Allocation of Arrays of Value Types](https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/runtime.md#stack-allocation-of-arrays-of-value-types)

**値型の**配列のスコープが漏れないとき（寿命が短いとき）、ランタイムは配列をヒープではなくスタックに作成して最適化するようです。

```cs
static void Sum()
{
    int[] numbers = {1, 2, 3};
    int sum = 0;

    for (int i = 0; i < numbers.Length; i++)
    {
        sum += numbers[i];
    }

    Console.WriteLine(sum);
}
```

既存の C# 機能である `stackalloc` と似ていますが、`stackalloc` は `unmanaged` 型（参照型を含まない値型）である必要があったため、若干強化されています。また配列の長さ次第で `StackOverflowException` のリスクがあったわけですが、今回の最適化はそのへんをランタイムがうまく調整してくれそうです。つまり配列が十分に短い場合はスタックに、それ以外はヒープにメモリを確保する挙動になりそうです。

> Because of this, stack allocation is key to reducing the abstraction penalty of reference types.  
> （このため、スタックの割り当ては、参照型の抽象化ペナルティを軽減するカギとなる。）

確か Java には寿命が短いオブジェクトはスタックに作成する、みたいな最適化機能があったような気がするので、C# でも一般的な参照型に対象を広げられる・・・？

### Span&lt;T&gt; ReadOnlySpan&lt;T&gt; は言語サポートする
[Implicit Span conversions](https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/csharp.md#implicit-span-conversions)

```cs
var stringArray = new string[] { "a", "b", "c" };
// 共変性: ReadOnlySpan<string> は ReadOnlySpan<object> に代入できる
ReadOnlySpan<object> objectSpan = stringArray;

var names = new List<string> { "Alice", "Bob", "Charlie" };
Comparison<object> comparison = (a, b) =>
    a.GetHashCode().CompareTo(b.GetHashCode());
// 反変性: Comparison<object> は Comparison<string> に代入できる
names.Sort(comparison);
```

Span&lt;T&gt; ReadOnlySpan&lt;T&gt; は値型なので共変性・反変性をもたせることはできないわけですが、C# が特別扱いすることでこれを解決しています。  
その他オーバーロード解決で Span&lt;T&gt; ReadOnlySpan&lt;T&gt; を優先したり、いろいろと特別扱いしているようです。

参考: <https://ufcpp.net/blog/2025/1/first-class-span/>


### フィールドに基づくプロパティ宣言

[`field` - フィールドに基づくプロパティ宣言](https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/keywords/field)

バッキングフィールドをコンパイラが自動生成してくれる機能です。プロパティ中の `field` はキーワードで、一応破壊的変更です。

> field という名前のフィールドがあるクラスでは、field キーワード機能を使用する場合は注意が必要です。 新しい field キーワードは、プロパティ アクセサーのスコープ内の field という名前のフィールドをシャドウします。 field変数の名前を変更するか、@ トークンを使用してfield識別子を@fieldとして参照できます。

```cs
file class Person
{
    [field: System.Diagnostics.CodeAnalysis.MaybeNull]
    internal string Name { get => field ?? ""; set => field = value; }
    internal int Age { get => Math.Max(field, 0); set => field = value; }
}

var person = new Person();
Assert.Equal("", person.Name);
Assert.Equal(0, person.Age);

person.Name = "Alice";
person.Age = 20;
Assert.Equal("Alice", person.Name);
Assert.Equal(20, person.Age);

person.Name = null!;
person.Age = -1;
Assert.Equal("", person.Name);
Assert.Equal(0, person.Age);
```

参照型プロパティだとバッキングフィールドが `null` になる場合が存在し、警告が出ます（nullable の場合）。`[field: System.Diagnostics.CodeAnalysis.MaybeNull]` のように、プロパティに属性を付けて回避できます。

### ラムダ式で参照引数を指定するときに型名を省略可能に

[Modifiers on simple lambda parameters](https://github.com/dotnet/core/blob/main/release-notes/10.0/preview/preview1/csharp.md#modifiers-on-simple-lambda-parameters)

```cs
file delegate void RefAction<T>(ref T arg);

// ここで ref int number としなくてよくなった（型名を省略可能）
RefAction<int> act = (ref number) => number++;

var value = 0;
act(ref value);
Assert.Equal(1, value);
```

ラムダ式で参照引数を使う場面は限定的なものの、少し便利になりました。ジェネリック型で名前が長いことはままあるため、入力の手間が省けますしコードが見やすくなります。

## おわりに

読み終わりましたか？ では SDK をダウンロードしましょう。

<https://dotnet.microsoft.com/ja-jp/download/dotnet/10.0>

**このページを見てしまったからには、ダウンロードしない手はありません。**