Установка значения в записи Nullable‹T› с помощью RTTI

Я работаю с сериализацией/десериализацией, используя библиотеку 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;

но пока безуспешно, любая помощь будет оценена.


person Rebelss    schedule 02.07.2020    source источник
comment
Итак, вы используете библиотеку сериализации, которая также имеет тип, допускающий значение NULL, а затем выполняете сериализацию для этого типа самостоятельно? Я уверен, что библиотека знает, как сериализовать собственный тип данных.   -  person Stefan Glienke    schedule 03.07.2020
comment
Здравствуйте Стефан, спасибо за ваш ответ. Я знаю, что библиотека может сериализовать свои собственные данные, но библиотека просто сериализует объекты в Json и Json в объекты, я хочу сериализовать набор данных в объект и объект в набор данных, чтобы сохранить данные в базе данных.   -  person Rebelss    schedule 03.07.2020


Ответы (1)


Nullable.SetValue() не имеет возвращаемого значения, но вы пытаетесь прочитать его, когда вызываете AsInteger для TValue, которое возвращает TRttiMethod.Invoke(). Это вызовет исключение во время выполнения.

Кроме того, когда вы читаете значение свойства TMyClass.MyIntegerProp, вы получите копию его записи Nullable, поэтому Invoke() присваивание SetValue() этой копии не приведет к обновлению свойства MyIntegerProp. Впоследствии вам придется назначить измененный Nullable обратно на MyIntegerProp, например:

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;
person Remy Lebeau    schedule 02.07.2020
comment
Привет @Remy, твое первое решение работает отлично, спасибо, ты спасаешь жизнь! Но я понимаю, что второе решение более элегантно, потому что не нужно вызывать метод SetValue, но когда я пытаюсь скомпилировать, я получаю несовместимые типы: 'Pointer' and 'TValue' в строке LRttiValueProp.SetValue(LValue, AQuery.Fields[n].AsInteger); - person Rebelss; 03.07.2020
comment
@Rebelss попробуйте использовать LValue.GetReferenceToRawData вместо LValue при вызове LRttiValueProp.SetValue(). - person Remy Lebeau; 03.07.2020