5

I have shown this to several colleagues and no one has an explanation. I ran into this by pure chance, as I thought I spotted a bug in our code, but was surprised to see that the code actually ran. Here is a simplified version. This has been done with XE-2.

Everyone I have spoke to so far, also expects that a NullReferenceException should be thrown.

 TUnexplainable = class(TObject)
  public
    function Returns19: Integer;
  end;

function TUnexplainable.Returns19: Integer;
begin
  Result := 19;
end;

The following test should NEVER work, but it runs succesfully. Why is no NullReferenceException thrown????

procedure TTestCNCStep.ShouldNeverEverWorkV4;
var
  Impossible: TUnexplainable;
  Int1: Integer;
begin
  Impossible := nil;
  Int1 := Impossible.Returns19; // A Null Reference Exception should ocurr here!!! Instead the method Returns19 is actually invoked!!!!
  Check(Int1 = 19);
end;

Test resuts

santiagoIT
  • 9,411
  • 6
  • 46
  • 57

2 Answers2

11

A non-static class method is compiled into a standalone function with a hidden Self parameter. So, your code is essentially doing the following, from the compiler's perspective:

//function TUnexplainable.Returns19: Integer;
function TUnexplainable_Returns19(Self: TUnexplainable): Integer;
begin
  Result := 19;
end;

//procedure TTestCNCStep.ShouldNeverEverWorkV4;
procedure TTestCNCStep_ShouldNeverEverWorkV4(Self: TTestCNCStep);
var
  Impossible: TUnexplainable;
  Int1: Integer;
begin
  Impossible := nil;
  Int1 := TUnexplainable_Returns19(Impossible);
  Check(Int1 = 19);
end;

As you can see, Returns19() is not referencing Self for anything, so there is no reason for a nil pointer error to occur.

Change your code to do something with Self, and then you will see the error you are expecting:

type
  TUnexplainable = class(TObject)
  public
    Number: Integer;
    function ReturnsNumber: Integer;
  end;

function TUnexplainable.ReturnsNumber: Integer;
begin
  Result := Number;
end;

procedure TTestCNCStep.ShouldNeverEverWorkV4;
var
  Impossible: TUnexplainable;
  Int1: Integer;
begin
  Impossible := nil;
  Int1 := Impossible.ReturnsNumber; // An EAccessViolation exception will now occur here!!!
end;

I have shown this to several colleagues and no one has an explanation.

I would be very worried working with a group of colleagues (assuming they are programmers) who do not understand the basics of how class methods and the Self parameter actually work, and how calling a class method via a nil pointer can avoid a null pointer error. This is like Object-Oriented Programming 101 kind of stuff.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • Thanks I understand now. Coming from c++ and c# I am quite sure this would have lead to an exception. One difficulty I have had with Delphi is that I found no recent books covering the language. So even if you want to learn Delphi more in depth there is barely any new material available. – santiagoIT Apr 06 '15 at 21:52
  • 1
    What I described is how C++ works, too. However, C# is different, as .NET is a managed environment that checks object pointers and method calls for validity. – Remy Lebeau Apr 06 '15 at 22:06
  • 1
    @santiagoIT - the behaviour involved here is not new to Delphi. It is a fundamental part of the behaviour of the runtime code produced by Delphi since version 1.0. e.g. the "Free" method on TObject explicitly relies on the ability to call methods with a NIL object reference. The excellent Delphi in a Nutshell is still highly relevant and useful for this kind of thing. Also worth noting is that this behaviour is limited only to non-virtual methods. Try the exact same thing with **Returns19** declared as virtual and calling that with a NIL object reference **will** result in an exception. – Deltics Apr 09 '15 at 01:06
  • @Deltics: "*this behaviour is limited only to non-virtual methods*" - very true. The act of calling a `virtual` or `dynamic` method requires access to the object's vtable/dispatch table, which can only be reached via a *valid* object pointer. – Remy Lebeau Apr 09 '15 at 01:24
5

Your method never references the Self pointer, so it happens to work.

Unlike C#, Delphi does not validate the Self (this in C#) pointer automatically.