查看文章
 
变体结构的使用(转)
2009-06-24 22:04

变体结构也就是变体记录, 是一个比较复杂的概念. 专家不提倡使用.
兴趣所致, 我想把它弄明白.

一个最大的无符号整数(Cardinal)是 4294967295, 它的大小是 4 字节, 它的二进制表示是:
11111111 11111111 11111111 11111111

它的低字节的值是 11111111, 也就是十进制的 255


//测试: var c: Cardinal; begin c := 4294967295; ShowMessage(IntToStr(Lo(c))); {会显示: 255; Lo 是获取低字节值的函数} end;

一个 Byte 类型的最大值是 255, 它的大小是 1 个字节, 用二进制表示是:
11111111

假如把一个 Cardinal 类型的值赋给一个 Byte 类型的值, Byte 将只取 Cardinal 的最低字节.


//测试: var c: Cardinal; b: Byte; begin c := 4294967295; b := c; ShowMessage(IntToStr(b)); {255} c := 258; {二进制表示: 00000000 00000000 00000001 00000010} b := c; {b 将只获取: 00000010} ShowMessage(IntToStr(b)); {2} end;

这是我们能否会想到, 在结构会储存时, 一个可以储存 Cardinal 的空间, 当然也能得放下一个 Byte 值;
如果这个值非此即彼, 我们完全不需要两个储存空间.
我猜测, 这应该是 Delphi 设计变体记录的初衷.


//假如有这样一个员工登记表
type
  TpersonRec = record
    ID: Integer;            {员工编号}
    case Boolean of         {根据分类}
      True:  (A: Cardinal); {如果是股东, 登记年薪}
      False: (B: Word);     {如果不是,   登记日薪}
  end;
var
  personRec: TpersonRec;
begin
  {先算一算这个结构的大小:
    ID 是 Integer  类型, 应该是   4  字节大小;
    A  是 Cardinal 类型, 也应该是 4  字节大小;
    B  是 Word     类型, 应该是   2  字节大小;
    合计为                        10 个字节.
  }
  {可事实, TpersonRec 只有 8 个字节}
  ShowMessage(IntToStr(SizeOf(TpersonRec))); {8}

  {
    原因是: 字段 A 和 字段 B 公用了一个储存空间;
    当然这个储存空间得依着大的, 是 Cardinal 的尺寸 4 个字节.
  }

  //赋值测试:
  personRec.ID := 110;
  personRec.A  := 100000; {一看就知道是个股东}

  //取值:
  ShowMessage(IntToStr(personRec.A)); {100000; 这不可能有错, 十万大洋}

  //但是:
  ShowMessage(IntToStr(personRec.B)); {34464 ?! 难道这是工人的日薪吗?}
  {
    首先, A 和 B 两个字段占用同一个空间, 给其中一个赋值, 另一个当然也就有值了;
    但因为数据类型的容量不同, 它们的值有可能是不一样的.
    在很多情况下, 我们可能根本不去理会另一个值, 但如果的确需要呢?
    看下一个例子:
  }
end;

type TpersonRec = record ID: Integer; case tag: Boolean of {在这里加了一个 tag 变量} True: (A: Cardinal); False: (B: Word); end; var personRec: TpersonRec; begin {我们可以用 tag 变量来区分, 记录中变体部分的值到底是谁的, 譬如:} personRec.ID := 110; personRec.tag := True; personRec.A := 100000; {股东的的年薪} personRec.ID := 111; personRec.tag := False; personRec.B := 100; {工人的日薪} end;
//最经典的变体结构莫过于 Delphi 定义的 TMessage 结构了, 两组数据分分合合都是一体, 多么巧妙啊!
TMessage = packed record Msg: Cardinal; case Integer of 0: ( WParam: Longint; LParam: Longint; Result: Longint); 1: ( WParamLo: Word; WParamHi: Word; LParamLo: Word; LParamHi: Word; ResultLo: Word; ResultHi: Word); end;

假如有这样三种结构, 分别来描述: 直线、圆与三角形:


type
  {描述直线的结构}
  TLine = packed record
    ID: Integer;    {编号}
    x1,y1: Integer; {第一点}
    x2,y2: Integer; {第二点}
  end;

  {描述圆的结构}
  TCircle = packed record
    ID: Integer;  {编号}
    x,y: Integer; {中心点}
    r: Integer;   {半径}
  end;

  {描述三角形的结构}
  TTriangle = packed record
    ID: Integer;    {编号}
    xa,ya: Integer; {a点}
    xb,yb: Integer; {b点}
    xc,yc: Integer; {c点}
  end;

//无需 SizeOf, 我们可以轻松看出三个结构的大小分别是: 20、16、28 字节


上面三个结构的数据有类似之处, 在实用中常常需要用一个综合的结构替代它们;
这就像 Delphi 中的 TWMKey、TWMMouse、TWMClose 等近 200 个结构都可以用 TMessage 代替一样.

下面是一个非常不好, 但容易理解的描述:


TMyShape = packed record
    ID: Integer;
    x1, y1, x2, y2: Integer;
    x, y, r: Integer;
    xa, ya, xb, yb, xc, yc: Integer;
  end;

//此结构大小是 56 字节
//之所以说它不好是因浪费太多, 譬如记录一个圆, 只需要 ID、x、y、r 四个字段, 会浪费其它 10 个字段.
//但如果需要一个结构同时描述一条直线、一个圆、一个三角的话, 这个结构是合适的; 这里讨论的并不是这种情况.


我们在某一时刻只需要它来表示一个形状(或者是直线、或者是圆、或者是三角);
假如我们让结构按照最大的需要分配空间, 譬如三角需要最多(除 ID 外, 是 6*4 个字节), 就分配 24 字节; 这个空间用来记录一个圆或者直线也是足够的.

Delphi 允许我们使用这样的语法来定义(这就是所谓的变体结构):


TMyShape = packed record
    ID: Integer;
    case Integer of
      0: (x1, y1, x2, y2: Integer);
      1: (x, y, r: Integer);
      2: (xa, ya, xb, yb, xc, yc: Integer); {注意结构成员无论如何是不能重名的}
  end;

//可以这样查看一下: ShowMessage(IntToStr(SizeOf(TMyShape))); 
//它刚好是 28 字节, 和上面的 TTriangle 大小一样.
//用这个结构可以非常方便地描述上面三种图形; 尽管有时也会有浪费, 但相比在使用时带来的方便, 那是可以接受的.


把上面这种描述用表格表示一下:

共 28 个字节
用于三角时 ID xa ya xb yb xc yc
用于直线时 ID x1 y1 x2 y2
用于圆时 ID x y r

对这样的一个结构变量(譬如是 rec: TMyShape)来讲, 不管你是不是需要, rec.ID、rec.x1 ... rec.yc 等所有结构成员都是存在的;
但有些数据是共享一块内存, 譬如 x1、x、xa 共享 4 个字节; x2、r、xb 共享 4 个字节, 这可以测试一下:


type
  TMyShape = packed record
    ID: Integer;
    case Integer of
      0: (x1, y1, x2, y2: Integer);
      1: (x, y, r: Integer);
      2: (xa, ya, xb, yb, xc, yc: Integer);
  end;
var
  rec: TMyShape;
begin
  rec.r := 123;
  ShowMessageFmt('%d, %d', [rec.x2, rec.xb]); {123, 123}
end;

这里的 case 并不是咱们经常用的 case 语句(譬如它没有 end;), 只是 Delphi 的语法规定而已;
case Integer of 中的 Integer 也没有再占用 4 字节的空间(但接下了的例子会占用), 也只是语法形式.

考虑另一个问题: 我们仅从一个结构变量的数据能看出它具体描述的图形类型吗?
应该说: 这不容易看出; 其实, 在实用中我们很少会有这种要求.

假如非要从结构数据中识别图形类型也可以, Delphi 提供了语法支持, 但这又要多占几个字节:

TMyShape = packed record
    ID: Integer;
    case flag: Integer of
      0: (x1, y1, x2, y2: Integer);
      1: (x, y, r: Integer);
      2: (xa, ya, xb, yb, xc, yc: Integer);
  end;

//此时的结构大小应该是 32 字节.
//像这样, flag 也是一个结构成员, 也可以读写; 我们可以给 flag 赋不同的值以区别图形类型.
//但不管 flag 这个值是什么, 都不会影响前面的内存共享机制; flag 也不会因为其他赋值而自动改变.


根据不同的需要, 上面的 flag 也可以是其他类型(譬如枚举);
我也经常见到 case Boolean of 的用法, 这只能用在两种可选状态的情形, 譬如:


TMyShape = packed record
    ID: Integer;
    case Boolean of
      True:  (x1, y1, x2, y2: Integer);
      False: (x, y, r: Integer);
  end;

//例子中的三个变量让 absolute 都给弄到一块了, 这有时会很方便; 但我总也想不到用, 还是不熟.
procedure TForm1.FormCreate(Sender: TObject);
var
  num: Int64;
  pt: TPoint absolute num;
  arr: array[0..1] of Integer absolute pt;
begin
  pt.X := 111;
  pt.Y := 222;

  ShowMessageFmt('%d, %d', [arr[0], arr[1]]);                {111, 222}
  ShowMessageFmt('%d, %d', [num shl 32 shr 32, num shr 32]); {111, 222}
end;



absolute 可让变量共享内存; 共享内存的其中一个变量赋值后, 其他变量会同时改变.

如果不用 absolute, 使用类型转换也可以, 不过这样更方便.

另外, Delphi 的可变记录、变体类型和 C 语言的联合也都是这个原理, 不如看个表更直观:

Int64: 1 2 3 4 5 6 7 8
TPoint: X Y
本例数组: arr[0] arr[1]
Integer: 1 2 3 4
Word: 1 2
Byte: 1

类别:编程资源-winapi||添加到搜藏 |分享到i贴吧|浏览(256)|评论 (0)
 
最近读者:
 
网友评论:
发表评论:
姓 名:
网址或邮箱: (选填)
内 容:
     

   
帮助中心 | 空间客服 | 投诉中心 | 空间协议
©2012 Baidu