web-dev-qa-db-ja.com

なぜこれをチェックするのですか!= null?

時々、私は.NETコードを見て、舞台裏でどのように実装されているかを確認するのが好きです。 Reflectorを介してString.Equalsメソッドを見ているときに、この宝石に出くわしました。

C#

[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
public override bool Equals(object obj)
{
    string strB = obj as string;
    if ((strB == null) && (this != null))
    {
        return false;
    }
    return EqualsHelper(this, strB);
}

[〜#〜] il [〜#〜]

.method public hidebysig virtual instance bool Equals(object obj) cil managed
{
    .custom instance void System.Runtime.ConstrainedExecution.ReliabilityContractAttribute::.ctor(valuetype System.Runtime.ConstrainedExecution.Consistency, valuetype System.Runtime.ConstrainedExecution.Cer) = { int32(3) int32(1) }
    .maxstack 2
    .locals init (
        [0] string str)
    L_0000: ldarg.1 
    L_0001: isinst string
    L_0006: stloc.0 
    L_0007: ldloc.0 
    L_0008: brtrue.s L_000f
    L_000a: ldarg.0 
    L_000b: brfalse.s L_000f
    L_000d: ldc.i4.0 
    L_000e: ret 
    L_000f: ldarg.0 
    L_0010: ldloc.0 
    L_0011: call bool System.String::EqualsHelper(string, string)
    L_0016: ret 
}

thisnullに対してチェックする理由は何ですか?私は目的があると仮定しなければなりません、さもなければこれはおそらく今までに捕らえられて取り除かれたでしょう。

71
Brian Gideon

.NET 3.5の実装を見ていましたか? .NET4の実装は少し違うと思います。

ただし、これは、仮想インスタンスメソッドでさえ非仮想的に呼び出すことができるためであると私はひそかに疑っていますnull参照で。 ILで可能です。 null.Equals(null)を呼び出すILを生成できるかどうかを確認します。

編集:さて、ここにいくつかの興味深いコードがあります:

_.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       17 (0x11)
  .maxstack  2
  .locals init (string V_0)
  IL_0000:  nop
  IL_0001:  ldnull
  IL_0002:  stloc.0
  IL_0003:  ldloc.0
  IL_0004:  ldnull
  IL_0005:  call instance bool [mscorlib]System.String::Equals(string)
  IL_000a:  call void [mscorlib]System.Console::WriteLine(bool)
  IL_000f:  nop
  IL_0010:  ret
} // end of method Test::Main
_

私は次のC#コードをコンパイルすることでこれを取得しました:

_using System;

class Test
{
    static void Main()
    {
        string x = null;
        Console.WriteLine(x.Equals(null));

    }
}
_

...次に、ildasmで分解して編集します。この行に注意してください:

_IL_0005:  call instance bool [mscorlib]System.String::Equals(string)
_

元々、それはcallvirtではなくcallでした。

それで、それを再組み立てするとどうなりますか? .NET 4.0では、次のようになります。

_Unhandled Exception: System.NullReferenceException: Object
reference not set to an instance of an object.
    at Test.Main()
_

うーん。 .NET 2.0ではどうですか?

_Unhandled Exception: System.NullReferenceException: Object reference 
not set to an instance of an object.
   at System.String.EqualsHelper(String strA, String strB)
   at Test.Main()
_

これはもっと興味深いことです...私たちは明らかにEqualsHelperに入ることができましたが、これは通常は予想していなかったでしょう。

十分な文字列...自分でリファレンスイコリティを実装して、null.Equals(null)がtrueを返すことができるかどうかを確認してみましょう。

_using System;

class Test
{
    static void Main()
    {
        Test x = null;
        Console.WriteLine(x.Equals(null));
    }

    public override int GetHashCode()
    {
        return base.GetHashCode();
    }

    public override bool Equals(object other)
    {
        return other == this;
    }
}
_

以前と同じ手順-分解し、callvirtcallに変更し、再組み立てして、true ..が出力されるのを確認します。

別の回答が このC++の質問 を参照していますが、ここではさらに悪意があります... virtualメソッドを非仮想的に呼び出しているためです。 。通常、C++/CLIコンパイラでさえ仮想メソッドにcallvirtを使用します。言い換えれば、この特定のケースでは、thisがnullになる唯一の方法は、ILを手動で書き込むことだと思います。


編集:私はちょうど何かに気づきました...私は実際に私たちの小さなサンプルプログラムのどちらかで正しいメソッドを呼び出していませんでした。最初のケースの呼び出しは次のとおりです。

_IL_0005:  call instance bool [mscorlib]System.String::Equals(string)
_

これが2番目の呼び出しです:

_IL_0005:  call instance bool [mscorlib]System.Object::Equals(object)
_

前者の場合、I meantSystem.String::Equals(object)を呼び出し、後者の場合、I meantTest::Equals(object)を呼び出します。これから、3つのことがわかります。

  • オーバーロードに注意する必要があります。
  • C#コンパイラは、仮想メソッドのdeclarerへの呼び出しを発行します-仮想メソッドの最も具体的なoverrideではありません。 IIRC、VBは逆に機能します
  • object.Equals(object)はnullの「this」参照を喜んで比較します

C#オーバーライドにコンソール出力を少し追加すると、違いがわかります。次のように、明示的に呼び出すようにILを変更しない限り、呼び出されません。

_IL_0005:  call   instance bool Test::Equals(object)
_

だから、私たちはあります。 null参照でのインスタンスメソッドの面白さと乱用。

ここまで進んだら、 値型の方法についての私のブログ投稿もご覧くださいcan ILでパラメーターなしのコンストラクター ...を宣言する。

85
Jon Skeet

その理由は、thisnullになる可能性があるからです。関数を呼び出すために使用できる2つのILオペコードがあります:callとcallvirt。 callvirt関数により、CLRはメソッドを呼び出すときにnullチェックを実行します。呼び出し命令はそうではないため、thisnullとしてメソッドを入力できます。

怖いですか?確かにそれは少しです。ただし、ほとんどのコンパイラは、これが発生しないことを保証します。 .call命令は、nullが不可能な場合にのみ出力されます(C#は常にcallvirtを使用すると確信しています)。

ただし、これはすべての言語に当てはまるわけではありません。そのため、この場合、BCLチームがSystem.Stringクラスをさらに強化することを選択したかどうかは正確にはわかりません。

これがポップアップする可能性のある別のケースは、逆ピンボーク呼び出しです。

17
JaredPar

簡単に言うと、C#のような言語では、メソッドを呼び出す前にこのクラスのインスタンスを作成する必要がありますが、フレームワーク自体は作成しません。 CILには、関数を呼び出す2つの異なる方法があります。callcallvirt..。一般的に、C#は常にcallvirtを発行します。これには、thisがnullでないことが必要です。しかし、他の言語(C++/CLIが頭に浮かぶ)はcallを出力する可能性があり、それはその期待を持っていません。

(¹わかりました、calli、newobjなどを数えると5に近いですが、単純にしましょう)

9
Warren Rumak

ソースコード には次のコメントがあります:

これは、callvirt命令を使用しないリバースピンボークやその他の発信者から保護するために必要です。

4

見てみましょう...thisは比較する最初の文字列です。 objは2番目のオブジェクトです。つまり、それはある種の最適化のように見えます。最初にobjを文字列型にキャストします。それが失敗した場合、strBはnullになります。また、strBがnullで、thisがnullでない場合、それらは完全に等しくなく、EqualsHelper関数はスキップできます。

これにより、関数呼び出しが保存されます。それを超えて、おそらくEqualsHelper関数をよりよく理解することで、この最適化が必要な理由が明らかになるかもしれません。

編集:

ああ、EqualsHelper関数は(string, string)をパラメーターとして受け入れています。 strBがnullの場合、それは本質的に、それが最初からnullオブジェクトであったか、文字列に正常にキャストできなかったことを意味します。 strBがnullである理由が、オブジェクトが文字列に変換できない別の型であったためである場合、本質的に2つのnull値を使用してEqualsHelperを呼び出したくないでしょう( llはtrueを返します。この場合、Equals関数shouldはfalseを返します。したがって、このifステートメントは最適化以上のものであり、実際には適切な機能も保証します。

1
Steve Wortham

引数(obj)が文字列にキャストされない場合、strBはnullになり、結果はfalseになります。例:

    int[] list = {1,2,3};
    Console.WriteLine("a string".Equals(list));

falseを書き込みます。

String.Equals()メソッドは、他の文字列だけでなく、任意の引数タイプに対して呼び出されることに注意してください。

0
ja72