🔨

Unity 2020.2 以降の C# 8.0 で await using を用いるとコンパイルエラーが起きる件とそれのワークアラウンド

2021/09/04に公開

Updated 2021-10-27:
Unity 2021.2 から .NET Standaed 2.1 準拠となるため、本稿の問題は発生しません。
2021/10/26 に TECH Stream にリリースされた Unity 2021.2.0f1 にて問題が解消していることを確認済です。

はじめに

Unity 2020.3 LTS が出て久しい今日この頃、皆さま如何お過ごしでしょうか。

この記事は、Unity 2020.2 から利用できるようになった C# 8.0 の機能の一つである await using を用いると発生するコンパイルエラーとそれの回避方法について説明します。

TL; DR

以下のようなコードをプロジェクトのどこかに置いておきましょう。

#if NET_STANDARD_2_0
namespace System
{
    public interface IAsyncDisposable
    {
    }
}
#endif

くわしく

前提

自分が本現象を踏んだ際の開発環境としては以下のようなものした。

  • Unity 2021.1.19f1
  • JetBrains Rider 2021.2
  • Platform: Standalone
  • Scripting Backend: Mono/IL2CPP のどちらでも

起きた現象

C# 8.0 で追加された機能の一つである「非同期 using」(って言うのが正しいのかな?)を使って、async な Dispose 処理を書いてスッキリしたコードにリファクタリングしようと思い立ちました。

そこで、意気揚々と次のようなコードを Rider で書いて、「さーて動かしてみるかー」と Unity にフォーカスを合わせてみました。

using System;
using CySharp.Threading.Tasks;
using UnityEngine;

namespace Monry
{
    public class Foo : IUniTaskAsyncDisposable
    {
        public async UniTask DisposeAsync()
        {
            await UniTask.Delay(TimeSpan.FromSeconds(1.0));
            Debug.Log("Dispose()");
        }
    }

    public class Bar : MonoBehaviour
    {
        private async void Start()
        {
            await using (var foo = new Foo())
            {
                // 何か foo を使った処理
            }
            Debug.Log("After Dispose()");
        }
    }
}

するとアラ不思議、コンパイルエラーが発生するではありませんか!

Assets/Scripts/Foo.cs(12,21): error CS0518: Predefined type 'System.IAsyncDisposable' is not defined or imported

「Rider 上ではコンパイル通ってるのに、Unity 上でコンパイルエラーが起きる」という謎現象に軽くテンパりつつ、原因・対策を調査してみました。

原因

エラーメッセージは「System.IAsyncDisposable が定義またはインポートされていないとコンパイル通させないよ」というものです。

で、短絡的な私は「Unity 2020.2 以降で C# 8.0 使えるんちゃうんかい!Unity が組み込んでいる .NET のナニカがおかしいんちゃうんけ?」と思ってバグレポ送るまでしてしまったのですが、その後 Twitter で @neuecc さんとやりとり する中で「そもそも Unity 2020.2 でサポートしている .NET プロファイルは .NET Standard 2.0 であり、System.IAsyncDisposable は .NET Standard 2.1 で追加された interface」であるという事実を教えていただきました。

(Twitter でのやりとりでは、「公式に Unsupported つってるよ」と教えていただいた感じですが。)

対策

まぁ、仕様というコトで await using 使わずに try .. finally で対処するというのがスマートなやり方だと思いますが、諦めの悪い自分は「もう少し何とかならんか?」と思って悪あがきしてみました。

で、次のようなコードをプロジェクトに配置してみました。

#if NET_STANDARD_2_0
namespace System
{
    public interface IAsyncDisposable
    {
    }
}
#endif

(実際に試した時は、 Task DisposeAsync(); なメソッドも宣言してました。)

すると何と言うことでしょう、匠の手によりコンパイルが通るようになったではありませんか!

(少なくとも、自分のケースでは using 抜ける時にちゃんと DisposeAsync() を待ってくれてました。)

@neuecc さんの引用リツイートにもあるように、「System.IAsyncDisposable っていう名前の型が存在していればコンパイルが通る」という仕様なようで、何なら enum とかで宣言していても OK という感じっぽいです。

あくまで .NET Standard 2.0 向けのワークアラウンドなので、 #if NET_STANDARD_2_0 で囲んでおく方が安心安全だと思います。

(将来的に Unity が .NET 5 とかをサポートするようになった場合に邪魔にならないようにしておいた方が良いかな、と。)

また、System.IAsyncDisposable が異なる Assembly に定義されていたとしても、その Assembly を Assembly Definition Files の Assembly Definition References を利用するなどして参照しておけばコンパイルは通るもようです。

さらに、複数の Assembly で重複定義されていたとしても、特に怒られることはありませんでした。

なお、Unity の Player Settings > Other Settings > Api Compatibility Level で .NET 4.x を選択している場合の動作は未検証です。

まとめ

  • 現時点(2021年9月)の Unity で await using を使いたい場合は System.IAsyncDisposable を何処かに置いておく。
  • System.IAsyncDisposableinterface じゃなくて enum とか struct とか何でも良い。
  • System.IAsyncDisposable は別 Assembly に定義されていたり、Assembly 間で重複定義されていても大丈夫。

Discussion