Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9749

Assertionで副作用を起こすな

$
0
0

TL;DR

boolで実行結果がわかるような、副作用を起こすメソッドmethodThatWontFail()があるとする。
これが失敗して欲しくない時にUnityのAssertionを発生させるという目的で、Assert.IsTrue(methodThatWontFail());とやってはいけない。
ビルドした時に「基本的にAssertは丸ごと取り除かれる」という仕様のおかげでそのメソッドを実行するという事実ごと吹き飛ぶ。

Backgrounds

C#のList<T>()型には、bool Remove(T item)というメソッドがある。その名の通り、itemに指定したもので一番最初にヒットしたものを削除しようとするメソッドである。
この「しようとする」という部分が今回重要になってくる。
このメソッドは実際にListの中にitemで指定したものがなくても例外を吐かない仕様となっている。その代わり、boolの戻り値がtrueだと削除成功という扱いになる。

varlist=newList<int>{10,42};list.Remove(42);// true - 中身は [10]list.Remove(5);// false - 削除失敗, 中身は [10] のまま

さて、Assertionとはプログラマがコード中に「何があってもtrueとなってほしい条件」を記述するデバッグの手法の一つであるが、UnityにはこれがUnityEngine.Assertions.Assertクラスで提供されている。ちなみにこのクラスでは条件の記述が読みやすくなるようにラッピングされており、例えば

usingUnityEngine.AssertionsAssert.AreEqual(4,2+2);// (期待値, 実際値)Assert.AreEqual(3,1+1);// false - AssertionException!Assert.IsTrue(5<3);// false - AssertionException!

といった記述ができる。

バグの概要

先ほどのList<T>::Removeのコードにおいて、例えば42を取り除こうとする時点で42がリストに存在しない状況を絶対に起こしたくない、という要求があった。これをAssertionを利用して検出することを考え:

varlist=newList<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でチェックしてはいけない。どうしてもチェックする必要があるなら、副作用を起こさないメソッドで確認するのが良い。

例:

varlist=newList<int>{10,42};Assert.IsTrue(list.Contains(42));// will fail if 42 is not in listlist.Remove(42);// will ALWAYS return true (otherwise, assertion failure at the line above)

  1. Unityではリリースビルドでも無理やりAssertionを含めるようにすることはできるが、こういう事故が起こった以上、自分はおすすめしない。 


Viewing all articles
Browse latest Browse all 9749

Trending Articles