Delphi: Records in Classes - delphi

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!

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:
Assign MyClass.Rec to a temporary record which you manipulate and assign back to MyClass.Rec.
Expose TMyClass.Rec.Member as a property on its own right.

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

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;

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.

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;

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;

Related

how to read/write set enumeration from text file with delphi

I want to use SQL like condition in Delphi as
if VarI in SOMESET then
...
[SOMESET] to read from any text file.
Store few numbers in any text file like ini/txt and read it from file and replace it with set so that we can use in operator with if condition.
If I understand what you are asking correctly, the simple answer is yes. Your terminology may be a bit off - set means something quite specific in Delphi, but I don't think that is what you mean. Also, I don't think you are asking about the specifics of loading and saving so I have not included that. Instead I think you are asking how to allow 'in' to be defined in this specific case, and here is how
unit Unit1;
interface
uses
System.SysUtils;
type
TMySet = record
private
function GetEntry(const i: integer): integer;
public
Entries : array of integer;
// procedure LoadFromFile( const pFromFile : TFileName );
class operator in ( const pTest : integer; pMyset : TMyset ) : boolean;
property Entry[ const i : integer ] : integer
read GetEntry; default;
end;
implementation
{ TMySet }
function TMySet.GetEntry(const i: integer): integer;
begin
Result := Entries[ i ]; // allow default exceptions to occur
end;
class operator TMySet.in(const pTest: integer; pMyset: TMyset): boolean;
var
i: Integer;
begin
for i := Low(pMyset.Entries) to High(pMyset.Entries) do
begin
if i = pMyset[ i ] then
begin
Result := TRUE;
exit;
end;
end;
// else
Result := FALSE;
end;
end.
I hope I have understood your question, and that this helps.

How to change my code to get the correct enumerate name value?

I аm trying to get the enumeration name value using RTTI.
My objective is to get the corresponding enumerate name value in Enum1(Tsex) from the selected enumerate name value in Enum2(iterator) using a string value.
Here is the code that I have implemented. I am using Delphi 7.
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,Dialogs,typinfo;
type
Tsex = (homme,femme);
iterator = (H,F);
TForm1 = class(TForm)
procedure FormShow(Sender: TObject);
private
{ Déclarations privées }
public
{ Déclarations publiques }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.FormShow(Sender: TObject);
var
i : integer;
OT: Tsex;
FT: iterator;
begin
i:=0;
OT := Low(Tsex);
for FT := Low(iterator) to High(iterator) do
if GetEnumName(TypeInfo(iterator), Ord(FT)) = 'F' then
begin
showmessage(GetEnumName(TypeInfo(Tsex), Ord(OT)));
end;
i:=i+1;
OT:=Succ(OT);
end;
When I use H as a string I get homme, but when I use F I also get homme. But it needs to be femme.
Problem:
The problem in your code is that you are missing a begin after for, and this causes increment of i and assignment of OT to happen after the iteration is complete.
What you need to change is:
var
i : integer;
OT: Tsex;
FT: iterator;
begin
i:=0;
OT := Low(Tsex);
for FT := Low(iterator) to High(iterator) do
begin // <- Add begin here
if GetEnumName(TypeInfo(iterator), Ord(FT)) = 'F' then
begin
showmessage(GetEnumName(TypeInfo(Tsex), Ord(OT)));
end;
i:=i+1;
OT:=Succ(OT);
end;
end; // <- Add end; here
Alternative solutions:
As David has pointed out, it is better to use an array to map another set of values to your enum. Like this:
type
TSex = (homme, femme);
const
SexDBValues: array [TSex] of string =
('H', 'F');
type
TForm1 = class(TForm)
procedure FormShow(Sender: TObject);
private
public
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
function GetMyEnumValue(const aDBValue: string): TSex;
var
value: TSex;
begin
for value := Low(TSex) to High(TSex) do
begin
if SameText(SexDBValues[value], aDBValue) then
begin
Result := value;
Exit;
end;
end;
end;
procedure TForm1.FormShow(Sender: TObject);
var
value: TSex;
begin
value := GetMyEnumValue('H');
ShowMessage(GetEnumName(TypeInfo(TSex), Ord(value)));
end;
And when your enum type contains only two values, and is unlikely to have additional values in future, you can just use good old if-else operator:
function GetMyEnumValue(const aDBValue: string): TSex;
begin
if SameText(aDBValue, 'F') then
begin
Result := femme;
end else
begin
Result := homme;
end;
end;
In few words, avoid overengineering problems.
Note: We are using string to store the character value and SameText to compare it, as it compares text case-insensitively. Plus, it allows you to compare text of multiple characters, if in future you change your mind on how values are stored in DB.
Advice:
I would also recommend you to consult with Delphi Coding Style Guide.
It might seem unrelated to problem, but following good practice on indentation helps to avoid such problems.
Guidelines on naming types and variables are also important. They will similarly save you in other situations.

How to tell old-school `object` and `record` apart?

program Project15;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.Rtti, System.TypInfo;
type
TRecord = record
public
AField: integer;
constructor Init(test: integer);
end;
TOldObject = object
public
AField: integer;
constructor Init(test: integer);
procedure Fancy; virtual; <<--- compiles
class operator Implicit(test: TRecord): TOldObject; <<-- does not compile.
end;
procedure IsObjectARecord;
var
ARecord: TRecord;
AObject: TOldObject;
v: TValue;
s: String;
begin
v:= TValue.From(ARecord);
case v.Kind of
tkRecord: WriteLn('it''s a Record');
end;
ARecord:= TRecord.Init(10);
AObject.Init(10);
v:= TValue.From(AObject);
case v.Kind of
tkRecord: begin
WriteLn('object is a record?');
if v.IsObject then s:= 'true'
else s:= 'false';
WriteLn('isObject = ' + s);
WriteLn('ToString says: '+v.ToString);
end;
end;
end;
{ TOldSkool }
constructor TOldObject.Init(test: integer);
begin
AField:= 10;
end;
constructor TRecord.Init(test: integer);
begin
AField:= 10;
end;
begin
IsObjectARecord;
Readln;
end.
The outcome of the test proc reads:
ARecord is a Record
AObject is a record?
isObject(AObject) = false
AObject.ToString says: (record)
However object <> record from a functionality point of view.
Object supports inheritance and virtual calls.
Record supports class operators.
Is there a way to tell TP5.5-objects and records apart using RTTI?
Is there even a need to tell them apart -ever-?
Note that I'm not planning to use object, I'm just enumerating types using RTTI so that my generic HashTable with pointers can clean up after itself properly.
Yes I know that object lives on the stack by default (or the heap with special effort) and do not normally need to be freed.
Bonus points if someone knows why virtual calls with TP5.5-objects no longer work, they used to work in Delphi 2007
To the very best of my knowledge, in the eyes of Delphi's RTTI framework, an old-style object cannot be distinguished from a record. This program
{$APPTYPE CONSOLE}
uses
System.Rtti;
type
TOldObject = object
end;
var
ctx: TRttiContext;
RttiType: TRttiType;
begin
RttiType := ctx.GetType(TypeInfo(TOldObject));
Writeln(TValue.From(RttiType.TypeKind).ToString);
Writeln(RttiType.IsRecord);
Readln;
end.
outputs
tkRecord
TRUE
Old object is deprecated.
So you should not use it in conjunction with the new rtti.
First step of deprecation was to disallow virtual methods. Due I suppose to compiler regressions.
This is the Embarcadero decision to mimic C# and his struct / class paradigm. Wrong decision imho.

Delphi: Types other than Integer for indexing TStringList items

Arrays can be indexed using user-defined enumerated types. For example:
type
TIndexValue = (ZERO = 0, ONE, TWO, THREE, FOUR);
var
MyArray: array[Low(TIndexValue) .. High(TIndexValue)] of String;
Elements from this array can then be referenced using TIndexValue values as an index:
MyArray[ZERO] := 'abc';
I am trying to obtain this same general functionality with a TStringList.
One simple solution is to cast every index value to an Integer type at the time of reference:
MyStringList[Integer(ZERO)] := 'abc';
Another solution (to hide all the casting) is to create a subclass of TStringList and defer all the casting to this subclass's subroutines that access the inherited Strings property:
type
TIndexValue = (ZERO = 0, ONE, TWO, THREE, FOUR);
type
TEIStringList = class(TStringList)
private
function GetString(ItemIndex: TIndexValue): String;
procedure SetString(ItemIndex: TIndexValue; ItemValue: String);
public
property Strings[ItemIndex: TIndexValue]: String
read GetString write SetString; default;
end;
function TEIStringList.GetString(ItemIndex: TIndexValue): String;
begin
Result := inherited Strings[Integer(ItemIndex)];
end;
procedure TEIStringList.SetString(ItemIndex: TIndexValue; ItemValue: String);
begin
inherited Strings[Integer(ItemIndex)] := ItemValue;
end;
This works fine for a single implementation that uses the enumerated type TIndexValue.
However, I would like to re-use this same logic or subclass for several different TStringList objects that are indexed by different enumerated types, without having to define TStringList subclasses for each possible enumerated type.
Is something like this possible? I suspect I may have to depend on Delphi's Generics, but I would be very interested to learn that there are simpler ways to achieve this.
I think that generics would be by far the most elegant solution. Using them would be as simple as rewriting your class above as:
TEIStringList<T> = class(TStringList)
and then replacing all TIndexValue references with T. Then you could create it just as any other generic:
var
SL: TEIStringList<TIndexValue>;
begin
SL:=TEIStringList<TIndexValue>.Create;
(...)
ShowMessage(SL[ZERO])
(...)
end;
If you insist on avoiding generics, maybe operator overloading would be of use. Something like the following should work:
type
TIndexValueHolder = record
Value : TIndexValue;
class operator Implicit(A: TMyRecord): integer;
end;
(...)
class operator TIndexValueHolder.Implicit(A: TMyRecord): integer;
begin
Result:=Integer(A);
end;
Then use with:
var
Inx : TIndexValueHolder;
begin
Inx.Value:=ZERO;
ShowMessage(SL[Inx]);
end
UPDATE:
You could adapt TIndexValueHolder for use in a for or while loop by adding Next, HasNext, etc. methods. This might end defeating the purpose, though. I'm still not sure what the purpose is, or why this would be useful, but here's some ideas for how to do it, anyways.
You probably can use a class helper and declare the default property index as Variant:
type
TEnum1 = (Zero = 0, One, Two, Three, Four);
TEnum2 = (Nul = 0, Een, Twee, Drie, Vier);
TEnum3 = (Gds = 0, Psajs, Oeroifd, Vsops, Wowid);
TStringListHelper = class helper for TStringList
private
function GetString(Index: Variant): String;
procedure SetString(Index: Variant; const Value: String);
public
property Strings[Index: Variant]: String read GetString write SetString;
default;
end;
function TStringListHelper.GetString(Index: Variant): String;
begin
Result := inherited Strings[Index];
end;
procedure TStringListHelper.SetString(Index: Variant; const Value: String);
begin
inherited Strings[Index] := Value;
end;
Testing code:
procedure TForm1.Button1Click(Sender: TObject);
var
Strings: TStringList;
begin
Strings := TStringList.Create;
try
Strings.Add('Line 1');
Strings.Add('Second line');
Strings[Zero] := 'First line';
Memo1.Lines.Assign(Strings);
Caption := Strings[Psajs];
finally
Strings.Free;
end;
end;
See edit history for a previous less successful attempt.

Pass record parameter without declaring it first as a variable

If I am trying to call a procedure which has a record type (not object) as a parameter, is it possible to somehow pass details of that parameter "inline" without having to declare a variable of that type first?
eg assume I have this simple record type:
type TMyRecord = record
AString: string;
AnInt: Integer;
end;
and this procedure declaration:
procedure MyProcedure(Rec: TMyRecord);
If I want to call MyProcedure do I have to declare a variable of type TMyRecord or can I do something like:
MyProcedure(TMyRecord("Test", 10));
That doesn't work (XE2) (get a compiler error about it expecting a ")").
So, can I do something like that? Or not possible.
Thanks
It is possible using the advanced record structure.
For more information about advanced records, see the Records (advanced) section in Delphi help.
This is a small prototype to see how it works in your case to preinitialize a record in a function/procedure call :
Type
TRecord = record
AString : String;
AnInt : Integer;
Constructor Create( Const s : String; i : Integer);
end;
constructor TRecord.Create(const s: String; i: Integer);
begin
AString := s;
AnInt := i;
end;
procedure DoSomething( theRec : TRecord);
begin
WriteLn(theRec.AString, ' ',theRec.AnInt);
end;
begin
DoSomeThing( TRecord.Create('S',1));
ReadLn;
end.
Looking at the Delphi RTL, see the definitions of the record types TPoint and TRect in unit system.types (XE2).
They define some overloaded Create constructors, which are used in lots of places to preinitialize the record structures in function/procedure calls.
The question you are asking relates to code readability and there is a solution that avoids having to create a variable. The VCL uses this solution with the records TPoint and TRect.
Consider the definition of TPoint:
type
TPoint = record
X,Y integer
end;
To pass a TPoint to a procedure you might do:
var
MyPoint : TPoint;
begin
MyPoint.X := 5;
MyPoint.Y := 7;
DoSomething( MyPoint );
end;
This is fine but takes 3 lines when one is also possible using the factory function Point:
begin
DoSomething( Point(5,7) );
end;
In Delphi, a function has been declared as follows:
function Point( X, Y : integer ) : TPoint;
begin
Result.X := X;
Result.Y := Y;
end;
You can then call this function 'inline' to create the record 'on the fly' to to quickly
You will see the same has been provided for TRect etc. I often put such a factory function together with the record declaration as follows, even if I don't plan to use them yet:
type
TMyRecord = record
A : integer;
B : string;
end;
function MyRecord( A : integer; const B : string ) : TMyRecord;
begin
Result.A := A;
Result.B := B;
end;
Use of this technique can improved the readability of code and also ensures that you don't accidently omit setting a record element.
Just having fun with John Easley's idea:
type TRec = record
X: string;
Y: Integer;
end;
procedure TestRec(const Rec: array of const);
var
R: TRec;
begin
R.X:= string(Rec[0].VUnicodeString);
R.Y:= Rec[1].VInteger;
ShowMessage(R.X + IntToStr(R.Y));
end;
procedure TForm1.Button7Click(Sender: TObject);
begin
TestRec(['Test', 22]);
end;
It is possible to pass record fields as array of const parameters and assign these parameters to local record variable.
It would be nice! But, no.
If passing things inline is really your objective, then perhaps Open Array Parameters would suit you.
Procedure MyProcedure(const Vars: Array of Variant);
begin
ShowMessage(VarToStr(Vars[0])+' '+VarToStr(Vars[1]));
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
MyProcedure(['Test', 12]);
end;
You could also pass an Array of Const, which is basically an array of TVarRec which is a variant record that also includes type information as VType. This is fun stuff..
An excellent article can be found on Rudy's Delphi Corner here:
Rudy's Delphi Corner, Open Array Parameters

Resources