【译】Rust中的Sizedness (七)

973 阅读5分钟

原文链接: https://github.com/pretzelhammer/rust-blog/blob/master/posts/sizedness-in-rust.md

原文标题: Sizedness in Rust

公众号: Rust碎碎念

用户定义的伪Never类型(User-Defined Pseudo Never Types)

尽管定义一个能够强制转换到任意其他类型的类型是不可能的,但是定义一个无法创建实例的类型是有可能的,例如一个没有任何variant的enum:

enum Void {}

这可以让我们从前面的两个例子中移除feature标记并且使用stable版本的Rust来实现它们:

enum Void {}

// example 1
impl FromStr for String {
    type Err = Void;
    fn from_str(s: &str-> Result<StringSelf::Err> {
        Ok(String::from(s))
    }
}

// example 2
fn run_server() -> Result<Void, ConnectionError> {
    loop {
        let (request, response) = get_request()?;
        let result = request.process();
        response.send(result);
    }
}

这是Rust标准库里使用的技术,因为StringFromStr实现里的Err类型是std::convert::Infallible, 其定义如下:

pub enum Infallible {}

PhantomData

第三常见的使用ZST的应该是PhantomDataPhantomData是一个零大小标记结构体,可以用于“标记(mark)”一个包含(containing)结构体含有特定的属性。在使用目的上类似于像SizedSendSync这样的自动标记trait,但是作为一个标记结构体在使用上略有不同。对PhantdomData作一个详尽的解释并探讨它的各种用例已经超出了本文的范围,所以,让我们仅仅来看一个简单的示例。回顾一下前面出现的这个代码示例:

#![feature(negative_impls)]

// this type is Send and Sync
struct Struct;

// opt-out of Send trait
impl !Send for Struct {}

// opt-out of Sync trait
impl !Sync for Struct {}

很不幸地,我们不得不使用一个feature标记,我们能否只使用stable版本的Rust就实现相同的结果?正如我们所知,一个类型只有在它所有的成员都是SendSync的时候,它才是SendSync,因此,我们可以在结构体中添加一个!Send!Sync成员,比如Rc<()>:

use std::rc::Rc;

// this type is not Send or Sync
struct Struct {
    // adds 8 bytes to every instance
    _not_send_or_sync: Rc<()>,
}

这样并不理想,因为它为Struct的每个实例都增加了大小,而且现在每次要创建一个Struct的时候,我们不得不凭空构造出一个Rc<()>。而PhantomData是一个ZST,同时解决了这两个问题:

use std::rc::Rc;
use std::marker::PhantomData;

type NotSendOrSyncPhantom = PhantomData<Rc<()>>;

// this type is not Send or Sync
struct Struct {
    // adds no additional size to instances
    _not_send_or_sync: NotSendOrSyncPhantom,
}

关键点(Key Takeaway)

  • PhantomData是一个零大小标记结构体,可以用于标记一个包含结构体为拥有特定的属性

译者注:因不确定PhantomData译为幻影数据是否合适,故此处保留原词。

总结(Conclusion)

  • 只有确定大小类型(sized type)的实例才可以放到栈上,也就是,可以通过传值的方式传递

  • 不确定大小类型(unsized tpe)的实例不能放到栈上而且必须通过传引用的方式传递

  • 指向不确定大小类型(unsized tpe)的指针是双宽度的,因为除了保存指向数据的指针外,还需要额外的比特位来追踪数据的长度或者指向一个vtable

  • Sized是一个"自动(auto)"标记trait

  • 所有的泛型类型参数默认是被Sized自动约束

  • 如果我们有一个泛型函数,它接收隐于指针后的类型T为参数,例如&TBox<T>Rc<T>等,那么我们总是选择退出默认的Sized约束而选用T:?Sized约束

  • 利用切片和Rust的自动类型强制转换能够让我们写出灵活的API

  • 所有的trait默认都是?Sized

  • 对于impl Trait for dyn Trait,要求Trait: ?Sized

  • 我们可以在每个方法上要求Self:Sized

  • Sized约束的trait不能转为trait对象

  • Rust不支持超过2个宽度的指针,因此

    • 我们不能把不确定大小类型转为trait对象
    • 我们不能有多trait对象,但是我们可以通过把多个trait合并到一个trait里来解决这个问题
  • 用户定义的不确定类型大小类型是个不成熟的特性,现在其局限性超过所能带来的益处

  • ZST的所有实例都相等

  • Rust编译器会去优化和ZST相关的交互

  • !可以被强制转换为其他类型

  • 无法创建一个!的实例,我们可以利用这一点在类型级别把特定状态标记为不可能

  • PhantomData是一个零大小标记结构体,可以用于把一个包含结构体标记为含有特定属性

讨论(Discuss)

可以在下面这些地方讨论这篇文章

  • official Rust users forum[1]
  • learnrust subreddit[2]
  • Twitter[3]
  • rust subreddit[4]

Further Reading

  • Common Rust Lifetime Misconceptions[5]
  • Learning Rust in 2020[6]

本文禁止转载,谢谢配合!欢迎关注我的微信公众号: Rust碎碎念

Rust碎碎念
Rust碎碎念

参考资料

[1]

official Rust users forum: https://users.rust-lang.org/t/blog-post-sizedness-in-rust/46293?u=pretzelhammer

[2]

learnrust subreddit: https://www.reddit.com/r/learnrust/comments/hx2jd0/sizedness_in_rust/

[3]

Twitter: https://twitter.com/pretzelhammer/status/1286669073137491973

[4]

rust subreddit: https://www.reddit.com/r/rust/comments/hxips7/sizedness_in_rust/

[5]

Common Rust Lifetime Misconceptions: ./common-rust-lifetime-misconceptions.md

[6]

Learning Rust in 2020: ./learning-rust-in-2020.md