本文档列出了网络栈团队发现会产生以下问题的模式:
- 代码更能适应远处的行为变化。
- 代码更易于审核,需要的“工作内存”更少。
这些准则是对 Rust API 评分标准的补充。
此处建议的模式提供了集中的指导和知识。我们鼓励您贡献各种类型的内容,包括修改、添加、移除等。
在建议的代码编写或审核期间遇到粗糙边缘 指南都会重新纳入本文档,以便我们可以 在需要时将替代模式编入代码。
避免未使用的结果
自 https://fxrev.dev/510442 起,这项要求主要由机器通过 rustc 的 unused-results lint 强制执行。请参阅 文档,了解有关 推理。
舍弃结果时,需要对忽略字段的类型进行编码,使其成为 合同条款。
舍弃结果时,如果类型无法立即明确说明语义,并且不会影响 drop
语义,最好使用前缀为 _
的命名变量。
添加的信息可供作者和审核者参考:
- 是否有应使用返回值检查的不变量?
- 此函数是否应返回值?
注意:过去,该团队在作业中使用表单
let () =
来表明不会丢弃任何信息。这种做法仍然可行,但在引入 lint 后,已不再需要这样做。
示例
使用提示
// Bad. The dropped type is unknown without knowing the signature of write.
let _ = writer.write(payload);
let _n = writer.write(payload);
// Okay. The dropped type is locally visible and enforced. Absence of invariant
// enforcement such as a check for partial writes is obvious and can be flagged
// in code review.
let _: usize = writer.write(payload);
let _n: usize = writer.write(payload);
// Good.
let n = writer.write(payload);
assert_eq!(n, payload.len());
采用了丢弃格式
// Encode the type of the ignored return value.
let _: Foo = foo::do_work(foo, bar, baz);
// When destructuring tuples, type the fields not in use.
let (foo, _) : (_, Bar) = foo::foo_and_bar();
// When destructuring named structs, no annotations are necessary, the field's
// type is encoded by the struct.
let Foo{ fi, fo: _ } = foo::do_work(foo, bar, baz);
// Encode the most specific type possible.
let _: Option<_> = foo.maybe_get_trait_impl(); // Can't name opaque type.
let _: Option<usize> = foo.maybe_get_len(); // Can name concrete type.
// Use best judgement for unwieldy concrete types.
// If the type expands to formatted code that would span >= 5 lines of type
// annotation, use best judgement to cut off the signature.
let _: futures::future::Ready<Result<Buffer<_>>, anyhow::Error> = y();
// Prefer to specify if only a couple of levels of nesting and < 5 lines.
let _: Result<
Result<(bool, usize), fidl_fuchsia_net_interfaces_admin::AddressRemovalError>,
fidl::Error,
> = x();
击败模式
请注意以下破解模式:
// Bad, this is a drop that does not encode the type.
std::mem::drop(foo());
// Prefer instead:
let _: Foo = foo();
测试
声明
请根据要测试的内容为测试命名,且不带 test_
前缀。这就是
在 Rust 标准库中采用的模式。
如果测试名称不足以对测试目标进行编码,请添加 在其前面或函数正文顶部(用于说明)的简短非文档注释 这个测试正在进行的任务我们使用非文档注释,因为我们希望目标受众是代码读者,而不是公共 API 读者。
测试应始终位于名为 tests
或其后代之一的模块中。
仅包含集成测试的 crate 不需要 tests
模块,例如 network/tests/integration、network/tests/fidl。
示例:
// Tests Controller::do_work error returns.
#[test]
fn controller_do_work_errors() { /* ... */ }
测试支持函数应位于名为 testutil
的模块中。如果模块仅用于 crate 中,则应声明为 pub(crate)
和 #[cfg(test)]
。此模块不应包含任何 #[test]
函数。如果需要对 testutil
模块中的功能进行测试,则应创建一个名为 tests
的子模块(即testutil::tests
)。
首选恐慌
切勿使用 Rust 对返回结果的测试 结果;此类测试不会自动发出回溯, 依赖错误本身进行回溯测试失败时 emit 回溯通常很难解释。在撰写本文时, Rust 中的回溯功能不稳定,并且 已在 Fuchsia Rust build 配置中停用,但 即使已启用,并非所有错误都包含回溯;最好不要惊慌, 依赖外部因素进行回溯。
导入
以下规则适用于样式导入。
每个 crate 或直接子模块使用一个 use
语句。合并
将它们放到同一个 use 语句中
use child_mod::{Foo, Bar};
use futures::{
stream::{self, futures_unordered::FuturesUnordered},
Future, Stream,
};
始终将导入但未引用的特征别名为 _
。这样可以避免
命名空间,并通知读取器未引用该标识符。
use futures::FutureExt as _;
避免将其他 crate 中的符号引入到作用域中。尤其是在
它们的名称都很相似但对于广泛使用的自解释性 std
类型(例如 HashMap
、HashSet
等),则不受此限制。
系统始终允许从同一 crate 导入符号,包括遵循
声明 crate-local Error
和 Result
类型的模式。
一些 crate 严重依赖于外部类型。如果预计这些类型在整个 crate 中无处不在,则可以为了减少冗长性而导入这些类型,前提是不会引入歧义。
// DON'T. Parsing this module quickly grows out of hand since it's hard to make
// sense where types are coming from.
mod confusing_mod {
use packet_formats::IpAddress;
use crate::IpAddr;
use fidl_fuchsia_net::Ipv4Address;
}
// DO. Import only types from this crate.
mod happy_mod {
use crate::IpAddress;
fn foo() -> packet_formats::IpAddress { /* .. */ }
fn bar() -> IpAddress { /* .. */ }
fn baz() -> fidl_fuchsia_net::Ipv4Address { /* .. */ }
}
一些知名 crate 采用了别名或别名形成规则。这些功能包括:
- 可以将
fuchsia_async
的别名设为fasync
。 fidl_fuchsia_*
前缀可以别名为f*
,例如:use fidl_fuchsia_net_interfaces_admin as fnet_interfaces_admin;
use fidl_fuchsia_net_routes as fnet_routes;
强烈建议不要导入 *
。测试从 super
导入的模块
可接受的例外情况;测试模块会假设
在其父项中声明的所有符号。
始终允许在函数正文中导入类型,因为这很容易推断 类型来源的位置。
fn do_stuff() {
use some::deeply::nested::{Foo, bar::Bar};
let x = Foo::new();
let y = Bar::new();
/* ... */
}
首选详尽的匹配
尽可能进行详尽匹配,避免使用万能模式。匹配 详尽地在审核过程中提供更多本地背景信息, 在枚举更新时重新访问,并且需要更明确的形式才能 丢弃信息。
某些模式会隐式地胜过穷尽匹配。我们应谨慎使用这些功能:
- 避免使用
if let
模式,因为这实际上并不是详尽无遗的。周三 划分出异常,以允许绑定到Some
Option<T>
,因为它是一种知名类型,且None
变体不携带 任何信息请注意,如果T
是一个枚举,则if let Some(Foo::Variant)
不是对异常的有效使用,因为它回避了T
。 - 避免在枚举接收器上指示未匹配的变体的方法,例如:
is_foo(&self) -> bool
枚举方法(如Result::is_ok
或Option::is_none
。foo(self) -> Option<T>
枚举方法(例如Result::ok
),这些方法可用作单变体提取帮助程序,在审核中很容易被忽略。
Rust 提供了 non_exhaustive
属性,该属性违背了
会进行详尽匹配,而 flexible
FIDL 类型则使用
属性。在处理此类类型时,尝试进行详尽匹配很容易过时,因为类型可以演变而不会破坏代码,因此应避免这样做。
// Don't attempt to match exhaustively, exhaustive enumeration is prone to
// becoming stale and misleading future readers if `Foo` takes more variants.
fn bad_flexible_match(foo: fidl_foo::FlexibleFoo) {
match foo {
fidl_foo::FlexibleFoo::Bar => { /* ... */ },
foo @ fidl_foo::FlexibleFoo::Baz |
foo => panic!("unexpected foo {:?}", foo)
}
}
// Use the catch-all pattern instead when the type is non_exhaustive.
fn good_flexible_match(foo: fidl_foo::FlexibleFoo) {
match foo {
fidl_foo::FlexibleFoo::Bar => { /* ... */ },
foo => panic!("unexpected foo {:?}", foo)
}
}
// Note that if the type was not marked non_exhaustive we'd prefer to match
// exhaustively.
fn strict_match(foo: fidl_foo::StrictFoo) {
match foo {
fidl_foo::StrictFoo::Bar => { /* ... */ },
foo @ fidl_foo::StrictFoo::Baz |
foo @ fidl_foo::StrictFoo::Boo => panic!("unexpected foo {:?}", foo)
}
}
TODO(https://github.com/rust-lang/rust/issues/89554):重新访问
non_exhaustive
non_exhaustive_omitted_patterns
lint 稳定后提供相关指南。
避免使用默认类型参数
Rust 支持使用默认类型形参定义参数化类型。 对于某些参数(例如仅用于替换测试中的行为的参数),这非常方便。例如:
// This can be used as `Foo<X>` or `Foo<X, Y>`.
struct Foo<A, B = u32>(A, B);
// Blanket impl for all possible `Foo`s.
impl<A, B> MyTrait for Foo<A, B> { /* ... */ }
默认类型参数的问题在于,它很容易导致不完整的通用实现。假设 Foo
使用另一个默认的类型参数进行扩展:
// Now `Foo<A> = Foo<A, u32> = Foo<A, u32, ()>.
struct Foo<A, B = u32, C = ()>(A, B, C);
impl MyTrait
代码块仍会在不修改的情况下正常运行,因此编译器不会发出覆盖率不完整的信号:它仅涵盖 Foo<A, B, ()>
,而不是所有可能的 Foo<A, B, C>
。避免使用默认类型参数会让作者承担确保所有实现(无论是全面实现还是其他实现)都涵盖正确的类型集的责任。
日志记录
日志对开发者来说是关键的调试资源,但遗憾的是, 是日志记录的几个隐患,会降低它们的价值和信号。同时记录 或者日志的严重级别过高,也会导致日志变得杂乱, 对给定问题而言非常重要的调试信号反之,记录 或者严重程度过低意味着调试信号 最初可能不会记录到日志中。为了生成有用日志,必须找到合适的平衡点。
Netstack3 利用 跟踪 用于日志记录的 crate,公开了多个日志级别。在深入了解 Netstack3 组件的日志记录指南,但有必要知道一些 日志严重性的默认配置:
- 最低日志严重性(即,抑制低于此值的日志级别)
在正式版设置中设置为
info
,使用调试时设置为debug
netstack 组件(例如,在测试中)。在本地运行 Netstack3 时,开发者可以将此值配置得更低。 - 测试的最大日志严重性(即高于此值的日志级别会导致
否则,将通过测试报告为已失败)设置为
warn
默认值。各个测试套件可以调高或调低此阈值。
那么,在什么情况下应该使用各个日志级别呢?
error
:预留用于严重失败,并且很有可能表现为用户可见的问题。示例:- 移除接口,因为底层端口意外关闭。
- 观察
PEER_CLOSED
以外的 FIDL 错误。
warn
:虽然不太严重,以至于证明error
的合理性的事件或失败情况, 可能仍然会体现在用户可见的问题或其他 组件。示例:- 从未实现的 API 成功返回。
- 拒绝语义不正确的 API 请求(例如参数无效)。
- 观察底层端口的实际状态是否已更改为离线。
info
:重要事件和低成本调试信号。示例- 网络状态的更改:例如添加/移除地址、路由或接口。
- 有趣的网络事件(例如重复地址检测失败)。
debug
:仅与 Netstack 开发者相关的事件和调试信号,或者噪声过大,不适合在生产设备上使用。- 控制来自网络的事件,例如路由器通告。
- 传入的 API 调用(例如套接字操作或 FIDL 方法),以及 返回给调用方的结果。
- 启动/停止定期任务,例如邻居表垃圾回收器。
trace
:调试玩具以外的代价非常高昂的信号 环境(即单元测试)。示例- 极其频繁的消息,例如与发送/接收数据路径数据包相关的消息,或与高频率计时器触发相关的消息。
目的是让代码中 error
和 trace
的实例
极其罕见,而 warn
的实例则并不常见。大多数日志实例应为 info
或 debug
。
将 debug
视为默认日志级别选项。在决定进行宣传之前
向 info
发送消息,问问自己预计收到消息的频率是多少
以及此消息在对字段问题进行分类时提供的值。
在决定向warn
宣传某条消息之前,请先问问自己这样做的可能性有多大
向其他团队反映问题(例如,是否适合其他团队)
团队给您分配一个 bug,因为他们在日志中看到了此警告?)。
此页面的更改流程
欢迎所有人就 Netstack 团队采用的模式提出更改建议。在通过讨论达成共识后,团队会接受或拒绝提议的模式,如果无法达成共识,则采用简单的多数投票方式决定是否接受。
如需提出更改建议,请按以下步骤操作。
- 编写并发布用于更改此页面的 CL。
- [可选]与小群体交流并迭代。
- 通过电子邮件和聊天请求整个团队进行审核。非 Google 员工可以通过讨论群组与我们联系。
- 根据评价评论和线下会话对 CL 进行迭代改进。 请务必将线下会话的结果发布回 CL。
- 团队成员可以表达支持
+1
、反对-1
或不介意。 通过 Gerrit 上的单个评论会话表达了不同的看法 成员状态的差异化该会话会保持未解决状态,直到 CL 合并。团队成员可以随时更改投票。 - 提案的审核期最长为 2 周。最后一次通话通知是 。如果出现以下情况,时间轴可能会短路: 整个团队已投票。
- 如果无法达成共识,该团队将统计投票结果,并根据简单多数决定是否继续。
注意事项:
- 作者和主管将引导您完成此流程。
- 尊重他人;仅针对其优点提出意见。
- 避免发表长篇大论;简要说明您对相关论据的不同看法。
- 不建议在 CL 中来回切换。回退到分组会议或 线下会议(最好是公开会议),并将共识编码回 评论串。
- 如果存在以下情况,则可以忽略争议性点并在后续提案中予以解决 。
- 不介意投票用于衡量编码某些模式的预期效益。将强烈的差异信号解释为 都不适合作为模式进行编码。