【Unity】Unity2020.2から使えるC#8の機能まとめ

Unity2020.2から使えるC#8の機能をまとめました。

Unity2020.2.3

Unity2020.2からC#8の機能が使えるようになりました

Unity2020.2のアップデートで、C#8の機能の大部分が使えるようになりました。
以下のページに列挙されている機能のうち一部機能を除いたものが使えます。

docs.microsoft.com

使えない機能

ライブラリ依存やCLIを解釈するランタイムの制限により以下の機能は使えないようです。

  • インターフェースのデフォルト実装(Default interface methods)
  • 非同期ストリーム(Asynchronous streams)
  • 非同期Disposable(Asynchronous disposable)
  • 範囲アクセス(Indices and ranges)

readonly members

C#8では構造体のメソッドに以下のようにreadonlyキーワードを付けられるようになりました。

using System;
using UnityEngine;

public struct Position
{
    public float X;
    public float Y;
    
    public readonly void LogDistance()
    {
        Debug.Log(Math.Sqrt(X * X + Y * Y));
    }
}

これは「そのメソッドが構造体の変数を書き換えないことを保証する」ためのものです。
readonlyなメソッド内では変数を書き換えることができません。
またreadonlyなメソッドからreadonlyではないメソッドを呼び出すと、以下のような警告が表示されます。

warning CS8656: Call to non-readonly member 'Position.Distance.get' from a 'readonly' member results in an implicit copy of 'this'.

using System;
using UnityEngine;

public struct Position
{
    public float X;
    public float Y;

    public double Distance => Math.Sqrt(X * X + Y * Y);
    
    public readonly void LogDistance()
    {
        // 書き換えはできない
        X = 1;

        // readonlyじゃないメソッドを呼び出すと警告
        Debug.Log(Distance);
    }
}

この機能を使って変数の書き換えを行わないメソッドをreadonlyとして定義しておくことにより、
構造体のDefensive Copy(防御的なコピー)を防ぐことができます。

Switchが式として使えるように

Switchが以下のように式として使えるようになりました。

using System;
using UnityEngine;

public static class Example
{
    public enum MyColor
    {
        Red,
        Green,
        Blue
    }
    
    // Switchが式として使えるようになった
    public static Color ToColor(this MyColor color) =>
        color switch
        {
            MyColor.Red => Color.red,
            MyColor.Green => Color.green,
            MyColor.Blue => Color.blue,
            _ => throw new ArgumentException("invalid enum value", nameof(color))
        };
}

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

C#8ではパターンマッチングが強化されました。
具体的には以下のようなことができるようになりました。

プロパティパターン

プロパティパターンを使うとプロパティやフィールドの値を使って以下のようなSwitch文の判定ができます。

public class Example
{
    public class Person
    {
        public string Name;
        public int Age;
    }

    public bool CanDrink(Person person)
    {
        switch (person)
        {
            // プロパティやフィールドを使って判定できる
            case { Name: "Taro" }:
                return false;
            case { Age: var age } when age >= 20:
                return true;
            default:
                return false;
        }
    }
}

もちろん上述のSwitch式と組み合わせることも可能です。

public bool CanDrink(Person person)
{
    // Switch式
    return person switch
    {
        { Name: "taro" } => false,
        { Age: var age } when age >= 20 => true,
        _ => false
    };
}
タプルパターン

タプルパターンを使うとC#7で追加されたタプル型でSwitchの条件分岐を判定できます。

public string GetCommand(bool shift, string keyCode)
{
    switch (shift, keyCode)
    {
        // タプル型で判定できる
        case (true, "S"):
            return "Save";
        case (true, "Z"):
            return "Undo";;
        case (true, "Y"):
            return "Redo";
        default:
            return null;
    }
}

Switch式で書く場合はこんな感じです。

public string GetCommand(bool shift, string keyCode)
    => (shift, keyCode) switch
    {
        (true, "S") => "Save",
        (true, "Z") => "Undo",
        (true, "Y") => "Redo",
        _ => null
    };

なおタプルなどC#7の主要機能については以下の記事にまとめています。

light11.hatenadiary.com

位置指定パターン(Positional Patterns)

Deconstructメソッドを定義してあるクラスは分解しつつSwitchすることができます。
タプルと似ていますがこれは位置指定パターンといいます。

public struct Input
{
    public bool Shift;
    public string Key;

    // Deconstructメソッドで分解できるように
    public void Deconstruct(out bool shift, out string key)
    {
        (shift, key) = (Shift, Key);
    }
}

public string GetCommand(Input input)
{
    switch (input)
    {
        case (true, "S"):
            return "Save";
        case (true, "Z"):
            return "Undo";;
        case (true, "Y"):
            return "Redo";
        default:
            return null;
    }
}

Switch式で書く場合はこんな感じです。

public string GetCommand(Input input)
    => input switch
    {
        (true, "S") => "Save",
        (true, "Z") => "Undo",
        (true, "Y") => "Redo",
        _ => null
    };

using宣言

変数をusing付きで宣言できるようになりました。
using付きで宣言した変数は、そのスコープを抜けた時にDisposeされます。

要は今まで↓のように書いていたのが

private class FooDisposable : IDisposable
{
    public void Dispose()
    {
        Debug.Log("Dispose");
    }
}

private void Foo()
{
    using (var foo = new FooDisposable())
    {
        // 何らかの処理
        
        // 抜けるときにfooはDisposeされる
    }
}

以下のように書けるようになります。

private class FooDisposable : IDisposable
{
    public void Dispose()
    {
        Debug.Log("Dispose");
    }
}

private void Foo()
{
    // using付きで変数宣言
    using var foo = new FooDisposable();
    
    // 何らかの処理
    
    // 抜けるときにfooはDisposeされる
}

静的ローカル関数

C#7で追加されたローカル関数に、C#8でstatic修飾子が付けられるようになりました。
staticなローカル関数は、その外側の変数をキャプチャできなくなります。

private void Foo()
{
    var bar = 1;

    static void SomeStaticLocalMethod()
    {
        // これが出来ない
        var barCopy = bar;
    }
}

ローカル関数や匿名メソッドが外側の変数をキャプチャするとメモリのアロケーションが発生します。
使いどころを誤るとパフォーマンスの低下につながるため、キャプチャを行わないことを明示的に表せる静的ローカル関数は有用です。

なお匿名メソッドによるメモリ割り当てについては以下の記事に少し書いています。

light11.hatenadiary.com

破棄可能なref構造体

今までref構造体にはインターフェースの実装ができないためDisposeメソッドを実装できませんでした。
C#8からはref構造体に関してはpublicなDisposeメソッドを定義しておけばusingステートメントを使えるようになりました。

private ref struct FooStruct // IDisposableは不要(というかできない)
{
    // publicなDisposeメソッドを実装しておく
    public void Dispose()
    {
        Debug.Log("Dispose");
    }
}

null参照許容型

C#の参照型はデフォルトでnullを許容します。
null参照許容型を使うとnullを許容するかしないかを明示的に指定できるようになります。

使用するためには#nullable enableディレクティブを記述する必要があります。

// 有効化するためにコレを書いておく必要がある
#nullable enable

public class Example
{
    // not nullableな参照型を定義
    // 初期化されていないと警告が表示される
    public string NotNullableText;
    
    // こっちはnullableなので警告なし
    public string? NullableText;

    private Example()
    {
        // nullを代入しようとすると警告が表示される
        NotNullableText = null;
    }
}

null合体割り当て演算子

「参照型変数がnullだったらインスタンスを生成して代入する」みたいなケースでnull合体割り当て演算子が使えるようになりました。
以下のように??=と書いて使用します。

public void Foo()
{
    List<int> nums = null;
    
    // numsがnullだったらListを作っていれる
    nums ??= new List<int>();

    // これと同じ
    nums = nums ?? new List<int>();
    
    nums.Add(1);
    nums.Add(2);
    nums.Add(3);
}

Genericな構造体のアンマネージドの取り扱い

C#8からはGenericな構造体であっても、アンマネージド型だけを持つ場合にはアンマネージドとして取り扱えるようになりました。

public struct Foo<T>
{
    public T Bar;
}

// こう使う場合はアンマネージド型になる
private Foo<int> _foo;

文字列挿入の書き方

C#6から文字列の前に$を付けることでstring.Formatのような書き方ができるようになりました。
さらに$@を付けることで複数行の文字列に対してこれを適用できました。
C#8からはこの「$@」を「@$」とも書けるようになりました。

関連

light11.hatenadiary.com

light11.hatenadiary.com

参考

blogs.unity3d.com

unity.com

docs.microsoft.com