在const数组中传递通用记录

时间:2011-05-10 08:48:47

标签: delphi generics delphi-2010

是否有可能以任何方式将const参数数组中的通用记录传递给函数调用?

我想使用Allen Bauer的Nullable记录在一种自制的ORM中使用“Plain Old Delphi Objects”来映射数据库行:

type
  Nullable<T> = record
    ...
  end;

  TMyTableObject = class(TDbOject)
  private
    FId: Integer;
    FOptionalField: Nullable<string>;
  protected
    procedure InternalSave; override;
  public
    property Id: Integer read FId write SetId;
    property OptionalField: Nullable<string> read FOptionalField write SetOptionalField;
  end;

  ...

  implementation

  procedure TMyTableObject.InternalSave;
  begin
    { FDbController is declared and managed in TDbObject, it contains fonction to run queries
      on the database }

    FDbController.Execute(
      'update or insert into MY_TABLE(TABLE_ID, OPTIONAL_FIELD) ' +
      'values (?, ?) ' + 
      'matching(TABLE_ID) returning TABLE_ID', [FId, FOptionalField],
      procedure (Fields: TSQLResult)
      begin
        FId := Fields.AsInteger[0];
      end;
    end;      
  end;

上面的代码导致错误:“E2250:没有覆盖版本的”执行“功能可以用这些参数调用”(翻译自法语:E2250 Aucuneversionsurchargéede'Execute'nepeutêtreaposléeavecces arguments )

我可以显式地将FOptionalField转换为字符串或其他任何东西,因为Nullable会覆盖ad-hoc运算符但我真的必须知道Nullable是否有值,直到我将const数组映射到数据库查询对象的Params字段:

procedure TDbContext.MapParams(Q: TUIBQuery; Params: TConstArray);
const
  BOOL_STR: array[Boolean] of string = ('F', 'V');
var
  i: Integer;
begin
  for i := 0 to High(Params) do
    case Params[i].VType of
      vtInteger :      Q.Params.AsInteger[i]       := Params[i].VInteger;
      vtInt64   :      Q.Params.AsInt64[i]         := Params[i].VInt64^;
      vtBoolean :      Q.Params.AsString[i]        := BOOL_STR[Params[i].VBoolean];
      vtChar    :      Q.Params.AsAnsiString[i]    := Params[i].VChar;
      vtWideChar:      Q.Params.AsString[i]        := Params[i].VWideChar;
      vtExtended:      Q.Params.AsDouble[i]        := Params[i].VExtended^;
      vtCurrency:      Q.Params.AsCurrency[i]      := Params[i].VCurrency^;
      vtString  :      Q.Params.AsString[i]        := string(Params[i].VString^);
      vtPChar   :      Q.Params.AsAnsiString[i]    := Params[i].VPChar^;
      vtAnsiString:    Q.Params.AsAnsiString[i]    := AnsiString(Params[i].VAnsiString);
      vtWideString:    Q.Params.AsUnicodeString[i] := PWideChar(Params[i].VWideString);
      vtVariant   :    Q.Params.AsVariant[i]       := Params[i].VVariant^;
      vtUnicodeString: Q.Params.AsUnicodeString[i] := string(Params[i].VUnicodeString);
      vtPointer :
      begin
        if Params[i].VPointer = nil then
          Q.Params.IsNull[i] := True
        else
          Assert(False, 'not nil pointer is not supported');
      end
    else
      Assert(False, 'not supported');
    end;
end;

您对如何使这种构造成为可能有任何想法吗?我找到了一种方法,通过使用RTTI向Nullable中的Variant添加显式的强制转换操作符覆盖,但这有点棘手,因为它需要在const调用数组中显式强制转换为Variant:

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

...

    FDbController.Execute(
      'update or insert into MY_TABLE(TABLE_ID, OPTIONAL_FIELD) ' +
      'values (?, ?) ' + 
      'matching(TABLE_ID) returning TABLE_ID', [FId, Variant(FOptionalField)],
      procedure (Fields: TSQLResult)
      begin
        FId := Fields.AsInteger[0];
      end;
    end;  

2 个答案:

答案 0 :(得分:5)

正如您所说,在编译器的当前状态下,record参数中不允许array of const

实际上,TVarRec.VType值没有任何vtRecord。

Variant类型本身没有Delphi处理的varRecord类型。 Windows世界中存在这样的变体类型(例如,在COM中将DotNet结构映射到vt_Record类型),但这种变体不是由Delphi处理的。

可能的是将指针传递给记录的typeinfo,然后传递给记录:

case Params[i].VType of
  vtPointer:
  if (i<high(Params)) and (Params[i+1].VType=vtPointer) then
    if Params[i].VPointer = TypeInfo(MyRecord) then begin
      ...
    end;

也许这是一个不太好的答案,因为你使用泛型...但至少是一个开始...在所有情况下,这就是我如何使用预先通用的Delphi编译器。 TypeInfo()关键字已经非常强大,并且比“新”RTTI实现更快。

另一种可能性(与泛型兼容)应该是在记录内容中添加一些记录类型ID。然后将记录作为指针传递,读取ID,并按预期使用它:

case Params[i].VType of
  vtPointer:
  if Params[i].VPointer <> nil then
  case PRecordType(Params[i].VPointer)^ of
  aRecordType: ...
      ...
  end;

这可以通过简单扩展Nullable<T>定义的初始值来实现。

Post-Scriptum:

在ORM中使用记录可能不是最佳解决方案......

class模型比Delphi中的record/object模型更先进,并且因为ORM是关于对象建模的,所以你应该使用最好的OOP模型,恕我直言。依靠类而不是记录,您可以拥有继承,嵌入式类型信息(通过Class方法或仅通过PPointer(aObject)^),可空值和虚拟方法(对于ORM来说非常方便)。即使array of const参数问题也允许直接处理此类vtObject类型,以及专用的类虚方法。基于记录的ORM只是序列化表行的一种方式,而不是使用OOP最佳实践设计应用程序。关于性能,class实例的内存分配与复制记录的潜在问题(_CopyRecord RTL函数 - 与编译器以隐藏方式调用,例如函数参数 - {{}相比,不是问题。 3}})。

例如,在我们的ORM中,我们can be very slow(Delphi不为已发布的记录属性生成RTTI,这是当前类实现的另一种不一致行为)。我们使用“旧”RTTI将handle dynamic array properties, even dynamic arrays of records in properties序列化为JSON或二进制,并且它运行良好。最好的两个对象世界。

答案 1 :(得分:1)

感谢Robert Love的想法,我在Nullable上添加了Implcit Cast给TValue,并将Params: array of const替换为const Params: array of TValue

我现在可以在没有变体投射的情况下传递参数:

procedure TMyTableObject.InternalSave;
begin
  { FDbController is declared and managed in TDbObject, it contains fonction to run queries
    on the database }

  FDbController.Execute(
    'update or insert into MY_TABLE(TABLE_ID, OPTIONAL_FIELD) ' +
    'values (?, ?) ' + 
    'matching(TABLE_ID) returning TABLE_ID', [FId, FOptionalField],
    procedure (Fields: TSQLResult)
    begin
      FId := Fields.AsInteger[0];
    end;
  end;      
end;

MapParams方法已成为:

procedure TDbContext.MapParams(Q: TUIBQuery; const Params: array of TValue);
const
  { maps to CREATE DOMAIN BOOLEAN AS CHAR(1) DEFAULT 'F' NOT NULL CHECK (VALUE IN ('V', 'F')) }
  BOOL_STR: array[Boolean] of string = ('F', 'V'); 
var
  I: Integer;
begin
  Q.Prepare(True);
  for I := 0 to Q.Params.ParamCount - 1 do
  begin
    if Params[I].IsEmpty then
      Q.Params.IsNull[I] := True
    else
      case Q.Params.FieldType[I] of
      uftChar,
      uftVarchar,
      uftCstring:
      begin
        { Delphi Booleans are tkEnumeration in TValue }
        if Params[I].Kind = tkEnumeration then
          Q.Params.AsString[I] := BOOL_STR[Params[I].AsBoolean]
        else
          Q.Params.AsString[I] := Params[I].ToString;
      end;
      uftSmallint,
      uftInteger:          Q.Params.AsSmallint[I] := Params[I].AsInteger;
      uftFloat,
      uftDoublePrecision : Q.Params.AsDouble[I]   := Params[I].AsExtended;
      uftNumeric:          Q.Params.AsCurrency[I] := Params[I].AsCurrency;
      uftTimestamp:        Q.Params.AsDateTime[I] := Params[I].AsExtended;
      uftDate:             Q.Params.AsDate[I]     := Params[I].AsInteger;
      uftTime:             Q.Params.AsDateTime[I] := Params[I].AsInteger;
      uftInt64:            Q.Params.AsInt64[I]    := Params[I].AsInt64;

      uftUnKnown, uftQuad, uftBlob, uftBlobId, uftArray
    {$IFDEF IB7_UP}
     ,uftBoolean
    {$ENDIF}
    {$IFDEF FB25_UP}
     ,uftNull
    {$ENDIF}:       Assert(False, 'type de données non supporté');
     end;
  end;
end;

请注意,我通过要求数据库描述并强制输入参数的类型来强制执行类型检查,并且我颠倒了访问参数的方式,优先查询参数,因此当转换不可能时会引发异常:数据类型少得多在数据库世界中可用,而不是在德尔福世界。