readonly
修飾子
フィールドにreadonly
修飾子を付けて宣言すると、__フィールド初期化子__もしくは__コンストラクタ__でしか値を代入できなくなるため、不用意に値を変更される心配がなくなって非常に安心できます。
public class TestClass
{
private readonly int readonlyNum = 5; //フィールド初期化子での初期化可
public TestClass()
{
readonlyNum = 3; //コンストラクタでの初期化可
}
public void SomeMethod()
{
//readonlyNum = 10; //代入できない
}
}
ところがUnityでは(というかMonoBehaviour
なクラスでは)、コンストラクタを定義できないため、readonly
なフィールドを初期化することができないのです。
これではいつどこで値が書き換えられるか不安で夜も眠れません。
Start
メソッド内でreadonly
を初期化したいシチュエーション
別にフィールド初期化子で初期化すればいいじゃんと思うかもしれませんが、Start
メソッド内で初期化したいシチュエーションは結構あります。
例えば、次のようにSerializeField
で受け取った値を元にして別のクラスを作るような場合などが挙げられます。
public class TestMonoBehaviour : MonoBehaviour
{
[SerializeField]
private Component m_component1;
[SerializeField]
private Component m_component2;
private readonly IReadOnlyDictionary<ComponentType, Component> m_componentTable;
private void Start()
{
//エラー!Startはコンストラクタではないため値を代入できない
m_componentTable = new Dictionary<ComponentType, Component>()
{
{ComponentType.Type1, m_component1 },
{ComponentType.Type2, m_component2 }
};
}
}
SerializeField
で受け取った値を元にしてDictionary
を作っているので、フィールド初期化子で値を初期化できません。
このような場合、どうしてもStart
内で初期化する必要が出てきます。
しかし、かといってreadonly
をやめてしまえば、いつかの自分もしくは別の誰かが手が滑ってnull
を代入してしまう可能性が残ってしまい、夜も眠れなくなります。
そこで、どうにかしてMonoBehaviour
なクラスでreadonly
を実現する方法を考えました。
MonoBehaviour
でreadonly
を実現するUniReadOnly<T>
クラス
通常の方法ではMonoBehaviour
でreadonly
を使用できないので、同等の機能を実現するUniReadOnly<T>
クラスを作りました。
このクラスは以下の特徴を持ちます。
-
T
インスタンスを内部にラップする - 1回のみ
T
で初期化可能 - あとは
T
の読み取り専用プロパティのみを公開する
1回のみ初期化可能で、あとは読み取り専用として機能するので、readonly
の代わりとして利用できます。
実装
実装は次の通りです。
そのままコピペするだけで使えます。
/// <summary>
/// 一度しか初期化できない値です。
/// readonlyキーワードの代わりに使用します。
/// </summary>
/// <typeparam name="T">ラップする型</typeparam>
public class UniReadOnly<T>
{
///// <summary>
///// 値を取得します。
///// </summary>
public T Value { get; private set; }
/// <summary>
/// 初期化されているかどうかを取得します。
/// </summary>
public bool IsInitialized { get; private set; }
/// <summary>
/// 値を初期化します。このメソッドは一度しか呼ぶことができません。
/// </summary>
/// <param name="value">初期化する値</param>
/// <exception cref="AlreadyInitializedException">複数回初期化しようとしたときにスローされます。</exception>
public void Initialize(T value)
{
if (IsInitialized) throw new AlreadyInitializedException();
IsInitialized = true;
Value = value;
}
public override string ToString() => Value.ToString();
public static implicit operator T(in UniReadOnly<T> readOnly) => readOnly.Value;
}
internal class AlreadyInitializedException : Exception
{
public AlreadyInitializedException() : base("すでに初期化されています。")
{
}
}
使い方
使い方は簡単です。以下のように使用します。
-
readonly
にしたいオブジェクトをUniReadOnly<T>
として読み取り専用フィールドに持つ -
Initialize
メソッドで値を初期化する -
Value
プロパティで値を取り出す
具体的な使用例は以下のようになります。
※処理内容についてのツッコミは勘弁してください
public class TestMonoBehaviour : MonoBehaviour
{
private readonly UniReadOnly<string> m_readonlyString = new UniReadOnly<string>();
void Start()
{
//読み取り専用にする値で初期化する
m_readonlyString.Initialize("初期化します");
}
void Update()
{
//値を読み取り専用で取り出す
Debug.Log(m_readonlyString);
}
}
Start
メソッド内でInitialize
メソッドを呼び出し、読み取り専用にする値を設定しています。
これ以降に再びInitialize
メソッドを呼び出すと、自作例外クラスであるAlreadyInitializedException
がスローされるため、値が書き換わらないことが保証されます。
さらに、UniReadOnly<T>
のインスタンス自体はフィールド初期化子でインスタンス化してしまえば良いので、readonly
修飾子を付けることができます。
そのため、UniReadOnly<T>
インスタンス自体を書き換えられる心配もなく、Initialize
メソッドで設定したT
インスタンスの不変性1がしっかりと担保されます。
以上から、UniReadOnly<T>
クラスを使用することで、MonoBehaviour
なクラスでもreadonly
修飾子とほぼ同等の機能を実現することが可能となります。
implicit operator
ただ、通常のreadonly
修飾子を使った方法とは違って、__毎回毎回Valueプロパティから中身にアクセスしなければならないのが面倒__です。
そこで、UniReadOnly<T>
にはT
へのimplicit operator
を実装してあります。
implicit operator
は聞き慣れない人も多いと思いますが、暗黙的な型変換を定義することができる演算子__です。
例えば、double
型の変数にint
型の値をそのまま入れられるのは、double
からint
へ暗黙的な型変換が行われているから__です。
int i = 5;
double d = i; //intからdoubleへの型変換が暗黙的に行われている
このように、UniReadOnly<T>
にも、UniReadOnly<T>
からT
への暗黙的な変換を定義しました。これにより、以下のような記述が可能になり、ただのreadonly
フィールドと同じような扱いを可能にしています。
UniReadOnly<int> readonlyInt = new UniReadOnly<int>();
readonlyInt.Initialize(5);
int i = readonlyInt; //UniReadOnly<int>からintへの型変換が暗黙的に行われている
implicit operator
の適用範囲
ただし、UniReadOnly<T>
からT
への暗黙的な型変換がいつも都合良く行われるというわけではありません。
以下のような場合、暗黙の型変換が行われず、素直にValueプロパティを使用する羽目になります。
T
インスタンス自体を直接使用しない場合
暗黙的な型変換が行われるのは、「変換先の型インスタンス(T
)が求められる場面で、変換元のインスタンス(UniReadOnly<T>
)が代入された場合」となります。
つまり、以下のようにT
インスタンスを直接使用しない場合は暗黙的な型変換が行われず、エラーとなります。
UniReadOnly<string> readonlyStr = new UniReadOnly<string>();
readonlyStr.Initialize("初期値");
int stringLength = m_readonlyString.Length; //エラー!UniReadOnly<string>のLengthプロパティを見に行ってしまう
このようなことがしたい場合は、以下のようにきちんとValue
プロパティを書かなければなりません。
UniReadOnly<string> readonlyStr = new UniReadOnly<string>();
readonlyStr.Initialize("初期値");
int stringLength = m_readonlyString.Value.Length;
インターフェイスが絡む場合
インターフェイスが絡む場合も何故か暗黙的な型変換が使用できないみたいです。以下のようなことはできません。
UniReadOnly<IEnumerable<int>> uniReadOnlyCollection = new UniReadOnly<IEnumerable<int>>();
uniReadOnlyCollection.Initialize(Enumerable.Range(1, 5));
IEnumerable<int> vs = uniReadOnlyCollection; //エラー!インターフェイスが絡んでいるので暗黙的な型変換が行われない
このようなことがしたい場合は、Value
プロパティを使うか、もしくはキャスト(明示的な型変換)を行う必要があります。
UniReadOnly<IEnumerable<int>> uniReadOnlyCollection = new UniReadOnly<IEnumerable<int>>();
uniReadOnlyCollection.Initialize(Enumerable.Range(1, 5));
IEnumerable<int> vs = uniReadOnlyCollection.Value;
IEnumerable<int> vs2 = (IEnumerable<int>)uniReadOnlyCollection;
なぜインターフェイスが絡むと暗黙的な型変換が使用できないか、いまいち理由がわかりません。誰か教えて下さい。
implicit operator
の是非
implicit operator
のおかげで、ある場面では通常の読み取り専用フィールドと同様の使用感を得られます。
しかし、また別の場面ではValue
プロパティを介す必要がある場合もあります。
このように、統一した使い方ができないことから、読みづらくなる、可読性が下がると感じる方もいると思います。
僕自身は、そこまで問題とは感じない(むしろそこら中に.Value
がある方が読みづらいと感じる)のですが、implicit operator
の実装の是非は好みが分かれるところだと思いますので、好みに応じて調整してください(というか意見いただけると嬉しいです)。
最後に
readonly
のためにここまでする必要あるかって意見もあると思いますが、僕はどうしても気になるタチなので専用クラスを作ってしまいました。
賛否両論あると思いますが、よければ使ってみてください。ご意見ご感想あればコメントよろしくお願いします!
-
言うまでもありませんが、
T
インスタンスがイミュータブルであることが保証されるという意味ではありません。 ↩