17. nullを回避する
Unity使いを最も多く悩ませてきたエラーといえば、間違いなく NullReferenceException: Object reference not set to an instance of an object でしょう。これは要するに値がnullである変数を参照してしまったということです。Unityを始めたての頃は特にこのエラーに悩まされてきたことでしょう。単にインスペクターにセットし忘れていただけならまだ良いのですが、どんなに原因を探して潰しても一向に消えないこともあり、そうなるともうお手上げです。一体このnullはどこで混入してしまったのか…!?
そんな皆のトラウマであるnullですが、実際こいつは昔から世界中で猛威を振るっています。nullの生みの親であるイギリスのTony Hoareはこの状況についてこのような言葉を残しました。
私はそれを10億ドルの過ちと呼んでいる。それは1965年のnull参照の発明だ。
(I call it my billion-dollar mistake. It was the invention of the null reference in 1965.)
Tony Hoare
彼は1965年、「ALGOL W」という言語の設計中に「単に実装が非常に簡単だったから」という理由で全ての参照にnullを含めることを許可してしまいました。その結果、今に至るまでnullは世界中でシステムをクラッシュさせ、莫大な経済的損失が生まれることになりました。その教訓を活かし、RustやTypeScriptのように現在主流となっている言語の多くはnull安全という仕組みを取り入れ、最初からnullを許容しないように設計されています。
非null三原則 (返さない、渡さない、代入しない)
では、我々はこの悪魔にどう立ち向かうべきでしょうか?単にあらゆる場所でnullチェックを行えば一応安全にはなりますが、これは現実的ではありません。コードの見通しも悪くなるし、nullチェックを忘れればエラーの原因になります。
一番の解決法は、そもそもnullをコードで扱わないようにすることです。すなわち、以下の非null三原則に従います。
- nullを返さない
- メソッドの戻り値としてnullをreturnしないこと
- nullを渡さない
- メソッドの引数にnullを渡さないこと
- nullを代入しない
- 変数にnullを代入しないこと
// ❌ Bad: nullを返す・渡す・代入する
ISkill GetSpecialSkill()
{
if (hasSpecialSkill)
return specialSkill;
return null; // nullを返している!
}
void UseSkill()
{
ISkill skill = GetSpecialSkill();
if (skill != null) // nullチェックを忘れたらそこで終わり
{
skill.Execute();
}
}
void ClearWeapon()
{
currentWeapon = null; // nullを代入している!
}nullの代わりとなるダミーオブジェクトを作る
では、今まで「何もない」ということを表すためにnullを渡していた場所ではどうすればいいのでしょうか。それは、nullの代わりに「何もしない」という振る舞いを持つ安全なダミーオブジェクトを作ってそれを渡すことです。そうすればnullを参照することなく、nullチェックも不要になります。
// ✅ Good: 「何もしない」ダミーオブジェクトを使う (Null Objectパターン)
public interface ISkill
{
void Execute();
}
// 「何もしない」という振る舞いを持つダミー
public class NullSkill : ISkill
{
public void Execute() { } // 何もしない
}
ISkill GetSpecialSkill()
{
if (hasSpecialSkill)
return specialSkill;
return new NullSkill(); // nullの代わりにダミーを返す
}
void UseSkill()
{
ISkill skill = GetSpecialSkill();
skill.Execute(); // nullチェック不要! NullSkillなら何も起きないだけ
}RequireComponentを使う
Unityのインスペクターでコンポーネントをアタッチし忘れた結果nullエラーが起きた経験はよくあるでしょう。そこで、Unityの [RequireComponent] 属性を使用することで、自動的にそのコンポーネントを追加してくれ、アタッチし忘れを防いでくれます。
[RequireComponent(typeof(Rigidbody))]
public class PlayerMovement : MonoBehaviour
{
Rigidbody rb;
void Awake()
{
rb = GetComponent<Rigidbody>(); // RequireComponentがあるのでnullにならない
}
}