Jerry Dodge
Jerry Dodge

Reputation: 27266

Can a record be used as a property of an object?

I'd like to make a record as an object's property. The problem is that when I change one of the fields of this record, the object isn't aware of the change.

type
  TMyRecord = record
    SomeField: Integer;
  end;

  TMyObject = class(TObject)
  private
    FSomeRecord: TMyRecord;
    procedure SetSomeRecord(const Value: TMyRecord);
  public
    property SomeRecord: TMyRecord read FSomeRecord write SetSomeRecord;
  end;

And then if I do...

MyObject.SomeRecord.SomeField:= 5;

...will not work.

So how do I make the property setting procedure 'catch' when one of the record's fields is written to? Perhaps some trick in how to declare the record?

More Info

My goal is to avoid having to create a TObject or TPersistent with an OnChange event (such as the TFont or TStringList). I'm more than familiar with using objects for this, but in an attempt to cleanup my code a little, I'm seeing if I can use a Record instead. I just need to make sure my record property setter can be called properly when I set one of the record's fields.

Upvotes: 13

Views: 11061

Answers (6)

linluk
linluk

Reputation: 1670

this is an alternative to @SourceMaid's answer.

You could use a record (not a pointer to a record) inside your object and have a read only property which returns a pointer to your record.

the class:

type
  TMyRecord = record
    I:Integer;
  end;
  PMyRecord = ^TMyRecord;
  TMyObject = class
  private
    FMyRecord:TMyRecord;
  function GetMyRecordPointer: PMyRecord;
  public
    property MyRecord: PMyRecord read GetMyRecordPointer;
  end;

the getter:

function TMyObject.GetMyRecordPointer: PMyRecord;
begin
  result := @FMyRecord;
end;

usage:

o := TMyObject.Create;
o.MyRecord.I := 42;
ShowMessage(o.MyRecord.I.ToString);
o.MyRecord.I := 23;
ShowMessage(o.MyRecord.I.ToString);
o.Free;

you dont need a setter because you get a reference and work with. that mean that you cannot change the entire record by assigning a new one.
but you can manipulate the elements of the record directly without getting the error "Left side cannot be assigned to".

Upvotes: 3

norgepaul
norgepaul

Reputation: 6053

How about using a TObject instead of a Record?

type
  TMyProperties = class(TObject)
    SomeField: Integer;
  end;

  TMyObject = class(TObject)
  private
    FMyProperties: TMyProperties;     
  public
    constructor Create;
    destructor Destroy; override;

    property MyProperties: TMyRecord read FMyProperties;
  end;

implementation

constructor TMyObject.Create;
begin
  FMyProperties := TMyProperties.Create;
end;

destructor TMyObject.Destroy;
begin
  FMyProperties.Free;
end;

You can now read and set the properties of TMyProperties like this:

MyObject.MyProperties.SomeField := 1;
x := MyObject.MyProperties.SomeField;

Using this method, you don't need to individually get/set the values to/from the record. If you need to catch changes in FMyProperties, you can add a 'set' procedure in the property declaration.

Upvotes: 6

John Easley
John Easley

Reputation: 1570

Why not make the setter/getter part of the record?

TMyRecord = record
  fFirstname: string;
  procedure SetFirstName(AValue: String);
property
  Firstname : string read fFirstname write SetFirstName;
end;

TMyClass = class(TObject)
  MyRecord : TMyRecord;
end;

procedure TMyRecord.SetFirstName(AValue: String);
begin
  // do extra checking here
  fFirstname := AValue;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  MyClass: TMyClass;
begin
  MyClass := TMyClass.Create;
  try
    MyClass.MyRecord.Firstname := 'John';
    showmessage(MyClass.MyRecord.Firstname);
  finally
    MyClass.Free;
  end;
end;

Upvotes: 5

Brian Frost
Brian Frost

Reputation: 13454

Ultimately you will want to access the record's fields, yet as you propose, a record is often a suitable abstraction choice within a class. A class can neatly access the properties of a record as follows:

type
  TMyRec = record
    SomeRecInteger: integer;
    SomeRecString: string;
  end;

  TMyClass = class(TObject)
  private
    FMyRec: TMyRec;
    procedure SetSomeString(const AString: string);
  public
    property SomeInteger: integer read FMyRec.SomeRecInteger write FMyRec.SomeRecInteger;
    property SomeString: string read FMyRec.SomeRecString write SetSomeString;
  end;

procedure TMyClass.SetSomeRecString(const AString: string);
begin
  If AString <> SomeString then
  begin
    // do something special if SomeRecString property is set
    FMyRec.SomeRecString := AString;
  end;
end;

Note:

  1. The direct access to the record property SomeRecInteger
  2. The use of SetSomeRecString to implement some special processing on this field only.

Hope this helps.

Upvotes: 12

David Heffernan
David Heffernan

Reputation: 612794

Consider this line:

MyObject.SomeRecord.SomeField := NewValue;

This is in fact a compile error:

[DCC Error]: E2064 Left side cannot be assigned to

Your actual code is probably something like this:

MyRecord := MyObject.SomeRecord;
MyRecord.SomeField := NewValue;

What happens here is that you copy the value of the record type to the local variable MyRecord. You then modify a field of this local copy. That does not modify the record held in MyObject. To do that you need to invoke the property setter.

MyRecord := MyObject.SomeRecord;
MyRecord.SomeField := NewValue;
MyObject.SomeRecord := MyRecord;

Or switch to using a reference type, i.e. a class, rather than a record.

To summarise, the problem with your current code is that SetSomeRecord is not called and instead you are modifying a copy of the record. And this is because a record is a value type as opposed to being a reference type.

Upvotes: 15

SourceMaid
SourceMaid

Reputation: 473

You're passing the record by value, so a copy of the record is stored by the object. From that point on there are effectively two objects; the original one and the copy held by the object. Changing one won't change the other.

You need to pass the record by reference.

type
  TMyRecord = record
    SomeField: Integer;
  end;
  PMyRecord = ^TMyRecord;

  TMyObject = class(TObject)
  private
    FSomeRecord: PMyRecord;
    procedure SetSomeRecord(const Value: PMyRecord);
  public
    property SomeRecord: PMyRecord read FSomeRecord write SetSomeRecord;
  end;

Upvotes: 4

Related Questions