一昨日くらいに来てました。

当日、このネタでライブ配信:

「一気に情報が来ても小一時間では話しきれない」って感じで極々一部しか話せませんでしたが。

「Visual Studio 2020 Preview 3 の方が CDN トラブルで配信が1日延期」というトラブルに見舞われ、 「SDK だけを先に .NET 6 Preview 7 に上げてしまうと、標準のテンプレートがコンパイル エラーを起こす」という事件もありましたが、1日経って問題は解消済みです。

とりあえず、ブログとしては「今回入った C# 10.0 機能」の話を書こうと思います。 ちなみに、今回の更新でほぼ C# 10.0 の全機能が入っています。 (1個だけまだなものがあるけども、「10.0 リリース時点で preview 機能として残る」判定を受けている機能なので、非 preview な 10.0 機能は全部 merge 済み。)

(全機能一覧はトラッキング issue を立ててるので現状そちらを見ていただけると。)

.NET 6 Preview 7 での C# 10.0 新機能

Language Feature Statusで Merged into 17.0 と 17.0p3 になっているやつが今回入っています。 (17.0 になってる2つはもっと前に入ってた疑惑ちょっとあり。 Visual Studio 2020 Preview 2.1 のときかも。)

以下の6つ。

あと、Lambda improvements も1個前の Preview では動いていなかった機能が増えているので、合計7つ。

Improved Definite Assignment

C# には元々、確実な代入ルールってのがあって、「未初期化変数から未定義な値を取り出す」みたいなことはできない仕様になっています。

int x;

Console.WriteLine(x); // コンパイルエラー

if (int.TryParse(Console.ReadLine(), out x))
{
    // ここでは x が初期化済みな保証があるのでエラーが消える。
    Console.WriteLine(x);
}

これのためのフロー解析に改善の余地があることが周知の事実で長らく手つかずだったんですが、それが C# 10.0 でちょっと改善します。

これまで ?. とか ?? とか ? : が絡むときの解析が甘くて、過剰にエラーになっていました。 それが緩和されて、例えば、以下のようなコードがコンパイルできるようになっています。

using System.Diagnostics.CodeAnalysis;

m(null);
m(new R<string>(null));
m(new R<string>("abc"));

void m(R<string>? x)
{
    if (x?.TryGetValue(out var v) == true) // ここの var v の definite assignment 判定が改善された。
    {
        Console.WriteLine(v.Length); // 前までこの行がエラーになってた(C# 10.0 から OK に)。
    }
    else
    {
        Console.WriteLine("null");
    }
}

record class R<T>(T? Value)
{
    public bool TryGetValue([NotNullWhen(true)] out T value)
    {
        if(Value is { } v)
        {
            value = v;
            return true;
        }
        else
        {
            value = default!;
            return false;
        }
    }
}

Extended property patterns

プロパティ パターンで、 多段のメンバーを . でつないでマッチングできるようになりました。

var x = new A(new B("a"));

if (x is A { X.Value.Length: 1 })
{
    Console.WriteLine("len 1");
}

record A(B X);
record B(string Value);

Interpolated string improvements

文字列補間のパフォーマンスが大幅に向上します。

以下のようなコードがあったとして、

Console.WriteLine(m(1, 2, 3, 4));

string m(int a, int b, int c, int d) => $"{a}.{b}.{c}.{d}";

これまでは string.Format("{0}.{1}.{2}.{3}", new object[] { a, b, c, d }) に展開されていました。 それが、所定の条件を満たせば(普通にやってれば .NET 6 をターゲットにして C# 10.0 でコンパイルすると)、以下のようなコードに変化します。

var h = new System.Runtime.CompilerServices.DefaultInterpolatedStringHandler(3, 4);
h.AppendFormatted(a);
h.AppendLiteral(".");
h.AppendFormatted(b);
h.AppendLiteral(".");
h.AppendFormatted(c);
h.AppendLiteral(".");
h.AppendFormatted(d);
return h.ToStringAndClear();

ちなみに、C# コンパイラーのレベルで頑張っていることなので再コンパイルが必要です。 これに関しては「既存のコンパイル済みプログラムを .NET 6 で動かすだけで速くなる」みたいなことはないです。

File-scoped namespace

いままで:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class A
    {
    }
}

これから:

namespace ConsoleApp1;

class A
{
}

「たかが1インデント」と言われてたやつなんですが… まあ確かにこの1インデントが深い言語の方が、今となっては少なく。

Parameterless struct constructors

さかのぼること C# 6.0 の時に、Activator のバグでできなかったやつ、再チャレンジ(成功)。

構造体のフィールドでも非 null 保証とかがやりやすくなります。

struct A
{
    public string S { get; } = "abc"; // 前まで初期化子を書けなかった
}

struct B
{
    public int[] Array { get; }
    public B() => Array = new int[4]; // 前まで B() を書けなかった
}

まあ、default からは逃げられないんですが…

// これは大丈夫。引数なしコンストラクターで new int[] されてる。
Array4 a = new();
Console.WriteLine(a[0]);

// default は引数なしコンストラクターを呼ばない。
a = default;
Console.WriteLine(a[0]); // ぬるぽ

struct Array4
{
    private readonly int[] _array;
    public Array4() => _array = new int[4];
    public int this[int index] => _array[index];
}

Caller expression attribute

CallerInfo 系の属性に新しい仲間が増えました。

CallerArgumentExpression 属性で、「引数に渡した式」を取れるようになります。

using System.Runtime.CompilerServices;

m(2 * 3 * 4); // 2 * 3 * 4 = 24

var (x, y, z) = (1, 2, 3);
m(x + y + z); // x + y + z = 6

static void m(int result, [CallerArgumentExpression("result")] string? expression = null)
{
    Console.WriteLine($"{expression} = {result}");
}

主にロギング用途になると思います。

Lambda improvements

.NET 6 Preview 6 時点で以下のようなコードは書けていたんですが。

Delegate f = int (int x) => x * x;

Prevew 7 から以下のようなコードも書けるようになりました。

var f = int (int x) => x * x;

この場合、f の型は Func<int, int> になります。 System.ActionSystem.Func が使える場合にはそれを、 使えない場合には internal なデリゲート型をコンパイラー生成して使うそうです。

デリゲートの仕様上、以下のような挙動をするのでその点には注意が必要です。

// これは target-typed 型決定で、Predicate<int> になる(コンパイル可)。
m(x => x == 0);

// 一方で、これは f の型が Func<int, bool> になる。
var f = (int x) => x == 0;
m(f); // Func<int, bool> を Predicate<int> に変換でしません(コンパイル エラー)。

static void m(Predicate<int> f) { }