そうである必要はありません (明示的なチェックがある場合もあります) が、アクセス違反の例外をトラップすることで機能します。
.NET オブジェクトはネイティブ オブジェクトに変換されます。そのフィールドは特定の方法で配置されたメモリ ブロックになり、そのメソッドはネイティブ マシン コード メソッドに組み込まれ、v テーブルまたはその他の仮想メソッド オーバーロード メカニズムが作成されます。
フィールドへのアクセスとは、オブジェクトのアドレスを見つけ、メンバーのオフセットを追加し、参照されているメモリの一部を読み書きすることを意味します。
仮想メソッドの呼び出しとは、オブジェクトのアドレスを見つけ、そのメソッド テーブルを見つけ (オブジェクト内でオフセットを設定)、メソッドのアドレスを見つけて (テーブル内でオフセットを設定)、渡されたオブジェクトのアドレスを指定してそのアドレスでメソッドを呼び出すことを意味します。 (this
ポインター)。
非仮想メソッドの呼び出しとは、渡されたオブジェクトのアドレス (this
ポインター) を使用してメソッドを呼び出すことを意味します。
明らかに、問題のアドレスに実際のオブジェクトがない場合、ケース 1 と 2 は何らかの形で失敗しますが、ケース 3 は機能します (ただし、ケース 1 または 2 につながる可能性があります)。これがうまくいかない主な原因は 2 つあります。
実際にはこのタイプのオブジェクトではないメモリの任意のビットにアクセスする可能性があり、あらゆる種類の刺激的で追跡が非常に困難なバグにつながります (.NET コードは通常、このシナリオを引き起こすものにはなりません)。
保護されているメモリの任意のビットにアクセスして、アクセス違反を引き起こす可能性があります。
2 番目のケースについては、C、C++、または ASM コーディングで知っているかもしれません。そうでない場合でも、おそらくプログラムがクラッシュし、息を切らしながらあるアドレスでのアクセス違反について話しているのを見たことがあるでしょう。その場合、指定されたアドレスはほぼ何でもかまいませんが、ほとんどの場合、0x00000000 または 0x00000020 のような非常に低いアドレスであることに気付いたかもしれません。これらは、コードがフィールドにアクセスするか、仮想メソッドを呼び出すかにかかわらず、null ポインターを逆参照しようとすることが原因でした (これは、基本的にフィールドにアクセスしてから、取得した内容に応じて呼び出します)。
現在、最初の 64k またはメモリは常に保護されているため、null ポインターを逆参照すると、最初のケース (任意のメモリが誤用され、奇妙な「コアのファンダンゴ」バグが発生する) ではなく、常に 2 番目のケース (アクセス違反) が発生します。 )。
これは .NET でもまったく同じです (というか、それによって生成された jit されたコード)。 jit されたコードは に変換されNullReferenceException
、それ以外の場合は に変換されますAccessViolationException
。
逆参照はしないが、保護されたメモリにアクセスするコードでこれをシミュレートできます (読み取りのみを行うため、誤って保護されていないメモリにヒットした場合でも、結果はそれほど奇妙ではありません!) :
次のコードは、AccessViolationException を発生させます。
unsafe
{
int read = *((int*)long.MaxValue - 8);
}
次のコードは NullReferenceException を発生させます。
unsafe
{
int read = *((int*)8);
}
どちらのコードも、実際には何も逆参照していません。どちらもアクセス違反を引き起こしますが、CLR は、後者はおそらく null 参照が原因であると想定し (公平に言えば、最も可能性の高いシナリオ)、それを発生させます。
したがって、どのようにフィールドにアクセスし、callvirt
これを引き起こす可能性があるかを確認できます。
C# が安全に実行できる場合でも null 参照でメソッドを呼び出すことを許可しないという決定により、C#callvirt
の大部分のケースで IL として使用され、唯一の例外は静的メソッドまたはコンパイル時にnull参照にならないように示すことができます。callvirt
(編集:メソッドが実際に仮想である場合でも、コンパイラがa を a に置き換えることができることをコンパイラが確認できる他のいくつかのケースがありcall
ます[コンパイラがどのオーバーロードがヒットするかを知ることができる場合]、後のコンパイラはこれをもう少し行いますただし、callvirt
想像以上に頻繁に使用されます)。
興味深いケースは、最適化により、 with で呼び出されたメソッドcallvirt
がインライン化される可能性があることを意味しますが、コンパイル時に非 null であることが保証されていない場合です。このような場合、「呼び出し」(実際には呼び出しではない) が発生する場所の前にフィールド アクセスを追加してNullReferenceException
、メソッドの途中ではなく開始時に正確にトリガーすることができます。これは、最適化によって観測された動作が変更されないことを意味します。