如何保存/加载类型集?

20

我有这段代码

type
  TXSample = (xsType1, xsType2, xsType3, xsType4, xsType5, xsType6, xsType6, xsTyp7, xsType8); // up to FXSample30;  
..

private
  FXSample = Set of TXSample;  
..

published
  property Sample: TXSample read FXSample  write FXSample; 
..

  //if Sample has a value of
  Sample := [xsType2, xsType4, xsType5, xsType6, xsTyp7];

如何保存/加载Sample的属性?
我想将它保存在数据库中。
这可能吗?


你可以有一堆布尔字段。或者你可以使用每个标志一个比特将其压缩为整数或多个整数。我会使用前者。 - David Heffernan
2
请查看Classes.pas中的TReader.ReadSetTWriter.WriteProperty中的WriteSet。这是VCL将属性与dfm文件流入和流出的方式。如果您使用这些,您需要在数据库中存储一个字符串字段来存储写出的集合。请注意,这种方法会使读取/写入比将值存储在单独的布尔字段中更容易,但使用SQL过滤数据集可能会更困难。 - Marjan Venema
1
不是说这样做对错,但为什么你不先定义一个名为 TXSamples = set of TXSample 的类型,然后再声明 FXSample: TXSamples 呢?我有点困惑你为什么要在私有字段中这样声明它。最好还是创建另一个类型 TXSamples 并将其用于 FXSample... 至少我一直是这样看待并实践的。 - Jerry Dodge
@Jerry Dodge,抱歉如果不太清楚。我为问题进行了详细编辑。 - XBasic3000
@XBasic3000,虽然没有什么不清楚的地方,但我强烈建议为这个集合定义一个类型,而不是每次使用时都进行转换。 - Jerry Dodge
请尝试此链接:https://dev59.com/AEXRa4cB1Zd3GeqPv_OD - Fabricio Araujo
10个回答

29

假设你的集合不超过32个可能性 (Ord(High(TXSample)) <= 31),那么将集合强制转换为 Integer 再转回来是完全没有问题的:

type
  TXSamples = set of TXSample;
var 
  XSamples: TXSamples;
begin
  ValueToStoreInDB := Integer(XSamples);
  Integer(XSamples) := ValueReadFromDB;
end;

更具体地说:SizeOf(TXSamples) 必须精确等于 SizeOf(StorageTypeForDB)。 因此,将 TXSamples 强制类型转换为以下类型时,适用以下范围:

  • Byte: Ord(High(TXSample)) < 8
  • Word: 8 <= Ord(High(TXSample)) < 16
  • Longword: 16 <= Ord(High(TXSample)) < 32
  • UInt64: 32 <= Ord(High(TXSample)) < 64

这真的能行吗?当将集合转换为整数时,我收到了E2089无效类型转换错误。 - dan-gph
1
@Dangph 是的。你检查了最大集合大小/枚举边界吗?(例如,当你的集合由具有16个可能性的枚举组成时,你必须将集合转换为Word。当最大集合大小为17时,你必须转换为Integer。) - NGLN
我明白了,谢谢。我需要将其转换为Byte,而不是Integer。我一直以为编译器会自动将值扩展为Integer。 - dan-gph
非常感谢,这真的很酷,我使用以下代码将8行压缩成了一行:CustomFont.Style := TFontStyles(Byte( (ord(fsBold) and (Ord(isBold) shl Byte(ord(fsBold)))) or (ord(fsItalic) and (Ord(isItalic) shl Byte(ord(fsItalic)))) or (ord(fsStrikeOut) and (Ord(isStrikeout) shl Byte(ord(fsStrikeOut)))) or (ord(fsUnderline) and (Ord(isUnderline) shl Byte(ord(fsUnderline))))) ); 其中isBold等是布尔值。 - Peter Turner
1
在32位Delphi中,对于32到64之间的大小,您无法强制转换为UInt64。请参阅我在此处的答案,了解32位和64位之间的区别。https://dev59.com/h4vda4cB1Zd3GeqPUAra - Graymatter

7
在Delphi中,直接将一个集合变量强制转换是不可能的,但是Delphi在内部将集合存储为字节值。通过使用无类型移动,可以轻松地将其复制到整数中。请注意,这些函数仅限于32(整数的边界)。要增加边界,请改用Int64
function SetToInt(const aSet;const Size:integer):integer;
begin
  Result := 0;
  Move(aSet, Result, Size);
end;

procedure IntToSet(const Value:integer;var aSet;const Size:integer);
begin
  Move(Value, aSet, Size);
end;

演示

type
  TMySet = set of (mssOne, mssTwo, mssThree, mssTwelve=12);
var
  mSet: TMySet;
  aValue:integer;
begin
  IntToSet(7,mSet,SizeOf(mSet));
  Include(mSet,mssTwelve);
  aValue := SetToInt(mSet, SizeOf(mSet));
end;

最好在某个地方进行测试,以确保 sizeof(TMySet) = sizeof(integer),以防您的集合过小或过大。 - dan-gph

3

个人而言,我会将集合转换为整数,并将其存储在数据库中作为INT字段,就像其他人建议的那样。@teran建议使用TIntegerSet类型,这里是我的方法,使用位运算处理原生整数。

请注意,您可以使用SampleInInteger()来确定由SampleSetToInteger()生成的整数掩码中是否存在枚举中的某个元素。

以下是代码:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  { .: TXSample :. }
  TXSample = (xsType1 = 0, xsType2, xsType3, xsType4, xsType5,
    xsType6, xsType7, xsType8); // up to FXSample30;
  TXSampleSet = set of TXSample;

// Converts a TXSampleSet to an integer.
function SampleSetToInteger(const S: TXSampleSet): Integer;
var
  Sample: TXSample;
begin
  Result := 0;

  for Sample := Low(TXSample) to High(TXSample) do
    if (Sample in S) then
      Result := Result or (1 shl Ord(Sample));
end;

// Converts an integer to TXSampleSet.
function IntegerToSampleSet(const Int: Integer): TXSampleSet;
var
  I: Integer;
begin
  Result := [];

  for I := 0 to Ord(High(TXSample)) do
    if Int and (1 shl I) <> 0 then
      Result := Result + [TXSample(I)];
end;

// Checks if a TXSample is present in the integer.
function SampleInInteger(const S: TXSample; const Int: Integer): Boolean;
begin
  Result := Int and (1 shl Ord(S)) <> 0;
end;

var
  XSample, XSample1: TXSampleSet;
  Tmp: Integer;
begin
  XSample := [xsType2, xsType4, xsType5, xsType6, xsType7];
  XSample1 := [xsType1];
  Tmp := SampleSetToInteger(XSample);

  Writeln(Tmp);
  XSample1 := IntegerToSampleSet(Tmp);
  if (xsType5 in XSample1) then
    Writeln('Exists');
  if (SampleInInteger(xsType1, Tmp)) then
    Writeln('Exists in int');


  Readln;
end.

3
一个Delphi集合就是一组(可能)相关布尔标志。每个布尔标志对应于匹配的序数值是否在集合中。
你可以通过将集合表示为位集来将集合打包成整数值。或者您可以创建一个文本表示形式的集合。
然而,这两个选项都无法让您在SQL级别上查询数据库。因此,我建议您将集合中的每个值,即每个布尔标志,表示为数据库表的单独字段(即列)。这为您提供了最强大的数据表示形式。

这要看情况。如果集合枚举中有超过20个元素,为每个元素创建一列可能只是浪费资源。我个人会最多为5个元素创建列。对于更多的元素,我会在SQL查询中使用位运算符(虽然不太方便),或者对于那些了解SQL但无法使用位运算符的人,我会创建表视图。 - TLama

2

将set存储到数据库中最简单的方法(正如@DavidHeffernan在评论中提到的),是将set转换为位掩码。 在int32(整数)值中,您有32个位,并且可以保存set最多达32个字段; Delphi有TIntegerSet类型(请参见http://docwiki.embarcadero.com/Libraries/en/System.SysUtils.TIntegerSet),该类型在SysUtils中定义。它被声明为:

TIntegerSet = set of 0..SizeOf(Integer) * 8 - 1;

因此,使用它,将集合转换为整数并进行反向转换非常简单(只需将TIngeterSet强制转换为整数或反之亦然);

位掩码也是一个不错的选择,因为它是数据库表中唯一的一个INT字段。

您还可以在数据库中创建单独的表来存储集合内容(主表(id,...)和setValuesTable(main_id,setElementValue))(这个选项适用于在数据库查询中使用)。

以下是使用TIntegerSet的示例:

program Project1;
{$APPTYPE CONSOLE}
uses System.SysUtils;

type
    TXSample = (xsType1, xsType2, xsType3, xsType4, xsType5, xsType6,  xsType7, xsType8);
    TSampleSet = set of TXSample;



    function SampleSetToInteger(ss : TSampleSet) : integer;
    var intset : TIntegerSet;
        s : TXSample;
    begin
        intSet := [];
        for s in ss do
            include(intSet, ord(s));

        result := integer(intSet);
    end;

    function IntegerToSampleSet(mask : integer) : TSampleSet;
    var intSet : TIntegerSet;
        b : byte;
    begin
        intSet := TIntegerSet(mask);
        result := [];
        for b in intSet do
            include(result, TXSample(b));
    end;

var xs : TSampleSet;
    mask : integer;
begin
    xs := [xsType2, xsType6 .. xsType8];

    mask := SampleSetToInteger(xs);     //integer mask
    xs := IntegerToSampleSet(mask);
end.

2

可以将设置变量成功保存到TStream后代。以下是一个例子。

只需创建一个新的vcl表单应用程序,添加两个TButton组件,并在每个按钮的OnClick事件中填写如下示例所示即可。

这是在XE4中创建的,因此对于其他版本的Delphi,uses子句可能不同,但通过在uses子句中删除每个单元之前的名称空间指示器来更改应该是微不足道的。使用Delphi轻松地将带有关节值的集类型变量保存到二进制文件中。换句话说,

还建议查看TypInfo单元(如果您拥有源代码)或仅使用提供的函数,这使得分解Set类型到其文本表示变得相当简单,尽管此处未提供任何示例。如果您想将其保存到配置或ini文件中或以可编辑的持久性格式保存,则建议这样做。

下面这个是我知道的最简单的方法。查看保存到像下面这样的流中的集合类型的二进制输出意味着它基于其大小保存在最小可能的位图表示中。下面的映射到磁盘上的一个字节(值为5),这意味着必须将每个值映射到2的幂(seThis = 1,seThat = 2,seTheOther = 4),就像手动创建的常量位掩码值一样。编译器可能会强制执行它遵循强制使集保留其序数的规则。此示例已在Delphi XE4中进行了测试,运行正常。

希望对您有所帮助。

Brian Joseph Johns

unit Unit1;

interface

uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
     Vcl.StdCtrls;

type
  TSomeEnum = (seThis, seThat, seTheOther);
  TSomeEnumSet = set of TSomeEnum;

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var

  Form1: TForm1;
  SomeSetVar: TSomeEnumSet;
  SomeBoolean: Boolean;
  SomeInt: Integer;

implementation

{$R *.dfm}


procedure TForm1.Button1Click(Sender: TObject);
begin
  SomeSetVar := [seThis, seTheOther];
  SomeBoolean := True;
  SomeInt := 31415;

  with TFileStream.Create('SetSave.bin',fmCreate or fmOpenWrite or fmShareCompat) do
  try
    Write(SomeSetVar,SizeOf(SomeSetVar));
    Write(SomeBoolean,SizeOf(Boolean));
    Write(SomeInt,SizeOf(Integer));
  finally
    Free;
  end;
  SomeSetVar := [];
  SomeInt := 0;
  SomeBoolean := False;

end;

procedure TForm1.Button2Click(Sender: TObject);
var
  ResponseStr: string;
begin
  with TFileStream.Create('SetSave.bin',fmOpenRead or fmShareCompat) do
  try
    Read(SomeSetVar,SizeOf(SomeSetVar));
    Read(SomeBoolean,SizeOf(Boolean));
    Read(SomeInt,SizeOf(Integer));
  finally
    Free;
  end;

  ResponseStr := 'SomeSetVar = ';
  if (seThis in SomeSetVar) then
    ResponseStr := ResponseStr + 'seThis ';

  if (seThat in SomeSetVar) then
    ResponseStr := ResponseStr + 'seThat ';

  if (seTheOther in SomeSetVar) then
    ResponseStr := ResponseStr + 'seTheOther ';

  ResponseStr := ResponseStr + ' SomeBoolean = ' + BoolToStr(SomeBoolean);

  ResponseStr := ResponseStr + ' SomeInt = ' + IntToStr(SomeInt);

  ShowMessage(ResponseStr);

end;

end.

1

通过一点RTTI的帮助,可以以通用的方式实现:

program SetConverter;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  System.RTTI, System.SysUtils;

type
  SetConverter<T> = class abstract
  strict private
    class var FRttiContext: TRttiContext;
  public
    class function ToInteger(aSet: T): Integer;
    class function FromInteger(aValue: Integer): T;
  end;

  { SetConverter<T> }

class function SetConverter<T>.FromInteger(aValue: Integer): T;
var
  ResultValues: TIntegerSet;
  ReturnType: TRttiType;
  SetValues: TIntegerSet;
  EnumSet: T absolute SetValues;
begin
  ReturnType := FRttiContext.GetType(Self).GetMethod('FromInteger').ReturnType;

  if not((ReturnType is TRttiSetType) and (TRttiSetType(ReturnType).ElementType is TRttiEnumerationType)) then
    Exit;

  SetValues := TIntegerSet(aValue);
  Result := EnumSet;
end;

class function SetConverter<T>.ToInteger(aSet: T): Integer;
var
  RttiParameter: TRttiParameter;
  ResultValues: TIntegerSet;
  SetValues: TIntegerSet;
  EnumSet: T absolute SetValues;
  EnumType: TRttiEnumerationType;
  SetType: TRttiSetType;
  i: Integer;
begin
  Result := 0;
  RttiParameter := FRttiContext.GetType(Self).GetMethod('ToInteger').GetParameters[0];
  if not(RttiParameter.ParamType is TRttiSetType) then
    Exit;

  SetType := RttiParameter.ParamType as TRttiSetType;

  if not(SetType.ElementType is TRttiEnumerationType) then
    Exit;

  EnumType := SetType.ElementType as TRttiEnumerationType;

  EnumSet := aSet;

  ResultValues := [];

  for i := EnumType.MinValue to EnumType.MaxValue do
    if i in SetValues then
      Include(ResultValues, i);

  Result := Integer(ResultValues);
end;

type
  TXSample = (xsType1, xsType2, xsType3, xsType4, xsType5, xsType6, xsType7, xsType8);
  TSampleSet = set of TXSample;

var
  Before, After: TSampleSet;
  i: Integer;
begin
  Before := [xsType2, xsType6 .. xsType8];
  i := SetConverter<TSampleSet>.ToInteger(Before);
  After := SetConverter<TSampleSet>.FromInteger(i);

  WriteLN('Before = After: ' + (Before = After).ToString(TUseBoolStrs.True));
  Readln;
end.

0

最简单的解决方案 - 直接将集合作为数字变量进行处理。 "absolute" 是一个关键字:

procedure Foo(FXSample: TFXSample);
var
  NumericFxSample: Byte absolute FXSample;
begin
WriteLn(YourTextFile, NumericFxSample);//numeric value from a set
end;

如果您的类型宽度超过8位,则需要使用更宽的数字类型,如字(最多16个项目)或双字。


0

你可以使用这个单元将集合转换为整数。如果你需要更多的settoint函数,你可以通过查看下面的代码添加自己的函数。

集合可能只占用1个字节的内存空间。 所以你可以获得yourSet的大小,并以此结果的模数作为结果。

例子:你的集合大小:1个字节,你可以得到结果 -->

Result := pINT^ mod maxVal

你应该通过计算变量类型的最大值来获得maxval。

maxVal = Power(2, (8*sizeof(MySet)-1))

    unit u_tool;

interface
uses Graphics;

type
  TXSample = (xsType1, xsType2, xsType3, xsType4, xsType5, xsType6, xsType6, xsTyp7, xsType8); // up to FXSample30;
  FXSample = Set of TXSample;

  function FXSampleToInt(FXSample: FXSample ): Integer;
  function IntToFXSample(Value: Integer): FXSample;


  function FontStyleToInt(FontStyle: TFontStyles ): Integer;
  function IntToFontStyle(Value: Integer): TFontStyles;

implementation


function FXSampleToInt(FXSample: FXSample ): Integer;
var
  pInt: PInteger;
begin
  pInt := @FXSample;
  Result := pInt^;
end;

function IntToFXSample(Value: Integer): FXSample;
var
  PFXSample: ^FXSample;
begin
  PFXSample := @Value;
  Result := PFXSample^;
end;





function FontStyleToInt(FontStyle: TFontStyles ): Integer;
var
  pInt: PInteger;
begin
  pInt := @FontStyle;
  Result := pInt^;
end;

function IntToFontStyle(Value: Integer): TFontStyles;
var
  PFontStyles: ^TFontStyles;
begin
  PFontStyles := @Value;
  Result := PFontStyles^;
end;






end.

0

或者我们可以让编译器完全忘记类型,然后定义它应该看到什么(如果我们在编译时知道它应该看到什么)。这个解决方案非常糟糕,因为它可以写成一行。

type
  // Controls.TCMMouseWheel relies on TShiftState not exceeding 2 bytes in size 
  TShiftState = set of (ssShift, ssAlt, ssCtrl,
                        ssLeft, ssRight, ssMiddle, 
                        ssDouble, ssTouch, ssPen, 
                        ssCommand, ssHorizontal); 

var 
  Shifts : TShiftState;
  Value :  Integer;
begin
  Shifts := TShiftState((Pointer(@Value))^):

  Value  := (PInteger(@Shifts))^;

  if ssShift in TShiftState((Pointer(@Value))^) then 
     Exit;
end;

有时候未使用的(顶部)位被设置(或未设置),但它对于set操作(in=+-* ..)没有影响。

在Delphi中的这行代码:

Shifts := TShiftState((Pointer(@Value))^);

在汇编语言中(Delphi XE6),就像这样:

lea eax,[ebp-$0c]
mov ax,[eax]
mov [ebp-$06],ax

在 Delphi 2007 中(其中 TShiftState 更小,因此可以使用 Byte),这个汇编代码:
movzx eax,[esi]
mov [ebp-$01],al

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