目次

Ver. 9.0
リリース時期 未定(おそらく .NET 5 ど同じ 2020/11)
同世代技術
  • .NET 5
要約・目玉機能

.NET 5 Preview 2 (2020/3)頃から、C# 9.0 機能もちらほらプレビュー実装され始めました。 プレビュー状態なので言語バージョンpreview の指定が必要です。

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

トップ レベル ステートメント

トップ レベル(top-leve: クラスや名前空間よりも外側、ファイル直下)にステートメントを直接書けるようになりました。

例えばよくある「Hello World」であれば、単に以下のように書けるようになります。

using System;
Console.WriteLine("Hello World!");

詳しくは「トップ レベル ステートメント」で説明します。

パターンの追加

C# 7.0から脈々と改善されてきたパターン マッチングですが、 C# 9.0 でもいくつかのパターンが追加されています。

// not, and, or や、 <, <=, >, >= などのパターンが増えた。
int M(uint x) => x switch
{
    0 or 2 or 4 or 6 or 8 => 0,
    1 or 3 or 5 or 7 or 9 => 1,
    >= 10 => -1,
};

3世代かけてようやく当初予定(C# に追加すること自体は最初から決めていた機能)が全て入りました。 当初から予定に入ってたのは、既存のいくつかのプログラミング言語が同様の文法を持っていて、 網羅性重複の検査を含めて実装手段が既知で、検討コストが低いからです。 それでも、需要が高いものから少しずつ実装した結果、3世代に分かれました。 3世代目なことを指して「パターン v3」(patterns v3)という俗称があったりもします。

詳しくは「パターン マッチング」で説明します。 C# 9.0 で追加されているのは以下の3つです。

ターゲット型推論の強化

ターゲットからの new 型推論

ターゲット型からの推論が効く場合に、new T()T の部分を省略できるようになりました。 (target-typed new とか呼ばれたりします。)

特に、var が使えず、 型名が長い特に便利です。

using System.Collections.Generic;
 
class Sample
{
    // フィールドに対しては var が使えない。
    // 代わりに new 型推論を使うと便利なことがある(特に、型名が長い時)。
    Dictionary<string, List<(int x, int y)>> _cache = new();
}

詳しくは「ターゲットからの new 型推論」で説明します。

条件演算子のターゲット型推論

条件演算子の第2・第3項がターゲット型からの型推論するようになりました。

void M(bool b)
{
    // int? を明示するとコンパイルできる(var だとダメ)。
    int? i = b ? 1 : null;
 
    // Base を明示するとコンパイルできる(var だとダメ)
    Base c = b ? new A() : new B();
}
 
class Base { }
class A : Base { }
class B : Base { }

詳しくは「条件演算子のターゲット型推論」で説明します。 「型の決定」も参考にしてください。

unsafe/ネイティブ相互運用向け機能

C# 7.2の辺りから、 言語の方向性として生産性や安全性を優先する C# でも、 パフォーマンス改善を目的とするような言語機能が結構増えてきました。

また、 クロスプラットフォーム化が進んだことで、ネイティブ相互運用関連の機能も増えています。

この手の機能は一般的な開発者が直接触れることは少ないですが、 .NET ランタイム自体や、大規模に使われているライブラリのパフォーマンス改善につながり、 間接的にすべての C# 開発者が恩恵を受けるものになります。

ローカル変数の0初期化抑止

/unsafe オプション指定時限定ですが、ローカル変数の0初期化を抑止できるようになりました。

using System;
using System.Runtime.CompilerServices;
using System.Text.Unicode;
 
m("aあ😀");
 
// この属性を付けると stackalloc の要素の0初期化がなくなる。
[SkipLocalsInit]
static void m(string s)
{
    // UTF-16 の文字数に大して、UTF-8 のバイト数は最大でも3倍以内。
    Span<byte> buffer = stackalloc byte[s.Length * 3];
    Utf8.FromUtf16(s, buffer, out _, out var bytesWritten);
 
    // FromUtf16 の仕様上、bytesWritten バイト目までは必ず上書きされる。
    // 上書きされた部分だけを使う分には0初期化は「余計なお世話」。
    var written = buffer[..bytesWritten];
 
    foreach (var b in written)
    {
        Console.WriteLine(b);
    }
}

詳しくは「ローカル変数の0初期化抑止」で説明します。

その他

null 許容参照型の改善

C# 8.0 で入った null 許容参照型に対してちょっと改善が入っています。 主に以下の2点です。

ラムダ式の引数を破棄

ラムダ式の引数で、_ を使った値の破棄ができるようになりました。

static void Subscribe(INotifyPropertyChanged source)
{
    // _ を破棄扱いして、2個以上並べられる
    source.PropertyChanged += (_, _) => { };
}

詳細は「値の破棄 - ラムダ式の引数」で説明します。

ローカル関数への属性適用

ローカル関数に属性を付けられるようになりました。

using System;
using System.Diagnostics.CodeAnalysis;
 
m("", "");
 
static void m(string? a, string? b)
{
    // C# 9.0 からローカル関数に属性を付けれる。
    // C# 8.0 の null 許容参照型がらみで特に有用。
    [return: NotNullIfNotNull("s")]
    string? toLower(string? s) => s?.ToLower();
 
    if (a is not null && b is not null)
    {
        // a, b の null 許容性が、NotNullIfNotNull 属性のおかげで al, bl に伝搬。
        string al = toLower(a);
        string bl = toLower(b);
 
        // a, b が非 null なので、al, bl は非 null で確定済み。改めてのチェック不要。
        Console.WriteLine(al.GetHashCode());
        Console.WriteLine(bl.GetHashCode());
    }
}

拡張メソッドでの GetEnumerator 実装

パターン ベースforeachawait foreachで、拡張メソッドによる実装ができるようになりました。

更新履歴

ブログ