如何在Rust中实际使用动态大小类型?

24

理论上,动态大小类型(DST)已经到位,我们现在应该能够使用动态大小的类型实例。但实际上,我既无法使其工作,也无法理解其周围的测试。

一切似乎都围绕着Sized?关键字展开...但你到底如何使用它呢?

我可以组合一些类型:

// Note that this code example predates Rust 1.0
// and is no longer syntactically valid

trait Foo for Sized? {
    fn foo(&self) -> u32;
}

struct Bar;
struct Bar2;

impl Foo for Bar { fn foo(&self) -> u32 { return 9u32; }}
impl Foo for Bar2 { fn foo(&self) -> u32 { return 10u32; }}

struct HasFoo<Sized? X> {
    pub f:X
}

但是,我该如何创建一个包含 HasFoo 的实例(它是一个动态大小类型),以便拥有 Bar 或者 Bar2

尝试这样做总是会导致以下问题:

<anon>:28:17: 30:4 error: trying to initialise a dynamically sized struct
<anon>:28   let has_foo = &HasFoo {

我大致理解,动态大小的类型无法直接使用;只能通过指针与之交互,但我不知道如何操作。


你应该包含与你发布的任何错误信息相对应的代码。 - huon
你确定 DST 真的已经降落了吗?我猜在这种情况下应该会有一个适当的公告。而且 DST 问题尚未关闭。DST issue - Vladimir Matveev
@VladimirMatveev:据我所知,它的实现是不完整的,并且是在一个功能门限后面。 - sellibitze
我在测试中没有看到功能门,DSTs似乎有点工作,但在它们当前未完成的状态下还不如它们本可以成为的那么有用(正如@VladimirMatveev提供的链接中所指出的)。请参见http://is.gd/4IEW99以获取使用示例。 - Paolo Falabella
4个回答

35

免责声明:这些只是我进行的一些实验的结果,结合 阅读Niko Matsakis的博客

DSTs是指大小在编译时不一定已知的类型。

DSTs出现之前

[i32] 这样的 切片 或者像 IntoIterator 这样的 裸trait 不是有效的对象类型,因为它们没有已知的大小。

一个结构体可能看起来像这样:

// [i32; 2] is a fixed-sized vector with 2 i32 elements
struct Foo {
    f: [i32; 2],
}

或者像这样:
// & is basically a pointer.
// The compiler always knows the size of a
// pointer on a specific architecture, so whatever
// size the [i32] has, its address (the pointer) is
// a statically-sized type too
struct Foo2<'a> {
    f: &'a [i32],
}

但不是这样:
// f is (statically) unsized, so Foo is unsized too
struct Foo {
    f: [i32],
}

这也适用于枚举和元组。
使用DSTs
您可以像上面的Foo一样声明一个包含未定类型的结构体(或枚举或元组)。 包含未定类型的类型也将是未定类型。
虽然定义Foo很容易,但是创建Foo的实例仍然很困难,并且可能会更改。 由于从定义上无法创建未定类型,因此必须创建大小已知的Foo的对应项。例如, Foo { f: [1, 2, 3] },一个Foo<[i32; 3]>,它具有静态已知的大小和一些管道以让编译器知道如何将其强制转换为其静态未定类型的对应项Foo<[i32]>。 在安全和稳定的Rust中完成此操作的方法仍在研究中,截至Rust 1.5(此处是DST coercion的RFC以获取更多信息)。
幸运的是,定义新的DST不是你通常需要做的事情,除非你正在创建一种新类型的智能指针(如Rc),这应该是一个很少见的情况。
想象一下,Rc像我们上面的Foo一样被定义。由于它具备从有大小限制到无大小限制的强制转换的所有管道,因此可以用于执行以下操作:
use std::rc::Rc;

trait Foo {
    fn foo(&self) {
        println!("foo")
    }
}
struct Bar;

impl Foo for Bar {}

fn main() {
    let data: Rc<Foo> = Rc::new(Bar);
    // we're creating a statically typed version of Bar
    // and coercing it (the :Rc<Foo> on the left-end side)
    // to as unsized bare trait counterpart.
    // Rc<Foo> is a trait object, so it has no statically
    // known size
    data.foo();
}

playground示例

?Sized限定

在你日常的Rust编程中,由于你不太可能创建新的DST,那么DST有什么用处呢?最常见的是,它们让你编写通用代码,既适用于大小类型又适用于它们的现有的非大小类型。最常见的情况是Vec/[]切片或String/str

你可以通过?Sized“限制”来表达这一点。在某种程度上,?Sized是一个限制的相反;它实际上表示T可以是具有大小或无大小的,因此它扩大了我们可以使用的可能类型,而不是像限制一样限制它们。

举个假例!假设我们有一个简单的FooSized结构体,它只是包裹一个引用和一个简单的Print trait,我们想要为它实现。

struct FooSized<'a, T>(&'a T)
where
    T: 'a;

trait Print {
    fn print(&self);
}

我们希望为所有实现了Display的包装类型T定义一个通用的实现。
impl<'a, T> Print for FooSized<'a, T>
where
    T: 'a + fmt::Display,
{
    fn print(&self) {
        println!("{}", self.0)
    }
}

让我们试着让它工作:

// Does not compile. "hello" is a &'static str, so self print is str
// (which is not sized)
let h_s = FooSized("hello");
h_s.print();

// to make it work we need a &&str or a &String
let s = "hello"; // &'static str
let h_s = &s; // & &str
h_s.print(); // now self is a &str

嗯...这有点尴尬...幸运的是,我们有一种方法来将结构体泛化,以便直接与str(以及无大小限制的类型)一起使用:?Sized

//same as before, only added the ?Sized bound
struct Foo<'a, T: ?Sized>(&'a T)
where
    T: 'a;

impl<'a, T: ?Sized> Print for Foo<'a, T>
where
    T: 'a + fmt::Display,
{
    fn print(&self) {
        println!("{}", self.0)
    }
}

现在这个可以工作:
let h = Foo("hello");
h.print();

playground

关于你的问题回答:

为了举一个不那么人为(但简单)的实际例子,你可以查看标准库中的Borrow特质。

回到你的问题

trait Foo for ?Sized {
    fn foo(&self) -> i32;
}

for ?Sized 语法现已过时。它曾经指的是 Self 的类型,声明 `Foo 可以由无大小限制的类型实现,但这现在是默认的。任何 trait 现在都可以针对无大小限制的类型进行实现,也就是说,现在可以有:

trait Foo {
    fn foo(&self) -> i32;
}

//[i32] is unsized, but the compiler does not complain for this impl
impl Foo for [i32] {
    fn foo(&self) -> i32 {
        5
    }
}

如果您不希望您的特质适用于未定大小的类型,您可以使用 Sized 约束:
// now the impl Foo for [i32] is illegal
trait Foo: Sized {
    fn foo(&self) -> i32;
}

嗨@Paolo,感谢您的示例,有一点我跟不上// Does not compile. "hello" is a &'static str, so self print is str // (which is not sized) let h_s = FooSized("hello"); 因为 FooSized 被定义为:
struct FooSized<'a, T>(&'a T) where T:'a; 我们有 Tstr,所以 FooSized 将持有对 str 的引用(在这种情况下是 &'a str),并且它的大小为指针的大小,那么为什么编译器会抱怨它没有大小?
- Ryan Le
@RyanLe 因为 T 是无大小限制的(正如你所说,它是一个 str)。在 DST 之前,你无法表达这个限制,因为 str 不能单独用作类型。即使在 DST 之后,你也不能直接使用 str,它总是需要在指针后面,这并没有改变。 - Paolo Falabella
@PaoloFalabella 为了避免混淆,我建议你在这里更正你的说法:“你通过'Sized'边界来表达这个概念。'Sized'在某种程度上是边界的相反;它实际上表示T可以是有大小限制的或无大小限制的,因此它扩大了我们可以使用的可能类型,而不是像边界通常所做的那样限制它们。”并且修改为“...它实际上表示T可以实现或者不实现?Sized特性,因此它扩大了我们可以使用的可能类型...” - Developer

2
为了修正Paolo Falabella所给的例子,下面是另一种使用属性的方法来看待它。
struct Foo<'a, T>
where
    T: 'a + ?Sized,
{
    printable_object: &'a T,
}

impl<'a, T> Print for Foo<'a, T>
where
    T: 'a + ?Sized + fmt::Display,
{
    fn print(&self) {
        println!("{}", self.printable_object);
    }
}

fn main() {
    let h = Foo {
        printable_object: "hello",
    };
    h.print();
}

1

目前,要创建一个存储类型擦除的FooHasFoo,您需要首先创建具有固定具体类型的HasFoo,然后将指向它的指针强制转换为DST形式,即

let has_too: &HasFoo<Foo> = &HasFoo { f: Bar };

调用 has_foo.f.foo() 将会按照您的预期执行。

未来,这些 DST 转换几乎肯定可以使用 as 实现,但目前需要通过显式类型提示进行强制转换。


1

这是一个基于huon的回答的完整示例。重要的技巧是将您想要包含DST的类型作为通用类型,其中通用类型不需要大小(通过?Sized)。然后,您可以使用Bar1Bar2构造具体值,然后立即进行转换。

struct HasFoo<F: ?Sized = dyn Foo>(F);

impl HasFoo<dyn Foo> {
    fn use_it(&self) {
        println!("{}", self.0.foo())
    }
}

fn main() {
    // Could likewise use `&HasFoo` or `Rc<HasFoo>`, etc.
    let ex1: Box<HasFoo> = Box::new(HasFoo(Bar1));
    let ex2: Box<HasFoo> = Box::new(HasFoo(Bar2));

    ex1.use_it();
    ex2.use_it();
}

trait Foo {
    fn foo(&self) -> u32;
}

struct Bar1;
impl Foo for Bar1 {
    fn foo(&self) -> u32 {
        9
    }
}

struct Bar2;
impl Foo for Bar2 {
    fn foo(&self) -> u32 {
        10
    }
}

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