可预测性

智能指针不添加固有方法 (C-SMART-PTR)

例如,这就是 Box::into_raw 函数定义方式的原因。

#![allow(unused)]
fn main() {
impl<T> Box<T> where T: ?Sized {
    fn into_raw(b: Box<T>) -> *mut T { /* ... */ }
}

let boxed_str: Box<str> = /* ... */;
let ptr = Box::into_raw(boxed_str);
}

如果将其定义为一个固有方法,那么在调用点就会产生混淆,不知道被调用的方法是 Box<T> 上的方法还是 T 上的方法。

#![allow(unused)]
fn main() {
impl<T> Box<T> where T: ?Sized {
    // 不要这样做。
    fn into_raw(self) -> *mut T { /* ... */ }
}

let boxed_str: Box<str> = /* ... */;

// 这是通过智能指针 Deref 实现访问的 str 上的方法。
boxed_str.chars()

// 这是 Box<str> 上的方法...?
boxed_str.into_raw()
}

转换发生在涉及的最具体的类型上 (C-CONV-SPECIFIC)

在有疑问时,优先使用 to_/as_/into_ 而不是 from_,因为它们更符合使用习惯(并且可以与其他方法链式调用)。

对于两种类型之间的许多转换,其中一种类型显然更加“具体”:它提供了一些在其他类型中不存在的附加不变性或解释。例如,str&[u8] 更具体,因为它是一个 UTF-8 编码的字节序列。

转换应当存在于涉及的类型中更具体的那个上。因此,str 提供了 as_bytes 方法和 from_utf8 构造器用于与 &[u8] 值之间的转换。除了直观之外,这种约定还避免了使用具体类型如 &[u8] 被无穷尽的转换方法污染。

具有明确接收对象的函数是方法 (C-METHOD)

倾向于

#![allow(unused)]
fn main() {
impl Foo {
    pub fn frob(&self, w: widget) { /* ... */ }
}
}

而非

#![allow(unused)]
fn main() {
pub fn frob(foo: &Foo, w: widget) { /* ... */ }
}

用于任何明显与特定类型关联的操作。

方法相较于函数有很多优势:

  • 它们不需要导入或限定即可使用:你只需要一个合适类型的值。
  • 它们的调用执行自动借用(包括可变借用)。
  • 它们使回答“我可以用类型 T 的值做什么”这一问题变得容易(特别是在使用 rustdoc 时)。
  • 它们提供 self 表示法,更简洁且通常更清晰地传达所有权区别。

函数不接受输出参数 (C-NO-OUT)

倾向于

#![allow(unused)]
fn main() {
fn foo() -> (Bar, Bar)
}

而非

#![allow(unused)]
fn main() {
fn foo(output: &mut Bar) -> Bar
}

用于返回多个 Bar 值。

诸如元组和结构体的复合返回类型编译得非常高效,不需要堆分配。如果一个函数需要返回多个值,应通过这些类型之一来返回。

主要的例外情况:有时候函数旨在修改调用者已经拥有的数据,例如重用一个缓冲区:

#![allow(unused)]
fn main() {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize>
}

操作符重载应不令人惊讶 (C-OVERLOAD)

带有内建语法(*, |, 等)的操作符可以通过实现 std::ops 中的特征为类型提供。这些操作符伴随着强烈的期望:仅为与乘法有某种相似性的运算(并共享期望的属性, 如结合律)实现 Mul,其他特征同理。

只有智能指针实现 DerefDerefMut (C-DEREF)

Deref 特征在很多情况下被编译器隐式使用,并与方法解析交互。相关规则专门设计用于适应智能指针,因此这些特征应仅用于此目的。

标准库中的示例

构造函数是静态的、固有的方法 (C-CTOR)

在 Rust 中,“构造函数”只是一个约定。关于构造函数命名有各种约定,区别往往微妙。

最基本形式的构造函数是一个无参数的 new 方法。

#![allow(unused)]
fn main() {
impl<T> Example<T> {
    pub fn new() -> Example<T> { /* ... */ }
}
}

构造函数是为它们所构造的类型定义的静态(无 self)固有方法。结合完全导入类型名称的做法,这种约定可导致信息丰富但简洁的构造:

#![allow(unused)]
fn main() {
use example::Example;

// 构造一个新的 Example。
let ex = Example::new();
}

名称 new 通常用于实例化类型的主要方法。有时它不带参数,如上例。有时它确实带有参数,如将值放入 BoxBox::new

一些类型的构造函数,尤其是 I/O 资源类型,使用与其构造函数相对应的命名约定,如 File::openMmap::openTcpStream::connectUdpSocket::bind。在这些情况下,名称是根据具体领域选择的。

通常,有多种方式构造一个类型。在这些情况下,辅助构造函数常常带有 _with_foo 后缀,如 Mmap::open_with_offset。如果你的类型有多种构造选项,请考虑使用构建器模式 (C-BUILDER)。

一些构造函数是“转换构造函数”,即从其他类型的现有值创建新类型的方法。它们通常具有以 from_ 开头的名称,如 std::io::Error::from_raw_os_error。但也请注意 From 特征 (C-CONV-TRAITS),它与此非常相似。from_ 前缀的转换构造函数和 From<T> 实现之间有三个区别:

  • from_ 构造函数可以是 unsafe 的;而 From 实现不能。例如 Box::from_raw
  • from_ 构造函数可以接受附加参数来消除源数据的意义,以 u64::from_str_radix 为例。
  • From 实现仅适用于当源数据类型足以确定输出数据类型的编码时。当输入只是数据包,如在 u64::from_beString::from_utf8 中,转换构造函数名称能够标识其意义。

注意,通常且预期的做法是类型同时实现 Default 和一个 new 构造函数。对于同时拥有这两者的类型,它们应具有相同的行为。任何一个都可以通过另一个来实现。

标准库中的示例