【问题标题】:How to cast a record's attributes to integer and back?如何将记录的属性转换为整数并返回?
【发布时间】:2017-05-14 08:38:53
【问题描述】:

我用隐式运算符编写了以下记录类型,以在此记录类型和字符串之间进行转换。它代表一个standard weather code,简要描述了当前的天气状况:

type    
  TDayNight = (dnDay, dnNight);
  TCloudCode = (ccClear = 0, ccAlmostClear = 1, ccHalfCloudy = 2, ccBroken = 3,
    ccOvercast = 4, ccThinClouds = 5, ccFog = 6);
  TPrecipCode = (pcNone = 0, pcSlight = 1, pcShowers = 2, pcPrecip = 3, pcThunder = 4);
  TPrecipTypeCode = (ptRain = 0, ptSleet = 1, ptSnow = 2);

  TWeatherCode = record
  public
    DayNight: TDayNight;
    Clouds: TCloudCode;
    Precip: TPrecipCode;
    PrecipType: TPrecipTypeCode;
    class operator Implicit(const Value: TWeatherCode): String;
    class operator Implicit(const Value: String): TWeatherCode;
    function Description: String;
    function DayNightStr: String;
  end;

implementation

{ TWeatherCode }

class operator TWeatherCode.Implicit(const Value: TWeatherCode): String;
begin
  case Value.DayNight of
    dnDay:    Result:= 'd';
    dnNight:  Result:= 'n';
  end;
  Result:= Result + IntToStr(Integer(Value.Clouds));
  Result:= Result + IntToStr(Integer(Value.Precip));
  Result:= Result + IntToStr(Integer(Value.PrecipType));
end;

class operator TWeatherCode.Implicit(const Value: String): TWeatherCode;
begin
  if Length(Value) <> 4 then raise Exception.Create('Value must be 4 characters.');

  case Value[1] of
    'd','D': Result.DayNight:= TDayNight.dnDay;
    'n','N': Result.DayNight:= TDayNight.dnNight;
    else raise Exception.Create('First value must be either d, D, n, or N.');
  end;

  if Value[2] in ['0'..'6'] then
    Result.Clouds:= TCloudCode(StrToIntDef(Value[2], 0))
  else
    raise Exception.Create('Second value must be between 0 and 6.');

  if Value[3] in ['0'..'4'] then
    Result.Precip:= TPrecipCode(StrToIntDef(Value[3], 0))
  else
    raise Exception.Create('Third value must be between 0 and 4.');

  if Value[4] in ['0'..'2'] then
    Result.PrecipType:= TPrecipTypeCode(StrToIntDef(Value[4], 0))
  else
    raise Exception.Create('Fourth value must be between 0 and 2.');
end;

function TWeatherCode.DayNightStr: String;
begin
  case DayNight of
    dnDay:    Result:= 'Day';
    dnNight:  Result:= 'Night';
  end;
end;

function TWeatherCode.Description: String;
begin
  case Clouds of
    ccClear:        Result:= 'Clear';
    ccAlmostClear:  Result:= 'Mostly Clear';
    ccHalfCloudy:   Result:= 'Partly Cloudy';
    ccBroken:       Result:= 'Cloudy';
    ccOvercast:     Result:= 'Overcast';
    ccThinClouds:   Result:= 'Thin High Clouds';
    ccFog:          Result:= 'Fog';
  end;
  case PrecipType of
    ptRain: begin
      case Precip of
        pcNone:         Result:= Result + '';
        pcSlight:       Result:= Result + ' with Light Rain';
        pcShowers:      Result:= Result + ' with Rain Showers';
        pcPrecip:       Result:= Result + ' with Rain';
        pcThunder:      Result:= Result + ' with Rain and Thunderstorms';
      end;
    end;
    ptSleet: begin
      case Precip of
        pcNone:         Result:= Result + '';
        pcSlight:       Result:= Result + ' with Light Sleet';
        pcShowers:      Result:= Result + ' with Sleet Showers';
        pcPrecip:       Result:= Result + ' with Sleet';
        pcThunder:      Result:= Result + ' with Sleet and Thunderstorms';
      end;
    end;
    ptSnow: begin
      case Precip of
        pcNone:         Result:= Result + '';
        pcSlight:       Result:= Result + ' with Light Snow';
        pcShowers:      Result:= Result + ' with Snow Showers';
        pcPrecip:       Result:= Result + ' with Snow';
        pcThunder:      Result:= Result + ' with Snow and Thunderstorms';
      end;
    end;
  end;
end;

可以转换为这种类型的字符串示例是......

  • d310 = 多云和小雨(白天)
  • d440 = 阴雨和雷暴(白天)
  • n100 = 基本晴朗(夜间)

此字符串将始终采用此格式,并且始终为 4 个字符:1 个字母和 3 个数字。实际上,您可以将其视为以下选项:

  • 0, 1
  • 0、1、2、3、4、5、6
  • 0、1、2、3、4
  • 0、1、2

如果我可以将它变得足够小,我还想做一个选项,将其隐式转换为整数甚至字节。但是,我讨厌必须添加大量 if/then/else 或 case 语句。

我知道可以获取给定的(小)字符集并将它们转换为单个值。但是,我不知道它是如何完成的。例如,我知道这种技术用于DrawTextEx 上的标志和其他类似的WinAPI 调用等地方。我认为这可能与 shr / shl 的使用有关,但我不知道如何使用它们,并且在那种类型的数学上很糟糕。

如何将这 4 个属性组合成一个整数或字节,然后再将它们转换回来?

【问题讨论】:

  • class operator TWeatherCode.Implicit(const Value: String): TWeatherCode; - 这段代码是错误的,Delphi 向你发出警告。 Value = 'D111' 时的结果是什么? Value = 'xABC' 会是什么结果?
  • @Arioch'My IDE 在构建时不会产生任何提示或警告。而且我还没有添加这种保护,它仍在进行中:-)
  • 它应该 - 你的 CASE 没有针对未列出元素的安全检查......你最好从一开始就做好所有保护 - 以后添加它永远不会起作用,你只是忘记了一些地方.所以基本上你从“这是错误!!!”的骨架开始。检查,然后添加正确的非错误值。这样你可能会忘记一些正确的值 - 你会在检查中找到它,但你不会忘记错误保护
  • ....在更改 Delphi 版本后,经过长期测试的始终有效的函数开始对函数 8 的局部变量进行隐蔽的精确打击,从而调用堆栈。那么,你真的想成为为未来不幸的人隐藏那些地雷的人吗?在这里和现在穿着他们的鞋子走路?如果没有别的,请表示同情:-D
  • 这不是演员表。这是编码。为什么要编码?您将如何处理编码值?

标签: delphi enums casting


【解决方案1】:

最简单的方法是将其打包成 32 位有符号或无符号整数。 我更喜欢未签名的(又名Cardinal

您说 - “将始终是 4 个字符:1 个字母和 3 个数字”。 “字符”是指较低的拉丁字符。 这些字符可以用单字节AnsiChar 类型很好地表示。

警告:在不同的 Delphi 版本中,CharWideCharAnsiChar 类型的快捷方式。在进行低级别类型转换时,您要避免模棱两可的高级快捷方式并使用原始类型。

string -> UnicodeStringAnsiString 歧义也是如此。

class operator TWeatherCode.Implicit(const Value: TWeatherCode): Cardinal;
var R: packed array [1..4] of AnsiChar absolute Result; 
begin
  Assert( SizeOf( R ) = SizeOf ( Result ) );
  // safety check - if we ( or future Delphi versions ) screwed data sizes
  //   or data alignment in memory - it should "go out with a bang" rather
  //   than corrupting your data covertly

  case Value.DayNight of
    dnDay:    R[1] := 'd';
    dnNight:  R[1] := 'n';
    else raise ERangeError.Create('DayNight time is incorrect!');
        // future extensibility safety check needed. 
        // Compiler is right with Warnings here
  end;
  R[2] := AnsiChar(Ord('0') + Ord(Value.Clouds));
  R[3] := AnsiChar(Ord('0') + Ord(Value.Precip));
  R[4] := AnsiChar(Ord('0') + Ord(Value.PrecipType));
end;

类似的事情要回去。

class operator TWeatherCode.Implicit(const Value: Cardinal): TWeatherCode;
var V: packed array [1..4] of AnsiChar absolute Value; 
    B: array [2..4] of Byte;
begin
  Assert( SizeOf( V ) = SizeOf ( Value ) );
  // safety check - if we ( or future Delphi versions ) screwed data sizes
  //   or data alignment in memory - it should "go out with a bang" rather
  //   than corrupting your data covertly

  case UpCase(V[1]) of
    'D': Value.DayNight:= TDayNight.dnDay;
    'N': Value.DayNight:= TDayNight.dnNight;
    else raise .....
  end;

  B[2] := Ord(V[2]) - Ord('0');
  B[3]....
  B[4]....

  // again: error-checking is going first, before actual processing legit values. that makes it harder to forget ;-)
  if (B[2] < Low(Value.Clouds)) or (B[2] > High(Value.Clouds)) then
     raise ERangeError(ClassName + ' unpacking from Cardinal, #2 element is: ' + V[2] );
  Value.Clouds := TCloudCode( B[2] );

  .......

 end;

UPD。关于下面断言的好问题。我希望在或多或少的现代 Delphi 中,断言应该被重写为编译时检查。我不完全确定语法,我几乎从未使用过它,但它应该是这样的:

 //  Assert( SizeOf( R ) = SizeOf ( Result ) );
 {$IF SizeOf( R ) <> SizeOf ( Result )} 
    {$MESSAGE FATAL 'Data size mismatch, byte-tossing operations are broken!'}
 {$IFEND}

现在回到打包成一个字节.... 见-https://en.wikipedia.org/wiki/Power_of_two

您的组件需要以下单元格:

  1. 0 到 1 => 0..1 => 1 位
  2. 0 到 6 => 0..7 => 3 位
  3. 0 到 4 => 0..7 => 3 位
  4. 0 到 2 => 0..3 => 2 位

1+3+3+2 = 9 > 8 位 = 1 个字节。

您仍然可以尝试与可变基https://en.wikipedia.org/wiki/Positional_notation相处

组合总数为 2*7*5*3 = 210 个组合。一个字节小于 256。

所以你可能会逃脱这样的事情:

 Result := 
   Ord(Value.DayNight)   + 2*( 
   Ord(Value.Clouds)     + 7*(
   Ord(Value.Precip)     + 5*(
   Ord(Value.PrecipType) + 3*(
   0 {no more 'digits' }
 ))));

这可以解决问题,但我会非常谨慎。您打包的数据越多 - 它的冗余就越少。冗余越少 - 随机不稳定值看起来像一些合法数据的可能性就越大。 这也让您没有未来扩展的空间,

想象一下,将来#3 元素将从0, 1, 2, 3, 4 范围扩展到0 to 5 范围。您将不得不更改公式。但是……你真的知道这样做吗?或者你的旧程序会得到新数据吗?而且,如果您确实更改了公式-您将如何分辨用新公式和旧公式计算的字节数???

坦率地说,我做了类似的技巧将日期打包成两个字节。 但是我有 100% 的保证,我永远不会延长这些日期,而且我有 100% 的方法来告诉非初始化空间中的书面日期,而且 - 还有一个额外的 - 如果需要延长,我知道我可以放弃那个过时的计划它完全并在另一个内存位置重新开始。不需要与旧数据兼容。我不太确定你的情况。

【讨论】:

  • o0o,一旦我回到我的 IDE - 没想到会这么快 :D 但是Cardinal > TWeatherCode 呢?
  • 完全相同的技巧:将打包数组变量放在基数变量上。然后相同的转换代码只是倒退
  • 顺便说一句,这个 Cardinal 表示现在几乎匹配你的字符串转换,保存但固定大小和非 Unicode 字符。你甚至可以将你的 to-string 转换基于它:-D Kind of var T: packed arr........ute Var-Cardinal; begin ..... Result := StringOfChar(#0,4); Result[1] := T[1];...
  • 如果我可能会问,如果开发人员可以选择关闭断言,为什么还要使用Assert?我认为引发手动异常是对不受支持的值的适当处理。我可以扩展到我自己的 EInvalidWeatherCode 类。
  • 好吧,我想开发人员在进行任何重大更改后至少会以调试模式运行程序。只是...Assert 是一种自我记录。它在大喊我的意图“我害怕坏情况,我在这里检查一下”。实际上我更喜欢编译时异常,我想我只是弄错了。哪个....是的,我想我还没有完全从 D5 转移到 DXE2 :-D
【解决方案2】:

您可以使用variant 部分声明您的记录,如下所示

  TWeatherCode = record
   public
    class operator Implicit(const Value: TWeatherCode): String;
    class operator Implicit(const Value: String): TWeatherCode;
    class operator Implicit(const Value: TWeatherCode): Cardinal;
    class operator Implicit(const Value: Cardinal): TWeatherCode;
    function Description: String;
    function DayNightStr: String;
   public
    case Storage: Boolean of
      True: (
        DayNight: TDayNight;
        Clouds: TCloudCode;
        Precip: TPrecipCode;
        PrecipType: TPrecipTypeCode);
      False: (IntValue: Cardinal);
  end;

...

class operator TWeatherCode.Implicit(const Value: TWeatherCode): Cardinal;
begin
  Result := Value.IntValue;
end;

class operator TWeatherCode.Implicit(const Value: Cardinal): TWeatherCode;
begin
  Result.IntValue := Value;
end;

在这种情况下,您可以分配单个字段或 IntValue,它们将使用相同的内存。

【讨论】:

猜你喜欢
  • 2011-04-15
  • 1970-01-01
  • 2019-03-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多