“
原文链接: https://github.com/pretzelhammer/rust-blog/blob/master/posts/sizedness-in-rust.md
原文标题: Sizedness in Rust
公众号: Rust碎碎念
Sized Trait
Rust中的Sized
trait是自动(auto)trait和标记(marker)trait。
自动trait是能够为满足某些条件的类型自动实现的trait。标记trait是标记一个类型拥有某种特定属性的trait。标记trait没有任何trait项,比如方法,关联函数,关联常量或者关联类型。所有的自动trait都是标记trait但是并非所有的标记trait都是自动trait。自动trait必须是标记trait,这样编译器才能为它们提供一个自动的默认实现,如果trait有任何trait项,这一点就无法做到。
如果一个类型的所有成员都是Sized
,那么这个类型就会自动实现Sized
。成员(member)
的含义取决于具体的类型,例如:一个结构体的字段,枚举的变体(variants),数组的元素,元组(tuple)的项(item)等等。一旦一个类型被一个Sized
实现“标记(marked)”则意味着在编译期可以确定它的字节数大小。
另外两个自动标记trait的例子是Send
和Sync
trait。如果一个类型能够安全地在线程间被发送(send),那么这个类型是Send
。一个类型如果能够在线程间安全地共享引用,那么这个类型是Sync
。如果一个类型的所有成员都是Send
和Sync
,那么这个类型也会自动实现Send
和Sync
。Sized
比较特殊的一点是,不像其他的自动标记trait可以选择退出(opt-out),Sized
不能选择退出(opt-out)。
#![feature(negative_impls)]
// this type is Sized, Send, and Sync
struct Struct;
// opt-out of Send trait
impl !Send for Struct {}
// opt-out of Sync trait
impl !Sync for Struct {}
impl !Sized for Struct {} // compile error
这似乎是合理的,因为有时候我们不想我们的类型在线程间能够发送或者共享,但是,很难想象这样一个场景,即我们想要编译器"忘记(forget)"我们的类型的大小而把它作为一个不确定大小类型(unsized type),因为这样没有任何益处且仅仅是让类型更难使用。
而且,更学究一点儿来讲,Sized
从技术上来讲不算是一个自动trait,因为它不是使用auto
关键字定义而是得到了编译器的特殊处理从而使它表现得和自动trait非常相似,所以,在实践中把它看做一个自动trait也是可以的。
关键点(Key Takeaway)
Sized
是一个"自动(auto)"标记trait
泛型中的Sized (Sized in Generic)
当我们写泛型代码的时候,每一个泛型类型参数默认会自动和Sized
trait绑定,这件事并不总是显而易见。
// this generic function...
fn func<T>(t: T) {}
// ...desugars to...
fn func<T: Sized>(t: T) {}
// ...which we can opt-out of by explicitly setting ?Sized...
fn func<T: ?Sized>(t: T) {} // compile error
// ...which doesn't compile since t doesn't have
// a known size so we must put it behind a pointer...
fn func<T: ?Sized>(t: &T) {} // compiles
fn func<T: ?Sized>(t: Box<T>) {} // compiles
Pro tips
?Sized
可以是明显的“可选大小(optionally sized)”或者“可能大小(maybe sized)”,将其添加到类型参数的约束(bound)上,允许该类型是确定大小(sized)或者不确定大小(unsized)?Sized
一般是指一个“不断扩大的约束(widening bound)”或者一个“宽松约束(relaxed bound)”,因为它是放松(relax)而不是限制了类型参数?Sized
是Rust中惟一的宽松约束
所以,为什么这很重要?当我们处理泛型参数的时候并且那个类型隐藏在指针背后,我们几乎总是想要选择退出默认的Sized
约束来让我们的函数在其将要接受的参数类型上更加自由。而且,如果我们没有选择退出默认的Sized
约束,我们将最终得到一些令人惊讶和迷惑的编译错误信息。
让我带你踏上我用Rust编写的第一个泛型函数的旅程。我开始学习Rust的时候,dbg!
宏还没有被加入到stable版本里,所以我打印调试值的唯一方式就是使用println!()"{:?}", some_value);
。 每次调试都是这么枯燥,所以我决定写一个类似下面这样的debug
辅助函数:
use std::fmt::Debug;
fn debug<T: Debug>(t: T) { // T: Debug + Sized
println!("{:?}", t);
}
fn main() {
debug("my str"); // T = &str, &str: Debug + Sized ✔️
}
到目前为止,一切都还不错,但是这个函数会获取传递进来的值的所有权,这有点烦人,所以我把下面的函数改成只获取引用的方式:
use std::fmt::Debug;
fn dbg<T: Debug>(t: &T) { // T: Debug + Sized
println!("{:?}", t);
}
fn main() {
dbg("my str"); // &T = &str, T = str, str: Debug + !Sized ❌
}
这段代码抛出了下面的错误:
error[E0277]: the size for values of type `str` cannot be known at compilation time
--> src/main.rs:8:9
|
3 | fn dbg<T: Debug>(t: &T) {
| - required by this bound in `dbg`
...
8 | dbg("my str");
| ^^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `std::marker::Sized` is not implemented for `str`
= note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
help: consider relaxing the implicit `Sized` restriction
|
3 | fn dbg<T: Debug + ?Sized>(t: &T) {
|
当我第一次看到这个错误信息,我发现它非常令人困惑。尽管我已经让这个函数在获取参数上比之前更加地严格,但现在却莫名其妙地抛出个错误!发生了什么?
我已经在上面的代码注释中给出了答案,但基本的:Rust在编译时,当把T解析到具体对应的类型时,会执行模式匹配。下面两张表有助于说明:
Type | T | &T |
---|---|---|
&str | T = &str | T=str |
Type | Sized |
---|---|
str | ❌ |
&str | ✔️ |
&&str | ✔️ |
这就是为什么我在将函数改为传引用之后不得不添加一个?Sized
约束来让它按照预期来工作。正常工作的函数如下:
use std::fmt::Debug;
fn debug<T: Debug + ?Sized>(t: &T) { // T: Debug + ?Sized
println!("{:?}", t);
}
fn main() {
debug("my str"); // &T = &str, T = str, str: Debug + !Sized ✔️
}
关键点(Key Takeaway)
- 所有的泛型类型参数默认都是被
Sized
自动约束的 - 如果我们有一个泛型函数,该函数接收一个隐藏在指针背后的参数T,比如,
&T
,Box<T>
,Rc<T>
等等,那么我们几乎总是要选择退出默认的Sized
约束然后用T:?Sized
来约束
本文禁止转载,谢谢配合!欢迎关注我的微信公众号: Rust碎碎念