数字解码的危险发生在累计过程中
一段文本可能完全由合法数字组成,却仍然超出目标数据类型的范围。只验证字符集合,会把容量问题留给后续算术,最终得到无法解释的结果。
因此,解码需要同时维护两条边界:当前字符在目标进制中是否合法,以及把它加入已有结果之后是否仍然可表示。两者都通过,才继续推进。
常见解码写法是:当前值乘以基数,再加当前位的数值。如果等循环结束后才检查结果是否超过上限,某一步运算可能已经溢出,原来的数值关系就不再可靠。
正确的检查位置应在每次乘加之前。把“下一步必须不超过上限”改写为当前值和下一位之间的比较,就能在不执行危险运算的前提下拒绝输入。
把乘加不等式拆成商与余数
text = "4294967295"
base, limit, value = 10, (1 << 32) - 1, 0
for char in text:
if not "0" <= char <= "9":
raise ValueError("非法数字")
digit = ord(char) - ord("0")
if value > limit // base or (value == limit // base and digit > limit % base):
raise OverflowError("数值超出范围")
value = value * base + digit
assert value == limit如果当前值小于上限除以基数的商,下一步还有空间;如果恰好等于商,就必须进一步比较当前位与余数。这样所有比较都发生在仍可表达的范围内。
Python 本身支持大整数,这里显式设置上限,是为了演示固定宽度目标类型的判断方式。移植到原生代码时,应保证参与比较的变量类型一致,避免有符号与无符号隐式转换改变判断。
字符映射要先于数值累计
面向固定范围的接口,应在逐位累积前检查是否还能乘进制并加入新位。面向超长数字的接口,则可以将结果保留为数字序列,使用另一套容量约束。
两种路线应在接口层明确区分。不要在普通输入时返回整数,遇到大数又无说明地变成其他类型,否则调用方难以建立稳定的处理逻辑。
alphabet = "0123456789ABCDEF"
lookup = {char: index for index, char in enumerate(alphabet)}
assert len(lookup) == len(alphabet)
text = "2AF"
value = 0
for char in text:
if char not in lookup:
raise ValueError("字符不属于当前进制")
value = value * len(alphabet) + lookup[char]
assert value == 687不能仅凭字符代码落在某个大区间,就假设它是有效数字。字母表中可能包含不连续的范围,大小写也可能表示不同数值。对小字符集,固定查找表可以让规则更直接。
这个片段只演示映射步骤,正式的有界解码还需要结合前面的逐位范围检查。把不同职责分开说明,有助于测试时分别定位是字符不被支持,还是有效数字超出了目标范围。
带符号输入还需要处理不对称边界
如果目标是有符号整数,负方向的绝对值上限通常比正方向多一个单位。可以先保存符号,在无符号容器中累计绝对值,再在成功后恢复符号;最小负值的恢复需要专门处理,不能先构造无法容纳的正值。
空字符串、只有符号、符号出现在中间,以及小数点等都应在格式层明确拒绝。对于只接受无符号标识的接口,直接禁止符号,比解析以后再试图判断负数是否“其实等于零”更简单。
| 位置 | 预期 |
|---|---|
| 最大允许值减一 | 成功 |
| 最大允许值 | 成功 |
| 最大允许值加一 | 拒绝 |
| 很长但全是前导零 | 由长度规则与数值规则共同决定 |
| 中间混入非法字符 | 格式错误而非返回前缀 |
长度限制不能代替数值检查。同样位数的数字可能一部分有效、一部分越界;反过来,大量前导零又可能让很长的文本仍然代表很小的数值。两类限制各有用途。
不要用饱和结果假装解码成功
零值可能是合法结果,而空输入、非法字符或越界属于失败。调用方需要根据约定区分这些情况,不能仅用结果是否为真来判断解码成功。
测试除了随机往返样例,还应覆盖最大可表示值、紧邻越界值和中途出现非法字符的内容。正确处理边界,往往比追求常见输入路径少一次判断更值得保留。
某些业务会把超限值压到最大值,这应当是明确的业务变换,而不是基础解码器默认行为。对于标识、权限或数量字段,静默饱和可能把两个不同输入变成同一个有效值。
测试可以用高精度参考实现计算预期,再确认固定宽度实现只在允许范围内接受结果。随机生成合法字符串有助于覆盖组合,但边界附近的相邻值仍需要专门列出。
最后检查失败路径是否保留旧输出。更清楚的接口是只有完整成功时才提交最终值,失败则返回独立状态,让调用者不可能误把某个部分累计值继续用于业务。