Delphi中的“descending”记录是什么?

8
我知道实际上你不能从记录中获取任何东西,但我不确定如何用一句话概括我的问题。如果你可以,请编辑标题。
在这里,我想做的是创建一个某种通用类型的数组,可以是 X 种类型之一,该数组将被填充具有不同字段的自定义类型(这是重要的)。简单的方法是只创建一个变体记录数组,每个变体都有自己的类型,但显然无法重新声明标识符,如下所示:
GenericRec = Record
  case SubTypeName: TSubTypeName of
    type1name: (SubRec: Type1);
    type2name: (SubRec: Type2);
    ...
    typeNname: (SubRec: TypeN);
  end;

SubRec更改为SubRec1,SubRec2……SubRecN会使引用变得困难,但并非不可能。自从我开始寻找解决上述问题的替代方案以来,类就浮现在我的脑海中。
显而易见的示例是TObject,它的数组可以分配给许多不同的东西。这就是我想要的,但使用记录是不可能的,因为我希望能够将记录保存到文件中,并重新读取它们(也因为我已经熟悉了这方面的内容)。制作一个简单的类不是问题,从那个类派生出后代类来表示我的子类型-我可以做到这一点。但是如何将其写入文件并重新读取呢?这归结为序列化,我不知道该怎么做。据我所了解,这并不容易,类必须是从TComponent中派生出来的。
TMyClass = Class

我像上面那样创建类,有什么区别吗?这不是什么高级的东西,最多只有10个字段,包括一些自定义类型。

暂且先不考虑序列化(因为我需要在这个主题上阅读很多内容),在这里使用类也可能不太合适。

此时,我的选择有哪些?应该放弃记录并尝试使用类吗?还是坚持使用记录,处理变体“限制”会更简单?我非常愿意学习,如果采用类方法可能使我更聪明,我会尝试。我还刚刚研究了 TList (从未使用过),但似乎它与记录结构集成不太好,也许可以做到,但目前可能超出了我的能力范围。我可以接受任何建议。我该怎么办?


1
在基本层面上,对象不仅仅是具有继承概念和可能添加智能(方法)的记录... 我会从类和子类的角度重新思考这个问题。 - fvu
5
通常处理记录的方法是使用一个字节来表示记录类型,在保存时写入该字节标记和记录。在读取时,先读取该字节,然后读取与前一个字节指示的记录类型相对应的字节数。(类似于接受同一结构的不同定义的 API 调用,因此包含一个 cbSize 成员,提供您实际提供的结构的大小。) - Ken White
2
@Ken,Q中的代码是一个数据结构,正如您所描述的那样。 - David Heffernan
1
@DavidHeffernan:不是的,:-) Delphi在处理变体记录方面做得相当好,但不太适合序列化;我描述的是完全不同的东西,可能对早期使用Delphi(甚至Turbo Pascal)的人很熟悉。但没关系,我故意发布了一条评论而不是答案,因为我并没有提供解决方案。 - Ken White
1
@Ken,将记录转换为字符串再转回来就是序列化。这不是唯一的序列化形式,但它是其中一种方式。 - Rob Kennedy
显示剩余5条评论
3个回答

6
你正在混淆序列化和“使用单个BlockWrite调用将所有内容写入磁盘”。无论对象是否是来自于TComponentTPersistent,你都可以对其进行序列化。
虽然一开始使用单个BlockWrite调用写入所有内容看起来很方便,但如果所需的记录类型将存储任何特别有趣的东西(比如字符串、动态数组、接口、对象或其他基于引用或指针的类型),你会很快发现这并不是你想要的。
你还可能发现变体记录不尽如人意,因为你将编写到最低公共分母。你不能在不检查实际包含类型的情况下访问记录中的任何内容,并且即使是最小的数据量的大小也将占用与最大数据类型相同的空间。
问题似乎描述了多态性,因此您可以利用语言已经提供的功能。使用对象的数组(或列表或任何其他容器)。然后,您可以使用虚方法统一地处理它们。如果需要,您可以为记录实现动态分派(例如,为每个记录赋予指向函数的函数指针,该函数知道如何处理该记录的包含数据类型),但最终您可能会发现自己重新发明类。

确实,必须开始接受类的概念,感谢提供建议。另外,我正在使用流来写入一些东西,结合TKBDynamic,我能够毫无问题地读取/写入动态数组和嵌套的动态数组,对于简单的“数据”,例如数组和字符串,它正好符合我的需求,也许不适用于接口/对象等复杂数据类型,但很好用。 - Raith

5
“自然”的处理此类数据的方式是使用一个class,而不是record。这样会更容易处理,无论是在定义时还是在处理实现时:特别是,virtual方法非常强大,可以为特定类型的类自定义进程。然后使用TList/TObjectListTCollection,或者在Delphi的新版本中使用基于泛型的数组来存储列表。
关于序列化,有几种方法可以做到。请参见Delphi: Store data in somekind of structure 在您的特定情况下,困难来自于您正在使用的“variant”类型的记录。我认为主要缺点是编译器将拒绝在“variant”部分设置任何引用计数的变量(例如string)。因此,您只能在此“variant”部分中编写“普通”变量(如integer)。在我看来,这是一个很大的限制,降低了此解决方案的兴趣。
另一种可能性是在记录定义的开头存储记录类型,例如使用 RecType:integer 或更好的是使用 RecType:TEnumerationType ,这将比数字更明确。但您需要手动编写大量代码,并且使用指针,如果您不熟练掌握指针编码,则有点容易出错。
因此,您还可以存储记录的类型信息,可通过 TypeInfo(aRecordVariable)访问。然后,您可以使用 FillChar 将记录内容初始化为零,仅在分配后使用,然后在取消分配后使用以下函数完成记录内容(这是 Dispose()在内部执行的操作,您应该调用它,否则将泄漏内存):
procedure RecordClear(var Dest; TypeInfo: pointer);
asm
  jmp System.@FinalizeRecord
end;

但是这样的实现模式只会重复造轮子!事实上,这就是class是如何实现的:任何TObject实例的第一个元素都是指向其ClassType的指针:
function TObject.ClassType: TClass;
begin
  Pointer(Result) := PPointer(Self)^;
end;

在 Delphi 中还有另一种结构,称为object。它是某种类型的record,但它支持继承-请参见this article。它是 Turbo Pascal 5.5 时代的旧式面向对象编程,已被弃用,但仍然可用。请注意,我在较新版本的 Delphi 上发现了一个奇怪的编译问题:有时,分配在堆栈上的object并不总是初始化。

请查看我们的TDynArray包装器及其相关函数,它能够将任何record内容序列化为二进制或JSON格式。详见Delphi (win32) serialization libraries问题。它适用于变体记录,即使其中包含一个string在其非变体部分中,而普通的“Write/BlockWrite”无法处理引用计数字段。

我正在使用TKBDynamic,从我尝试过的来看,它可以很好地处理变量,但你说得对,我对指针不是很熟悉,所以Remy Lebeau的答案让我望而却步(尽管它可能有效)。我现在采用类的方法,现在只剩下学习如何序列化东西并进行所有那些神奇的虚拟方法操作了,每个人都告诉我这些。 - Raith
@Raith 关于TObject类的序列化,可以看我上面放的链接,即http://stackoverflow.com/questions/7105995/delphi-store-data-in-somekind-of-structure/7107086#7107086 请不要低估我在这个回答中提出的最新建议:使用数据库!现在可以找到轻量级的数据库(如SQlite3或甚至是TClientDataSet)。 - Arnaud Bouchez

2

如果要使用记录来完成这个任务,您需要创建不同的记录类型,并在前面添加一个共同的字段,然后将这些相同的字段放入通用记录中。然后,在需要时,您可以将指向通用记录的指针强制转换为指向特定记录的指针。例如:

type
  PGenericRec = ^GenericRec;
  GenericRec = Record 
    RecType: Integer;
  end;

  PType1Rec = ^Type1Rec; 
  Type1Rec = Record 
    RecType: Integer;
    // Type1Rec specific fields...
  end;

  PType2Rec = ^Type2Rec; 
  Type2Rec = Record 
    RecType: Integer;
    // Type2Rec specific fields...
  end;

  PTypeNRec = ^TypeNRec;
  TypeNRec = Record
    RecType: Integer;
    // TypeNRec specific fields...
  end; 

var
  Recs: array of PGenericRec;
  Rec1: PType1Rec; 
  Rec2: PType2Rec; 
  RecN: PTypeNRec;
  I: Integer;
begin
  SetLength(Recs, 3);

  New(Rec1);
  Rec1^.RecType := RecTypeForType1Rec;
  // fill Rec1 fields ...
  Recs[0] := PGenericRec(Rec1);

  New(Rec2);
  Rec2^.RecType := RecTypeForType2Rec;
  // fill Rec2 fields ...
  Recs[1] := PGenericRec(Rec2);

  New(RecN);
  Rec3^.RecType := RecTypeForTypeNRec;
  // fill RecN fields ...
  Recs[2] := PGenericRec(RecN);

  for I := 0 to 2 do
  begin
    case Recs[I]^.RecType of
      RecTypeForType1Rec: begin
        Rec1 := PType1Rec(Recs[I]);
        // use Rec1 as needed...
      end;
      RecTypeForType1Re2: begin
        Rec2 := PType2Rec(Recs[I]);
        // use Rec2 as needed...
      end;
      RecTypeForTypeNRec: begin
        RecN := PTypeNRec(Recs[I]);
        // use RecN as needed...
      end;
    end;
  end;

  for I := 0 to 2 do
  begin
    case Recs[I]^.RecType of
      RecTypeForType1Rec: Dispose(PType1Rec(Recs[I]));
      RecTypeForType2Rec: Dispose(PType2Rec(Recs[I]));
      RecTypeForTypeNRec: Dispose(PTypeNRec(Recs[I]));
    end;
  end;
end;

关于序列化,你不需要使用TComponent。你可以序列化记录,只需手动完成即可。在写入时,先写出RecType值,然后再写出特定于记录的值。在读取时,先读取RecType值,然后为该值创建适当的记录类型,然后将特定于记录的值读入其中。

interface

type
  PGenericRec = ^GenericRec;
  GenericRec = Record 
    RecType: Integer;
  end;

  NewRecProc = procedure(var Rec: PGenericRec);
  DisposeRecProc = procedure(Rec: PGenericRec);
  ReadRecProc = procedure(Rec: PGenericRec);
  WriteRecProc = procedure(const Rec: PGenericRec);

function NewRec(ARecType: Integer): PGenericRec;
procedure DisposeRec(var Rec: PGenericRec);
procedure ReadRec(Rec: PGenericRec);
procedure WriteRec(const Rec: PGenericRec);

procedure RegisterRecType(ARecType: Integer; ANewProc: NewRecProc; ADisposeProc: DisposeRecProc; AReadproc: ReadRecFunc; AWriteProc: WriteRecProc);

implementation

type
  TRecTypeReg = record
    RecType: Integer;
    NewProc: NewRecProc;
    DisposeProc: DisposeRecProc;
    ReadProc: ReadRecProc;
    WriteProc: WriteRecProc;
  end;

var
  RecTypes: array of TRecTypeReg;

function NewRec(ARecType: Integer): PGenericRec;
var
  I: Integer;
begin
  Result := nil;
  for I = Low(RecTypes) to High(RecTypes) do
  begin
    with RecTypes[I] do
    begin
      if RecType = ARecType then
      begin
        NewProc(Result);
        Exit;
      end;
    end;
  end;
  raise Exception.Create('RecType not registered');
end;

procedure DisposeRec(var Rec: PGenericRec);
var
  I: Integer;
begin
  for I = Low(RecTypes) to High(RecTypes) do
  begin
    with RecTypes[I] do
    begin
      if RecType = Rec^.RecType then
      begin
        DisposeProc(Rec);
        Rec := nil;
        Exit;
      end;
    end;
  end;
  raise Exception.Create('RecType not registered');
end;

procedure ReadRec(var Rec: PGenericRec);
var
  LRecType: Integer;
  I: Integer;
begin
  Rec := nil;
  LRecType := ReadInteger;
  for I = Low(RecTypes) to High(RecTypes) do
  begin
    with RecTypes[I] do
    begin
      if RecType = LRecType then
      begin
        NewProc(Rec);
        try
          ReadProc(Rec);
        except
          DisposeProc(Rec);
          raise;
        end;
        Exit;
      end;
    end;
  end;
  raise Exception.Create('RecType not registered');
end;

procedure WriteRec(const Rec: PGenericRec);
var
  I: Integer;
begin
  for I = Low(RecTypes) to High(RecTypes) do
  begin
    with RecTypes[I] do
    begin
      if RecType = Rec^.RecType then
      begin
        WriteInteger(Rec^.RecType);
        WriteProc(Rec);
        Exit;
      end;
    end;
  end;
  raise Exception.Create('RecType not registered');
end;

procedure RegisterRecType(ARecType: Integer; ANewProc: NewRecProc; ADisposeProc: DisposeRecProc; AReadproc: ReadRecFunc; AWriteProc: WriteRecProc);
begin
  SetLength(RecTypes, Length(RecTypes)+1);
  with RecTypes[High(RecTypes)] do
  begin
    RecType := ARecType;
    NewProc := ANewProc;
    DisposeProc := ADisposeProc;
    ReadProc := AReadProc;
    WriteProc := AWriteProc;
  end;
end;

end.

.

type
  PType1Rec = ^Type1Rec; 
  Type1Rec = Record 
    RecType: Integer;
    Value: Integer;
  end;

procedure NewRec1(var Rec: PGenericRec);
var
  Rec1: PType1Rec;
begin
  New(Rec1);
  Rec1^.RecType := RecTypeForType1Rec;
  Rec := PGenericRec(Rec1);
end;

procedure DisposeRec1(Rec: PGenericRec);
begin
  Dispose(PType1Rec(Rec));
end;

procedure ReadRec1(Rec: PGenericRec);
begin
  PType1Rec(Rec)^.Value := ReadInteger;
end;

procedure WriteRec1(const Rec: PGenericRec);
begin
  WriteInteger(PType1Rec(Rec)^.Value);
end;

initialization
  RegisterRecType(RecTypeForType1Rec, @NewRec1, @DisposeRec1, @ReadRec1, @WriteRec1);

.

type
  PType2Rec = ^Type2Rec; 
  Type2Rec = Record 
    RecType: Integer;
    Value: Boolean;
  end;

procedure NewRec2(var Rec: PGenericRec);
var
  Rec2: PType2Rec;
begin
  New(Rec2);
  Rec2^.RecType := RecTypeForType2Rec;
  Rec := PGenericRec(Rec2);
end;

procedure DisposeRec2(Rec: PGenericRec);
begin
  Dispose(PType2Rec(Rec));
end;

procedure ReadRec2(Rec: PGenericRec);
begin
  PType2Rec(Rec)^.Value := ReadBoolean;
end;

procedure WriteRec2(const Rec: PGenericRec);
begin
  WriteBoolean(PType2Rec(Rec)^.Value);
end;

initialization
  RegisterRecType(RecTypeForType2Rec, @NewRec2, @DisposeRec2, @ReadRec2, @WriteRec2);

.

type
  PTypeNRec = ^Type2Rec; 
  TypeNRec = Record 
    RecType: Integer;
    Value: String;
  end;

procedure NewRecN(var Rec: PGenericRec);
var
  RecN: PTypeNRec;
begin
  New(RecN);
  RecN^.RecType := RecTypeForTypeNRec;
  Rec := PGenericRec(RecN);
end;

procedure DisposeRecN(Rec: PGenericRec);
begin
  Dispose(PTypeNRec(Rec));
end;

procedure ReadRecN(Rec: PGenericRec);
begin
  PTypeNRec(Rec)^.Value := ReadString;
end;

procedure WriteRecN(const Rec: PGenericRec);
begin
  WriteString(PTypeNRec(Rec)^.Value);
end;

initialization
  RegisterRecType(RecTypeForTypeNRec, @NewRecN, @DisposeRecN, @ReadRecN, @WriteRecN);

.

var
  Recs: array of PGenericRec;

procedure CreateRecs;
begin
  SetLength(Recs, 3);

  NewRec1(Recs[0]);
  PRecType1(Recs[0])^.Value : ...;

  NewRec2(Recs[1]);
  PRecType2(Recs[1])^.Value : ...;

  NewRecN(Recs[2]);
  PRecTypeN(Recs[2])^.Value : ...;
end;

procedure DisposeRecs;
begin
  for I := 0 to High(Recs) do
    DisposeRec(Recs[I]);
  SetLength(Recs, 0);
end;

procedure SaveRecs;
var
  I: Integer;
begin
  WriteInteger(Length(Recs));
  for I := 0 to High(Recs) do
    WriteRec(Recs[I]);
end;

procedure LoadRecs;
var
  I: Integer;
begin
  DisposeRecs;
  SetLength(Recs, ReadInteger);
  for I := 0 to High(Recs) do
    ReadRec(Recs[I]);
end;

1
你刚刚重新发明了class的实现方式!一个TObject实例以指向其ClassType的指针开头:与你的RecType非常相似。而且一次分配一个记录并存储指针,正是class模型所做的。在我看来,这里根本没有任何意义:手动完成这项工作很痛苦,需要编写大量代码才能模拟编译器自己执行的操作。你最好将记录的TypeInfo(...)存储在记录开头的整数标志中,然后使用低级别的RTTI记录初始化功能(例如InitializeRecord)。 - Arnaud Bouchez
在我看来,使用枚举而不是整数来表示“RecType”可能会使代码更易读。 - Arnaud Bouchez
我并没有说这会很漂亮。对于这种情况,类和虚方法确实更有效。我选择整数是因为它对于动态注册更加灵活。 - Remy Lebeau
你是对的。更好的灵活性将通过在记录中存储 TypeInfo() 指针 而不是 integer,并使用基于 RTTI 的记录初始化和清理来实现,使用此 TypeInfo() - 请参见我的答案中的 RecordClear()(可以使用 FillChar 进行初始化,或者在调用 SetLength() 时使用动态数组进行初始化)。 - Arnaud Bouchez

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接