views:

371

answers:

6

Following situation:

type
  TRec = record
    Member : Integer;
  end; 

  TMyClass = class
  private
    FRec : TRec;
  public
    property Rec : TRec read FRec write FRec;
  end;

The following doesn't work (left side cannot be assigned to), which is okay since TRec is a value type:

MyClass.Rec.Member := 0;

In D2007 though the following DOES work:

with MyClass.Rec do
  Member := 0;

Unfortunately, it doesn't work in D2010 (and I assume that it doesn't work in D2009 either). First question: why is that? Has it been changed intentionally? Or is it just a side effect of some other change? Was the D2007 workaround just a "bug"?

Second question: what do you think of the following workaround? Is it safe to use?

with PRec (@MyClass.Rec)^ do
  Member := 0;

I'm talking about existing code here, so the changes that have to be made to make it work should be minimal.

Thanks!

+6  A: 

That

MyClass.Rec.Member := 0;

doesn't compile is by design. The fact that the both "with"-constructs ever compiled was (AFAICT) a mere oversight. So both are not "safe to use".

Two safe solution are:

  1. Assign MyClass.Rec to a temporary record which you manipulate and assign back to MyClass.Rec.
  2. Expose TMyClass.Rec.Member as a property on its own right.
Ulrich Gerhardt
Thanks! That's what I was afraid of... your second solution means a lot of work and basically eliminates the advantages of using a record here. The first solution would work of course. I'll have to think about this.
Smasher
Another question: why does the workaround in my question even work? I would expect the property to return a copy of the record here...
Smasher
The reason the workaround works is because you're hacking the type system. The type system is trying to prevent you from writing to the property, because a future change may mean the property returns a copy (such as the return value of a getter), rather than the underlying field.
Barry Kelly
The *type system* tries to prevent it from compiling, but once that check has been by-passed, the *code generator* does the obvious implementation and simply replaces references to the `Ref` property with the underlying `FRef` field. Your new workaround isn't really supposed to work, either; I'm pretty sure you're not supposed to be able to take a pointer to a property.
Rob Kennedy
+1  A: 

The reason why it can't be directly assigned is here.
As for the WITH, it still works in D2009 and I would have expected it to work also in D2010 (which I can't test right now).
The safer approach is exposing the record property directly as Allen suggesed in the above SO post:

property RecField: Integer read FRec.A write FRec.A;
François
Unfortunately this pollutes the main class. We could just completely drop the record then. AND it means a lot of work to create these properties.
Smasher
There can still be advantages to using records. They are useful in TPersistent.AssignTo. Also, if you have a memory intensive app, tou can used packed records to reduce memory usage (a a performance cost)
Gerry
+2  A: 

In some situtations like this where a record of a class needs 'direct manipulation' I've often resorted to the following:

PMyRec = ^TMyRec;
TMyRec = record
  MyNum : integer
end;

TMyObject = class( TObject )
PRIVATE
  FMyRec : TMyRec;
  function GetMyRec : PMyRec;
PUBLIC
  property MyRec : PMyRec << note the 'P'
    read GetMyRec;
end;

function TMyObject.GetMyRec : PMyRec; << note the 'P'
begin
  Result := @FMyRec;
end;

The benefit of this is that you can leverage the Delphi automatic dereferencing to make readable code access to each record element viz:

MyObject.MyRec.MyNum := 123;

I cant remember, but maybe WITH works with this method - I try not to use it! Brian

Brian Frost
`With` does not work with pointer types.
Smasher
To avoid confusing if MyRec is a pointer or a var (which is not obvious from the name) I would strongly suggest to write MyObject.MyRec^.MyNum if using this...
Remko
Brian, this code is atrociously bad. I'd hate to be working on it after you moved on to another job.
Ken White
Once we get beyond the fact that `with` is being used at all, I have no problem with this code. It has a few nice things: 1. All direct field accesses (MyRec.MyNum) still work throughout the program without any changes. 2. Changes to existing `with` statements is minimal: add a `^` on the end. 3. The compiler will tell you exactly where every `with` statement is because they won't compile *without* adding the `^` on the end. Thus, there's no danger of making the change and then having identifiers in a `with` statement silently start referring to some other variable instead of the record field.
Rob Kennedy
Ken: What's atrocious about it?
Gerry
@Remko - there is no need to worry about "confusion". Delphi automatically de-references record pointers in exactly the same way that object references are automatically de-referenced. Remember that obj: TObject is a reference type (i.e. pointer) but you presumably never get confused by the lack of ^ in those cases. Delphi/Pascal is strongly typed - the declaration of the property identifies it as a pointer. And if it were changed in the future (imagine it becomes a read-only value type, due to usage), you won't then have to go through your code removing all the ^'s.
Deltics
@Ken Apart from my comment about the WITH, in what way might I have offended your delicate sensibilities?
Brian Frost
A: 

Records are values, they aren't meant to be entities.

They even have assignment-by-copy semantics! Which is why you can't change the property value in-place. Because it would violate the value type semantics of FRec and break code that relied on it being immutable or at least a safe copy.

They question here is, why do you need a value (your TRec) to behave like an object/entity?

Wouldn't it be much more appropriate for "TRec" to be a class if that is what you are using it for, anyways?

My point is, when you start using a language feature beyond its intent, you can easily find yourself in a situation where you have to fight your tools every meter on the way.

Robert Giesecke
You're probably right. As I said, I'm talking about existing code here. And when something works (and by that I mean compiles) in D2007 I don't like it to be broken in the next version.
Smasher
Well, you were relying on a bug that was asked to be fixed for quite a long time. Seems like it has finally been fixed. ;-)
Robert Giesecke
@Smasher: Just because something compiles doesn't mean it works. In this case, it was a long-standing bug in the compiler that was finally fixed. The fact that it used to compile was the bug, and the fact that it works was because of that bug.
Ken White
@Ken: I agree although not everyone knows that this is a bug. And since it's quite comfortable to use, there's a lot of this in our legacy code.
Smasher
@Smasher. Then your legacy code would be broken once someone put a getter method on the class. It would compile, but NOT WORK.
Gerry
A: 

Another solution is to use a helper function:

procedure SetValue(i: Integer; const Value: Integer);
begin
  i := Value;
end;
SetValue(MyClass.Rec.Member, 10);

It's still not safe though (see Barry Kelly's comment about Getter/Setter)

/Edit: Below follows the most ugly hack (and probably the most unsafe as well) but it was so funny I had to post it:

type
  TRec = record
    Member : Integer;
    Member2 : Integer;
  end;

  TMyClass = class
  private
    FRec : TRec;
    function GetRecByPointer(Index: Integer): Integer;
    procedure SetRecByPointer(Index: Integer; const Value: Integer);
  public
    property Rec : TRec read FRec write FRec;
    property RecByPointer[Index: Integer] : Integer read GetRecByPointer write SetRecByPointer;
  end;

function TMyClass.GetRecByPointer(Index: Integer): Integer;
begin
  Result := PInteger(Integer(@FRec) + Index * sizeof(PInteger))^;
end;

procedure TMyClass.SetRecByPointer(Index: Integer; const Value: Integer);
begin
  PInteger(Integer(@FRec) + Index * sizeof(PInteger))^ := Value;
end;

It assumes that every member of the record is (P)Integer sized and will crash of AV if not.

  MyClass.RecByPointer[0] := 10;  // Set Member
  MyClass.RecByPointer[1] := 11;  // Set Member2

You could even hardcode the offsets as constants and access directly by offset

const
  Member = 0;
  Member2 = Member + sizeof(Integer);  // use type of previous member

  MyClass.RecByPointer[Member] := 10;

    function TMyClass.GetRecByPointer(Index: Integer): Integer;
    begin
      Result := PInteger(Integer(@FRec) + Index)^;
    end;

    procedure TMyClass.SetRecByPointer(Index: Integer; const Value: Integer);
    begin
      PInteger(Integer(@FRec) + Index)^ := Value;
    end;

MyClass.RecByPointer[Member1] := 20;
Remko
That CERTAINLY does not work. The assignment will just change the local variable and nothing in the record.
Smasher
Ooops, you're right! So the only way is to use:procedure SetValue(const i: PInteger; const Value: Integer);begin i^ := Value;end;which is really the same as your workaround, you could also use a Class Helper: TMyClassHelper = class helper for TMyClass public procedure SetInteger(const i: PInteger; const Value: Integer); end;procedure TMyClassHelper.SetInteger(const i: PInteger; const Value: Integer);begin i^ := Value;end;MyClass.SetInteger(@t.Rec.Member, 10);
Remko
Which is the same as would happen once someone put a Getter on the class!
Gerry
@Gerry: Yes, I already said that
Remko
A: 

The reason it has been changed is that it was a compiler bug. The fact that it compiled didn't guarantee that it would work. It would fail as soon as a Getter was added to the property

unit Unit2;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm2 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    FPoint: TPoint;
    function GetPoint: TPoint;
    procedure SetPoint(const Value: TPoint);
    { Private declarations }
  public
    { Public declarations }
    property Point : TPoint read GetPoint write SetPoint;
  end;

var
  Form2: TForm2;

implementation

{$R *.dfm}

procedure TForm2.Button1Click(Sender: TObject);
begin
  with Point do
  begin
    X := 10;
    showmessage(IntToStr(x)); // 10
  end;

  with Point do
    showmessage(IntToStr(x)); // 0

  showmessage(IntToStr(point.x)); // 0
end;

function TForm2.GetPoint: TPoint;
begin
  Result := FPoint;
end;

procedure TForm2.SetPoint(const Value: TPoint);
begin
  FPoint := Value;
end;

end.

You code would suddenly break, and you'd blame Delphi/Borland for allowing it in the first place.

If you can't directly assign a property, don't use a hack to assign it - it will bite back someday.

Use Brian's suggestion to return a pointer, but drop the With - you can eaisly do Point.X := 10;

Gerry
The use of `with` is then just a matter of taste. No reason to drop it just as a general rule.
Smasher
Actually, Smasher, there are several reasons to drop `with`. It's not just a matter of taste. http://stackoverflow.com/questions/71419/whats-wrong-with-delphis-with
Rob Kennedy