灵活性
函数暴露中间结果以避免重复工作 (C-INTERMEDIATE)
许多函数在回答问题的同时也计算了相关的有趣数据。如果这些数据对客户端可能有用,应该考虑在 API 中暴露这些数据。
标准库中的示例
-
Vec::binary_search
不返回一个表示是否找到值的bool
,也不返回一个表示可能找到值的索引的Option<usize>
。它返回的是有关索引的信息,如果找到则返回索引,如果没找到则返回插入值所需的索引。 -
String::from_utf8
如果输入字节不是 UTF-8,则可能会失败。在错误情况下,它返回一个中间结果,该结果显示输入中有效 UTF-8 的字节偏移量,并返还输入字节的所有权。 -
HashMap::insert
返回一个Option<T>
,它返回给定键的预存在值,如果有的话。对于用户想要恢复这个值的情况,由插入操作返回它可以避免用户进行第二次哈希表查找。
调用者决定复制和放置数据的位置 (C-CALLER-CONTROL)
如果函数需要参数的所有权,它应该接收参数的所有权,而不是借用和克隆参数。
#![allow(unused)] fn main() { // 优先这样: fn foo(b: Bar) { /* 直接使用 b 的所有权 */ } // 而不是这样: fn foo(b: &Bar) { let b = b.clone(); /* 克隆后使用 b 的所有权 */ } }
如果函数不需要参数的所有权,它应该借用参数,而不是获取所有权并丢弃参数。
#![allow(unused)] fn main() { // 优先这样: fn foo(b: &Bar) { /* 借用 b 使用 */ } // 而不是这样: fn foo(b: Bar) { /* 借用 b 使用,函数返回前会隐式丢弃 */ } }
只有在绝对需要时才应将 Copy
trait 用作约束,而不是作为标记副本应该容易制作的方式。
函数通过使用泛型来最小化对参数的假设 (C-GENERIC)
函数对其输入做出的假设越少,其可用性就越广。
优先
#![allow(unused)] fn main() { fn foo<I: IntoIterator<Item = i64>>(iter: I) { /* ... */ } }
而不是任何以下实现
#![allow(unused)] fn main() { fn foo(c: &[i64]) { /* ... */ } fn foo(c: &Vec<i64>) { /* ... */ } fn foo(c: &SomeOtherCollection<i64>) { /* ... */ } }
如果函数只需要迭代数据。
更一般地,考虑使用泛型来准确地点出函数需要对其参数进行的假设。
泛型的优势
-
重用性。泛型函数可以应用于一个开放式的类型集合,同时为这些类型需要提供的功能提供一个清晰的契约。
-
静态调度和优化。每次使用泛型函数时,都会专门为实现 trait 约束的特定类型进行“单态化”,这意味着 (1) trait 方法的调用为静态的、直接的实现调用,(2)编译器可以内联和进一步优化这些调用。
-
内联布局。如果一个
struct
和enum
类型是某个类型参数T
的泛型,T
类型的值将在struct
/enum
中内联布局,而没有任何间接性。 -
推断。由于通常可以推断给泛型函数的类型参数,泛型函数可以帮助减少代码中的冗长,通常无需显式转换或其他方法调用。
-
精确类型。因为泛型为实现一个 trait 的特定类型提供了一个_名称_,所以可以准确地表示需要或产生该确切类型的地方。例如,一个函数
#![allow(unused)] fn main() { fn binary<T: Trait>(x: T, y: T) -> T }
可以保证使用和返回完全相同类型
T
的元素;不能用不同类型的参数调用此函数,即使这些类型都实现了Trait
。
泛型的劣势
-
代码大小。专用的泛型函数意味着函数主体被复制。必须权衡代码大小的增加与静态调度的性能收益之间的关系。
-
同质类型。这是“精确类型”问题的另一面:如果
T
是一个类型参数,它代表一个_单一_实际类型。因此,例如,Vec<T>
包含单一具体类型的元素(实际上,矢量表示被专门用于内联这些元素)。有时,异类集合是有用的;见 trait 对象。 -
签名冗长。大量使用泛型可能使函数的签名更难以阅读和理解。
标准库中的示例
std::fs::File::open
接受一个通用类型AsRef<Path>
的参数。这允许通过字符串字面量"f.txt"
、Path
、OsString
和一些其他类型方便地打开文件。
具有潜在用途的 trait 对象是对象安全的 (C-OBJECT)
trait 对象有一些显著的限制:通过 trait 对象调用的方法不能使用泛型,并且不能在接收者位置之外使用 Self
。
设计 trait 时,及早决定是将它用作对象还是用作泛型的约束。
如果一个 trait 是被用作对象,其方法应该接收和返回 trait 对象而不是使用泛型。
where
子句 Self: Sized
可用于将特定方法从 trait 的对象中排除出去。以下 trait 由于泛型方法而不是对象安全的。
#![allow(unused)] fn main() { trait MyTrait { fn object_safe(&self, i: i32); fn not_object_safe<T>(&self, t: T); } }
为泛型方法添加 Self: Sized
要求,将其从 trait 对象中排除,使 trait 对象安全。
#![allow(unused)] fn main() { trait MyTrait { fn object_safe(&self, i: i32); fn not_object_safe<T>(&self, t: T) where Self: Sized; } }
trait 对象的优点
- 异构性。当你需要它时,你真的需要它。
- 代码大小。与泛型不同,trait 对象不生成专用(单态化)的代码版本,这可以大大减少代码大小。
trait 对象的缺点
- 无泛型方法。trait 对象目前不能提供泛型方法。
- 动态调度和胖指针。trait 对象本身涉及间接性和 vtable 调度,这可能带来性能上的代价。
- 无 Self。除接收者参数以外,trait 对象上的方法不能使用
Self
类型。