This == null//Как это возможно?
Недавно я столкнулся с каким-то странным поведением моего приложения. Он был разработан в основном на С#, но CLI/С++ также использовался для достижения лучшей производительности. Я получал исключение System.NullReferenceException в очень простом методе при сравнении TimeSpan:
TimeSpan _timestamp;
void UpdateFrame(TimeSpan timestamp)
{
if(TimeSpan::Equals(_timestamp, timestamp) == false)
Было очевидно, что единственная ссылка, используемая в этом выражении, была неявной (this._timestamp). Я добавил утверждение assert, и оказалось, что это фактически null. После короткого расследования мне удалось подготовить короткую программу, представляющую это явление. Это С++/CLI.
using namespace System;
using namespace System::Reflection;
public class Unmanaged
{
public:
int value;
};
public ref class Managed
{
public:
int value;
Unmanaged* GetUnmanaged()
{
SampleMethod();
return new Unmanaged();
}
void SampleMethod()
{
System::Diagnostics::Debug::Assert(this != nullptr);
this->value = 0;
}
};
public ref class ManagedAccessor
{
public:
property Managed^ m;
};
int main(array<System::String ^> ^args)
{
ManagedAccessor^ ma = gcnew ManagedAccessor();
// Confirm that ma->m == null
System::Diagnostics::Debug::Assert(ma->m == nullptr);
// Invoke method on the null reference
delete ma->m->GetUnmanaged();
return 0;
}
Кто-нибудь знает, как это возможно? Это ошибка в компиляторе?
Ответы
Ответ 1
В С++ (и, предположительно, в С++/CLI) ничего не мешает вам пытаться вызвать методы на указателе NULL. В большинстве реализаций вызов виртуального метода разбивается в точке вызова, потому что среда выполнения не сможет прочитать таблицу виртуальных методов. Однако вызов не виртуального метода - это просто вызов функции с некоторыми параметрами, одним из которых является указатель this
. Если оно равно null, то это то, что передается функции.
Я считаю, что результат вызова любой функции-члена в указателе NULL
(или nullptr
) официально "поведение undefined".
Ответ 2
Спасибо, Грег за ваш ответ, это происходит так, как вы его описываете. Однако я не доволен этой ситуацией, потому что это означает, что я должен разместить
if(this == nullptr) throw gcnew ArgumentException("this");
в начале каждого метода. Только это гарантирует, что мой метод не будет отображаться в верхней части стека-трассировки как неисправный фрагмент кода без проверки аргумента.
Я никогда не сталкивался (это == null), когда писал на С#. Поэтому я решил узнать, как он отличается от С++/CLI. Я создал образец приложения в С++/CLI:
namespace ThisEqualsNull{
public ref class A
{
public:
void SampleMethod()
{
System::Diagnostics::Debug::Assert(this != nullptr);
}
};
public ref class Program{
public:
static void Main(array<System::String ^> ^args)
{
A^ a = nullptr;
a->SampleMethod();
}
};
}
И небольшая программа на С#, которая использует классы С++/CLI с тем же основным методом:
class Program
{
static void Main(string[] args)
{
A a = null;
a.SampleMethod();
}
}
Затем я разобрал их с Red Gate.NET Reflector:
C++/CLI
.method public hidebysig static void Main(string[] args) cil managed
{
.maxstack 1
.locals ( [0] class ThisEqualsNull.A a)
L_0000: ldnull
L_0001: stloc.0
L_0002: ldnull
L_0003: stloc.0
L_0004: ldloc.0
L_0005: call instance void ThisEqualsNull.A::SampleMethod()
L_000a: ret
}
C#
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init ( [0] class [ThisEqualsNull]ThisEqualsNull.A a)
L_0000: nop
L_0001: ldnull
L_0002: stloc.0
L_0003: ldloc.0
L_0004: callvirt instance void [ThisEqualsNull]ThisEqualsNull.A::SampleMethod()
L_0009: nop
L_000a: ret
}
Важными частями являются:
C++/CLI
L_0005: call instance void ThisEqualsNull.A::SampleMethod()
C#
L_0004: callvirt instance void [ThisEqualsNull]ThisEqualsNull.A::SampleMethod()
Где:
- call - вызов метода, указанного дескриптором переданного метода.
- callvirt - вызов метода поздней привязки объекта, нажатие возвращаемого значения на стек оценки.
И теперь окончательный вывод:
Компилятор С# в VS 2008 рассматривает каждый метод, как если бы он был виртуальным, поэтому всегда можно предположить, что (this!= null). В С++/CLI каждый метод вызывается так, как следует, поэтому необходимо обратить внимание на вызовы не виртуального метода.