Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Are Delphi record constructors really needed?

SITUATION

I am studying "More Coding in Delphi" by Nick Hodges, and he is using a TFraction record to explain operator overloading. I have written by myself this record:

type
  TFraction = record
  strict private
    aNumerator: integer;
    aDenominator: integer;
    function GCD(a, b: integer): integer;
  public
    constructor Create(aNumerator: integer; aDenominator: integer);
    procedure Reduce;
    class operator Add(fraction1, fraction2: TFraction): TFraction;
    class operator Subtract(fraction1, fraction2: TFraction): TFraction;
    //... implicit, explicit, multiply...
    property Numerator: integer read aNumerator;
    property Denominator: integer read aDenominator;
  end;

Of course, I had to create a constructor because in Q (rationals) I must have a denominator that is not equal to zero.

constructor TFraction.Create(aNumerator, aDenominator: integer);
begin
  if (aDenominator = 0) then
  begin
    raise Exception.Create('Denominator cannot be zero in rationals!');
  end;

  if ( (aNumerator < 0) or (aDenominator < 0) ) then
  begin
    Self.aNumerator := -aNumerator;
    Self.aDenominator := -aDenominator;
  end
  else
  begin
    Self.aNumerator := aNumerator;
    Self.aDenominator := aDenominator;
  end;
end;

PROBLEM

Since the operator overloads return a TFraction, I am going to define an operation like this:

class operator TFraction.Add(fraction1, fraction2: TFraction): TFraction;
var
  tmp: TFraction;
begin
  //simple algorithm of the sum
  tmp := TFraction.Create(fraction1.Numerator*fraction2.Denominator+fraction1.Denominator*fraction2.Numerator, fraction1.Denominator*fraction2.Denominator);
  tmp.Reduce;

  //return the result
  Result := tmp;
end;

As you can see here, I am creating a tmp that is returned from the function.

When I read Marco Cantu's book, he used another approach:

class operator TFraction.Add(fraction1, fraction2: TFraction): TFraction;
begin
  Result.aNumerator := (fraction1.Numerator*fraction2.Denominator+fraction1.Denominator*fraction2.Numerator);
  Result.aDenominator := fraction1.Denominator*fraction2.Denominator;
end;

I have made some tests, and I see that both give me the correct result, but there is something that I cannot understand. In the first approach, I am declaring tmp and then I call the constructor so I can return a TFraction. In the second approach, I am instead not creating anything because records have an automatic constructor. The documentation, in fact, says that:

Records are constructed automatically, using a default no-argument constructor, but classes must be explicitly constructed. Because records have a default no-argument constructor, any user-defined record constructor must have one or more parameters.

Here I have a user-defined record constructor. So:

  1. Is the constructor call on tmp of the first approach not needed? If I want to call Reduce (which is a procedure), I need to create a variable. Is the Result just returning a copy of tmp without creating anything?

  2. In the second approach, are Result.aNumerator and Result.aDenominator the parameters of the automatic created constructor?

like image 774
Alberto Miola Avatar asked Feb 08 '17 17:02

Alberto Miola


1 Answers

A record constructor isn't anything magical. It's just an instance method like any other. You write:

tmp := TFraction.Create(...);

But you may equally well write it like this:

tmp.Create(...);

I personally find neither to be especially useful because I am used to constructor calling semantics for classes which allocate and default initialise memory, and then call the constructor method.

And especially the second variant grates with me because that looks like the classic mistake that novice Delphi programmers make when starting out and trying to create an instance of a class. That code would be no good if TFraction were a class, but for a record it is fine.

Were it me I would get rid of the record constructor and instead use a static class function that returned a newly minted instance of your record type. My convention is to name such things New. But these are matters of personal preference.

If you did that it would be declared like this:

class function New(aNumerator, aDenominator: Integer): TFraction; static;

It would be implemented like this:

class function TFraction.New(aNumerator, aDenominator: Integer): TFraction;
begin
  Result.aNumerator := ...;
  Result.aDenominator := ...;
end;

You would then call it like this:

frac := TFraction.New(num, denom);

But as I said, that's a matter of preference. If you like record constructors, feel free to stick with them.


You ask whether or not you can skip the constructor. In terms of allocation of the record, yes you can skip it. In terms of running the code in the constructor, only you can determine that. Do you want that code to execute or not?

If you wish that code to be executed, but don't want to use a temporary variable, then you can write the code like this:

class operator TFraction.Add(fraction1, fraction2: TFraction): TFraction;
begin
  Result.Create(
    fraction1.Numerator*fraction2.Denominator + fraction1.Denominator*fraction2.Numerator,
    fraction1.Denominator*fraction2.Denominator
  );
  Result.Reduce;
end;

Or if you preferred a static class function it would be:

class operator TFraction.Add(fraction1, fraction2: TFraction): TFraction;
begin
  Result := TFraction.New(
    fraction1.Numerator*fraction2.Denominator + fraction1.Denominator*fraction2.Numerator,
    fraction1.Denominator*fraction2.Denominator
  );
  Result.Reduce;
end;
like image 77
David Heffernan Avatar answered Nov 16 '22 04:11

David Heffernan