はじめに
この記事におけるソースコードはすべて Public Domain
です。
また本記事において利用しているUnityバージョンは2019.2
です。
Assembly Definitionとは何か
Assembly Definitionという機能をご存知でしょうか。
これは「C#のビルドファイル(アセンブリ)を分割して出力する」ことができる機能であり、Unity 2017.3で追加されました。
特にライブラリや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
に収められます。
2. 差分ビルドされる
ソースコードに修正があった場合、該当するアセンブリのみがコンパイルされます。
そのため全体でコンパイル時間が短くなります。
3. 参照関係を明示的に設定する必要がある
Assembly Definition
を有効化するとアセンブリが分割されます。
そのため「どのアセンブリが、他のどのアセンブリに依存しているか」を手動で設定する必要があります。
4. Assembly-CSharpの扱いが特殊になる
Assembly-CSharp
はadf
を定義していないスクリプトがすべて押し込められる、いわゆる「全部入り」のアセンブリです。最初はすべてのスクリプトがこのアセンブリに入った状態となります。
つまり、adf
を定義するということは、「Assembly-CSharp
アセンブリからモジュールを抜き出して分割していく」と同義になります。
さて、このAssembly-CSharp
ですが、参照関係が特殊となっています。
- 他の
adf
で分割されたアセンブリへは、Assembly-CSharp
からすべて参照可能 - 逆に他のアセンブリから、
Assembly-CSharp
へは絶対にアクセスできない
Assembly Definition
を利用しているときはこの性質が問題となることがあるため、この挙動はしっかりと把握しておきましょう。
Assembly Definition Filesの定義方法
Assembly Definition Filesの作成
adf
はProject View
において、定義したいディレクトリ上で右クリックメニューから作成することができます。
adf
が配置された時点でそのディレクトリ以下のcsproj
ファイルが分割され、アセンブリも別に生成されます。
/Library/ScriptAssemblies
を覗くとBanana.dll
に分割されている。
Assembly Definition Filesの設定
単にdll
に分割するだけなら、adf
を作成するだけで完了です。
より突っ込んだ設定、たとえば次の設定が行いたい場合はInspector View
にて設定することができます。
- このアセンブリ内でのみ
unsafe
コードの利用を許可したい - 他の
adf
によって定義されたアセンブリに依存したい - 特定のプラットフォームでのみ有効化したい
- Editor拡張/Testコードだと設定したい
General
- Allow 'unsafe' Code : このアセンブリ内でunsafeコードを許可するか
-
Auto Referenced : 本体側のアセンブリ(
Assembly-CSharp
)から参照するか -
Override References : すでにコンパイル済みの
dll
を参照に追加するか
この「Auto Referenced」にチェックが入っている場合、毎回コンパイル対象になります。
Assembly-CSharp
から直接参照しないものはオフにしておくとよいです。
Define Constraints
文字列を設定します。ここに設定された文字列が Player Settings
のScripting Define Symbols
に定義されていた場合のみビルドされます。
(RIPENED_BANANA
が定義されていた場合のみこのアセンブリは有効となる。ちなみに意味は"完熟バナナ")
Assembly Definition References
他のadf
によって定義されたアセンブリを参照する場合に設定します。
超重要。
たとえばJuice Mixer
はBanana
とMilk
に依存している場合は、Assembly Definition References
でそれぞれに参照を追加しなければいけません。
Platforms
どのプラットフォームで有効化するか設定します。
/Editor
にadf
を定義した場合はEditor
にチェックを入れる必要があります。
(Editor Test
の場合も同様)
(エディタ拡張などはEditor
にチェックをいれてアセンブリを別ける必要がある)
Assembly Definition Filesのディレクトリ構造
adf
はディレクトリをネストして定義しても問題はありません。
その場合も「ディレクトリ単位」でアセンブリが分割されビルドされます。
Assembly Definitionを利用するメリット・デメリット
Assembly Definition
は基本的には定義した方がメリットは大きいのですが、一応デメリットも存在します。
メリット・デメリットを把握した上で実際に利用するかどうかを決めるとよいでしょう。
メリット
-
差分コンパイルになるためEditor上でのコンパイル時間が短くなる (
Auto Referenced
を適切にOFFにしておく必要あり) internal
やprivate protected
などのアクセスレベルが活用できるunsafe
コードを有効化する範囲をアセンブリ単位で限定できる- モジュールの依存関係を強制できる
- ライブラリ同士の干渉を最小限に抑えてプロジェクトに追加できる
- 対象プラットフォームごとにアセンブリレベルで別けてビルドができる
デメリット
-
Asset Bundle
と併用した場合に挙動がややこしいことになる - アセンブリをまたいで
partial
クラスが定義できない -
static
を使っていたときに分割しにくい - たまに謎のコンパイルエラーが出て動かなくなる
Assembly Definitionを利用するべきシチュエーション
次のようなシチュエーションでは必ずAssembly Definitionを利用するべきです。
ライブラリやアセットを公開する場合
ライブラリや、スクリプトを含んだアセットを公開する場合はadf
を定義してアセンブリを分割するべきです。
むしろやってくださいおねがいします。
理由としては「adf
が定義されていないライブラリはAssembly-CSharp
アセンブリに入ってしまう」からです。
これは自身のプロジェクトがAssembly Definition
を活用していた場合に非常に面倒な問題になります。
場合を分けて説明します。
セーフ:Assembly Definitionを一切利用していない場合
自身のプロジェクトも、導入した外部ライブラリも、どれもadf
を定義されていない場合です。
この場合は問題ありません。
セーフ:全員がAssembly Definitionを利用している場合
自身のプロジェクトと導入した外部ライブラリ、その両方がadf
を定義していた場合です。
この場合はアセンブリ同士の参照を正しく設定すれば問題ありません。
アウト:自身のプロジェクトのみadfを定義していた場合
この場合はアウトです。
自身のプロジェクトはadf
を定義しているが、導入したライブラリには定義されていない場合。
この場合はライブラリがAssembly-CSharp
に入り込んでしまうため、自身のモジュールからアクセスができないためエラーになってしまいます。
こうなると自身でライブラリにadf
を定義してアセンブリを分割する必要がでてきます。
そしてライブラリ側の構造が歪でadf
をキレイに定義できない、定義してもstatic
やpartial
を乱用していてエラーが出まくる…、みたいな地獄な状況になる場合もあります。
なのでもし、これからライブラリやモジュールを作って公開するつもりがある人は必ずAssembly Definitionを意識した作りにしてください。
マジでお願いします。マジで。
アーキテクチャに沿った開発を行う場合
クリーンアーキテクチャなど、レイヤ間での依存関係をしっかり定義したい場合はAssembly Definition
がかなり有用です。
adf
でモジュール間の依存を定義することで、「Domain
から外への参照を禁止する」といったことがプロジェクト設定で制御することが可能になります。
合わせて覚えておくべき技
Assembly Definition
を利用する場合、覚えておくと開発効率の向上につながるテクニックがいくつかあります。
Editor上でコンパイルエラーが出た場合の対策
Assembly Definition
を使っていると、コードに変更がなくてもコンパイルエラーが出てしまうことがあります。本当によく起きます。
これはエディタがキャッシュしているアセンブリがおかしくなってしまったことが原因の場合が多いです。
そのため次の手順を行うと復旧することがあります。
-
\Library\ScriptAssemblies
を削除してみる - (↑で直らないなら)Unity Editorを再起動する
- (↑で直らないなら)該当するAssetのReimportを行う
- (↑で直らないなら)
\Library
を消してUnity Editorを再起動する
アセンブリ単位でのアクセスレベルを利用する
C#のアクセス修飾子にはアセンブリ単位でアクセスを制御するものがあります。
外部公開するライブラリを作るときなどに便利に使えるためお勧めです。
詳しくは岩永さんのサイトを見てもらうとよいのですが、一応解説します。
アクセスレベル
C#のアクセス修飾子には次のものがあります。
レベル | 説明 | 備考 |
---|---|---|
public | どこからでもアクセス可能 | ガバガバ |
protected | クラス内とその派生クラスからのみアクセス可能 | 継承するときに使う |
internal | 同一アセンブリのクラス内からのみアクセス可能 | アセンブリ内からはpublic、外からはprivateに相当 |
protected internal | 同一アセンブリのクラス内 OR 派生クラスからアクセス可能 | |
private protected | 同一アセンブリのクラス内 AND 派生クラスからアクセス可能 | C# 7.2以降 |
private | クラス内からのみアクセス可能 | 一番厳しい |
とくにinternal
、protected internal
、private protected
はAssembly Definition
を利用している場合のみに利用できるアクセスレベルです。
これらを活用することでライブラリの実装が少し楽になったりします。
利用例1:管理用のメソッドの定義
internal
を利用することで、「同一アセンブリ内からのみ呼び出せるメソッド」を定義することができます。
これはライブラリやフレームワークを作成する場合に、オブジェクトの管理メソッドなどを定義するときに活用することができます。
/// <summary>
/// 敵
/// </summary>
public class Enemy
{
/// <summary>
/// ライブラリ外から呼びだせるのはこっち
/// </summary>
public void Destroy()
{
/*
* なんか実装
* いろいろ処理が走ったり走らなかったり
*/
}
/// <summary>
/// ライブラリ内部でリソース管理用に呼び出すのはこっち
/// ライブラリ内からのみコール可能
/// </summary>
internal void ForceDestroy()
{
/*
* なんか実装
* Force()より単純化されていたり、複雑だったり…
*/
}
}
利用例2:ファクトリの強制
クラスや構造体のコンストラクタのアクセスレベルをinternal
にすることで、ファクトリ経由したインスタンス化をライブラリ外に強制することができます。
/// <summary>
/// User
/// </summary>
public class User
{
/// <summary>
/// Userの識別子
/// </summary>
public int Id { get; }
/// <summary>
/// コンストラクタをinternalで定義する
/// </summary>
internal User(int id)
{
Id = id;
}
}
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
などのアクセスレベルを活用しよう