is it possible to have different kind of results when declaring property in delphi class?
Example:
property month: string read monthGet(string) write monthSet(integer);
In the example, I want, with the property month, that when I :
READ, I get a string; SET, I set an integer;
The closest you can get is to use Operator Overloading but the Getter/Setter must be the same type. There is no way to change that.
program so_26672343;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils;
type
TMonth = record
private
FValue: Integer;
procedure SetValue( const Value: Integer );
public
class operator implicit( a: TMonth ): string;
class operator implicit( a: Integer ): TMonth;
property Value: Integer read FValue write SetValue;
end;
TFoo = class
private
FMonth: TMonth;
public
property Month: TMonth read FMonth write FMonth;
end;
{ TMonth }
class operator TMonth.implicit( a: TMonth ): string;
begin
Result := 'Month ' + IntToStr( a.Value );
end;
class operator TMonth.implicit( a: Integer ): TMonth;
begin
Result.FValue := a;
end;
procedure TMonth.SetValue( const Value: Integer );
begin
FValue := Value;
end;
procedure Main;
var
LFoo: TFoo;
LMonthInt: Integer;
LMonthStr: string;
begin
LFoo := TFoo.Create;
try
LMonthInt := 4;
LFoo.Month := LMonthInt;
LMonthStr := LFoo.Month;
finally
LFoo.Free;
end;
end;
begin
try
Main;
except
on E: Exception do
Writeln( E.ClassName, ': ', E.Message );
end;
end.
That is not possible. But properties do not have to correspond to internal storage directly, so you can do:
private
FMonth: Integer;
function GetMonthName: string;
...
property Month: Integer read FMonth write FMonth;
property MonthName: string read GetMonthName;
...
procedure TMyClass.GetMonthName: string;
begin
// code that finds name that corresponds to value of FMonth and returns it in Result.
end;
In other words, you'll have to use two properties, one write-only (or normal), one read-only.
You can't directly do that in Delphi.
What you can do is having a "casting property" like:
private
//...
intMonth: integer
//...
public
//...
property StrMonth: string read GetStrMonth write SetStrMonth;
property IntMonth: integer read intMonth write intMonth;
//...
end;
function YourClass.GetStrMonth: string;
begin
case intMonth of
1: Result := "January";
//...
end;
end;
procedure YourClass.SetStrMonth(Value: string);
begin
if StrMonth = "January" then
intMonth := 1;
//...
end;
end;
There's no way to do that for a property. A property has a single type.
The obvious way to achieve you goal is to have getter and setter functions that you use directly.
function GetMonth: string;
procedure SetMonth(Value: Integer);
You might decide to make the type part of the name to reduce confusion in the calling code. Say GetMonthStr and SetMonthOrd.
You could expose these functions as two separate properties. One read only, the other write only.
Related
I am trying to write a spec utility library.
One of the Specification is a TExpressionSpecification. Basically, it implements the Specification pattern by evaluating an inner TExpression.
One of the TExpression is a TPropertyExpression. It's simply an expression that gets the value of a property by its name with Rtti.
I implemented it the simplest way I could, but really cannot understand why it throws an AV at me.
I stepped throrouly with the debugger. All types are what they are supposed to be. I just dont know why the TRttiProperty.GetValue is wrecking havoc.
Can anybody help?
unit Spec;
interface
uses
Classes;
type
TPropertyExpression<TObjectType, TResultType> = class
private
FPropertyName: string;
public
constructor Create(aPropertyName: string); reintroduce;
function Evaluate(aObject: TObjectType): TResultType;
property PropertyName: string read FPropertyName write FPropertyName;
end;
procedure TestIt;
implementation
uses
Rtti;
constructor TPropertyExpression<TObjectType, TResultType>.Create(aPropertyName:
string);
begin
inherited Create;
PropertyName := aPropertyName;
end;
function TPropertyExpression<TObjectType, TResultType>.Evaluate(aObject:
TObjectType): TResultType;
var
aCtx : TRttiContext;
aModelType : TRttiType;
aResultType : TRttiType;
aProperty : TRttiProperty;
aValue : TValue;
begin
aCtx := TRttiContext.Create;
aModelType := aCtx.GetType(System.TypeInfo(TObjectType));
aResultType := aCtx.GetType(System.TypeInfo(TResultType));
aProperty := aModelType.GetProperty(PropertyName);
aValue := aProperty.GetValue(Addr(aObject));
Result := aValue.AsType<TResultType>;
end;
procedure TestIt;
var
aComponent : TComponent;
aSpec : TPropertyExpression<TComponent, string>;
begin
aComponent := TComponent.Create(nil);
aComponent.Name := 'ABC';
aSpec := TPropertyExpression<TComponent, string>.Create('Name');
WriteLn(aSpec.Evaluate(aComponent));
Readln;
end;
end.
GetValue expects the instance pointer (aObject) but you are passing it the address of the pointer variable (#aObject).
Constrain your TObjectType to a class type:
type
TPropertyExpression<TObjectType: class; TResultType> = class...
Then, instead of Addr(aObject), pass the instance directly:
aValue := aProperty.GetValue(Pointer(aObject));
Is there a trick to pass records with different type as parameter in a procedure? For example, look at this pseudo-code:
type
TPerson = record
Species: string;
CountLegs: Integer;
end;
TSpider = record
Species: string;
CountLegs: Integer;
Color: TColor;
end;
var
APerson: TPerson;
ASpider: TSpider;
// Is there a trick to pass different record types as parameter in a procedure?:
procedure DoSomethingWithARecord(const ARecord: TAbstractRecord?);
begin
if ARecord is TPerson then
DoSomethingWithThisPerson(ARecord as TPerson)
else if ARecord is TSpider then
DoSomethingWithThisSpider(ARecord as TSpider);
end;
procedure DefineRecords;
begin
APerson.Species := 'Human';
APerson.CountLegs := 2;
ASpider.Species := 'Insect';
ASpider.CountLegs := 8;
ASpider.Color := clBtnFace;
DoSomethingWithARecord(APerson);
DoSomethingWithARecord(ASpider);
end;
Record instances don't contain type information in the same way that classes do. So you would need to pass an extra argument to indicate which type you were working with. For instance:
type
TRecordType = (rtPerson, rtSpider);
procedure DoSomething(RecordType: TRecordType; const ARecord);
begin
case RecordType of
rtPerson:
DoSomethingWithThisPerson(TPerson(ARecord));
rtSpider:
DoSomethingWithThisSpider(TSpider(ARecord));
end;
end;
You might contemplate putting the type code in the first field of each record:
type
TPerson = record
RecordType: TRecordType;
Species: string;
CountLegs: Integer;
end;
TSpider = record
RecordType: TRecordType;
Species: string;
CountLegs: Integer;
Color: TColor;
end;
function GetRecordType(ARecord): TRecordType;
begin
Result := TRecordType(ARecord);
end;
....
procedure DoSomething(const ARecord);
begin
case GetRecordType(ARecord) of
rtPerson:
DoSomethingWithThisPerson(TPerson(ARecord));
rtSpider:
DoSomethingWithThisSpider(TSpider(ARecord));
end;
end;
You could use generics:
type
TMyRecordDispatcher = record
class procedure DoSomething<T: record>(const Value: T); static;
end;
class procedure TMyRecordDispatcher.DoSomething<T>(const Value: T);
begin
if TypeInfo(T) = TypeInfo(TPerson) then
DoSomethingWithThisPerson(PPerson(#Value)^)
else if TypeInfo(T) = TypeInfo(TSpider) then
DoSomethingWithThisSpider(PSpider(#Value)^);
end;
And call the functions like this:
TMyRecordDispatcher.DoSomething(APerson);
TMyRecordDispatcher.DoSomething(ASpider);
This uses generic type inference and so allows you not to explicitly state the type. Although as an example of generics it makes me cringe. Please don't do this.
In my view all of this is messy and brittle. Much of the above reimplements run time method dispatch, polymorphism. Classes are more suited to this. I don't endorse any of the code above.
On the other hand, perhaps this is all needless. What's wrong with:
DoSomethingWithThisPerson(Person);
DoSomethingWithThisSpider(Spider);
Since you know the types at compile time, why opt for anything more complex?
You could use function overloading to make it possible to omit the type from the function name.
procedure DoSomething(const APerson: TPerson); overload;
begin
....
end;
procedure DoSomething(const ASpider: TSpider); overload;
begin
....
end;
....
DoSomething(Person);
DoSomething(Spider);
I'm making a Delphi VCL application. There is a class TStudent where I have two static functions: one which returns last name from an array of TStudent and another one which returns the first name of the student. Their code is something like:
class function TStudent.FirstNameOf(aLastName: string): string;
var i : integer;
begin
for i := 0 to Length(studentsArray) - 1 do begin
if studentsArray[i].LastName = aLastName then
begin
result := studentsArray[i].FirstName;
Exit;
end;
end;
result := 'no match was found';
end;
class function TStudent.LastNameOf(aFirstName: string): string;
var i : integer;
begin
for i := 0 to Length(studentsArray) - 1 do begin
if studentsArray[i].FirstName = aFirstName then
begin
result := studentsArray[i].LastName;
Exit;
end;
end;
result := 'no match was found';
end;
My question is how can I avoid writing almost same code twice. Is there any way to pass the property as parameter of the functions.
You can use an anonymous method with variable capture for this linear search. This approach gives you complete generality with your predicate. You can test for equality of any field, of any type. You can test for more complex predicates for instance an either or check.
The code might look like this:
class function TStudent.LinearSearch(const IsMatch: TPredicate<TStudent>;
out Index: Integer): Boolean;
var
i: Integer;
begin
for i := low(studentsArray) to high(studentsArray) do
begin
if IsMatch(studentsArray[i]) then
begin
Index := i;
Result := True;
exit;
end;
end;
Index := -1;
Result := False;
end;
Now all you need to do is provide a suitable predicate. The definition of TPredicate<T>, from the System.SysUtils unit, is:
type
TPredicate<T> = reference to function (Arg1: T): Boolean;
So you would code your method like this:
class function TStudent.GetFirstName(const LastName: string): string;
var
Index: Integer;
IsMatch: TPredicate<TStudent>;
begin
IsMatch :=
function(Student: TStudent): Boolean
begin
Result := Student.LastName=LastName;
end;
if not LinearSearch(IsMatch, Index) then
begin
raise ...
end;
Result := studentsArray[Index].FirstName;
end;
And likewise for GetLastName.
If your Delphi does not support anonymous methods then you won't be able to use variable capture and will have to find a more convoluted approach using of object method types. However, the basic idea will be much the same.
I haven't tested it, but I believe this could be one solution.
uses TypInfo;
class function TStudent.GetProperty( propertyName: string, searchValue : Variant ) : Variant ;
var i : integer;
begin
for i := 0 to Length(studentsArray) - 1 do begin
if GetPropValue( studentsArray[i], propertyName ) = searchValue
result := GetPropValue( studentsArray[i], propertyName );
end;
// your code in case of not finding anything
end;
If you are using Delphi 2010 or later, you could use Extended RTTI:
uses
..., Rtti;
type
TStudent = class
public
FirstName: String;
LastName: String;
class function GetNameOf(const aFieldToFind, aNameToFind, aFieldToReturn: string): string;
end;
class function TStudent.GetNameOf(const aFieldToFind, aNameToFind, aFieldToReturn: string): string;
var
i : integer;
ctx: TRttiContent;
StudentType: TRttiType;
Field: TRttiField;
Value: TValue;
begin
ctx := TRttiContext.Create;
StudentType := ctx.GetType(TStudent);
Field := StudentType.GetField(aFieldToFind);
for i := 0 to Length(studentsArray) - 1 do
begin
if Field.GetValue(#studentsArray[i]).AsString = aNameToFind then
begin
Result := StudentType.GetField(aFieldToReturn).GetValue(#studentsArray[i]).AsString;
Exit;
end;
end;
Result := 'no match was found';
end;
Then you can call it like this:
FirstName := TStudent.GetNameOf('LastName', 'Smoe', 'FirstName');
LastName := TStudent.GetNameOf('FirstName', 'Joe', 'LastName');
If you restructure the TStudent record a little, everything gets easier. Instead of having multiple string fields with different names, declare an array of strings with an enumeration range.
Give the enumeration meaningful names and add a search function where the search field and result field can be specified.
Type
TStudentField = (sfFirstName,sfLastName); // Helper enumeration type
TStudent = record
Field: array[TStudentField] of String;
class function SearchNameOf(searchField: TStudentField;
const aSearchName: string; resultField: TStudentField): string; static;
end;
Here is a test example:
program ProjectTest;
{$APPTYPE CONSOLE}
Type
TStudentField = (sfFirstName,sfLastName);
TStudent = record
Field: array[TStudentField] of String;
class function SearchNameOf(searchField: TStudentField; const aSearchName: string; resultField: TStudentField): string; static;
end;
var
studentsArray : array of TStudent;
class function TStudent.SearchNameOf(searchField: TStudentField; const aSearchName: string; resultField: TStudentField): string;
var
i : integer;
begin
for i := 0 to Length(studentsArray) - 1 do begin
if (studentsArray[i].Field[searchField] = aSearchName) then
begin
Result := studentsArray[i].Field[resultField];
Exit;
end;
end;
result := 'no match was found';
end;
begin
SetLength(studentsArray,2);
studentsArray[0].Field[sfFirstName] := 'Buzz';
studentsArray[0].Field[sfLastName] := 'Aldrin';
studentsArray[1].Field[sfFirstName] := 'Neil';
studentsArray[1].Field[sfLastName] := 'Armstrong';
WriteLn(TStudent.SearchNameOf(sfFirstName,'Neil',sfLastName));
ReadLn;
end.
You could use a several properties with index specifier backed by single getter function just as you do for regular array properties:
TDefault = class(TObject)
private
class function GetProp(const FindWhat: string; FindWhere: Integer): string;
static;
protected
/// <remarks>
/// You don't really need this one. I've added it for an illustration
/// purposes.
/// </remarks>
class property Prop[const FindWhat: string; FindWhere: Integer]: string read GetProp;
public
class property A[const FindWhat: string]: string index 0 read GetProp;
class property B[const FindWhat: string]: string index 1 read GetProp;
end;
{ ... }
class function TDefault.GetProp(const FindWhat: string; FindWhere: Integer): string;
begin
case FindWhere of
0: Result := 'Hallo!';
1: Result := 'Hello!';
end;
Result := Result + ' ' + Format('searching for "%s"', [FindWhat]);
end;
As you see, the class properties are just the same as instance properties.
And I must say its a pretty bad idea to perform a search in the property getter.
Of course, this piece of code will not compile. First I need to cast a TObject value to Integer. Then, read it as a string. What function should I use?
for i := 1 to 9 do begin
cmbLanguage.Items.AddObject(Format('Item %d', [i]), TObject(i));
end;
cmbLanguage.ItemIndex := 2;
ShowMessage(cmbLanguage.Items.Objects[cmbLanguage.ItemIndex]);
Or maybe it's possible to use String instead of Integer in the first place?
cmbLanguage.Items.AddObject(Format('Item %d', [i]), TObject(i));
Here you are adding an item with an "object" which is actually an integer (i) casted to a TObject.
Since you are actually storing an int in the object field, you can just cast it back to Integer, then convert that to a string:
ShowMessage(IntToStr(Integer(cmbLanguage.Items.Objects[cmbLanguage.ItemIndex])));
Note that you are not really converting anything here, you're just pretending that your integer is a TObject so the compiler doesn't complain.
If delphi xe or higher is used, I would use a generic class based on #Jerry answer.
Preparation:
unit CoreClasses;
interface
type
IPrimitiveBox<T> = interface
procedure setValue(value : T);
function getValue(): T;
end;
TPrimitiveBox<T> = class(TInterfacedObject, IPrimitiveBox<T>)
private
value : T;
public
constructor create(value : T);
// IPrimtiveBox<T>
procedure setValue(value : T);
function getValue(): T;
end;
implementation
{ TPrimitiveBox<T> }
constructor TPrimitiveBox<T>.create(value: T);
begin
self.value := value;
end;
function TPrimitiveBox<T>.getValue: T;
begin
Result := value;
end;
procedure TPrimitiveBox<T>.setValue(value: T);
begin
self.value := value;
end;
Usage (based on #Jerry example)
var
io: IPrimitive<Integer>;
sl := TStringList.create(true);
io := TPrimitive<Integer>.create(123);
sl.addObjects(io)
io := IPrimitive<Integer>(sl.objects[4]);
ShowMessage('Integer value: '+ IntToStr(io.getValue()));
If you know you will be using Delphi-7 for the rest of your life stick with the TObject(i) cast. Otherwise start using proper objects, this will save you headaches when upgrading to 64 bit.
Unit uSimpleObjects;
Interface
type
TIntObj = class
private
FI: Integer;
public
property I: Integer Read FI;
constructor Create(IValue: Integer);
end;
type
TDateTimeObject = class(TObject)
private
FDT: TDateTime;
public
property DT: TDateTime Read FDT;
constructor Create(DTValue: TDateTime);
end;
Implementation
{ TIntObj }
constructor TIntObj.Create(IValue: Integer);
begin
Inherited Create;
FI := IValue;
end;
{ TDateTimeObject }
constructor TDateTimeObject.Create(DTValue: TDateTime);
begin
Inherited Create;
FDT := DTValue;
end;
end.
Usage:
var
IO: TIntObj;
SL: TStringList;
Storage:
SL := TStringList.Create(true); // 'OwnsObjects' for recent Delphi versions
IO := TIntObj.Create(123);
SL.AddObjects(IO);
Retrieval:
IO := TIntObj(SL.Objects[4]);
ShowMessage('Integer value: '+ IntToStr(IO.I));
For Delphi-7
TIntObj := TStringList.Create;
and you have to free the objects yourself:
for i := 0 to Sl.Count-1 do
begin
IO := TIntObj(SL.Objects[i]);
IO.Free;
end;
SL.Free;
You cannot simply convert an object to a string. This is something you will have to do by yourself using a method of your choice, depending on the reasoning behind it. For example, you could concatenate a string in XML format representing the data in your object. However, Delphi has absolutely no way of concatenating this data for you.
As others have pointed out, you are actually trying to cast a TObject as an Integer. This means that if you stored an integer in a TObject field, then you need to cast it back, for example Integer(MyIntObject)
I have a class in my Delphi app where I would like an easy and dynamic way of resetting all the string properties to '' and all the boolean properties to False
As far as I can see on the web it should be possible to make a loop of some sort, but how to do it isn't clear to me.
if you are an Delphi 2010 (and higher) user then there is a new RTTI unit (rtti.pas). you can use it to get runtime information about your class and its properties (public properties by default, but you can use {$RTTI} compiler directive to include protected and private fields information).
For example we have next test class with 3 public fields (1 boolean and 2 string fields (one of them is readonly)).
TTest = class(TObject)
strict private
FString1 : string;
FString2 : string;
FBool : boolean;
public
constructor Create();
procedure PrintValues();
property String1 : string read FString1 write FString1;
property String2 : string read FString2;
property BoolProp : boolean read FBool write FBool;
end;
constructor TTest.Create();
begin
FBool := true;
FString1 := 'test1';
FString2 := 'test2';
end;
procedure TTest.PrintValues();
begin
writeln('string1 : ', FString1);
writeln('string2 : ', FString2);
writeln('bool: ', BoolToStr(FBool, true));
end;
to enumerate all properties of object and set it values to default you can use something like code below.
First at all you have to init TRttiContext structure (it is not neccesary, because it is a record). Then you should get rtti information about your obejct, after that you can loop your properties and filter it (skip readonly properties and other than boolean and stirng). Take into account that there are few kind of strings : tkUString, tkString and others (take a look at TTypeKind in typinfo.pas)
TObjectReset = record
strict private
public
class procedure ResetObject(obj : TObject); static;
end;
{ TObjectReset }
class procedure TObjectReset.ResetObject(obj: TObject);
var ctx : TRttiContext;
rt : TRttiType;
prop : TRttiProperty;
value : TValue;
begin
ctx := TRttiContext.Create();
try
rt := ctx.GetType(obj.ClassType);
for prop in rt.GetProperties() do begin
if not prop.IsWritable then continue;
case prop.PropertyType.TypeKind of
tkEnumeration : value := false;
tkUString : value := '';
else continue;
end;
prop.SetValue(obj, value);
end;
finally
ctx.Free();
end;
end;
simple code to test:
var t : TTest;
begin
t := TTest.Create();
try
t.PrintValues();
writeln('reset values'#13#10);
TObjectReset.ResetObject(t);
t.PrintValues();
finally
readln;
t.Free();
end;
end.
and result is
string1 : test1
string2 : test2
bool: True
reset values
string1 :
string2 : test2
bool: False
also take a look at Attributes, imo it is good idea to mark properties (wich you need to reset) with some attribute, and may be with default value like:
[ResetTo('my initial value')]
property MyValue : string read FValue write FValue;
then you can filter only properties wich are marked with ResetToAttribute
Please note, the following code works only for published properties of a class! Also, the instance of a class passed to the function below must have at least published section defined!
Here is how to set the published string property values to an empty string and boolean values to False by using the old style RTTI.
If you have Delphi older than Delphi 2009 you might be missing the tkUString type. If so, simply removeit from the following code:
uses
TypInfo;
procedure ResetPropertyValues(const AObject: TObject);
var
PropIndex: Integer;
PropCount: Integer;
PropList: PPropList;
PropInfo: PPropInfo;
const
TypeKinds: TTypeKinds = [tkEnumeration, tkString, tkLString, tkWString,
tkUString];
begin
PropCount := GetPropList(AObject.ClassInfo, TypeKinds, nil);
GetMem(PropList, PropCount * SizeOf(PPropInfo));
try
GetPropList(AObject.ClassInfo, TypeKinds, PropList);
for PropIndex := 0 to PropCount - 1 do
begin
PropInfo := PropList^[PropIndex];
if Assigned(PropInfo^.SetProc) then
case PropInfo^.PropType^.Kind of
tkString, tkLString, tkUString, tkWString:
SetStrProp(AObject, PropInfo, '');
tkEnumeration:
if GetTypeData(PropInfo^.PropType^)^.BaseType^ = TypeInfo(Boolean) then
SetOrdProp(AObject, PropInfo, 0);
end;
end;
finally
FreeMem(PropList);
end;
end;
Here is a simple test code (note the properties must be published; if there are no published properties in the class, at least empty published section must be there):
type
TSampleClass = class(TObject)
private
FStringProp: string;
FBooleanProp: Boolean;
published
property StringProp: string read FStringProp write FStringProp;
property BooleanProp: Boolean read FBooleanProp write FBooleanProp;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
SampleClass: TSampleClass;
begin
SampleClass := TSampleClass.Create;
try
SampleClass.StringProp := 'This must be cleared';
SampleClass.BooleanProp := True;
ResetPropertyValues(SampleClass);
ShowMessage('StringProp = ' + SampleClass.StringProp + sLineBreak +
'BooleanProp = ' + BoolToStr(SampleClass.BooleanProp));
finally
SampleClass.Free;
end;
end;