为什么在这个 trait 中需要 `Sized` 约束?

73

我有一个带有两个关联函数的特征:

trait WithConstructor: Sized {
    fn new_with_param(param: usize) -> Self;

    fn new() -> Self {
        Self::new_with_param(0)
    }
}

为什么第二个方法(new())的默认实现强制我在类型上放置Sized限定? 我认为这是因为栈指针操作,但我不确定。

如果编译器需要知道大小来在堆栈上分配内存, 为什么下面的示例不需要TSized限定?

struct SimpleStruct<T> {
    field: T,
}

fn main() {
    let s = SimpleStruct { field: 0u32 };
}
2个回答

180
正如您可能已经知道的那样,Rust中的类型可以是大小和不定大小的。不定大小的类型,顾名思义,没有存储此类型值所需的大小,这点对编译器来说是未知的。例如,[u32] 是一个不定大小的 u32 数组;因为元素数量在任何地方都未指定,编译器也不知道它的大小。另一个例子是裸的 trait 对象类型,例如,Display,当它直接用作类型时。
let x: Display = ...;

在这种情况下,编译器不知道实际使用的类型是什么,它被擦除了,因此它不知道这些类型的值的大小。上面的代码行是无效的 - 你不能创建一个本地变量而不知道它的大小(为了在堆栈上分配足够的字节),并且你不能将未定大小的类型的值作为参数传递给函数或从函数中返回
然而,未定大小的类型可以通过指针来使用,这个指针可以携带额外的信息 - 切片可用数据的长度(&[u32])或者虚表的指针(Box<SomeTrait>)。因为指针总是有固定和已知的大小,所以它们可以存储在本地变量中,并被传递到或从函数中返回。
对于任何具体的类型,您总是可以说它是否是有大小限制的或者没有大小限制的。但是,对于泛型类型,一个问题就出现了 - 某些类型参数是否有大小限制?
fn generic_fn<T>(x: T) -> T { ... }

如果 T 是无大小限制的,那么这个函数定义是不正确的,因为你不能直接传递无大小限制的值。如果有大小限制,那么一切都没问题。
在 Rust 中,所有泛型类型参数默认情况下都是有大小限制的 - 在函数、结构体和 trait 中都是如此。它们有一个隐式的 Sized 约束;Sized 是用于标记有大小限制类型的 trait:
fn generic_fn<T: Sized>(x: T) -> T { ... }

这是因为在绝大多数情况下,您希望您的泛型参数具有大小。然而,有时您可能想取消大小性,并且可以通过?Sized限定来实现:

fn generic_fn<T: ?Sized>(x: &T) -> u32 { ... }

现在可以像这样调用generic_fn("abcde"),并且T将被实例化为无大小的str,但这没关系-此函数接受对T的引用,因此不会发生任何问题。
然而,在Rust中,Trait总是针对某种类型实现的,这里大小的问题也很重要。
trait A {
    fn do_something(&self);
}

struct X;
impl A for X {
    fn do_something(&self) {}
}

然而,这只是为了方便和实用性而必要的。可以定义特征始终采用一个类型参数,并且不指定实现特征的类型:

// this is not actual Rust but some Rust-like language

trait A<T> {
    fn do_something(t: &T);
}

struct X;
impl A<X> {
    fn do_something(t: &X) {}
}

这就是 Haskell 类型类的工作方式,实际上在 Rust 中也是在较低层次上实现的。每个 Rust 中的 trait 都有一个隐式类型参数,称为 `Self`,它指定了该 trait 所实现的类型。它始终在 trait 的主体中可用:
trait A {
    fn do_something(t: &Self);
}

这就涉及到大小问题。在Rust中,默认情况下,Self参数是非定长的。每个trait都对Self有一个隐含的?Sized限制。其中一个原因是因为有很多trait可以实现非定长类型并且仍然有效。例如,任何只包含通过引用获取和返回Self的方法的trait都可以实现非定长类型。你可以在RFC 546中阅读更多关于动机的内容。
当你只定义trait及其方法的签名时,大小不是问题。由于这些定义中没有实际代码,编译器无法做出任何假设。但是,当你开始编写使用此trait的通用代码时,包括默认方法,因为它们带有隐含的Self参数,你应该考虑大小问题。由于默认情况下Self是非定长的,因此默认trait方法不能按值返回Self或将其作为按值参数传递。因此,你需要指定Self必须是默认情况下定长的:
trait A: Sized { ... }

或者您可以指定仅当Self具有大小时才能调用方法:

trait WithConstructor {
    fn new_with_param(param: usize) -> Self;

    fn new() -> Self
    where
        Self: Sized,
    {
        Self::new_with_param(0)
    }
}

16
谢谢你提供如此详尽的答案。我不知道 "默认情况下是 Sized 但 Self 不是" 这部分内容。这就是我感到困惑的主要原因。 - eulerdisk
1
@Vladimir,不幸的是,Rust Book的高级特性高级类型章节已经被冻结。否则,您应该考虑在那里提出您的解释。 - Cryptor

9
让我们看看如果您使用未定大小的类型会发生什么。 new()将您的new_with_param(_)方法的结果移动到调用方。但是,除非该类型具有大小,否则应移动多少字节?我们无法知道。这就是为什么移动语义要求类型为Sized的原因。
注意:各种Box已经被设计为针对这个问题提供运行时服务。

3
为什么它不抱怨 new_with_param 呢?尽管它也需要在其调用者的堆栈上保留足够的空间。 - Matthieu M.
所以我的想法是正确的,但是为什么通用结构体中不需要 Size? 我已经更新了问题。 - eulerdisk
3
@Matthieu M. new_with_param 只是一个特征方法定义,而不是实现。 - llogiq
4
@AndreaP:默认情况下,struct 总是具有 Sized 属性。 - Matthieu M.
1
我想我明白了。显然,通用类型 T(而不是结构体)在结构体中默认被视为 Sized(除非您放置 ?Sized),但不能用于 Traits。https://doc.rust-lang.org/book/unsized-types.html - eulerdisk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接