«Le côté gauche ne peut pas être affecté» pour les propriétés de type d'enregistrement dans Delphi
Question
Je suis curieux de savoir pourquoi Delphi traite les propriétés de type d'enregistrement en lecture seule:
TRec = record
A : integer;
B : string;
end;
TForm1 = class(TForm)
private
FRec : TRec;
public
procedure DoSomething(ARec: TRec);
property Rec : TRec read FRec write FRec;
end;
Si j'essaie d'attribuer une valeur à l'un des membres de la propriété Rec, j'obtiendrai "Le côté gauche ne peut pas être attribué à" erreur:
procedure TForm1.DoSomething(ARec: TRec);
begin
Rec.A := ARec.A;
end;
tout en faisant la même chose avec le champ sous-jacent est autorisé:
procedure TForm1.DoSomething(ARec: TRec);
begin
FRec.A := ARec.A;
end;
Existe-t-il une explication à ce comportement?
La solution
Depuis " Rec " est une propriété, le compilateur la traite un peu différemment car il doit d’abord évaluer le paramètre "read" de la propriété décl. Considérez ceci, qui est sémantiquement équivalent à votre exemple:
...
property Rec: TRec read GetRec write FRec;
...
Si vous le regardez comme ça, vous pouvez voir que la première référence à " Rec " (avant le point '.'), doit appeler GetRec, ce qui créera une copie locale temporaire de Rec. Ces temporaires sont conçus "en lecture seule". C'est ce que vous rencontrez.
Une autre chose que vous pouvez faire ici est de découper les champs individuels de l'enregistrement en tant que propriétés de la classe contenante:
...
property RecField: Integer read FRec.A write FRec.A;
...
Cela vous permettra d’affecter directement, par l’intermédiaire de la propriété, le champ de cet enregistrement incorporé à l’instance de la classe.
Autres conseils
Oui, c'est un problème. Mais le problème peut être résolu en utilisant les propriétés d’enregistrement:
type
TRec = record
private
FA : integer;
FB : string;
procedure SetA(const Value: Integer);
procedure SetB(const Value: string);
public
property A: Integer read FA write SetA;
property B: string read FB write SetB;
end;
procedure TRec.SetA(const Value: Integer);
begin
FA := Value;
end;
procedure TRec.SetB(const Value: string);
begin
FB := Value;
end;
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
FRec : TRec;
public
property Rec : TRec read FRec write FRec;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Rec.A := 21;
Rec.B := 'Hi';
end;
Ceci compile et fonctionne sans problème.
Une solution que j'utilise fréquemment consiste à déclarer la propriété en tant que pointeur sur l'enregistrement.
type
PRec = ^TRec;
TRec = record
A : integer;
B : string;
end;
TForm1 = class(TForm)
private
FRec : TRec;
function GetRec: PRec;
procedure SetRec(Value: PRec);
public
property Rec : PRec read GetRec write SetRec;
end;
implementation
function TForm1.GetRec: PRec;
begin
Result := @FRec;
end;
procedure TForm1.SetRec(Value: PRec);
begin
FRec := Value^;
end;
Avec cela, assigner directement Form1.Rec.A: = MyInteger
fonctionnera, mais aussi Form1.Rec: = MyRec
fonctionnera en copiant toutes les valeurs de < code> MyRec dans le champ FRec
comme prévu.
Le seul inconvénient ici est que lorsque vous souhaitez récupérer une copie de l'enregistrement avec lequel travailler, vous devrez utiliser quelque chose comme MyRec: = Form1.Rec ^
Le compilateur vous empêche d'attribuer à un temporaire. L'équivalent en C # est autorisé, mais il n'a pas d'effet; la valeur de retour de la propriété Rec est une copie du champ sous-jacent et l’affectation au champ de la copie est un nop.
Parce que vous avez des fonctions getter et setter implicites et que vous ne pouvez pas modifier le résultat d’une fonction car c’est un paramètre const.
(Remarque: si vous transformez l'enregistrement en objet, le résultat serait en fait un pointeur, équivalent à un paramètre var).
Si vous souhaitez conserver un enregistrement, vous devez utiliser une variable intermédiaire (ou la variable Field) ou une instruction WITH.
Voir les différents comportements dans le code suivant avec les fonctions explicites de lecture et de définition:
type
TRec = record
A: Integer;
B: string;
end;
TForm2 = class(TForm)
private
FRec : TRec;
FRec2: TRec;
procedure SetRec2(const Value: TRec);
function GetRec2: TRec;
public
procedure DoSomething(ARec: TRec);
property Rec: TRec read FRec write FRec;
property Rec2: TRec read GetRec2 write SetRec2;
end;
var
Form2: TForm2;
implementation
{$R *.dfm}
{ TForm2 }
procedure TForm2.DoSomething(ARec: TRec);
var
LocalRec: TRec;
begin
// copy in a local variable
LocalRec := Rec2;
LocalRec.A := Arec.A; // works
// try to modify the Result of a function (a const) => NOT ALLOWED
Rec2.A := Arec.A; // compiler refused!
with Rec do
A := ARec.A; // works with original property and with!
end;
function TForm2.GetRec2: TRec;
begin
Result:=FRec2;
end;
procedure TForm2.SetRec2(const Value: TRec);
begin
FRec2 := Value;
end;
Cela est dû au fait que les propriétés sont réellement respectées en tant que fonction. Les propriétés renvoient ou définissent uniquement une valeur. Ce n'est pas une référence ou un pointeur sur l'enregistrement
alors:
Testing.TestRecord.I := 10; // error
revient à appeler une fonction comme celle-ci:
Testing.getTestRecord().I := 10; //error (i think)
Ce que vous pouvez faire est:
r := Testing.TestRecord; // read
r.I := 10;
Testing.TestRecord := r; //write
C’est un peu brouillon mais inhérent à ce type d’architecture.
Comme d'autres l'ont déjà dit, la propriété read retournera une copie de l'enregistrement. Par conséquent, l'attribution des champs n'agit pas sur la copie appartenant à TForm1.
Une autre option est quelque chose comme:
TRec = record
A : integer;
B : string;
end;
PRec = ^TRec;
TForm1 = class(TForm)
private
FRec : PRec;
public
constructor Create;
destructor Destroy; override;
procedure DoSomething(ARec: TRec);
property Rec : PRec read FRec;
end;
constructor TForm1.Create;
begin
inherited;
FRec := AllocMem(sizeof(TRec));
end;
destructor TForm1.Destroy;
begin
FreeMem(FRec);
inherited;
end;
Delphi va déréférencer le pointeur PRec pour vous, donc ce genre de chose fonctionnera toujours:
Form1.Rec.A := 1234;
Il n’est pas nécessaire d’écrire la propriété, sauf si vous souhaitez échanger le tampon PRec sur lequel pointe FRec. De toute façon, je ne recommanderais pas de faire un tel échange via une propriété.
L’approche la plus simple est:
procedure TForm1.DoSomething(ARec: TRec);
begin
with Rec do
A := ARec.A;
end;