時々、私は.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
}
this
をnull
に対してチェックする理由は何ですか?私は目的があると仮定しなければなりません、さもなければこれはおそらく今までに捕らえられて取り除かれたでしょう。
.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;
}
}
_
以前と同じ手順-分解し、callvirt
をcall
に変更し、再組み立てして、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 meantはSystem.String::Equals(object)
を呼び出し、後者の場合、I meantはTest::Equals(object)
を呼び出します。これから、3つのことがわかります。
object.Equals(object)
はnullの「this」参照を喜んで比較しますC#オーバーライドにコンソール出力を少し追加すると、違いがわかります。次のように、明示的に呼び出すようにILを変更しない限り、呼び出されません。
_IL_0005: call instance bool Test::Equals(object)
_
だから、私たちはあります。 null参照でのインスタンスメソッドの面白さと乱用。
ここまで進んだら、 値型の方法についての私のブログ投稿もご覧くださいcan ILでパラメーターなしのコンストラクター ...を宣言する。
その理由は、this
がnull
になる可能性があるからです。関数を呼び出すために使用できる2つのILオペコードがあります:callとcallvirt。 callvirt関数により、CLRはメソッドを呼び出すときにnullチェックを実行します。呼び出し命令はそうではないため、this
をnull
としてメソッドを入力できます。
怖いですか?確かにそれは少しです。ただし、ほとんどのコンパイラは、これが発生しないことを保証します。 .call命令は、null
が不可能な場合にのみ出力されます(C#は常にcallvirtを使用すると確信しています)。
ただし、これはすべての言語に当てはまるわけではありません。そのため、この場合、BCLチームがSystem.String
クラスをさらに強化することを選択したかどうかは正確にはわかりません。
これがポップアップする可能性のある別のケースは、逆ピンボーク呼び出しです。
簡単に言うと、C#のような言語では、メソッドを呼び出す前にこのクラスのインスタンスを作成する必要がありますが、フレームワーク自体は作成しません。 CILには、関数を呼び出す2つの異なる方法があります。call
とcallvirt
..。一般的に、C#は常にcallvirt
を発行します。これには、this
がnullでないことが必要です。しかし、他の言語(C++/CLIが頭に浮かぶ)はcall
を出力する可能性があり、それはその期待を持っていません。
(¹わかりました、calli、newobjなどを数えると5に近いですが、単純にしましょう)
ソースコード には次のコメントがあります:
これは、callvirt命令を使用しないリバースピンボークやその他の発信者から保護するために必要です。
見てみましょう...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ステートメントは最適化以上のものであり、実際には適切な機能も保証します。
引数(obj)が文字列にキャストされない場合、strBはnullになり、結果はfalseになります。例:
int[] list = {1,2,3};
Console.WriteLine("a string".Equals(list));
false
を書き込みます。
String.Equals()メソッドは、他の文字列だけでなく、任意の引数タイプに対して呼び出されることに注意してください。