this == null // Как это возможно? - PullRequest
22 голосов
/ 21 апреля 2010

Недавно я столкнулся с каким-то странным поведением моего приложения. Он был разработан в основном на C #, но CLI / C ++ также использовался для достижения лучшей производительности. Я получаю исключение System.NullReferenceException очень простым методом при сравнении TimeSpan:

TimeSpan _timestamp;
void UpdateFrame(TimeSpan timestamp)
{
    if(TimeSpan::Equals(_timestamp, timestamp) == false) 

Было очевидно, что единственная ссылка, используемая в этом выражении, была неявной this (this._timestamp). Я добавил утверждение assert и оказалось, что это на самом деле null. После короткого исследования мне удалось подготовить короткую программу, представляющую это явление. Это C ++ / 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;
}

Кто-нибудь знает, как это возможно? Это ошибка в компиляторе?

Ответы [ 2 ]

19 голосов
/ 21 апреля 2010

В C ++ (и, вероятно, в C ++ / CLI) ничто не мешает вам пытаться вызывать методы с указателем NULL. В большинстве реализаций вызов виртуального метода завершится сбоем в точке вызова, поскольку среда выполнения не сможет прочитать таблицу виртуальных методов. Однако не виртуальный вызов метода - это просто вызов функции с некоторыми параметрами, одним из которых является указатель this. Если он нулевой, то это то, что передается в функцию.

Я считаю, что результатом вызова любой функции-члена по указателю NULL (или nullptr) официально является "неопределенное поведение".

12 голосов
/ 22 апреля 2010

Спасибо, Грэг, за ваш ответ, который происходит так, как вы его описываете. Тем не менее, я не доволен этой ситуацией, потому что это означает, что я должен разместить

if(this == nullptr) throw gcnew ArgumentException("this");

в начале каждого метода. Только это гарантирует, что мой метод не будет отображаться в верхней части трассировки стека как неисправный фрагмент кода без проверки аргументов.

Я никогда не сталкивался (это == ноль), когда я писал на C #. Поэтому я решил выяснить, чем он отличается от C ++ / CLI. Я создал пример приложения в C ++ / 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();
        }
    };
}

И небольшая программа на C #, которая использует классы C ++ / CLI с тем же методом Main:

class Program
{
    static void Main(string[] args)
    {
        A a = null;
        a.SampleMethod();
    }
}

Затем я разобрал их с помощью .NET Reflector от Red Gate:

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 - вызывает метод с поздней привязкой для объекта, помещая возвращаемое значение в стек оценки.

А теперь окончательный вывод:

Компилятор C # в VS 2008 рассматривает каждый метод как виртуальный, поэтому всегда можно предположить, что это (this! = Null). В C ++ / CLI каждый метод вызывается так, как следует, поэтому необходимо обращать внимание на вызовы не виртуальных методов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...