Rust 风格指南
动机 - 为什么使用格式化工具?
格式化代码主要是一项机械任务,既耗时又耗费精力。通过使用自动格式化工具,程序员可以从这项任务中解脱出来,专注于更重要的事情。
此外,通过坚持既定的风格指南(例如本指南),程序员无需制定临时风格规则,也不需要与其他程序员争论应该使用什么风格规则,从而节省时间、沟通成本和精力。
人类通过模式匹配来理解信息。通过确保所有 Rust 代码具有相似的格式,理解新项目所需的脑力劳动减少了,从而降低了新开发人员的入门门槛。
因此,使用格式化工具(例如 rustfmt
)可以提高生产力,而通过使用社区一致的格式化(通常是使用格式化工具的默认设置)可以获得更大的好处。
默认 Rust 风格
Rust 风格指南定义了默认的 Rust 风格,并建议开发人员和工具遵循默认的 Rust 风格。rustfmt
等工具使用风格指南作为默认风格的参考。本风格指南中的所有内容,无论是否使用“必须”之类的语言,或诸如“插入空格...”或“在...之后换行”之类的祈使语气,都指的是默认风格。
这不应被解释为禁止开发人员遵循非默认风格,或禁止工具添加任何特定的配置选项。
Bug
如果风格指南与 rustfmt 不同,则可能表示 rustfmt 中存在 bug,或者风格指南中存在 bug;无论是哪种情况,请将其报告给风格团队或 rustfmt 团队,或两者都报告,以便进行调查和修复。
如果基于风格指南和默认 Rust 风格实现新的格式化工具,请在现有 Rust 代码库上对其进行测试,并避免造成大范围的破坏。这种工具的实现和测试可能会暴露出风格指南或 rustfmt 中的错误,以及工具本身的错误。
我们通常以避免大范围破坏的方式解决错误。
格式化约定
缩进和行宽
- 使用空格,而不是制表符。
- 每个缩进级别必须为 4 个空格(也就是说,字符串文字和注释之外的所有缩进都必须是 4 的倍数)。
- 一行的最大宽度为 100 个字符。
块缩进
优先使用块缩进,而不是视觉缩进
#![allow(unused)] fn main() { // Block indent a_function_call( foo, bar, ); // Visual indent a_function_call(foo, bar); }
这会使差异更小(例如,如果在上面的示例中重命名了 a_function_call
)并减少了向右偏移。
尾随逗号
在任何类型的逗号分隔列表中,当后跟换行符时,请使用尾随逗号
#![allow(unused)] fn main() { function_call( argument, another_argument, ); let array = [ element, another_element, yet_another_element, ]; }
这使移动代码(例如,通过复制和粘贴)更容易,并使差异更小,因为添加或删除项目不需要修改另一行来添加或删除逗号。
空行
用零个或一个空行(即,一个或两个换行符)分隔项目和语句。例如,
#![allow(unused)] fn main() { fn foo() { let x = ...; let y = ...; let z = ...; } fn bar() {} fn baz() {} }
排序
在各种情况下,默认的 Rust 风格都指定对事物进行排序。如果未另行指定,则此类排序应为“版本排序”,以确保(例如)x8
排在 x16
之前,即使字符 1
排在字符 8
之前。
出于 Rust 风格的目的,要比较两个字符串以进行版本排序
- 从头到尾将两个字符串处理为最大长度块的两个序列,其中每个块由一系列非 ASCII 数字的字符或一系列 ASCII 数字(数字块)组成,并比较字符串中对应的块。
- 要比较两个数字块,请按数字值比较它们,忽略前导零。如果两个块具有相同的数值,但前导数字的数量不同,并且这是这些字符串第一次发生这种情况,则将这些块视为相等(移至下一个块),但记住哪个字符串具有更多前导零。
- 如果两个块都不是数字,则按 Unicode 字符按字典顺序比较它们,但有两个例外
_
(下划线)紧跟在- 除非另有说明,否则版本排序应在小写字符之前对非小写字符(可以启动
UpperCamelCase
标识符的字符)进行排序。
- 如果比较到达字符串的末尾并且认为每对块相等
- 如果其中一个数字比较指出一个字符串比另一个字符串具有更多前导零的最早点,则首先对具有更多前导零的字符串进行排序。
- 否则,字符串相等。
请注意,存在各种称为“版本排序”的算法,它们通常尝试解决相同的问题,但在各种方面有所不同(例如,它们处理带有前导零的数字的方式)。此算法并非旨在精确匹配任何其他特定算法的行为,而仅旨在为 Rust 格式化生成简单且令人满意的结果。特别是,此算法旨在为一组具有相同数量前导零的符号生成令人满意的结果,并为一组具有不同数量前导零的符号生成可接受且易于理解的结果。
例如,版本排序将按给定的顺序对以下字符串进行排序
_ZYXW
_abcd
A2
ABCD
Z_YXW
ZY_XW
ZY_XW
ZYXW
ZYXW_
a1
abcd
u_zzz
u8
u16
u32
u64
u128
u256
ua
usize
uz
v000
v00
v0
v0s
v00t
v0u
v001
v01
v1
v009
v09
v9
v010
v10
w005s09t
w5s009t
x64
x86
x86_32
x86_64
x86_128
x87
zyxw
模块级项
语句
表达式
类型
注释
以下注释指南仅为建议,机械格式化程序可能会跳过注释的格式化。
优先使用行注释(//
),而不是块注释(/* ... */
)。
使用行注释时,在开头的符号后放置一个空格。
使用单行块注释时,在开头的符号后和结尾的符号前放置一个空格。对于多行块注释,在开头的符号后放置一个换行符,并在结尾的符号前放置一个换行符。
优先将注释放在单独的行上。如果注释跟在代码之后,请在其前放置一个空格。如果块注释内联出现,请像对待标识符或关键字一样使用周围的空格。不要在注释之后或多行注释中的任何行的末尾包含尾随空格。示例
#![allow(unused)] fn main() { // A comment on an item. struct Foo { ... } fn foo() {} // A comment after an item. pub fn foo(/* a comment before an argument */ x: T) {...} }
注释通常应为完整的句子。以大写字母开头,以句点(.
)结尾。内联块注释可以被视为没有标点符号的注释。
完全是注释的源代码行的长度应限制为 80 个字符(包括注释符号,但不包括缩进)或行的最大宽度(包括注释符号和缩进),以较小者为准
#![allow(unused)] fn main() { // This comment goes up to the ................................. 80 char margin. { // This comment is .............................................. 80 chars wide. } { { { { { { // This comment is limited by the ......................... 100 char margin. } } } } } } }
文档注释
优先使用行注释(///
),而不是块注释(/** ... */
)。
优先使用外部文档注释(///
或 /** ... */
),仅使用内部文档注释(//!
和 /*! ... */
)来编写模块级或 crate 级文档。
将文档注释放在属性之前。
属性
将每个属性放在单独的行上,缩进到项目级别。对于内部属性(#!
),将其缩进到项目内部的级别。如果可能,优先使用外部属性。
对于带有参数列表的属性,请像函数一样格式化。
#![allow(unused)] fn main() { #[repr(C)] #[foo(foo, bar)] #[long_multi_line_attribute( split, across, lines, )] struct CRepr { #![repr(C)] x: f32, y: f32, } }
对于带有等号的属性,在 =
的前后放置一个空格,例如,#[foo = 42]
。
只能有一个 derive
属性。工具作者的说明:如果要将多个 derive
属性合并为一个属性,则必须通常保留派生名称的顺序以确保正确性:#[derive(Foo)] #[derive(Bar)] struct Baz;
必须格式化为 #[derive(Foo, Bar)] struct Baz;
。
小的 项目
在本指南的许多地方,我们指定了依赖于代码结构为小的格式。例如,单行与多行结构字面量
#![allow(unused)] fn main() { // Normal formatting Foo { f1: an_expression, f2: another_expression(), } // "small" formatting Foo { f1, f2 } }
我们让各个工具来决定小的确切含义。特别是,工具可以自由地在不同情况下使用不同的定义。
一些合适的启发式方法是项目的大小(以字符为单位)或项目的复杂性(例如,所有组件都必须是简单的名称,而不是更复杂的子表达式)。有关合适启发式方法的更多讨论,请参阅此问题。