- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
导读
std::time::Duration是每个 Rust 开发者都会用到的类型,但它的 API 形态并非一蹴而就。本文以仓库中的 text/1040-duration-reform.md 为骨架,系统讲解 RFC 1040 如何在 2015 年为Duration划定一条"够用于超时(timeout)"的稳定化边界:为什么它只有秒与纳秒两个字段、为什么刻意排除周/天/小时/分钟与毫秒转换方法,以及它与 Windows、POSIX 系统调用如何对接。读完本文,你将理解Duration设计取舍背后的时间语义难题,也能借助仓库中 text/1047-socket-timeouts.md、text/1288-time-improvements.md 等后续 RFC,看到这一精简设计如何在真实标准库中落地并持续演进。
一、背景:Rust 需要对"时间"进行审慎建模
RFC 1040 的出发点非常朴素:Rust 标准库需要与大量要求超时(timeout)的系统调用互操作,而当时已有的DurationAPI 表面很大、处处是坑。提案认为,"时间"这个概念远没有看上去那么简单,不同语义的时间应该有不同的 API 设计。
1.1 借用 Joda-Time 的三分法
RFC 引用了 Java 生态中流行的 Joda-Time 库对时间的分类,这套术语后来也贯穿了 Rust 标准库的时间设计:
Duration:以高精度单位描述的一段时间量,与任何时间点无关;Period:以人类语言描述的一段时间(如"5 分 27 秒"),只能相对某个时间点才能解析为精确的Duration;Instant:某个时间点,表示为自某个纪元(epoch)以来的Duration。
RFC 明确指出,闰秒(leap second)、一个月中的天数、闰年等人类时间复杂性,以及 NTP 校时等机器时间复杂性,会让这些概念及其完整 API 远比表面看起来复杂。因此 RFC 1040 只聚焦一件事:设计一个足以充当超时参数的Duration,把Period、Instant及其他概念留给后续提案。
1.2 系统调用实际需要什么
RFC 分析了Duration将要打交道的系统 API 的两种典型形态:
- 类
timespec结构:u64秒 +u32纳秒; - 直接以毫秒为单位的超时参数:例如 Windows IOCP 中
GetQueuedCompletionStatus的dwMilliseconds参数是DWORD,即u32;部分 Windows API 甚至使用"tick"或 100 纳秒为单位。
由此导出 RFC 1040 的两大核心目标:
- 定义一种能描述可移植超时的类型,可跨平台与各类系统 API 对接;
- 明确当一个很大的
Duration被传入不接受这么大超时的 API 时应当发生什么。
二、核心设计:一个刻意精简的Duration
2.1 数据结构
RFC 保留了当时Duration已有的两个字段,没有引入任何新存储布局:
pub struct Duration { secs: u64, nanos: u32 // may not be more than 1 billion }要点:
secs为u64秒数;nanos为u32附加纳秒精度,约定不得超过 10 亿(1,000,000,000);- 不存在负
Duration。RFC 给出的理由是:许多接收Duration的 API(尤其是超时类 API)对负数没有有意义的行为,如果允许负值,每个调用方都要自行决定如何处理,反而制造大量歧义;无符号类型能消除一批这类问题。
2.2 构造器与访问器
RFC 提出的最小 API 面只有四个方法:
impl Duration { /// create a Duration from a number of seconds and an /// additional nanosecond precision. If nanos is one /// billion or greater, it carries into secs. pub fn new(secs: u64, nanos: u32) -> Timeout; /// create a Duration from a number of seconds pub fn from_secs(secs: u64) -> Timeout; /// create a Duration from a number of milliseconds pub fn from_millis(millis: u64) -> Timeout; /// the number of seconds represented by the Duration pub fn secs(self) -> u64; /// the number of additional nanosecond precision pub fn nanos(self) -> u32; }几个值得注意的语义约定:
Duration::new(secs, nanos)中,若nanos达到或超过 10 亿,会自动向secs进位;from_millis存在而from_secs存在,是因为毫秒是超时 API 最常用的单位;secs()与nanos()是取值方法,便于调用方把Duration还原成系统 API 期望的(秒, 纳秒)元组形式。
注:RFC 原文把返回类型写作
Timeout,这是早期草案的占位记号;该提案最终稳定化的类型就是std::time::Duration。仓库中 text/1242-rust-lang-crates.md 曾把 RFC 1040 与集合 RFC 一起列为"1.0 稳定化 RFC"的成功范例,说明它最终成为标准库的一部分。
2.3 与系统 API 对接时的两条规则
当Duration被用于期望u32毫秒的系统 API 时,RFC 明确了两条行为规则:
- 降低粒度(coarsening):把纳秒精度粗化到毫秒,例如只支持毫秒的 API 就丢弃亚毫秒精度;
- 截断(truncation):超过
u32::MAX毫秒的超时直接截断到上限。
RFC 特别提示:u32的毫秒数已经能支撑超过 45 天的超时,对绝大多数场景绰绰有余。此外,RFC 假设超时 API 本身允许"虚假唤醒/提前返回"(例如 POSIX 的pthread_cond_timedwait就明说可能发生虚假唤醒),因此降低粒度与截断都不会破坏 API 契约。
三、运算符与 trait 实现
RFC 为Duration规划了如下 trait 实现:
| Trait | 语义说明 |
|---|---|
Add、Sub、Mul、Div | 对secs字段遵循u64的溢出与下溢规则;Sub在结果将为负时panic。纳秒始终保持在[0, 1_000_000_000)区间,超出时向secs进位 |
Display | 打印秒、毫秒、纳秒(若大于 0),例如"15 seconds, 306 milliseconds, and 13 nanoseconds" |
Debug、Ord、PartialOrd、Eq、PartialEq、Copy、Clone | 直接 derive |
其中Sub在结果为负时 panic 的设计,与"不存在负Duration"这一前提一脉相承——它把错误尽早暴露在减法发生的那一刻,而不是让负值在后续 API 中悄悄传播。
3.1 仓库佐证:checked_sub的后续演进
RFC 1040 的Sub是直接 panic 的。仓库中的 text/1640-duration-checked-sub.md 正是对这一行为的后续补充:为Duration增加checked_add、checked_sub、checked_mul、checked_div,返回Option<Duration>而非 panic,典型应用是"以固定间隔渲染/发包"这类循环场景:
fn render() { // 10ms delay results in 100 frames per second let wait_time = Duration::from_millis(10); // `Instant` for elapsed time let start = Instant::now(); // execute code here render_and_output_frame(); // there are no negative `Duration`s so this does nothing if the elapsed // time is longer than the defined `wait_time` start.elapsed().checked_sub(wait_time).and_then(std::thread::sleep); }其实现思路正是"与sub()相同,只是把底层的checked_sub一路传递、以Option表达下溢":
impl Duration { fn checked_sub(self, rhs: Duration) -> Option<Duration> { if let Some(mut secs) = self.secs.checked_sub(rhs.secs) { let nanos = if self.nanos >= rhs.nanos { self.nanos - rhs.nanos } else { if let Some(secs) = secs.checked_sub(1) { self.nanos + NANOS_PER_SEC - rhs.nanos } else { return None; } }; debug_assert!(nanos < NANOS_PER_SEC); Some(Duration { secs: secs, nanos: nanos }) } else { None } } }可以看到,NANOS_PER_SEC(10 亿)这一不变量从 RFC 1040 一路贯彻到了后续实现中。
四、刻意排除的设计:为什么"少"反而好
RFC 1040 的精华在于明确画出了不做什么。它刻意排除了三类能力:
4.1 不提供周/天/小时/分钟的构造器
原因是这些单位都存在闰秒之类的语义陷阱:只有相对某个具体起点,才能正确理解"1 天"包含多少秒。RFC 引用 Joda-Time 文档来阐释这一困惑:
A period in Joda-Time represents a period of time defined in terms of fields, for example, 3 years 5 months 2 days and 7 hours...A period can only be resolved to an exact number of milliseconds by specifying the instant (including chronology and time zone) it is relative to.
RFC 进一步点破:人们期望"23:50:00 + 10 分钟 = 00:00:00",但不考虑闰秒就无法确认这一结果是否成立。Joda-Time 的应对是提供standardDays/toStandardDays这类带 "standard" 前缀的方法,明示"以标准小时数计算"而非日常口语的"小时"。RFC 认为 Rust 未来可以采用类似思路,但1040 本身刻意把范围限制在没有重大取舍的区域,让最小方案更快推进。
4.2 不提供从Duration取毫秒数的方法
因为毫秒总数可能超过u64,届时必须决定返回Option、panic 还是等待标准大整数类型——每个选择都有代价。RFC 选择把这个问题整体推迟。
4.3 后续 RFC 补齐的标准单位构造器
这一"留白"在 text/1288-time-improvements.md(RFC 1288)中得到了兑现:该 RFC 引入了Duration::from_standard_minutes、from_standard_hours、from_standard_days,并明确"standard minute 恒为 60 秒、standard hour 恒为 3600 秒,与闰秒无关",正是 Joda-TimestandardXxx思路的 Rust 落地。同时 RFC 1288 也贯彻了 1040 的"正Duration"前提:超时类 API 只接受正的Duration,并把单调时间(Instant)与系统时钟时间(SystemTime)分开建模。
五、设计权衡:Drawbacks 与 Alternatives
5.1 主要缺点
RFC 直言最大缺点是新 API 比当时的Duration显著更精简。但它的论点是:这套精简 API 对超时场景完全够用,而且没有旧 API 的那些陷阱。
5.2 备选方案一:直接稳定现有DurationAPI
RFC 罗列了旧 API 的严重问题:
- 上文提到的单位语义陷阱(周/天/小时/分钟等);
- 它支持把
Duration转成微秒/纳秒数,但这类转换无法可靠完成,所以方法只能返回Option,导致需要纳秒数的 API 要么被迫把Option一路传递(不 ergonomic),要么 panic; - 整体 API 面过大,几乎每个方法都带着需要逐一厘清的 caveat。
5.3 备选方案二:把各种单位到Duration的便捷转换一次做全
RFC 认为这些便捷方法"将来大概率会补上",但每个便捷方法的设计都存在歧义,不适合塞进初始提案。
5.4 备选方案三:干脆不用Duration,标准库统一用毫秒
RFC 指出两个缺点:
- 无法在支持更高精度的系统上表达高精度超时;
- 从"精简类型出发、逐步扩充设计空间"的生长路径会丢失——先用简单类型起步,正是为未来留出成长空间。
六、落地验证:Duration在真实 API 中的用法
仓库中的 text/1047-socket-timeouts.md(RFC 1047)是 RFC 1040 最直接的下游消费者。它为TcpStream、UdpSocket等std::net类型引入 sockopt 风格超时:
impl TcpStream { pub fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> { ... } pub fn read_timeout(&self) -> io::Result<Option<Duration>>; pub fn set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()> { ... } pub fn write_timeout(&self) -> io::Result<Option<Duration>>; } impl UdpSocket { pub fn set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> { ... } pub fn read_timeout(&self) -> io::Result<Option<Duration>>; pub fn set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()> { ... } pub fn write_timeout(&self) -> io::Result<Option<Duration>>; }其底层对应SO_RCVTIMEO与SO_SNDTIMEO两个 socket 选项,直接通过setsockopt实现。几个值得注意的细节印证了 RFC 1040 的设计:
Option<Duration>语义:None表示"不设超时";而Some(Duration::new(0, 0))是合法参数,但 setter 会返回io::ErrorKind::InvalidInput错误,避免"零超时"被静默解释为"立即超时/永不超时"的歧义;- RFC 1047 讨论过直接接收
Duration并把零值当"无超时"的替代方案(C 语言传统做法),最终因"不够 Rust 风格、容易误传零值"而放弃; - 该 RFC 还引用了
Duration正在经历稳定化(即 duration-reform),可见 1040 与 1047 是同步设计的姊妹提案。
在仓库其他文档中也能看到这套Duration的普遍使用,例如 text/3654-return-type-notation.md 中的tokio::time::sleep(Duration::from_secs(1))、text/1479-unix-socket.md 中重复出现的set_read_timeout(&self, timeout: Option<Duration>)签名,说明这一 API 形态已成为 Rust 生态超时接口的事实标准。
七、遗留问题与历史回响
RFC 1040 在结尾留下了一个未决问题:"是否应该实现列出的全部 trait?是否还有其他 trait?"从后续仓库内容看,Add/Sub/Mul/Div、Display、Debug、Ord、Eq、Copy、Clone均已在标准库中落地,并不断补充新方法(如 text/1640-duration-checked-sub.md 的checked_*系列)。
这一"先划清边界、再渐进扩充"的策略,也正是 RFC 流程的价值所在:以一份足够小、足够正确的设计换取稳定化的推进速度,把有争议的部分留到设计空间更清晰时再议。
结语与延伸阅读
RFC 1040 为 Rust 的超时语义奠定了一块干净的地基:u64 秒 + u32 纳秒、无负值、可粗化可截断。理解它的设计取舍,能帮你更准确地使用Duration,也能让你读懂后续时间类 API 的演进逻辑。本仓库中与该主题直接相关的文档包括:
- 本提案:text/1040-duration-reform.md
- 下游应用:text/1047-socket-timeouts.md
- 时间体系扩展:text/1288-time-improvements.md
- 检查算术补充:text/1640-duration-checked-sub.md
- 稳定化流程背景:text/1242-rust-lang-crates.md
如果你正在为网络库设计超时参数、或想深入理解标准库时间 API 的设计哲学,从这份 RFC 出发是最佳起点。
- 文档
【免费下载链接】rfcs
RFCs for changes to Rust
相关推荐
GDAL 中 GDT_Int8 有符号 8 位栅格数据类型的来龙去脉与实现剖析(RFC 87)
GDAL 中 GDT_Int8 有符号 8 位栅格数据类型的来龙去脉与实现剖析(RFC 87) 本文以 GDAL 开发文档中的 RFC 87( rfc87_si
GIS遥感数据工程Rust RFC 522 解析:在 impl 中使用 `Self` 类型的设计、动机与演进脉络
Rust RFC 522 解析:在 impl 中使用 Self 类型的设计、动机与演进脉络 本文以 Rust 语言提案 RFC 522 https://link
文档Prettier 设计哲学全解析:opinionated formatter 背后每一个格式决策的来龙去脉
Prettier 设计哲学全解析:opinionated formatter 背后每一个格式决策的来龙去脉 Prettier 自诩为 "opinionated
开发工具格式化CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考