首页 > 解决方案 > 在 Nullable 中设置值使用 RTTI 录制

问题描述

我正在使用Paolo Rossi 的NEON库进行序列化/反序列化。

我正在尝试使用从数据库中获取的数据使用 RTTI 填充此类。类上的属性与数据库中的字段具有相同的名称。

在图书馆里,我有这个 Nullable Record :

unit Neon.Core.Nullables;

interface

uses
  System.SysUtils, System.Variants, System.Classes, System.Generics.Defaults, System.Rtti,
  System.TypInfo, System.JSON;

type
  ENullableException = class(Exception);

  {$RTTI EXPLICIT FIELDS([vcPrivate]) METHODS([vcPrivate])}
  Nullable<T> = record
  private
    FValue: T;
    FHasValue: string;
    procedure Clear;
    function GetValueType: PTypeInfo;
    function GetValue: T;
    procedure SetValue(const AValue: T);
    function GetHasValue: Boolean;
  public
    constructor Create(const Value: T); overload;
    constructor Create(const Value: Variant); overload;
    function Equals(const Value: Nullable<T>): Boolean;
    function GetValueOrDefault: T; overload;
    function GetValueOrDefault(const Default: T): T; overload;

    property HasValue: Boolean read GetHasValue;
    function IsNull: Boolean;

    property Value: T read GetValue;

    class operator Implicit(const Value: Nullable<T>): T;
    class operator Implicit(const Value: Nullable<T>): Variant;
    class operator Implicit(const Value: Pointer): Nullable<T>;
    class operator Implicit(const Value: T): Nullable<T>;
    class operator Implicit(const Value: Variant): Nullable<T>;
    class operator Equal(const Left, Right: Nullable<T>): Boolean;
    class operator NotEqual(const Left, Right: Nullable<T>): Boolean;
  end;

  NullString = Nullable<string>;
  NullBoolean = Nullable<Boolean>;
  NullInteger = Nullable<Integer>;
  NullInt64 = Nullable<Int64>;
  NullDouble = Nullable<Double>;
  NullDateTime = Nullable<TDateTime>;

implementation

uses
  Neon.Core.Utils;

{ Nullable<T> }

constructor Nullable<T>.Create(const Value: T);
var
  a: TValue;
begin
  FValue := Value;
  FHasValue := DefaultTrueBoolStr;
end;

constructor Nullable<T>.Create(const Value: Variant);
begin
  if not VarIsNull(Value) and not VarIsEmpty(Value) then
    Create(TValue.FromVariant(Value).AsType<T>)
  else
    Clear;
end;

procedure Nullable<T>.Clear;
begin
  FValue := Default(T);
  FHasValue := '';
end;

function Nullable<T>.Equals(const Value: Nullable<T>): Boolean;
begin
  if HasValue and Value.HasValue then
    Result := TEqualityComparer<T>.Default.Equals(Self.Value, Value.Value)
  else
    Result := HasValue = Value.HasValue;
end;

function Nullable<T>.GetHasValue: Boolean;
begin
  Result := FHasValue <> '';
end;

function Nullable<T>.GetValueType: PTypeInfo;
begin
  Result := TypeInfo(T);
end;

function Nullable<T>.GetValue: T;
begin
  if not HasValue then
    raise ENullableException.Create('Nullable type has no value');
  Result := FValue;
end;

function Nullable<T>.GetValueOrDefault(const Default: T): T;
begin
  if HasValue then
    Result := FValue
  else
    Result := Default;
end;

function Nullable<T>.GetValueOrDefault: T;
begin
  Result := GetValueOrDefault(Default(T));
end;

class operator Nullable<T>.Implicit(const Value: Nullable<T>): T;
begin
  Result := Value.Value;
end;

class operator Nullable<T>.Implicit(const Value: Nullable<T>): Variant;
begin
  if Value.HasValue then
    Result := TValue.From<T>(Value.Value).AsVariant
  else
    Result := Null;
end;

class operator Nullable<T>.Implicit(const Value: Pointer): Nullable<T>;
begin
  if Value = nil then
    Result.Clear
  else
    Result := Nullable<T>.Create(T(Value^));
end;

class operator Nullable<T>.Implicit(const Value: T): Nullable<T>;
begin
  Result := Nullable<T>.Create(Value);
end;

class operator Nullable<T>.Implicit(const Value: Variant): Nullable<T>;
begin
  Result := Nullable<T>.Create(Value);
end;

function Nullable<T>.IsNull: Boolean;
begin
  Result := FHasValue = '';
end;

class operator Nullable<T>.Equal(const Left, Right: Nullable<T>): Boolean;
begin
  Result := Left.Equals(Right);
end;

class operator Nullable<T>.NotEqual(const Left, Right: Nullable<T>): Boolean;
begin
  Result := not Left.Equals(Right);
end;

procedure Nullable<T>.SetValue(const AValue: T);
begin
  FValue := AValue;
  FHasValue := DefaultTrueBoolStr;
end;

end.

这是模型类:

type
  TMyClass = class(TPersistent)
  private
    FMyIntegerProp: Nullable<Integer>;
    procedure SetMyIntegerProp(const Value: Nullable<Integer>);
  published
    Property MyIntegerProp: Nullable<Integer> read FMyIntegerProp write SetMyIntegerProp;
  end;

implementation

{ TMyClass }

procedure TMyClass.SetMyIntegerProp(const Value: Nullable<Integer>);
begin
  FMyIntegerProp := Value;
end;

到目前为止我的代码:

procedure DatasetToObject(AObject: TObject; AQuery: TFDQuery);
var
  n: Integer;
  LRttiContext: TRttiContext;
  LRttiType: TRttiType;
  LRttiProperty: TRttiProperty;
  LFieldName: string;
  Value: TValue;

  LValue: TValue;
  LRttiMethod : TRttiMethod;
begin
  LRttiContext := TRttiContext.Create;
  try
    LRttiType := LRttiContext.GetType(AObject.ClassType);

    for n := 0 to AQuery.FieldCount - 1 do
    begin
      LRttiProperty := LRttiType.GetProperty(AQuery.Fields[n].FieldName);

      if (LRttiProperty <> nil) and (LRttiProperty.PropertyType.TypeKind = tkRecord) then
      begin
        LValue := LRttiProperty.GetValue(AObject);
        LRttiMethod := LRttiContext.GetType(LValue.TypeInfo).GetMethod('SetValue');

        if (LRttiProperty.PropertyType.Name = 'Nullable<System.Integer>') then
          LRttiMethod.Invoke(LValue, [AQuery.Fields[n].AsInteger]).AsInteger;
      end;
    end;
  finally
    LRttiContext.Free;
  end;
end;

但到目前为止没有成功,任何帮助将不胜感激。

标签: delphiserializationrtti

解决方案


Nullable.SetValue()没有返回值,但是当您调用返回值时,您正试图读取一个AsInteger值。这将导致在运行时引发异常。TValueTRttiMethod.Invoke()

此外,当您读取TMyClass.MyIntegerProp属性的值时,您最终会得到其记录的副本,因此在副本上执行不会更新属性。您必须在之后将修改后的内容分配回,例如:NullableInvoke()SetValue()MyIntegerPropNullableMyIntegerProp

LValue := LRttiProperty.GetValue(AObject);
LRttiMethod := LRttiContext.GetType(LValue.TypeInfo).GetMethod('SetValue');
LRttiMethod.Invoke(LValue, [AQuery.Fields[n].AsInteger]);
LRttiProperty.SetValue(AObject, LValue); // <-- add this!

话虽如此,该Nullable.Value属性是只读的,但Nullable有一个SetValue()方法,所以我建议将Value属性更改为读写,例如:

property Value: T read GetValue write SetValue;

然后您可以Value通过 RTTI 设置属性,而不是直接Invoke()使用该SetValue()方法:

var
  ...
  //LRttiMethod: TRttiMethod;
  LRttiValueProp: TRttiProperty;
  ...

...

LRttiProperty := LRttiType.GetProperty(AQuery.Fields[n].FieldName);

if (LRttiProperty <> nil) and
   (LRttiProperty.PropertyType.TypeKind = tkRecord) and
   (LRttiProperty.PropertyType.Name = 'Nullable<System.Integer>') then
begin
  LValue := LRttiProperty.GetValue(AObject);
  {
  LRttiMethod := LRttiContext.GetType(LValue.TypeInfo).GetMethod('SetValue');
  LRttiMethod.Invoke(LValue, [AQuery.Fields[n].AsInteger]);
  }
  LRttiValueProp := LRttiContext.GetType(LValue.TypeInfo).GetProperty('Value');
  LRttiValueProp.SetValue(LValue.GetReferenceToRawData, AQuery.Fields[n].AsInteger);
  LRttiProperty.SetValue(AObject, LValue);
end;

推荐阅读