How can I convert from generic to Variant in Delphi
Asked Answered
S

3

13

I have a Delphi generic class that exposes a function with an argument of the generic type. Inside this function, I need to pass an instance of the generic type on to another object expecting a Variant type. Similar to this:

type
  IMyInterface = interface
    DoStuff(Value: Variant);
  end;      

  TMyClass<T> = class
    FMyIntf: IMyInterface
    procedure DoStuff(SomeValue: T);
  end;

[...]

procedure MyClass<T>.DoStuff(SomeValue: T);
begin
  FMyIntf.DoStuff((*convert SomeValue to Variant here*));
end;

I tried using Rtti.TValue.From(SomeValue).AsVariant. This worked for integral types, but blew up for Booleans. I don't quite see why, since normally I'd be able to assign a Boolean value to a Variant...

Is there a better way to make this conversion? I only need it to work for simple built-in types (excluding enumerations and records)

Saltillo answered 19/1, 2012 at 11:32 Comment(2)
Have you tried creating a local variable of type Variant, assign SomeValue to it, and then pass the local variable to FMyIntf.DoStuff()?Protege
Yes. I can't do that because there is no valid cast from 'T' to 'Variant'...Saltillo
M
11

I think there is no direct way to convert generic type to variant because variant cannot hold all the possible types. You must write your specific conversion routine. E.g.:

interface
//...
type
  TDemo = class
  public
    class function GetAsVariant<T>(const AValue: T): Variant;
  end;
//...
implementation
uses
  Rtti,
  TypInfo;
//...

{ TDemo}

class function TDemo.GetAsVariant<T>(const AValue: T): Variant;
var
  val: TValue;
  bRes: Boolean;
begin
  val := TValue.From<T>(AValue);
  case val.Kind of
    tkInteger: Result := val.AsInteger;
    tkInt64: Result := val.AsInt64;
    tkEnumeration: 
    begin
      if val.TryAsType<Boolean>(bRes) then
        Result := bRes
      else
        Result := val.AsOrdinal;
    end;
    tkFloat: Result := val.AsExtended;
    tkString, tkChar, tkWChar, tkLString, tkWString, tkUString:
      Result := val.AsString;
    tkVariant: Result := val.AsVariant
    else
    begin
      raise Exception.Create('Unsupported type');
    end;
  end;
end;

Because TValue.AsVariant handles most of the type conversions internally, this function can be simplified. I will handle enumerations in case you could need them later:

class function TDemo.GetAsVariant<T>(const AValue: T): Variant;
var
  val: TValue;
begin
  val := TValue.From<T>(AValue);
  case val.Kind of
    tkEnumeration:
    begin
      if val.TypeInfo = TypeInfo(Boolean) then
        Result := val.AsBoolean
      else
        Result := val.AsOrdinal;
    end
    else
    begin
      Result := val.AsVariant;
    end;
  end;

Possible usage:

var
  vValue: Variant;
begin
  vValue := TDemo.GetAsVariant<Boolean>(True);
  Assert(vValue = True); //now vValue is a correct Boolean
Monroe answered 19/1, 2012 at 13:0 Comment(7)
I was afraid it'd be something like that :-P After a bit of poking about in the internals of TValue, it turns out the TypeKind of Boolean is tkEnumeration, and TValue raises an exception when calling AsVariant on a value having TypeKind tkEnumeration. Which also means your example - although it doesn't raise an exception - still returns the wrong Variant, since my Boolean gets converted 'AsOrdinal', resulting in a Variant of integral type - not boolean... Perhaps I need to check both the typekind and the typename....Saltillo
@MathiasFalkenberg I've edited my answer to handle booleans correctly.Monroe
+1. You can make an optimal GenericToVariant by converting as many types as you feel like converting, into some representation that can be a variant. If you wanted, you could add code to convert important local class (TObject) types, using the new TObject.ToString method.Footpath
It would seem only booleans are a problem. I wonder if it wouldn't be sufficient to shorten the case statement in @Linas' example to only take into account tkEnumeration and everything else. TValue.AsVariant throws it's own exception for unsupported types, and tkInteger, tkInt64, tkFloat, tkString and so forth seem to convert fine using TValue.AsVariant...Saltillo
In fact, wouldn't this do it? if val.TypeInfo = TypeInfo(Boolean) then Result := val.AsBoolean else Result := val.AsVariant;Saltillo
@MathiasFalkenberg It would do it but it won't handle real enumerations. I can post simplified version of my function.Monroe
@Monroe : Works like a charm. The code resides deep in the bowels of my code. I'm quite confident I am not going to need enumerations there, but nevertheless it might come in handy elsewhere... Thanks!Saltillo
A
1

Looks like in my Delphi version 10.2 the Boolean problem is gone and TValue.From<T>(FValue).AsVariant is enough.

Here an example with some other helpful things like comparing the generic type:

  TMyValue<T> = class(TPersistent)
  private
    FValue: T;
    procedure SetValue(const AValue: T);
    function GetAsVariant: Variant; override;
  public
    procedure Assign(Source: TPersistent); override;
    property Value: T read FValue write SetValue;
    property AsVariant: Variant read GetAsVariant;
  end;

function TMyValue<T>.GetAsVariant: Variant;
begin
  Result:= TValue.From<T>(FValue).AsVariant;
end;

procedure TMyValue<T>.SetValue(const AValue: T);
begin
  if TEqualityComparer<T>.Default.Equals(AValue, FValue) then Exit;
  FValue:= AValue;
  //do something
end;

procedure TMyValue<T>.Assign(Source: TPersistent);
begin
  if Source is TMyValue<T> then Value:= (Source as TMyValue<T>).Value
  else inherited;
end;
Abecedary answered 15/3, 2018 at 15:13 Comment(0)
R
0

Another way (tested XE10)

Var
  old : variant;
  val : TValue;
Begin
  val := TValue.FromVariant(old);
End;
Rubierubiginous answered 11/1, 2018 at 13:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.