Help us understand the problem. What is going on with this article?

Unity Assembly Definition 完全に理解した

はじめに

この記事におけるソースコードはすべて Public Domain です。
また本記事において利用しているUnityバージョンは2019.2です。

Assembly Definitionとは何か

Assembly Definitionという機能をご存知でしょうか。
これは「C#のビルドファイル(アセンブリ)を分割して出力する」ことができる機能であり、Unity 2017.3で追加されました。

WhatIsADF.gif

特にライブラリやC#アセットを公開することがある人は絶対に覚えておくべき機能です。
Assembly Definitionよくわからん」という状態でもプログラムは一応作成できますが、プロならば絶対に覚えておくべき機能です。

なお、Assembly Definition自体はUnityの機能の名前です。
実際にこの機能を有効化するためにユーザが定義するファイルのことをAssembly Definition Filesと呼びます。
(長いので略してadfと呼びます)。

Assembly Definitionを利用した場合のUnityの挙動

Assembly Definitionを利用した場合、アセンブリが分割されてコンパイル・ビルドされることになります。
そのためAssembly Definitionを利用するときと利用しないときとでは、コーディングの工程が大きく変化します。

そのためAssembly Definitionを利用する場合はその挙動をしっかり把握する必要があります。

1. csprojが分解される

adfを定義した場合、その定義ごとに別れてcsprojファイルが生成されることになります。

このとき、adfを定義していない従来のアセンブリはすべてAssembly-CSharpに収められます。

csproj.png

2. 差分ビルドされる

ソースコードに修正があった場合、該当するアセンブリのみがコンパイルされます。
そのため全体でコンパイル時間が短くなります。

3. 参照関係を明示的に設定する必要がある

Assembly Definitionを有効化するとアセンブリが分割されます。
そのため「どのアセンブリが、他のどのアセンブリに依存しているか」を手動で設定する必要があります。

ReferenceGif.gif

4. Assembly-CSharpの扱いが特殊になる

Assembly-CSharpadf定義していないスクリプトがすべて押し込められる、いわゆる「全部入り」のアセンブリです。最初はすべてのスクリプトがこのアセンブリに入った状態となります。
つまり、adfを定義するということは、「Assembly-CSharpアセンブリからモジュールを抜き出して分割していく」と同義になります。

さて、このAssembly-CSharpですが、参照関係が特殊となっています。

  • 他のadfで分割されたアセンブリへは、Assembly-CSharpからすべて参照可能
  • 逆に他のアセンブリから、Assembly-CSharpへは絶対にアクセスできない

AssemblyCSharp.gif

Assembly Definitionを利用しているときはこの性質が問題となることがあるため、この挙動はしっかりと把握しておきましょう。

Assembly Definition Filesの定義方法

Assembly Definition Filesの作成

adfProject Viewにおいて、定義したいディレクトリ上で右クリックメニューから作成することができます。

ModuleA.gif
adfが配置された時点でそのディレクトリ以下csprojファイルが分割され、アセンブリも別に生成されます。

Solution.png
Banana.csprojが作成された。

ScriptAssemblies.png
/Library/ScriptAssembliesを覗くとBanana.dllに分割されている。

Assembly Definition Filesの設定

単にdllに分割するだけなら、adfを作成するだけで完了です。

より突っ込んだ設定、たとえば次の設定が行いたい場合はInspector Viewにて設定することができます。

  • このアセンブリ内でのみunsafeコードの利用を許可したい
  • 他のadfによって定義されたアセンブリに依存したい
  • 特定のプラットフォームでのみ有効化したい
  • Editor拡張/Testコードだと設定したい

ADFSetting.png

General

  • Allow 'unsafe' Code : このアセンブリ内でunsafeコードを許可するか
  • Auto Referenced : 本体側のアセンブリ(Assembly-CSharp)から参照するか
  • Override References : すでにコンパイル済みのdllを参照に追加するか

この「Auto Referenced」にチェックが入っている場合、毎回コンパイル対象になります。
Assembly-CSharpから直接参照しないものはオフにしておくとよいです。

Define Constraints

文字列を設定します。ここに設定された文字列が Player SettingsScripting Define Symbolsに定義されていた場合のみビルドされます。

RIPENED.png
RIPENED_BANANAが定義されていた場合のみこのアセンブリは有効となる。ちなみに意味は"完熟バナナ")

Assembly Definition References

他のadfによって定義されたアセンブリを参照する場合に設定します。
超重要

Reference.png

たとえばJuice MixerBananaMilkに依存している場合は、Assembly Definition Referencesでそれぞれに参照を追加しなければいけません。

Platforms

どのプラットフォームで有効化するか設定します。
/Editoradfを定義した場合はEditorにチェックを入れる必要があります。
Editor Testの場合も同様)

EditorBuild.png

(エディタ拡張などはEditorにチェックをいれてアセンブリを別ける必要がある)

Assembly Definition Filesのディレクトリ構造

adfはディレクトリをネストして定義しても問題はありません。
その場合も「ディレクトリ単位」でアセンブリが分割されビルドされます。

directories.png
dlls.png

Assembly Definitionを利用するメリット・デメリット

Assembly Definitionは基本的には定義した方がメリットは大きいのですが、一応デメリットも存在します。
メリット・デメリットを把握した上で実際に利用するかどうかを決めるとよいでしょう。

メリット

  • 差分コンパイルになるためEditor上でのコンパイル時間が短くなる (Auto Referencedを適切にOFFにしておく必要あり)
  • internalprivate protectedなどのアクセスレベルが活用できる
  • unsafeコードを有効化する範囲をアセンブリ単位で限定できる
  • モジュールの依存関係を強制できる
  • ライブラリ同士の干渉を最小限に抑えてプロジェクトに追加できる
  • 対象プラットフォームごとにアセンブリレベルで別けてビルドができる

デメリット

  • Asset Bundleと併用した場合に挙動がややこしいことになる
  • アセンブリをまたいでpartialクラスが定義できない
  • staticを使っていたときに分割しにくい
  • たまに謎のコンパイルエラーが出て動かなくなる

Assembly Definitionを利用するべきシチュエーション

次のようなシチュエーションでは必ずAssembly Definitionを利用するべきです。

ライブラリやアセットを公開する場合

ライブラリや、スクリプトを含んだアセットを公開する場合はadfを定義してアセンブリを分割するべきです。
むしろやってくださいおねがいします。

理由としては「adfが定義されていないライブラリはAssembly-CSharpアセンブリに入ってしまう」からです。
これは自身のプロジェクトがAssembly Definitionを活用していた場合に非常に面倒な問題になります。

場合を分けて説明します。


セーフ:Assembly Definitionを一切利用していない場合

Library1.png

自身のプロジェクトも、導入した外部ライブラリも、どれもadfを定義されていない場合です。
この場合は問題ありません。


セーフ:全員がAssembly Definitionを利用している場合

Library3.png

自身のプロジェクトと導入した外部ライブラリ、その両方がadfを定義していた場合です。
この場合はアセンブリ同士の参照を正しく設定すれば問題ありません。


アウト:自身のプロジェクトのみadfを定義していた場合

Library2.png

この場合はアウトです。

自身のプロジェクトはadfを定義しているが、導入したライブラリには定義されていない場合。
この場合はライブラリがAssembly-CSharpに入り込んでしまうため、自身のモジュールからアクセスができないためエラーになってしまいます。

こうなると自身でライブラリにadfを定義してアセンブリを分割する必要がでてきます。
そしてライブラリ側の構造が歪でadfをキレイに定義できない、定義してもstaticpartialを乱用していてエラーが出まくる…、みたいな地獄な状況になる場合もあります。

なのでもし、これからライブラリやモジュールを作って公開するつもりがある人は必ずAssembly Definitionを意識した作りにしてください。
マジでお願いします。マジで。


アーキテクチャに沿った開発を行う場合

クリーンアーキテクチャなど、レイヤ間での依存関係をしっかり定義したい場合はAssembly Definitionがかなり有用です。
adfでモジュール間の依存を定義することで、「Domainから外への参照を禁止する」といったことがプロジェクト設定で制御することが可能になります。

合わせて覚えておくべき技

Assembly Definitionを利用する場合、覚えておくと開発効率の向上につながるテクニックがいくつかあります。

Editor上でコンパイルエラーが出た場合の対策

Assembly Definitionを使っていると、コードに変更がなくてもコンパイルエラーが出てしまうことがあります。本当によく起きます。

これはエディタがキャッシュしているアセンブリがおかしくなってしまったことが原因の場合が多いです。
そのため次の手順を行うと復旧することがあります。

  1. \Library\ScriptAssembliesを削除してみる
  2. (↑で直らないなら)Unity Editorを再起動する
  3. (↑で直らないなら)該当するAssetのReimportを行う
  4. (↑で直らないなら)\Libraryを消してUnity Editorを再起動する

アセンブリ単位でのアクセスレベルを利用する

C#のアクセス修飾子にはアセンブリ単位でアクセスを制御するものがあります。
外部公開するライブラリを作るときなどに便利に使えるためお勧めです。

詳しくは岩永さんのサイトを見てもらうとよいのですが、一応解説します。

アクセスレベル

C#のアクセス修飾子には次のものがあります。

レベル 説明 備考
public どこからでもアクセス可能 ガバガバ
protected クラス内とその派生クラスからのみアクセス可能 継承するときに使う
internal 同一アセンブリのクラス内からのみアクセス可能 アセンブリ内からはpublic、外からはprivateに相当
protected internal 同一アセンブリのクラス内 OR 派生クラスからアクセス可能
private protected 同一アセンブリのクラス内 AND 派生クラスからアクセス可能 C# 7.2以降
private クラス内からのみアクセス可能 一番厳しい

とくにinternalprotected internalprivate protectedAssembly Definitionを利用している場合のみに利用できるアクセスレベルです。

これらを活用することでライブラリの実装が少し楽になったりします。

利用例1:管理用のメソッドの定義

internalを利用することで、「同一アセンブリ内からのみ呼び出せるメソッド」を定義することができます。
これはライブラリやフレームワークを作成する場合に、オブジェクトの管理メソッドなどを定義するときに活用することができます。

/// <summary>
/// 敵
/// </summary>
public class Enemy
{
    /// <summary>
    /// ライブラリ外から呼びだせるのはこっち
    /// </summary>
    public void Destroy()
    {
        /*
         * なんか実装
         * いろいろ処理が走ったり走らなかったり
         */
    }

    /// <summary>
    /// ライブラリ内部でリソース管理用に呼び出すのはこっち
    /// ライブラリ内からのみコール可能
    /// </summary>
    internal void ForceDestroy()
    {
        /*
         * なんか実装
         * Force()より単純化されていたり、複雑だったり…
         */
    }
}

利用例2:ファクトリの強制

クラスや構造体のコンストラクタのアクセスレベルをinternalにすることで、ファクトリ経由したインスタンス化をライブラリ外に強制することができます。

User
/// <summary>
/// User
/// </summary>
public class User
{
    /// <summary>
    /// Userの識別子
    /// </summary>
    public int Id { get; }

    /// <summary>
    /// コンストラクタをinternalで定義する
    /// </summary>
    internal User(int id)
    {
        Id = id;
    }
}
UserFactory
public class UserFactory
{
    /// <summary>
    /// UserIdを発行してもらうのに使う
    /// </summary>
    private readonly UserClient _userClient;

    public UserFactory(UserClient userClient)
    {
        _userClient = userClient;
    }

    /// <summary>
    /// Userのファクトリメソッド
    /// </summary>
    public async Task<User> CreateUserAsync()
    {
        // Idをサーバから発行してもらって使う、など
        var newId = await _userClient.CreateUserIdAsync();
        return new User(newId);
    }
}
別のアセンブリから呼び出した場合
using UserAssembly;
using UnityEngine;

public class Sample : MonoBehaviour
{
    // Inject from DI Container
    [Inject] private UserFactory _userFactory;

    public async void Start()
    {
        /*
         * 「Cannot access internal constructor 'User' here」
         * とエラーが出てインスタンス化できない
         */

        // var user = new User(123);


        /*
         * ファクトリ経由ならインスタンス化できる
         */
        var user = await _userFactory.CreateUserAsync();
    }
}

internalアクセスを外部アセンブリに公開する

たとえばライブラリを作っているときにこのテストコードを用意した場合、ライブラリ本体とテストコードは別のアセンブリに別れてしまいます。
そうなってくると、internalなどのアクセスレベルにテストコードからはアクセスができない、といった状況になってしまいます。

この問題を解決する方法としてはAssemblyInfo.csを定義すればOKです。

これについてはすでに別の記事で解説しているため、そちらを参考にしてください。

まとめ

  • Assembly Definitionはプログラマなら必須で覚えておくべき機能
  • ライブラリやアセットを公開する場合は必ずadfを定義しよう
  • internalなどのアクセスレベルを活用しよう
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away