以下の続きの記事になります。
簡単におさらいするのでこの記事だけ読んでも問題ないとは思いますが、お時間があれば是非(1)、(2)の記事もお読み下さい。
検証内容のおさらい
-
UnityEngine.Object
では等値演算子==
(及び非等値演算子!=
)がオーバーロードされている - オーバーロードされた等値演算子では
Object.CompareBaseObjects
メソッドに判定処理を委譲している
public static bool operator ==(Object x, Object y) => Object.CompareBaseObjects(x, y);
public static bool operator !=(Object x, Object y) => !Object.CompareBaseObjects(x, y);
-
Object.CompareBaseObjects
メソッド内では純粋なnull
チェックやInstanceID
の等値判定に加えて、渡されたオブジェクトが「破棄」されていないか(Unityフレームワーク上で生存しているか)をチェックするObject.IsNativeObjectAlive
メソッドが実行されている -
Destroy実行前後で、検証対象メソッドの戻り値は以下のように変化した(
UnityEngine.Object
に生えている静的な非公開メソッド)
IsNativeObjectAlive | DoesObjectWithInstanceIDExist | GetCachedPtr | |
---|---|---|---|
破棄前 | true | true | インスタンス固有の値 |
破棄後 | false | false | 0 |
nullチェックの記法
インスタンスがnullでないことを確認する書き方はいくつかあります。
// 1. 標準的なnullチェックの書き方
if (sampleMonoBehaviour != null)
{
}
// 2. UnityEngine.Object→boolへの暗黙的な型変換が定義されているため、こう書いても1と同じ意味
if (sampleMonoBehaviour)
{
}
// 3. C# 7.0で導入された「is」と、C# 9.0で導入された「not」を使ったnullチェック
// これは1と2とは挙動が異なる
if (sampleMonoBehaviour is not null)
{
}
1番目の書き方は最も一般的なnullチェックの書き方だと思います。
これがどのような挙動を示すかは、記事(1)と(2)で検証し、この記事のおさらいにも書きました。
2番目は記事(1)の余談でも触れましたが、暗黙的な型変換演算子がオーバーロードされているために許容される記述です。
問題は3番目の書き方です。
これは、C# 7.0で導入されたパターンマッチングという文法でis
演算子の拡張(元々is
は型の一致判定を行う用途などに使われていた)が行われたことによって実現できるようになった記述です。
この場合null
「でない」ことをチェックしているため、is not
のように書いていますが、not
がこの形で使えるようになったのはC# 9.0以降でそれ以前は!(A is null)
のように記述していました。
直感的には1番と3番は同じ意味に見えますが、is
演算子は等値演算子==
と異なりユーザがオーバーロードした演算子を考慮しません。
つまり、is
は「その変数に本当にnullが代入されているか」を確認します。
この挙動は使用している多くの型が等値演算子をオーバーロードするUnityEngine.Object
を継承しているUnityの場合だと大きく問題になります。
以下のようなテストコードを書いて見ます。
using System.Collections;
using UnityEngine;
public class SampleTest : MonoBehaviour
{
private IEnumerator Start()
{
SampleMonoBehaviour sampleMonoBehaviour = gameObject.AddComponent<SampleMonoBehaviour>();
Debug.Log("Destroy前");
Debug.Log($"{nameof(sampleMonoBehaviour)} == null: {sampleMonoBehaviour == null}");
Debug.Log($"{nameof(sampleMonoBehaviour)} is null: {sampleMonoBehaviour is null}");
Destroy(sampleMonoBehaviour);
// 1フレーム待つ
yield return null;
Debug.Log("Destroy後");
Debug.Log($"{nameof(sampleMonoBehaviour)} == null: {sampleMonoBehaviour == null}");
Debug.Log($"{nameof(sampleMonoBehaviour)} is null: {sampleMonoBehaviour is null}");
}
}
このように、「破棄」されているかの判定を実装している==
を無視することで、破棄後も破棄されたインスタンス is null
がfalse
を返してしまうことが確認できました。
このため、UnityEngine.Object
に対するnull
チェック(および破棄判定)はis
は用いないほうが良さそうです。