第12章:基本数据类型
第12章:基本数据类型:把业务范围、精度、编码和运算规则写成可验证的数据合同。
学习目标
- 能为一个业务值选择覆盖范围、精度、编码和运算规则的基本类型,并写出拒绝条件。
- 能用正常值、恰好边界值和故障值验证整数、浮点数、字符串、枚举与数组的行为。
- 能回答:一个值看起来能存进去时,什么证据仍然不足以证明它满足业务合同?
为什么需要基本数据类型
想象一个仓库标签:箱子能不能放下,不只取决于箱子的外形,还取决于标签上的单位、允许的重量和读标签的人。如果只看“这个格子有几位”,同一个数字就可能被当成金额、时间、状态或字符。
基本数据类型解决的是这种错配。没有清楚的选择规则,程序可能在小样本上运行正常,却在金额换算、文本传输或边界计算时悄悄改变含义;错误往往直到数据已经持久化或跨服务发送后才被发现。
一条可验收的选择规则
把选择类型写成一个可检查的合同:
domain_range 是业务允许的输入域,representation_range 是实现真正能表示的范围。若业务还要求小数位、字符集或离散状态,就要把这些约束一起写进合同,不能只比较存储字节数。
第一次出现时,先区分三个容易混淆的词。↡一个类型能表示的最小值到最大值及其允许状态集合 决定哪些值有合法的落点;↡结果在有效位数和舍入规则下仍能满足业务误差预算的程度 决定落点之间是否足够细;↡计算结果超出表示范围而无法按原意保留的状态 则说明计算已经越过了边界。三者分别回答“能不能表示”“够不够准确”“越界后发生什么”。
例如,订单金额若以“分”为最小单位,整数通常比浮点数更能表达合同;温度传感器若允许误差,则应记录单位、误差预算和舍入策略。选择的证据不是“编译器接受了声明”,而是边界样本、失败路径和复位后的重放结果。
常见误区
第12章 基本数据类型
本章把“选一个声明”改写成一条决策链:先描述业务域,再检查表示范围和精度,然后确认编码、运算规则、默认值与失败语义。每个节点都要留下一个能被第二位读者重放的样本。
12.1 使用数的普遍规则
使用数值前先回答四个问题:值的单位是什么,允许的最小值和最大值是什么,是否允许小数,越界时是拒绝、饱和还是报错。只有这四个答案稳定,类型选择才有可比较的依据。
不要把“显示出来的数字”当成“内部的数值合同”。例如 1.5 可能是 1.5 秒、1.5 千克或 1.5 元;如果单位只藏在变量名或注释里,跨模块传递时仍然容易交换。代码中应让单位后缀、包装类型或转换函数成为边界上的可见证据。
12.2 整数
整数适合计数、索引、离散金额和明确不允许小数的业务值,但必须区分有符号与无符号范围。int32 能否容纳一个订单总量,不能只看今天的样本,还要考虑批量上限、乘法中间值和未来增长。
一个安全的检查顺序是:先把输入提升到足够宽的中间类型,计算最大可能结果,再执行运算,最后把结果收窄到目标类型。若目标语言提供 checked arithmetic,就让它产生明确的失败;不要把回绕后的值继续传给排序、金额或数组索引。
12.3 浮点数
浮点数适合范围很大、允许近似的测量和连续计算。它不保证每个十进制小数都有精确的二进制表示,因此 a + b == c 只有在合同明确要求这种比较时才成立。
比较浮点结果时,应先定义绝对误差、相对误差和数量级接近零时的规则。累计值还要记录舍入发生在哪里;把中间结果过早格式化为字符串,或把误差隐藏在默认转换中,都会让问题难以重放。金额、库存和需要审计的计数通常应优先采用整数或定点表示。
12.4 字符和字符串
字符不是“永远占一个字节的字母”,字符串也不是“末尾加一个特殊值就自然安全”。跨平台文本需要说明字符边界、长度单位、非法输入和截断策略。↡把字符映射为字节或码点的规则,决定不同系统如何读回文本 一旦在输入边界被确定,就应保持到存储、比较和输出的全过程。
长度检查必须和业务口径一致:用户看到的是字符、代码点还是字节,数据库限制又是哪一种。截断多字节文本时,不能从任意字节位置切开;拼接外部字符串时,还应验证终止、转义和容量,避免把显示层的长度误当成缓冲区安全证明。
C中的字符串
C 风格字符串以约定的终止字符标记结尾,所以“数组容量足够”不等于“字符串有合法终止”。复制、拼接和格式化前要同时核对目标容量、源长度和终止位置;一个少一位的容量计算就可能把后续内存当成文本。
在现代边界上,优先使用带长度的字符串表示或显式传递 (pointer, length),把所有权和生命周期写清楚。若必须调用 C API,应在适配层集中完成编码、容量与终止检查,让内部业务代码不再依赖隐含约定。
12.5 布尔变量
布尔值最适合表达一个可直接提问的事实,例如 isArchived、hasPermission 或 canRetry。不要让一个布尔值同时代表“已发送”“正在发送”和“发送失败”;这些状态已经超过二值域,应改为枚举或结果对象。
还要写清楚缺省值和未知状态。把数据库的 NULL、网络缺字段或解析失败默认为 false,会把“没有证据”伪装成“明确否定”。分支前先完成输入验证,或把未知状态保留到能作出业务决定的边界。
12.6 枚举类型
↡用有名字的有限成员表达离散状态,避免裸数字携带业务含义 让状态集合和代码分支保持同一份词汇。PaymentStatus.Paid 比 status == 3 更能说明意图,也更容易在新增成员时触发编译器或审查提醒。
枚举的价值不只是可读性:它能限制调用者传入的域,并把默认分支、未知值和序列化格式暴露出来。持久化时不要假设成员的底层数字永远不变;协议边界应使用稳定的显式编码,并对未来成员保留拒绝或兼容策略。
如果你的语言里没有枚举类型
可以用受限构造函数、常量集合加校验函数,或一个带私有构造的轻量包装类型模拟枚举。关键不是语法名字,而是外部代码不能随意制造未定义成员,分支能够覆盖合法域,并且序列化值有稳定的版本规则。
12.7 命名常量
↡给固定业务值取有意义且不可随意改变的名字,并可携带单位 把数字的用途和单位带到使用点。MaxLoginAttempts、CacheTtlSeconds 和 InvoiceMinorUnit 比 3、60 和 100 更能让审查者检查合同。
常量也需要作用域和生命周期判断。把环境配置硬编码成编译期常量,会掩盖部署差异;把真正不变的单位转换因子作为可变配置,又会让同一批数据在不同时间得到不同解释。命名、来源、单位和可变性应同时可见。
12.8 数组
数组把元素类型和位置关系放在一起,但它不会自动说明长度、所有权或索引是否合法。使用数组前先决定长度是固定还是动态、索引从哪里开始、空数组是否有效,以及遍历期间能否改变集合。
边界测试至少包含空数组、一个元素、最后一个合法索引和第一个非法索引。长度计算要避免有符号值转换为巨大的无符号值;外部长度也不能直接作为分配大小,必须先做上限检查。对多维数据,还要验证每一维的步长和布局,而不是只检查总元素数。
12.9 创建你自己的类型(类型别名)
类型别名可以改善可读性,却未必阻止同型值交换。例如把 UserId 和 OrderId 都别名为整数,编译器仍可能允许它们互相传递。若误用代价高,应使用真正不同的包装类型,并把合法构造、格式化和转换集中在边界。
自定义类型的最小合同包括:它表示什么,允许哪些值,如何构造,如何比较,如何序列化,失败如何报告。这样做不是为了增加语法,而是把原本散落在调用者脑中的约束放到一个能被工具和测试共同检查的位置。
为什么创建自己的类型的示例是用Pascal和Ada写的?
这些示例强调的是“让非法组合难以表达”的思想,而不是要求今天的项目使用某一种语言。Pascal 和 Ada 的强类型特性把不同单位或不同离散域的值区分得更早;现代语言可以用新类型、结构体、构造函数、泛型约束或静态分析取得相同的设计收益。
迁移时要保留原则而不是照抄语法:如果类型系统无法阻止误用,就在构造函数、验证器、测试和代码审查边界补上证据,并记录哪些值仍然只能依赖运行时检查。
创建自定义数据类型的指导原则
先按业务概念命名,再决定底层表示;让构造路径验证范围、单位和格式;把转换放在少数有名字的函数中;对不可变值优先使用不可变表示;对外部输入保留拒绝理由;对序列化格式写版本与兼容规则。只有当新类型减少了误读或非法状态,它才值得维护。
关键点
基本类型的选择应由业务域和失败语义驱动,而不是由“占几个字节”单独驱动。整数要防止中间值溢出,浮点数要声明误差预算,字符串要绑定编码和长度口径,布尔与枚举要覆盖状态域,常量和自定义类型要把单位与构造规则带到调用点。
验收时固定同一输入,分别运行正常值、恰好边界和一个故障值;记录预期、实际、拒绝理由和复位后的重放结果。只有当这些证据一致,才可以说类型满足了业务合同。
专属因果实验:从范围到语义
先预测:把输入从正常值推到边界,再切换精度策略或离散状态表示时,哪一个证据最先失效?依次操作三个检查点,观察数值如何穿过“输入 → 表示 → 决定”这条链;最后点击“重置实验”,确认控件、状态和图示回到同一基线。
1. 先核对表示范围
第 12 章 · 类型合同实验
先证明能表示,再决定能否接受
切换检查点和样本,观察输入如何经过范围、精度与语义三层检查。
输入落在业务域和整数表示范围内。 金额以最小单位保存,舍入点只出现在明确的结算边界。 状态成员有名字,未知成员不会静默变成已支付。
练习与答案
练习
问题 1:选择金额表示
订单金额以分为最小单位,单笔订单不超过 2,000,000 元,结算过程还会先乘以税率。请写出你的内部表示、乘法前的检查和越界后的结果,不要只回答“使用整数”。
问题 2:诊断浮点比较
代码用两个浮点结果判断发票是否平衡:if (debit + credit == total) ...。请指出至少两个隐含前提,并给出一个可验收的替代方案。
问题 3:限制状态与文本边界
一个接口接收 status: number 和 name: string。请设计最小的改造,使未知状态、非法编码、过长文本和未来新增状态都不会静默变成合法值。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 表示范围
一个类型能够表达的值域和状态集合;选择它时要和业务允许的最小值、最大值及非法值比较。
- 精度
数值表示能够保留的细节程度,以及舍入后的误差是否仍在业务预算之内。
- 溢出
计算结果越过表示范围的状态;必须在结果被破坏前检测,或由 checked 运算返回失败。
- 编码
把字符映射成字节或码点的规则;文本边界必须明确采用哪种编码和长度口径。
- 枚举类型
用有限的有名成员表达离散状态,帮助代码覆盖合法域并避免让裸数字携带含义。
- 命名常量
为固定业务值提供带用途和单位的稳定名字;它让数字的合同在使用点可见。
资料边界与独立改写
本章的单元标题与目录节点依据《代码大全(第2版)》2006 年中文公开试读目录核对;书目信息参考 Microsoft Press 官方书页。关于数值、类型边界和强类型设计的迁移说明参考 C++ Core Guidelines 与 ECMAScript 语言规范。这些资料用于限定目录范围、事实坐标和现代实践边界,不冒充原书未公开正文。
实验图、代码片段、练习与答案均为围绕本章目录的独立教学重写;它们用于让读者检验数据类型选择,不是原书段落的翻译或逐字复现。