Jump to content
Mike Torrettinni

Why empty dynamic arrays = NIL?

Recommended Posts

I know that empty dynamic array = nil, so you can check if array is empty by:

array = nil or Length(array) = 0

If a TList<T> is nil the Count will not return 0.


Why did they design it like that? What is the purpose of empty array to be also nil (undefined)? Why the same logic doesn't apply to lists?

 

Any explanation is appreciated!

Share this post


Link to post

A TList<T> is a class, so a variable of type TList<T> being nil is not an empty list, but no list instance at all. In contrast to nil, an existing TList<T> instance can be filled. For nil you first have to create with TList<T>.Create to get a list you can work with.

 

In contrast to that an array is not class.

  • Thanks 1

Share this post


Link to post

Probably that was done for perf purpose. I wouldn't rely on this, consider it implementation-specific, just the same as with empty strings or that TDatetime is days+fraction-of-the-day since December 30, 1899, etc.

Share this post


Link to post

To expand on what Uwe said, an array declaration allocates space for the variable and creates it when it's declared, much like an integer or string. Changing it's length sort-of redeclares it.

 

This is in contrast to the TList<T> where the declaration doesn't allocate space for the variable (instantiate it) so you have to actually call its Create constructor. Before it's "created" then, it's nil.

Edited by corneliusdavid
  • Like 1

Share this post


Link to post

And to further clarify my "array declaration" statement, a dynamic array declaration doesn't allocate the space for it until its length is set to at least 1. 

Share this post


Link to post
1 hour ago, Fr0sT.Brutal said:

I wouldn't rely on this, consider it implementation-specific,

Hm, I use it a lot. I prefer array <> nil over Length(array) > 0.

 

 

Share this post


Link to post
1 hour ago, Mike Torrettinni said:

Why did they design it like that?

Because a pointer to empty would be quite stupid. While you can technically build an allocated array with a capacity of zero with some hacking the RTL always cleans up the memory as soon as an arrays length becomes 0.

 

Fun fact - checking against nil for the purpose of "is it empty or not" opposed to comparing Length to 0 is slightly faster:

 

procedure test(const a: TBytes);
begin
  if a <> nil then
    Writeln;
  if Length(a) > 0 then
    Writeln;
end;

CheckArray.dpr.12: begin
0041D57C 53               push ebx
0041D57D 8BD8             mov ebx,eax
CheckArray.dpr.13: if a <> nil then
0041D57F 85DB             test ebx,ebx
0041D581 740F             jz $0041d592
CheckArray.dpr.14: Writeln;
0041D583 A1B4254200       mov eax,[$004225b4]
0041D588 E8CF7DFEFF       call @WriteLn
0041D58D E8B270FEFF       call @_IOTest
CheckArray.dpr.15: if Length(a) > 0 then
0041D592 8BC3             mov eax,ebx
0041D594 85C0             test eax,eax
0041D596 7405             jz $0041d59d
0041D598 83E804           sub eax,$04
0041D59B 8B00             mov eax,[eax]
0041D59D 85C0             test eax,eax
0041D59F 7E0F             jle $0041d5b0
CheckArray.dpr.16: Writeln;
0041D5A1 A1B4254200       mov eax,[$004225b4]
0041D5A6 E8B17DFEFF       call @WriteLn
0041D5AB E89470FEFF       call @_IOTest
CheckArray.dpr.17: end;
0041D5B0 5B               pop ebx
0041D5B1 C3               ret 

 

Edited by Stefan Glienke
  • Like 2
  • Thanks 1

Share this post


Link to post

 

2 hours ago, corneliusdavid said:

And to further clarify my "array declaration" statement, a dynamic array declaration doesn't allocate the space for it until its length is set to at least 1. 

Does any other type behave like this?

My little knowledge explains this to me that array is just declared variable that points nowhere, until Length() > 0. While a TList variable points to a reserved space in memory (4 bytes?), but not initialized, yet. And when initialized it will point to memory space reserved on TList<>.Create.

 

2 hours ago, Stefan Glienke said:

Because a pointer to empty would be quite stupid.

Isn't that the case for string? When it's empty, it's a pointer to empty, no?

 

 

Share this post


Link to post
16 minutes ago, Mike Torrettinni said:

a TList variable points to a reserved space in memory (4 bytes?), but not initialized

No, a TList is similar to the dynamic array with no elements in this case, it's just declared but doesn't point to anything yet until the Create constructor is called, THEN it points to the allocated memory for the TList. Even though TList, at that point, doesn't have any items, it still has other properties like Count which is initialized at Create to 0. The dynamic array isn't a class, in other words, it doesn't have other properties, such as Count, which is why you have to use the Length() function.

Share this post


Link to post
2 hours ago, Mike Torrettinni said:
4 hours ago, Stefan Glienke said:

Because a pointer to empty would be quite stupid.

Isn't that the case for string? When it's empty, it's a pointer to empty, no?

An empty string is equal to nil pointer. Try this:

var s := '';
Writeln(Format('%p', [Pointer(s)]));

One of the differences between arrays and lists is that even empty lists can have pre-allocated capacity. This is often an overlook feature of lists which helps reducing reallocations while populating the list if you know (or can estimate) the count in advance.

  • Thanks 1

Share this post


Link to post
14 hours ago, Stefan Glienke said:

Because a pointer to empty would be quite stupid.

Not sure I agree. It can be useful to distinguish between null and length 0 array. It can be useful to have these as different conceptual things. 

  • Like 1

Share this post


Link to post
1 hour ago, David Heffernan said:

Not sure I agree. It can be useful to distinguish between null and length 0 array. It can be useful to have these as different conceptual things. 

If you need that distinction you can always wrap it up in nullable type. Delphi implementation of strings and dynamic arrays is fine as it is. Adding that distinction into array or string itself would only complicate working with strings and arrays with no benefits in most of the code. 

 

What would be default value of the array or string? nil or empty? If it is nil, welcome to nil nightmare, if it is empty, you need nullable again, or you would just want the ability to nil after it is initialized to empty. This is the fastest road to hell.

  • Like 1

Share this post


Link to post
4 hours ago, Dalija Prasnikar said:

If you need that distinction you can always wrap it up in nullable type

Only there's no language support for that. 

 

5 minutes ago, 0x8000FFFF said:

I'm quite happy that I don't need any string.IsNullOrEmpty

Why does C# code ever have those all over the place? Mine doesn't. Is sounds a bit like poorly designed code. Or am I missing something? 

Share this post


Link to post
1 minute ago, Mike Torrettinni said:

We still need to do:


if List <> nil then
  iterate List ...

while arrays you can just loop and no errors. Right?

Generally you know that the list has been assigned. Usually they are created in the same function above the use, or passed in to the function and known to be assigned, or created in the constructor. So no its quite rare to see code like that. 

Share this post


Link to post
23 minutes ago, David Heffernan said:

Only there's no language support for that. 

But you can have your own implementation.

 

23 minutes ago, David Heffernan said:

Why does C# code ever have those all over the place? Mine doesn't. Is sounds a bit like poorly designed code. Or am I missing something? 

I don't know about C#, but handling strings or arrays in Java is nightmare. You need to check for nil before you can do anything with it. Yes, if you have created it few lines of code before then you don't have, but usually you don't know from where it comes from and you need to check. 

 

If I had a dime for every time I though "this string cannot be nil here, so I don't need to check" and then I had exceptions at runtime, I would be on my tropical island right now, drinking pina colada.

Share this post


Link to post
22 minutes ago, David Heffernan said:

Why does C# code ever have those all over the place? Mine doesn't. Is sounds a bit like poorly designed code. Or am I missing something? 

Legacy code, libraries, defensive programming, input sanitization and normalization, data from unknown sources, ...

Null-coalescing operator used with strings falls into the same category (s ?? "").

Luckily C# has made some improvements in this area by introducing nullable reference types since version 8.0.

Share this post


Link to post
45 minutes ago, Mike Torrettinni said:

We still need to do:


if List <> nil then
  iterate List ...

while arrays you can just loop and no errors. Right?

In theory you could create a method that checks for self <> nil and only then returns Count, zero otherwise, so the code would look simpler:

 

for i:=0 to List.getCount-1 do

 

Even thought the <> nil check is still there.

Edited by dummzeuch
  • Thanks 1

Share this post


Link to post
12 minutes ago, dummzeuch said:

In theory you could create a method that checks for self <> nil and only then returns Count, zero otherwise, so the code would look simpler:

 

for i:=0 to List.getCount-1 do

 

Even thought the <> nil check is still there.

So, I would need interposers for the most common used (in my code): TStringList, TList<integer> and TList<rec>.

That seems a good suggestion.

Share this post


Link to post
2 hours ago, Mike Torrettinni said:

That seems a good suggestion.

Not sure I meant this as a suggestion. It will still fail horribly if the object variable is neither nil nor a valid instance.

Share this post


Link to post
2 hours ago, Mike Torrettinni said:

So, I would need interposers for the most common used (in my code): TStringList, TList<integer> and TList<rec>.

That seems a good suggestion.

No. Really not a good suggestion. Please don't tell me that you have nil checks every time you use an object. 

Share this post


Link to post
14 minutes ago, dummzeuch said:

Not sure I meant this as a suggestion. It will still fail horribly if the object variable is neither nil nor a valid instance.

OK, I see what you mean.

 

This seems like a simple setup:

 

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, System.Classes;

type
  TStringList = class(system.Classes.TStringList)
  public
    function GetCount: integer;
  end;

  function TStringList.GetCount: integer;
  begin
    if not Assigned(Self) then
      Result := 0
    else
      Result := Self.Count;
  end;

procedure TestWhenNil;
var vSL: TStringList;
begin
  vSL := nil;
  Writeln('Items in SL = ' + vSL.GetCount.ToString);
end;

procedure TestWhenHasItems;
var vSL: TStringList;
begin
  vSL := TStringList.Create;
  try
    vSL.Add('test');
    Writeln('Items in SL = ' + vSL.GetCount.ToString);
  finally
    vSL.Free;
  end;
end;

begin
  TestWhenNil;
  TestWhenHasItems;

  Readln;
end.

 

And output is:

 

image.png.a8a735dc3b2a06687cfde3bca970a695.png

 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

×