TL;DR
bool
で実行結果がわかるような、副作用を起こすメソッドmethodThatWontFail()
があるとする。
これが失敗して欲しくない時にUnityのAssertionを発生させるという目的で、Assert.IsTrue(methodThatWontFail());
とやってはいけない。
ビルドした時に「基本的にAssertは丸ごと取り除かれる」という仕様のおかげでそのメソッドを実行するという事実ごと吹き飛ぶ。
Backgrounds
C#のList<T>()
型には、bool Remove(T item)
というメソッドがある。その名の通り、item
に指定したもので一番最初にヒットしたものを削除_しようとする_メソッドである。
この「しようとする」という部分が今回重要になってくる。
このメソッドは実際にList
の中にitem
で指定したものがなくても例外を吐かない仕様となっている。その代わり、bool
の戻り値がtrue
だと削除成功という扱いになる。
var list = new List<int>{10, 42};
list.Remove(42); // true - 中身は [10]
list.Remove(5); // false - 削除失敗, 中身は [10] のまま
さて、Assertionとはプログラマがコード中に「何があってもtrue
となってほしい条件」を記述するデバッグの手法の一つであるが、UnityにはこれがUnityEngine.Assertions.Assert
クラスで提供されている。ちなみにこのクラスでは条件の記述が読みやすくなるようにラッピングされており、例えば
using UnityEngine.Assertions
Assert.AreEqual(4, 2 + 2); // (期待値, 実際値)
Assert.AreEqual(3, 1 + 1); // false - AssertionException!
Assert.IsTrue(5 < 3); // false - AssertionException!
といった記述ができる。
バグの概要
先ほどのList<T>::Remove
のコードにおいて、例えば42
を取り除こうとする時点で42
がリストに存在しない状況を絶対に起こしたくない、という要求があった。これをAssertionを利用して検出することを考え:
var list = new List<int>{10, 42};
Assert.IsTrue(list.Remove(42)); // 削除失敗するとLogExceptionされる...?
とした。
その結果
リリースビルド時にこのコードが動かなくなった。
WHY?!?!?
Assertionは基本的に「完成品でその条件がfalse
になるようなことが起こらないようにデバッグ済みである」という前提となっている。そのため、UnityではRelease
でビルドすると基本的に
「すべてのAssertionに関係するコードがそもそもソースに記述されていなかったことにして」ビルドする1
という挙動をとる。
つまり上記のコードの場合
Assert.IsTrue(list.Remove(42));
がなかったことになる
すなわち
list.Remove(42);
が実行されない
という事故が起こった。
結論
今回はList<T>::Remove
だったが、それに限らず副作用を起こしてその結果をbool
などで返すようなコードをAssertionでチェックしてはいけない。どうしてもチェックする必要があるなら、副作用を起こさないメソッドで確認するのが良い。
例:
var list = new List<int>{10, 42};
Assert.IsTrue(list.Contains(42)); // will fail if 42 is not in list
list.Remove(42); // will ALWAYS return true (otherwise, assertion failure at the line above)
-
Unityではリリースビルドでも無理やりAssertionを含めるようにすることはできるが、こういう事故が起こった以上、自分はおすすめしない。 ↩