白猫のメモ帳

C#とかJavaとかJavaScriptとかHTMLとか機械学習とか。

nullチェックしたらNullReferenceException

こんにちは。

秋ですね。
ようやく暑さも収まりましたが、体調が安定しません…。

なんだか謎掛けみたいなタイトルですが、
変な罠に引っかかりそうになったので備忘録。

なぞなぞ

nullチェックするようなロジックを書くべきではないとかいう話はまぁちょいちょいありますが、
個人的にはnull自体はそんなに嫌いではないです。

いやべつにNullableとかOptionalを否定する気はないですけど。
null条件演算子とかnull合体演算子とかもありますしね。

で、まぁこういうコードよく書きますよね。

var hoge = GetHoge();
if (hoge != null) 
{
    hoge.Fuga();
}

このコードでFugaの呼び出しの際にhogeがnullでNullReferenceExceptionが発生するのはどんなときでしょう?

なんかJSのクイズでたまに見る次のコードで「false」がアラートされるaを答えなさいみたいな感じがありますが。

if (a === a) {
    alert("true");
} else {
    alert("false");
}

正解はHogeの「==」「!=」を演算子オーバーロードをミスって書いたときです。
ちなみにJSの方の答えは「NaN」ですね。

演算子オーバロードって

演算子オーバーロードについては前に記事を書きましたので詳しくはこちらをどうぞ。
shironeko.hateblo.jp

たとえばプロパティを一つだけ持つクラスの場合、
こんな感じで演算子オーバーロードを書きたいですよね。

class Hoge
{
    public int Piyo { get; }

    public Hoge(int piyo)
    {
        this.Piyo = piyo;
    }

    public static bool operator ==(Hoge a, Hoge b)
    {
        return a.Piyo == b.Piyo;
    }

    public static bool operator !=(Hoge a, Hoge b)
    {
        return a.Piyo != b.Piyo;
    }
}

が、これをnullと比較すると第二引数にnullが入ってきて死ぬわけです。
演算子オーバーロードは左右を意識するので、null == ~って書くと第一引数側で死にます)

public static bool operator ==(Hoge a, Hoge b)
{
    if (a == null && b == null)
    {
        return true;
    }
    else if (a != null && b != null)
    {
        return a.Piyo == b.Piyo;
    } 
    else
    {
        return false;
    }
}

public static bool operator !=(Hoge a, Hoge b)
{
    return !(a == b);
}

でまぁこんな感じかなと思って書くと、再帰になってStackOverflowで死ぬわけです。
というわけでこうですね。

public static bool operator ==(Hoge a, Hoge b)
{
    if (a as object == null && b as object == null)
    {
        return true;
    }
    else if (a as object != null && b as object != null)
    {
        return a.Piyo == b.Piyo;
    } 
    else
    {
        return false;
    }
}

public static bool operator !=(Hoge a, Hoge b)
{
    return !(a == b);
}

この辺の書き方を間違えたりすると最初の例のようにnullチェックは通るのに実態はnullでしたみたいなことになるんです。
親クラスで演算子オーバーロードしてたりするとより沼にハマりやすくなります。

そういえば

しれっと順番を意識するのでとか書いたのですが、
二項演算子オーバーロードでは片側が自分の型であればもう片側は別の型でも問題ありません。

ということはですよ。
こんなふうに書いたらどうなるのっていう疑問がわきます。

class Hoge
{
    public static bool operator ==(Fuga a, Hoge b)
    {
        (略)
    }

    public static bool operator !=(Fuga a, Hoge b)
    {
        (略)
    }
}

class Fuga
{
    public static bool operator ==(Fuga a, Hoge b)
    {
        (略)
    }

    public static bool operator !=(Fuga a, Hoge b)
    {
        (略)
    }
}

結論としてはコンパイル通りませんでした。なんて普通なオチ…。