How to set value to a dynamic array using the rtti? [duplicate] - delphi

I have a question. I am a newbie with Run Time Type Information from Delphi 2010. I need to set length to a dynamic array into a TValue. You can see the code.
Type TMyArray = array of integer;
TMyClass = class
publihed
function Do:TMyArray;
end;
function TMyClass.Do:TMyArray;
begin
SetLength(Result,5);
for i:=0 to 4 Result[i]=3;
end;
.......
.......
......
y:TValue;
Param:array of TValue;
.........
y=Methods[i].Invoke(Obj,Param);//delphi give me a DynArray type kind, is working, Param works to any functions.
if Method[i].ReturnType.TypeKind = tkDynArray then//is working...
begin
I want to set length for y to 10000//i don't know how to write.
end;
I don't like Generics Collections.

TValue wasn't designed for arbitrary manipulation of its contents (it would have more helpers for e.g. setting record fields etc. if so), but rather for transporting values between concrete static types and dynamic RTTI. In this respect, TValue.SetArrayElement is an anomaly, and in hindsight, perhaps should not have been included. However, what you ask is possible:
uses Rtti;
type
TMyArray = array of Integer;
TMyClass = class
function Go: TMyArray;
end;
function TMyClass.Go: TMyArray;
var
i: Integer;
begin
SetLength(Result, 5);
for i := 0 to 4 do
Result[i] := 3;
end;
procedure P;
var
ctx: TRttiContext;
v: TValue;
len: Longint;
i: Integer;
begin
v := ctx.GetType(TMyClass).GetMethod('Go').Invoke(TMyClass.Create, []);
Writeln(v.ToString);
len := 10;
DynArraySetLength(PPointer(v.GetReferenceToRawData)^, v.TypeInfo, 1, #len);
Writeln(v.GetArrayLength);
for i := 0 to v.GetArrayLength - 1 do
Writeln(v.GetArrayElement(i).ToString);
end;
begin
P;
end.

Related

Converting a generic TArray into a varArray

I'm trying to convert an array of T into a Variant (varArray).
With not-generic types (i.e: Integer), I'm using the following function:
function ToVarArray(AValues : array of Integer) : Variant;
var
i : integer;
begin
Result := VarArrayCreate(
[Low(AValues), High(AValues)],
varInteger
);
for i := Low(AValues) to High(AValues) do
Result[i] := AValues[i];
end;
I'm having some problems while trying to do the same thing with a generic TArray:
uses
System.Generics.Collections;
type
TArray = class(System.Generics.Collections.TArray)
public
class function ToVarArray<T>(const AValues: array of T) : Variant; static;
end;
I've tried the following:
class function TArray.ToVarArray<T>(const AValues: array of T) : Variant;
var
i : integer;
Tmp : T;
begin
Result := Tmp;
Result := VarArrayCreate(
[Low(AValues), High(AValues)],
VarType(Result)
);
for i := Low(AValues) to High(AValues) do
Result[i] := AValues[i];
end;
But It produces the following compile error at each row where I'm assigning a T to a Variant:
[dcc32 Error] Unit1.pas(36): E2010 Incompatible types: 'Variant' and
'T'
The title of the question says that you'd like to process generic TArray<T>, but the first sentence say it's array of T. You might think that both terms refer to the same data structure (dynamic array), and most of the time they do, but they make difference if you use them in place of a procedure/function argument declaration.
Therefore the following methods have different signatures and accept different types of parameters:
class function TArray.ToVarArray<T>(const AValues: TArray<T>): Variant;
class function TArray.ToVarArray<T>(const AValues: array of T): Variant;
While the first invariant accepts true dynamic array as a parameter, the latter takes an open array. This unfortunate language design is regular source of confusion for Delphi developers.
Delphi RTL already contains function that converts dynamic array to variant array of appropriate type - DynArrayToVariant. It takes pointer to initial element of dynamic array and array's type information as its arguments. To make use of generics you would write:
uses
System.Variants;
class function TArray.DynArrayToVarArray<T>(const AValues: TArray<T>): Variant;
begin
DynArrayToVariant(Result, #AValues[0], TypeInfo(TArray<T>));
end;
Your code would fail to compile if you try to use this routine with a static array:
var
SA: array[0..2] of Integer;
begin
SA[0] := 0;
SA[1] := 1;
SA[2] := 2;
{ E2010 Incompatible types: 'System.TArray<System.Integer>' and 'array[0..2] of Integer' }
TArray.DynArrayToVarArray<Integer>(SA);
end.
Open array solves this issue, but you need to "convert" it to dynamic array in order to use it with RTL's DynArrayToVariant.
class function TArray.OpenArrayToVarArray<T>(const AValues: array of T): Variant;
var
LArray: TArray<T>;
Index: Integer;
begin
SetLength(LArray, Length(AValues));
for Index := Low(AValues) to High(AValues) do
LArray[Index] := AValues[index];
Result := DynArrayToVarArray<T>(LArray);
end;
var
DA: TArray<Integer>;
SA: array[0..2] of Integer;
begin
DA := [0, 1, 2];
SA[0] := 0;
SA[1] := 1;
SA[2] := 2;
{ these all work }
TArray.OpenArrayToVarArray<Integer>(DA); // dynamic array
TArray.OpenArrayToVarArray<Integer>(SA); // static array
TArray.OpenArrayToVarArray<Integer>([0, 1, 2]); // open array constructor
end.
The above OpenArrayToVarArray<T> routine is pretty ineffective both in terms of performance and memory usage, because it copies elements one by one from open array to dynamic array. If you really need to support open arrays you should probably write better implementation inspired by RTL's DynArrayToVariant, however I find that one to be sub-optimal too.

How do I check whether an array contains a particular value?

how do I correctly write this ?:
If number is different from Array[1] to Array[x-1] the begin......
where number is an integer and array is an array of integers from 1 to x
I believe you want to do something if number is not found in the array MyArray. Then you can do it like this:
NoMatch := True;
for i := Low(MyArray) to High(MyArray) do
if MyArray[i] = number then
begin
NoMatch := False;
Break;
end;
if NoMatch then
DoYourThing;
You could create a function that checks if a number is found in an array. Then you can use this function every time you need to perform such a check. And each time, the code will be more readable. For example, you could do it like this:
function IsNumberInArray(const ANumber: Integer;
const AArray: array of Integer): Boolean;
var
i: Integer;
begin
for i := Low(AArray) to High(AArray) do
if ANumber = AArray[i] then
Exit(True);
Result := False;
end;
...
if not IsNumberInArray(number, MyArray) then
DoYourThing;
If you use a old version of Delphi, you have to replace Exit(True) with begin Result := True; Exit; end. In newer versions of Delphi, I suppose you could also play with stuff like generics.
You could also write a Generic version, however you can't use generics with stand-alone procedures, they need to be bound to a class or record. Something like the following
unit Generics.ArrayUtils;
interface
uses
System.Generics.Defaults;
type
TArrayUtils<T> = class
public
class function Contains(const x : T; const anArray : array of T) : boolean;
end;
implementation
{ TArrayUtils<T> }
class function TArrayUtils<T>.Contains(const x: T; const anArray: array of T): boolean;
var
y : T;
lComparer: IEqualityComparer<T>;
begin
lComparer := TEqualityComparer<T>.Default;
for y in anArray do
begin
if lComparer.Equals(x, y) then
Exit(True);
end;
Exit(False);
end;
end.
usage would be
procedure TForm6.Button1Click(Sender: TObject);
begin
if TArrayUtils<integer>.Contains(3, [1,2,3]) then
ShowMessage('Yes')
else
ShowMessage('No');
end;
Should work with parameters like TArray<integer> or array of integer as well as constant arrays (shown) - and you could add many other methods to the class, such as IndexOf or Insert...
From Delphi 10.3 you can omit the <integer> due to type inferencing so would look like TArrayUtils.Contains(3, [1,2,3]).
I had the same question and solved it like this:
if value in myArray then
...
But as I needed to compare with specific values, I simply did:
if value in [0, 1, 2, 3] then
...

How do I sort a generic list using a custom comparer?

I'm kinda a Delphi-newbie and I don't get how the Sort method of a TList of Records is called in order to sort the records by ascending integer value.
I have a record like the following:
type
TMyRecord = record
str1: string;
str2: string;
intVal: integer;
end;
And a generic list of such records:
TListMyRecord = TList<TMyRecord>;
Have tried to find a code-example in the help files and found this one:
MyList.Sort(#CompareNames);
Which I can't use, since it uses classes. So I tried to write my own compare function with a little different parameters:
function CompareIntVal(i1, i2: TMyRecord): Integer;
begin
Result := i1.intVal - i2.intVal;
end;
But the compiler always throws a 'not enough parameters' - error when I call it with open.Sort(CompareIntVal);, which seems obvious; so I tried to stay closer to the help file:
function SortKB(Item1, Item2: Pointer): Integer;
begin
Result:=PMyRecord(Item1)^.intVal - PMyRecord(Item2)^.intVal;
end;
with PMyRecord as PMyRecord = ^TMyRecord;
I have tried different ways of calling a function, always getting some error...
The Sort overload you should be using is this one:
procedure Sort(const AComparer: IComparer<TMyRecord>);
Now, you can create an IComparer<TMyRecord> by calling TComparer<TMyRecord>.Construct. Like this:
var
Comparison: TComparison<TMyRecord>;
....
Comparison :=
function(const Left, Right: TMyRecord): Integer
begin
Result := Left.intVal-Right.intVal;
end;
List.Sort(TComparer<TMyRecord>.Construct(Comparison));
I've written the Comparison function as an anonymous method, but you could also use a plain old style non-OOP function, or a method of an object.
One potential problem with your comparison function is that you may suffer from integer overflow. So you could instead use the default integer comparer.
Comparison :=
function(const Left, Right: TMyRecord): Integer
begin
Result := TComparer<Integer>.Default.Compare(Left.intVal, Right.intVal);
end;
It might be expensive to call TComparer<Integer>.Default repeatedly so you could store it away in a global variable:
var
IntegerComparer: IComparer<Integer>;
....
initialization
IntegerComparer := TComparer<Integer>.Default;
Another option to consider is to pass in the comparer when you create the list. If you only ever sort the list using this ordering then that's more convenient.
List := TList<TMyRecord>.Create(TComparer<TMyRecord>.Construct(Comparison));
And then you can sort the list with
List.Sort;
The concise answer:
uses
.. System.Generics.Defaults // Contains TComparer
myList.Sort(
TComparer<TMyRecord>.Construct(
function(const Left, Right: TMyRecord): Integer
begin
Result := Left.intVal - Right.intVal;
end
)
);
I want to share my solution (based on the input I have gathered here).
It's a standard setup. A filedata class that holds data of a single file in a generic TObjectList. The list has the two private attributes fCurrentSortedColumn and fCurrentSortAscending to control the sort order. The AsString-method is the path and filename combined.
function TFileList.SortByColumn(aColumn: TSortByColums): boolean;
var
Comparison: TComparison<TFileData>;
begin
result := false;
Comparison := nil;
case aColumn of
sbcUnsorted : ;
sbcPathAndName: begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
sbcSize : begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<int64>.Default.Compare(Left.Size,Right.Size);
if Result = 0 then
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
sbcDate : begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<TDateTime>.Default.Compare(Left.Date,Right.Date);
if Result = 0 then
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
sbcState : begin
Comparison := function(const Left, Right: TFileData): integer
begin
Result := TComparer<TFileDataTestResults>.Default.Compare(Left.FileDataResult,Right.FileDataResult);
if Result = 0 then
Result := TComparer<string>.Default.Compare(Left.AsString,Right.AsString);
end;
end;
end;
if assigned(Comparison) then
begin
Sort(TComparer<TFileData>.Construct(Comparison));
// Control the sort order
if fCurrentSortedColumn = aColumn then
fCurrentSortAscending := not fCurrentSortAscending
else begin
fCurrentSortedColumn := aColumn;
fCurrentSortAscending := true;
end;
if not fCurrentSortAscending then
Reverse;
result := true;
end;
end;
I found a much simpler modified sort function to alphabetize a TList of records or nonstandard list of items.
Example
PList = ^TContact;
TContact = record //Record for database of user contact records
firstname1 : string[20];
lastname1 : string[20];
phonemobile : Integer; //Fields in the database for contact info
phonehome : Integer;
street1 : string;
street2 : string;
type
TListSortCompare = function (Item1,
Item2: TContact): Integer;
var
Form1: TForm1;
Contact : PList; //declare record database for contacts
arecord : TContact;
Contacts : TList; //List for the Array of Contacts
function CompareNames(i1, i2: TContact): Integer;
begin
Result := CompareText(i1.lastname1, i2.lastname1) ;
end;
and the function to call to sort your list
Contacts.Sort(#CompareNames);

Why does assigning a NIL array to a Variant cause a non-empty array to be returned in Delphi 6?

Consider the code below which compiles and runs without error in Delphi 6. When I recover the dynamic string array, instead of seeing an empty array in sa, I see an array with a length of 1 with a single element containing an empty string. Why is this and how can I safely assign a NIL dynamic array to a Variant and recover it properly? Here's the code:
TDynamicStringArray = array of string;
var
V: Variant;
sa: TDynamicStringArray;
begin
sa := nil;
V := sa;
sa := V;
OutputDebugString('sa has a single element now with an empty string in it when I expect it to be empty.');
end;
There are two bugs here.
First of all in Variants.DynArrayVariantBounds. When the dynamic array is nil this erroneously returns a low/high bounds pair of (0, 0). It should return (0, -1). This bug is fixed in the latest versions of Delphi. That causes V := sa to return a variant array with a single, empty, element.
The second bug affects the other direction, sa := V. This bug is still present in the latest versions of Delphi. This bug is in Variants.DynArrayFromVariant. There is a repeat/until loop which walks over the input variant array and populates the output dynamic array. When the input variant array is empty, it should not enter that repeat/until loop. However, the code erroneously does so and attempts to read an element of the variant array with VarArrayGet. Since the array is empty, that provokes a runtime error. I have reported this: QC#109445.
Here is a very simply bit of code that fixes the bugs. Note that I have only consider the case where the arrays are one dimensional. If you need to support higher dimensional arrays then you can extend this approach to do so.
program Project1;
{$APPTYPE CONSOLE}
uses
Variants;
var
OriginalVarFromDynArray: procedure(var V: Variant; const DynArray: Pointer; TypeInfo: Pointer);
OriginalVarToDynArray: procedure(var DynArray: Pointer; const V: Variant; TypeInfo: Pointer);
function DynArrayVarType(typeInfo: PDynArrayTypeInfo): Integer;
const
tkDynArray = 17;
begin
Result := varNull;
if (typeInfo<>nil) and (typeInfo.Kind=tkDynArray) then
begin
Inc(PChar(typeInfo), Length(typeInfo.name));
Result := typeInfo.varType;
if Result=$48 then
Result := varString;
end;
if (Result<=varNull) or (Result=$000E) or (Result=$000F) or ((Result>varInt64) and not (Result=varString)) then
VarCastError;
end;
procedure VarFromDynArray(var V: Variant; const DynArray: Pointer; TypeInfo: Pointer);
var
VarType, DynDim: Integer;
begin
DynDim := DynarrayDim(PDynArrayTypeInfo(TypeInfo));
if DynDim=1 then
begin
//only attempt to deal with 1 dimensional arrays
if DynArray=nil then begin
VarClear(V);
VarType := DynArrayVarType(PDynArrayTypeInfo(TypeInfo));
if VarType = varString then
VarType := varOleStr;
V := VarArrayCreate([0, -1], VarType);
exit;
end;
end;
OriginalVarFromDynArray(V, DynArray, TypeInfo);
end;
procedure VarToDynArray(var DynArray: Pointer; const V: Variant; TypeInfo: Pointer);
var
DimCount: Integer;
Len: Integer;
begin
DimCount:= VarArrayDimCount(V);
if DimCount=1 then
begin
//only attempt to deal with 1 dimensional arrays
Len := VarArrayHighBound(V, 1) - VarArrayLowBound(V, 1) + 1;
if Len=0 then begin
DynArraySetLength(DynArray, PDynArrayTypeInfo(TypeInfo), 1, #Len);
exit;
end;
end;
OriginalVarToDynArray(DynArray, V, TypeInfo);
end;
procedure FixVariants;
var
VarMgr: TVariantManager;
begin
GetVariantManager(VarMgr);
OriginalVarFromDynArray := VarMgr.VarFromDynArray;
VarMgr.VarFromDynArray := VarFromDynArray;
OriginalVarToDynArray := VarMgr.VarToDynArray;
VarMgr.VarToDynArray := VarToDynArray;
SetVariantManager(VarMgr);
end;
type
TDynamicStringArray = array of string;
var
V: Variant;
sa: TDynamicStringArray;
begin
FixVariants;
sa := nil;
V := sa;
sa := V;
Writeln(Length(sa));
Readln;
end.

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