由于ShortString已被弃用,限制字符串长度的最佳方法是什么?

5

有这样的记录并不罕见:

TAddress = record
  Address: string[50];
  City   : string[20];
  State  : string[2];
  ZIP    : string[5];
end;

以前,硬编码字符串大小是不错的选择,可以确保字符串的大小不会超过数据库为该数据分配的字段大小。

然而,由于 ShortString 类型已被弃用,那么 Delphi 开发人员该如何“解决”这个问题呢?将记录字段声明为 string 可以完成任务,但无法防止数据长度超出正确的限制。

在这里,最好的解决方案是什么?


Nick,你想解决什么问题?当字符串被分配的字符串太长时,你想让它们自动被截断到特定长度吗? - RobertFrank
Robert -- 我想我只是有点紧张,从 string[xx] 改为 string 的声明。我正在迁移一个 Delphi 7 应用程序,不想错过任何东西。我会密切关注检查,但目前要找出是否需要错误检查和长度检查非常复杂。如果有更普遍接受的解决方案,我想知道它是什么。 - Nick Hodges
1
当您将较长的字符串推送到数据库时会发生什么?框架中是否有截断它的东西? - David Heffernan
1
我通过验证器(访问者模式)传递每个数据容器(特别是由用户编辑的容器),不仅检查长度。因此,我不会切断用户输入的任何数据,而是返回非常清晰的信息,说明为什么数据无法保存。 - Sir Rufo
1
CustomAttributes 可能会在您的情况下有所帮助。 - Sir Rufo
显示剩余2条评论
6个回答

5

如果我需要限制数据长度,我会让数据库代码尽可能处理它。在字段上设置大小限制,并在数据绑定控件中向用户显示数据。绑定到字符串字段的TDBEdit将正确地执行长度限制。将其设置为直接从数据集填充记录,并且它将始终具有正确的长度。

然后您需要担心的是来自不属于您的UI的外部源的数据进入记录的情况。对于这个问题,使用相同的流程。使导入代码将数据插入数据集,并让其长度约束为您执行验证。如果引发异常,请拒绝导入。如果没有,则您拥有一个有效的数据集行,可以用来填充一个记录。


5
您问题中的短字符串类型并不能真正保护字符串不超过正确长度。当您将较长的值赋给这些短字符串时,该值会被静默截断。我不确定您使用的是哪种数据库访问方法,但我想它会做同样的事情。也就是说,将任何超长字符串截断到最大长度。在这种情况下,无需进行任何操作。如果您的数据库访问方法在输入超长字符串时会抛出错误,则需要在向数据库传递值之前进行截断。如果必须明确地进行截断,则可能有很多选择截断的位置。我的哲学是在最后一刻截断,因为那是受限制的点。在任何其他地方截断似乎都是错误的。这意味着数据库限制正在扩散到与数据库不明显相关的代码部分。当然,所有这些都是基于您希望继续静默截断的假设。如果您希望在截断时提供用户反馈,则需要决定何时采取适当的行动来提供反馈。

5
如果字符串是用户输入的,那么你需要在最早的机会提供截断信息,即在用户界面(UI)中提供。在其他任何地方这样做只会让用户看到难以理解的开发者信息或数据丢失。 - Eric Grange
@Eric,那很有道理。显然这不是在Q中发生的短字符串代码。 - David Heffernan

3
据我所知,我的答案应该是“不要混淆层次”。
我怀疑字符串长度是在数据库层面(列宽度)或业务应用层面指定的(例如验证卡号)。
从“纯Delphi代码”的角度来看,除非你到达持久层甚至业务层,否则你不应该知道你的string变量具有最大长度。
使用属性可能是一个好主意。但是由于混淆层次的原因,它可能会“污染”源代码。
所以我建议使用专门的数据建模,在其中指定您的数据期望。然后,在Delphi var级别,您只需定义一个普通的string。这正是我们的mORMot框架实现数据过滤和验证的方法:在模型层面,使用一些专门的类-方便、可扩展和清晰。

3
如果你只是从Delphi 7迁移到XE3,那么就不要管它了。尽管ShortString可能已经被弃用,但我相信他们永远不会完全删除它,因为有许多代码片段将永远无法在没有它的情况下重建。ShortString + Records仍然是指定基于字节的记录文件数据存储的唯一实用方式。Delphi永远不会删除ShortString或改变其行为,这对现有的Delphi代码来说是毁灭性的。因此,如果您真的必须定义记录并限制其长度,并且您真的不希望这些记录支持Unicode,那么停止使用或停止编写ShortString代码是没有任何理由的。也就是说,我厌恶短字符串和File-of-record,希望它们消失,并且很高兴它们被标记为弃用。
话虽如此,我完全同意Mason和David的观点;我认为,长度检查和验证是表示/验证方面的问题,而Delphi的强类型不是处理它们的正确位置或正确方式。如果您需要对类施加验证约束,请编写实现约束存储的辅助类(EmployeeName是字符串字段,EmployeeName具有以下长度限制)。例如,在编辑控件中,这已经是一个属性。对我来说,使用新的绑定系统将DB字段映射到可视字段要比试图在代码中静态表达约束更可取。
用户输入验证和存储是不同的,长度限制应该在GUI控件中设置,而不是在数据结构中设置。
例如,如果您想要一个长度受限的Unicode宽字符串,则可以使用Array of UnicodeChar。您甚至可以使用Delphi中的新类助手方法编写自己的LimitedString类。但是这样的方法并不是一种可维护和稳定的设计。
如果您的SQL数据库具有声明为VARCHAR(100)类型的字段,并且您希望将用户的输入限制为100个字符,则应在GUI层进行限制,并忘记在幕后强制截断(实际上是数据损坏)。

当然可以更改代码以不需要它。这可能需要一些思考,但绝对不是不可能的。 - Rudy Velthuis
随着NEXTGEN和双字节字符串变得越来越普遍,重大变更并不罕见。我建立了一个修复旧的序列化/反序列化代码的职业生涯。 - nurettin
NEXTGEN 是一堆疯狂的东西。 - Warren P

1

我曾经遇到过这个问题——在从Delphi 6升级到2009时,由于一个程序正在/已经执行的操作,将旧的ASCII字符串视为单独的ASCII字符是至关重要的。

该程序输出ASCII文件(甚至没有ANSI),并具有诸如在最后一个数字上进行超打以表示负数等概念。因此,文件格式可以说有点老旧了!

在第一次使用2009构建之后(10年前的代码,嗯,你会这样做的!),在对单元名称进行排序等操作后,报告了数百个错误/非法赋值和数据丢失/转换警告...

无论Delphi的字符串和字符的后台操作/魔术有多好,我都不太信任它。最终,为了确保一切都恢复原状,我将它们全部重新声明为字节数组,然后相应地更改了代码。


1
似乎是浪费了努力。在XE3中,Shortstring和string[3](实际上是长度限制为3的ShortString)与Delphi 6中的工作方式相同,因此您的移植显然可以在不更改它们的类型的情况下完成。您选择了一种方式(但不是最简单或最好的方式)来解决编译错误。我感受到了您的痛苦,但我认为您可能已经走了一个弯路。有一些隐式转换警告,您可以通过添加强制转换来修复它们,或者您可以将其保留,但没有什么严重的问题是您无法通过一些小心的类型声明来解决的。 - Warren P
其实并不需要花费太多的精力。这与我10年前编写代码的方式有很大关系——它们本来就应该是数组,但使用shortstring[n]是一种简单的方法。整个操作集中在几个例程上,如“MoveChars,MovecharsAndFill”等,并且大量使用FillChar进行初始化。所以说,不同的任务需要不同的工具 :) - Despatcher
如果你的应用程序非常小,那太好了。我有一个900K行的应用程序,要从中删除Shortstring可能需要一年时间,最终,如果你没有至少将其升级到Unicode,那么除了作为增量移动之外,删除Shortstring就没有任何意义了。 - Warren P
正如我所说,它不能移动到Unicode或任何类似的东西 - 它就是它本身 - 文本文件(ASCII)输出必须完全按照规定的方式进行。你能想象在Unicode中执行“Over Punch”的操作吗?这是从打孔卡中提出的一个概念,正如它的名字所暗示的那样。打孔卡Unicode哈哈:http://en.wikipedia.org/wiki/Signed_overpunch - Despatcher

1

您没有指定 Delphi 版本,以下是我在 Delphi 2010 上的解决方案:

版本1:

TTestRecordProp = record
private
  FField20: string;
  ...
  FFieldN: string
  procedure SetField20(const Value: string);
public
  property Field20: string read FField20 write SetField20;
  ...
  property FieldN: string ...
end;
...
procedure TTestRecordProp.SetField20(const Value: string);
begin
  if Length(Value) > 20 then
    /// maybe raise an exception?
    FField20 := Copy(FField20, 1, 20)
  else
    FField20 := Value;
end;

版本2:

TTestRecordEnsureLengths = record
  Field20: string;
  procedure EnsureLengths;
end;
...
procedure TTestRecordEnsureLengths.EnsureLengths;
begin
  // for each string field, test it's length and truncate or raise exception
  if Length(Field20) > 20 then
    Field20 := Copy(Field20, 1, 20); // or raise exception...
end;

// 在将数据推送到数据库之前,您必须调用.EnsureLength...

个人建议使用对象替换记录,这样您可以做更多的操作。


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