Unity2021.2で使えるC#9の機能

エンジニアの小松です。ついに正式リリースされたUnity2021.2.0でのC#9サポートについて調査しました。

Unity2021.2からC#9.0の一部機能が使えるようになります。
サポートされている機能の一覧はリリースノートの「Scripting: Updated C# language version to 9.0 for compilation and IDE's」にあります。

これらの機能について簡単に紹介します。

パターンマッチングの強化

C#7の頃からどんどん改良され続けているパターンマッチングがさらに便利になりました。

特にUnityではis not nullを使えるようになるのが大きいです。GameObjectを単純に==や!=でnullと比較すると問題がありますが、is nullとis not nullを使えば回避できます。

以下にサンプルコードを載せます。

Component c;

// ...

// Unityで==や!=を使うと思わぬ挙動をするので is null か is not nullを使う
if (c is null)
{
    // cがnullのとき
}

if (c is not null)
{
    // cがnullでないとき
}

また、and/orと範囲を組み合わせて範囲指定できるのも知っていると便利です。

int GetValue()
{
    int v = 0;
    // ...
    return v;
}

// if (GetValue() < 0 || GetValue() >= 100)とするとGetValueが2回呼ばれるが
// 次の書き方だと1回しか呼ばれない
if (GetValue() is < 0 or >= 100)
{
    // GetValue()が0未満 or 100以上のとき
}

if (GetValue() is >= 10 and <= 20)
{
    // GetValue()が10以上 and 20以下のとき
}

パターンマッチングは他にも色々とできますが省略します。
やりすぎるとコードが読みづらくなるので簡単に読める範囲で有効活用できるといいと思います。

target-typed new

オブジェクトをnewするときに、型が推測できる場所ならnewの後ろの型名を省略できるようになりました。

Dictionary<string, List<int>> field = new() {
    { "item1", new() { 1, 2, 3 } },
    // C#8までは { "item1", new List<int>() { 1, 2, 3 } },
};

特にフィールドではvarが使えないので便利です。

public class Foo : MonoBehaviour
{
    private Dictionary<string, int> dic = new();
    // C#8までは private Dictionary<string, int> dic = new Dictionary<string, int>();
}

ちなみに"target-typed"というのは「代入される先の型」くらいの意味です。
「private Dictionary dic = new();」ではDictionaryに入る値をnewしているのだからDictionaryをnewしたいのだろう、とコンパイラが推測してくれます。

target-typed条件演算子

3項演算子の型推測が賢くなりました。あまり意識して使うことは無さそうです。

bool shouldBeList = true;
// ...
IEnumerable<int> enumerable = shouldBeList ? new List<int>() { 0, 1 } : new int[] { 0, 1 };
// C#8まではコンパイルエラー。(IEnumerable<int>)new List<int>()のように明示的にキャストが必要だった。

ラムダ式での引数の破棄

ラムダ式で使わない引数を破棄(discard)できるようになりました。
使わない引数を_にすると、その引数は無視するという意味になります。

System.Func<int, int, int> func = (_, _) => 0;
// C#8まではコンパイルエラー。

イベントやコールバックで一部の引数しか使わないことはよくあるので便利そうです。

静的匿名関数

ラムダ式が外の変数を使わないことをstaticで明示できるようになりました。

// 渡されたaとbを足して返すのデリゲート
// 外の状態に依存しないのでstaticを付けられる
Func<int, int, int> add = static (a, b) => a + b;

int captured = 10;

// capturedとaを足して返すデリゲート
// 外のcaptured変数を使っているのでstaticは付けられない
Func<int, int> add2 = a => a + captured;

staticが付いていれば変数キャプチャが発生しないことがひと目でわかるので付けられる場所には付けておくのがいいと思います。

ローカル関数の属性

ローカル関数に属性が付けられるようになりました。
Unity2021.2からnull許容関連の属性も使えるのでたまに必要になるかもしれません。

拡張メソッドによるGetEnumerator

拡張メソッドとして定義したGetEnumeratorがforeachで使われるようになりました。
あまり使うことはないと思います。

関数ポインタ

staticメソッドをデリゲートではなく関数ポインタを介して呼び出せるようになりました。
それによってパフォーマンスが向上します。

staticメソッド限定かつunsafeコンテキストでしか使えない、と制限が厳しいです。
本当にパフォーマンスが重要な箇所では使ってもいいかもしれません。

新しいpartialメソッド

メソッドにpartialを付けるとメソッドの宣言と定義を分散できます。
このメソッドには色々な制限があったのですが、それらが色々と変更になりました。

主にコード生成で使われる機能です。
UnityではまだSourceGeneratorに対応していないので使う機会はほとんど無いと思います。

未サポートの機能について

Unity2021.2のドキュメントでは以下の機能が未サポートとなっています。

  • init専用セッター
  • 共変戻り値
  • ModuleInitializer
  • Extensible calling conventions for unmanaged function pointers

このうちinit専用セッターについてはクラスを1つ定義しておくと使えるようになります。これを使うなら自己責任ということになると思います。

以下の打ち消し線の記述はドキュメント更新前の古い情報です。
現在はRecord型やNative size integersに関しても問題なく使えます。C#8の非同期Disposeや非同期using、interfaceのデフォルト実装などの機能も使えるようになりました。

これ以外のC#9の機能はUnity2021.2では未サポートとされています。

以下の機能はドキュメント通りにコンパイルエラーが発生します。

  • init専用セッター
  • 共変戻り値
  • SkipLocalsInit
  • ModuleInitializer

このうちinit専用セッターについてはクラスを1つ定義しておくと使えるようになります。これを使うなら自己責任ということになると思います。

一方で、以下の機能は手元で試す限り問題なく動いているように見えました。

  • Records
  • Native sized integers
  • 非同期ストリーム(C#8)
  • 非同期Disposeと非同期using(C#8)
  • interfaceのデフォルト実装​(C#8)
  • IndicesとRanges(C#8)

レコード型や非同期ストリーム、非同期DiposeなどはUnityでも使いそうなので困ります。
公式には未サポートというリスクを承知で使うか使わないか判断することになると思います。

とは言いましたが正式サポートされている範囲だけだと使える機能が小粒すぎます。未サポートであることも目立たないページにこっそり書いてあるだけです。書けば動いてしまうので当たり前にrecordや非同期ストリームも使われていくことになる気がします。みんなで知見を積み上げていきましょう。

ライブラリのアップデート

Unity2021.2ではC#が8になっただけでなく、ライブラリも.NET Standard2.1または.NET Framework4.8に更新されています。これによってSpanやMemory、ValueTask、IValueTaskSourceなどの機能が追加のdllなしで使えるようになりました。

今回はC#9の紹介なので説明しませんがまたどこかでライブラリの更新についても紹介できればと思います。