Z.B.
Z.B.

Reputation: 1205

Record constructor and field initialization mistery

The following code in Delphi 10.2.3:

uses
  System.SysUtils;

type
  TRec = record
  strict private
    FName: String;
    FValue: Integer;
  public
    property Name: String read FName;
    property Value: Integer read FValue;

    constructor Create(const AName: String);
    function WithValue(const AValue: Integer): TRec;
  end;

constructor TRec.Create(const AName: String);
begin
  FName := AName;
end;

function TRec.WithValue(const AValue: Integer): TRec;
begin
  Result := Self;
  Result.FValue := AValue;
end;

procedure Main;
var
  x: TRec;
begin
  x := TRec.Create('First').WithValue(666);
  x := TRec.Create('Second');
  Writeln('In stack: ', x.Value);
end;

var
  x: TRec;
begin
  x := TRec.Create('First').WithValue(666);
  x := TRec.Create('Second');
  Writeln('In global: ', x.Value);

  Main;
  Readln;
end.

Makes following output:

In global: 0
In stack: 666

Is this intended to be like this? When assignment is done to global variable in data segment then "call @CopyRecord" line is generated by the compiler, but when local variable from stack is used then this line is not added by the compiler...

For global:

Project17.dpr.47: x := TRec.Create('First').WithValue(666);
0041D56B 8D45E0           lea eax,[ebp-$20]
0041D56E BA44D64100       mov edx,$0041d644
0041D573 E87CDAFFFF       call TRec.Create
0041D578 8D55E0           lea edx,[ebp-$20]
0041D57B B8C0584200       mov eax,$004258c0
0041D580 8B0D64AF4100     mov ecx,[$0041af64]
0041D586 E865B2FEFF       call @CopyRecord
0041D58B B8C0584200       mov eax,$004258c0
0041D590 8D4DE8           lea ecx,[ebp-$18]
0041D593 BA9A020000       mov edx,$0000029a
0041D598 E877DAFFFF       call TRec.WithValue
0041D59D 8D55E8           lea edx,[ebp-$18]
0041D5A0 B8B8584200       mov eax,$004258b8
0041D5A5 8B0D64AF4100     mov ecx,[$0041af64]
0041D5AB E840B2FEFF       call @CopyRecord
Project17.dpr.48: x := TRec.Create('Second');
0041D5B0 8D45D8           lea eax,[ebp-$28]
0041D5B3 BA5CD64100       mov edx,$0041d65c
0041D5B8 E837DAFFFF       call TRec.Create
0041D5BD 8D55D8           lea edx,[ebp-$28]
0041D5C0 B8B8584200       mov eax,$004258b8
0041D5C5 8B0D64AF4100     mov ecx,[$0041af64]
0041D5CB E820B2FEFF       call @CopyRecord

For local:

Project17.dpr.39: x := TRec.Create('First').WithValue(666);
0041B074 8D45F0           lea eax,[ebp-$10]
0041B077 BAF8B04100       mov edx,$0041b0f8
0041B07C E873FFFFFF       call TRec.Create
0041B081 8D45F0           lea eax,[ebp-$10]
0041B084 8D4DF8           lea ecx,[ebp-$08]
0041B087 BA9A020000       mov edx,$0000029a
0041B08C E883FFFFFF       call TRec.WithValue
Project17.dpr.40: x := TRec.Create('Second');
0041B091 8D45F8           lea eax,[ebp-$08]
0041B094 BA10B14100       mov edx,$0041b110
0041B099 E856FFFFFF       call TRec.Create
Project17.dpr.41: Writeln('In stack: ', x.Value);
0041B09E A1ACF54100       mov eax,[$0041f5ac]
0041B0A3 BA2CB14100       mov edx,$0041b12c
0041B0A8 E87BA8FEFF       call @Write0UString

Should I always use line like this in every constructor of the record?

Self := Default(TRec);

Because if I add this line then the output is intuitive and returns 0 for both cases.

Upvotes: 4

Views: 322

Answers (1)

David Heffernan
David Heffernan

Reputation: 612993

Is this intended to be like this?

Records are value types. When you allocate local variables of such types, they are not default initialized. So, yes, this is as designed.

Default initialization is performed for:

  • Global variables
  • Class instances
  • All variables of managed types

Should I always use line like this in every constructor of the record?

Self := Default(TRec);

Yes, if you wish for the constructor to initialize each field of the record.


Personally, I'm not a big fan of record constructors, in terms of readability. When I see:

foo := TBar.Create(...);

I expect foo to be an instance of a class and therefore I would also expect to see a call to foo.Free when the lifetime of the instance ends.

Myself I tend to use static class methods, always named New, to construct newly minted value type instances.

I'm also not a great fan of your WithValue instance method. I think it feels a little clunky to force the consumer of your class to populate one instance with a name, and then call WithValue on that instance to finish the job off by providing a value. I'd write it like this:

type
  TRec = record
  strict private
    FName: String;
    FValue: Integer;
  public
    property Name: String read FName;
    property Value: Integer read FValue;
  public
    class function New(const Name: String; const Value: Integer): TRec; static;
  end;

class function TRec.New(const Name: String; const Value: Integer): TRec;
begin
  Result.FName := Name;
  Result.FValue := Value;
end;

Upvotes: 6

Related Questions