Question closed (answer accepted) replaced by Delphi 7: Access violation - TByteDynArray problem)
I have the following delcartion given to me
MyPhoto = class(TRemotable)
private
FhasPhoto: Boolean;
FphotoData: TByteDynArray;
published
property hasPhoto: Boolean read FhasPhoto write FhasPhoto;
property photoData: TByteDynArray read FphotoData write FphotoData;
end;
and I want to
var photo : MyPhoto;
photo := MyPhoto.Create();
SetLength(photo.photoData, 4);
but I get
[Error] mainForm.pas(340): Constant object cannot be passed as var parameter
1) How do I code it correctly, given that I can't change the definition of the class Photo?
2) can I effectively 'cast' any structure to a TByteDynArray just by assigning it?
(as you might have guessed, I'm a BCB guy trying to get into Ddlphi :-)
p.s I will settle for being able to assign each byte of the photo data individually...
This is a flaw in Delphi's implementation of properties. Even if the property can be read and written to, even if both the read and the write point to the same field in the underlying object, you can't pass a property to a var parameter, such as to SetLength. There's no good reason for this, but that's the way it works, for the time being at least.
There's a fairly simple workaround for this issue, though. You need a method on the object that takes an integer and will set the length of FPhotoData internally.
Here is a way :
MyPhoto = class(TRemotable)
...
public
procedure SetSize( aSize : Integer );
end;
procedure MyPhoto.SetSize( aSize : Integer );
begin
SetLength( FphotoData, aSize );
end;
var photo : MyPhoto;
photo := MyPhoto.Create();
MyPhoto.SetSize(4);
Related
This question already has an answer here:
Get TAdvEdit.Text directly from procedure/function
(1 answer)
Closed 2 years ago.
I have a custom component, with a TPersistent published to the object inspector. This class is filled with boolean properties, such as...
type
TMyClass = class(TPersistent)
...
published
property SomeBool: Boolean read FSomeBool write SetSomeBool;
end;
Then, elsewhere outside of this component, I'm trying to write to these properties by using a subroutine, accepting a boolean as a var parameter:
procedure LoadValues;
procedure CF(var AVal: Boolean);
begin
AVal:= False;
end;
begin
CF(MyComponent.MyClass.SomeBool);
// ... repeated on 40 boolean properties in this class
end;
The problem is that it fails to compile, telling me:
E2197 Constant object cannot be passed as var parameter
I've searched around and cannot find out why. Solutions for other people were along the lines of "Use const, not var". But that completely defeats the purpose of what I'm trying to do here. There are 40 Boolean properties in this class, and I'm trying to consolidate assigning these values to just a single line for each.
Why do I get this error, and how do I get around it?
I found a work-around for the issue. Note that in reality, I'm not assigning them all to False - that was just an example. Really, there's more to specify whether each value should be true or false.
As mentioned in the comments, a var parameter (of course) requires a variable, whereas a property does not qualify. Rather than trying to pass each property as a var parameter, instead the subroutine could be a function, returning the appropriate boolean value:
procedure LoadValues;
function CF: Boolean;
begin
Result:= False; //Or whatever value is needed
end;
begin
MyComponent.MyClass.SomeBool:= CF;
// ... repeated on 40 boolean properties in this class
end;
How can I use record in TDictionary?
TMyRec = record
a: Integer;
b: Integer;
end;
...
dictionary = TDictionary<String, TMyRec>.create();
...
dictionary[key].a := 30;<<<
Here the compiler gives an error: "Left side cannot be assigned to". How can I solve this problem without creating a separate function for writing myFunc(a, b: Integer): TMyRec?
dictionary[key] returns a copy of the record held by the dictionary. The compiler prevents you from modifying that because it would serve no purpose.
As an aside, older versions of the program would accept your code and it was very confusing that the modification to the record would be lost. You'd make an assignment but nothing visible changed because what you assigned was a nameless local variable.
Clearly you intend to modify the record held in the collection. In order to do that you need to assign the entire record. Read the record from the collection into a local variable. Modify the local variable. Write the updated value back to the collection. Like so:
var
rec: TMyRec;
...
rec := dictionary[key];
rec.a := 30;
dictionary[key] := rec;
One of the frustrating aspects of this is that the code needs to perform two dictionary lookups, even though we know that the second one will find the same record as the first one. Not even the mighty Spring4d dictionary can do this with a single lookup.
David Heffernans answer is what you're after, but I would like to offer an additional warning. Records can have properties just like classes, with getters and setters, and if your record has such properties your code will compile, but it will still not change the actual record value.
TMyRec = record
private
FA : integer;
procedure SetA(const Value: integer);
function GetA : integer;
public
{ Warning: When used on result from dictionary lookup, only the COPY will be
altered, not the actual record in the dictionary! }
property A : integer read GetA write SetA;
end;
A very simple workaround is to use the List property of the record.
You can say:
dictionary.list[key].a := 30;
This will access the dynamic array that backs up the TList via the List property. The compiler already supports direct access to a dynamic array.
If you can login to quality.embarcadero.com, you can see the full discussion of this issue raised as: RSP-23136: We should be able to assign a value to one element in a list of records - posted Dec 18, 2018 and resolved Nov 21, 2019.
The issue was closed with the comment:
"This works as expected. Alternative coding style was provided."
In working with setting up TCPServer and FTPServer, the thing I noticed most is the need for a UserID field and a UserFlag field in IdContext. The simple addition of these would greatly facilitate setting up the components for multiple clients. You could create a descendant, but that takes a lot of unnecessary coding for something so easily added to the source code. I modified IdContext.pas as follows:
Protected
FUserFlag: Boolean;
FUserID: Integer;
...
Public
Property UserFlag: Boolean Read FUserFlag Write FUserFlag Default False;
Property UserID: Integer Read FUserID Write FUserID Default -1;
By using these I'm able to signal a state between events and I have the reference readily available whenever an event is fired. I tried to say something in the Indy project but I couldn't find anywhere to say it :/
Thank you for your suggestion, but I am not inclined to make these kind of additions to the base TIdContext class. They simply do not belong there. Deriving a custom class and assigning it to the server's ContextClass property is the correct and appropriate solution in this situation. This is why that property exists in the first place. It is really not that much coding, eg:
type
TMyContext = class(TIdServerContext)
protected
FUserFlag: Boolean;
FUserID: Integer;
...
public
Property UserFlag: Boolean Read FUserFlag Write FUserFlag;
Property UserID: Integer Read FUserID Write FUserID;
end;
procedure TMyForm.FormCreate(Sender: TObject);
begin
// must do this before activating the server...
IdTCPServer1.ContextClass := TMyContext;
end;
And then you can type-cast TIdContext object pointers to TMyContext when needed.
Various Indy servers do exactly this internally. For instance TIdFTPServer uses a TIdFTPServerContext class that has Account and Username properties for the logged in session.
That being said, if you do not want to derive a custom class, the base TIdContext class does already have a public Data property (or DataObject and DataValue properties in Delphi ARC-based compilers), which can be used for storing user-defined data, eg:
type
TMyData = class
protected
FUserFlag: Boolean;
FUserID: Integer;
...
public
Property UserFlag: Boolean Read FUserFlag Write FUserFlag;
Property UserID: Integer Read FUserID Write FUserID;
end;
procedure TMyForm.IdTCPServer1Connect(AContext: TIdContext);
begin
AContext.Data := TMyData.Create;
...
end;
And then you can simply type-cast AContext.Data to TMyData when needed.
I'm implementing a package to convert and auto-generate components in the delphi IDE. I'm aware that GExperts has a similar function but I need to customize some specific properties.
Right now I'm stuck on accessing the TADOQuery.SQL property, which is an instance of TStrings:
var
aVal : TValue;
aSqlS : TStrings;
begin
[...]
if (mycomp.GetComponentType = 'TADOQuery') then
if mycomp.GetPropValueByName('SQL', aVal) then
begin
aSqlS := TStrings(aVal.AsClass);
if Assigned(aSqlS) then <----- problem is here
ShowMessage(aSqlS.Text); <----- problem is here
end;
end;
I'm not really sure whether using TValue from RTTI is the correct way to go.
Thanks
Assuming GetPropValueByName() is returning a valid TValue (you did not show that code), then using aVal.AsClass is wrong since the SQL property getter does not return a metaclass type. It returns an object pointer, so use aVal.AsObject instead, or even aVal.AsType<TStrings>.
Update If comp is actually IOTAComponent than TValue is definitely wrong to use at all. The output of IOTAComponent.GetPropValueByName() is an untyped var that receives the raw data of the property value, or an IOTAComponent for TPersistent-derived objects:
var
aVal: IOTAComponent;
aSqlS : TStrings;
begin
[...]
if (mycomp.GetComponentType = 'TADOQuery') then
if mycomp.PropValueByName('SQL', aVal) then
ShowMessage(TStrings(aVal.GetComponentHandle).Text);
end;
However, a better option would be to access the actual TADOQuery object instead:
if (mycomp.GetComponentType = 'TADOQuery') then
ShowMessage(TADOQuery(comp.GetComponentHandle).SQL.Text);
TRTTIProperty.SetValue( ) takes an TValue instance, but if the provided TValue instance is based on a different type then the property, things blow up.
E.g.
TMyObject = class
published
property StringValue: string read FStringValue write FStringValue;
end;
procedure SetProperty(obj: TMyObject);
var
context: TRTTIContext;
rtti: TRTTIType;
prop: TRTTIProperty;
value: TValue;
begin
context := TRTTIContext.Create;
rtti := context.GetType(TMyObject);
prop := rtti.GetProperty('StringValue');
value := 1000;
prop.SetValue(obj, value);
end;
Trying to cast the value to a string wont work either.
prop.SetValue(obj, value.AsString);
prop.SetValue(obj, value.Cast(prop.PropertyType.Handle));
Any ideas on how solve this?
UPDATE:
Some of you wonder why I want to assign an integer to an string, and I will try to explain.
(Actually, it's more likely that I want to assign a string to an integer, but that's not that relevant...)
What I'm trying to accomplish, is to make a general 'middle-man' between gui and model. I want to somehow hook a textedit field to an property. Instead of making such an middle man for each model that I have, I hoped that the new RTTI/TValue thing would work some magic for me.
I'm also new to generics, so I'm not sure how generics could have helped. Is it possible to instantiate a generic at runtime with a dynamically decided type, or do the compile need to know?
E.g.
TMyGeneric<T> = class
end;
procedure DoSomething( );
begin
prop := rtti.getProperty('StringValue');
mygen := TMyGeneric<prop.PropertyType>.Create;
//or
mygen := TMyGeneric<someModel.Class>.Create;
end;
Maybe the age of magic has yet to come... I guess I can manage with a couple of big case structures...
TValue is not a Variant. You can only read the datatype that "you" put into it.
TValue.Cast doesn't work because it has the same semantic that implicit type casts have. You cannot assign an integer to a string or vice versa. But you can assign an integer to a float, or you can assign an integer to a int64.
Can't try it right now, but I would have written:
value := '1000';
prop.SetValue(obj, value);
try
prop.SetValue(obj, value.ToString)
But for me it is same question as for François. Why you want to set the property with a value of the wrong datatype?