iovxw

Rust 100 黑

不定期更新

所谓爱到深处自然黑, 本文将会收集 Rust 的黑点, 并附上简单的解释和状态跟踪

至于能不能真的收集到 100 个, 希望不能吧

友情链接: https://3442853561.gitbooks.io/wtfrust/content/


  1. enum 的内存占用

    尽管某些特殊类型会优化, 但大部分需要额外保存 tag, 而这个 tag 是 1byte, 极端情况下是个灾难

  2. 尾递归优化并不总是生效

    当进行尾递归调用的作用域内有 Drop 变量存在时

    fn recur() {
        let x = vec![false];
        recur()
    }
    

    Playground

  3. &mut 不是 Copy 的,但却会被特殊处理

    fn drop_ref<T>(_: &mut T) {}
    fn drop<T>(_: T) {}
    fn assert_copy<T: Copy>(_: T) {}
    
    let x = &mut 1;
    // assert_copy(x);
    drop_ref(x);
    println!("{}", x);
    drop(x);
    // println!("{}", x);
    

    试着去掉注释, Playground

  4. 'static 并不 static

    'static 甚至不是一个生命期参数 (它是一个特殊的 "字面量", 并作为一个关键字实现),在很多情况下它只被用来表示变量不受某个生命期约束

    很多人认为所有 'static 的变量会永远留在内存中,这是不正确的,一个 owned 类型也是 'static

    fn assert_static<T: 'static>(_: T) {}
    assert_static(String::new());
    

    Playground

  5. 没有泛型特化

    要想特化只能重新包一层类型,不过快要解决了 #31844

  6. Range overflow

    for _ in 0..256_u8 {
        unreachable!();
    }
    

    Playground, 当然这有个警告, 以及还有 #28237

    更新: 现在警告变错误, #28237 也实现了, 这条可以划掉了

  7. 易混淆的花括号

    false && { true } && false
    

    会被解释成

    (false && { true } && false)
    

    { true } && false
    

    会被解释成

    { true }; && false
    

    Playground

  8. Shadow 常量时, 糟糕的错误信息

    const x: bool = false;
    let x = 1;
    
    error[E0308]: mismatched types
      --> src/main.rs:3:9
      |
    3 |     let x = 1;
      |         ^ expected integral variable, found bool
      |
      = note: expected type `{integer}`
                 found type `bool`
    

    类似的还有 Unit Struct

    struct x;
    let x = 1;
    

    Playground, 当然只要遵守命名规范, 就不会遇到这种问题 (然而一个例子是 Diesel)

  9. 特殊情况下数字类型的类型推导

    Rust 编译器对数字运算的类型推导有特殊处理, 以方便确定具体的长度和有无符号

    然而这些规则只针对特定运算符, 并且各个运算符的规则都不同, 在不了解的情况下会导致混乱, 比如下面这行代码可以编译

    (1u32 >> 2) | 3;
    

    而这个理应等价的不能:

    std::ops::Shr::shr(1u32, 2) | 3;
    
    2 |     std::ops::Shr::shr(1u32, 2) | 3;
      |                                 ^ no implementation for `u32 | i32`
    

    原因是编译器会默认 >> 的返回值和左手边的类型一样, 推导返回值类型时不需要依赖 2 的类型

    shr 这个函数的返回值类型依赖参数的类型, 只能在 2 推导完成后再推导, 这导致在同一个步骤中进行推导的 3 无法确定类型, 就成为默认的 i32

    2 加上类型标注 (2u32) 使返回值类型提前推导出来可以解决问题 Playground

  10. Float 的转换

    目前, float 对目标类型不能表示的值的转换是未定义行为

    比如 NaNInf 转换为 integer, 以及大于或小于 f32 所能表示值的 f64 转换为 f32

    Issue#10184

    更新: 不再是未定义行为了,这条划掉

  11. 闭包的捕获粒度

    struct Config {
        value: Option<String>,
        fallback: String,
    }
    impl Config {
        // 获取 value 的值, 如果 value 为 None, 自动将 value 的值设为 fallback
        // 不要吐槽为啥不直接返回 fallback, 这只是个例子
        fn get_value1(&mut self) -> &mut String {
            // 这样可以工作, 但是如果 value 不为 None, 会做一次无意义的 Clone
            self.value.get_or_insert(self.fallback.clone())
        }
        // 换一种实现方法
        fn get_value2(&mut self) -> &mut String {
            // 所以我们写成这样, 然而问题来了, 会提示不能再次借用 self 因为已经借用了 self.value
            // self.value.get_or_insert_with(|| self.fallback.clone())
    
            // 解决方法是不要让闭包捕获到整个 self
            let fallback = &self.fallback;
            self.value.get_or_insert_with(|| fallback.clone())
        }
    }
    

    Playground相关文章 以及 RFC#2229

    更新: Rust 2021 Edition 已默认启用 disjoint capture

  12. 智能指针的 DerefMove

    Box 实现了 Deref

    impl<T: ?Sized> Deref for Box<T> {
        type Target = T;
     
        fn deref(&self) -> &T {
            // ...
        }
    }
    

    所以 let x: bool = *Box::new(false); 成立, 并被翻译为 let x: bool = *Deref::deref(&Box::new(false));

    TCopy 时这一点问题都没有, 但假如不是呢? deref 接受的是 &self, 按理说无法将 T move 出去

    但下面代码是可以运行的

    let x: String = *Box::new(String::new());
    

    原因当然是编译器对 Box 做了特殊处理

    而为了让其他智能指针也能做到, 有人提出了 DerefMut, 但这是 Rust 1.0 发布之前的事, 拖到现在还没解决(或者说都没这需求)

    最近又有人提出了新的 RFC#2439, 拭目以待吧

  13. 智能指针的模式匹配

    感觉这个就不用讲了, 都知道

  14. 函数参数解构的语义

    Rust 的函数参数支持解构, 但不算在函数签名里, 就导致了和其他解构行为不一样

    fn foo(ref bar: String) {}
    
    let x = String::new();
    foo(x); // x moved here
    
  15. 闭包捕获环境的行为依赖 Copy trait

    因为太长, 拆到这篇独立文章里

  16. RawFd 缺少限制

    在许多库中, 接受任意 file descriptor 的 API 是这样写的:

    pub fn do_some_io<FD: AsRawFd>(input: &FD) -> io::Result<()> {
        some_syscall(input.as_raw_fd())
    }
    

    但由于 RawFd 只是 i32 的别名, 用户可以凭空造一个 RawFd 传进去:

    do_some_io(&7i32)
    

    很明显这是不应该的, 至少在 safe 的 Rust 中不应该允许, 是标准库设计上的安全漏洞

    而要修复的话因为 Rust 不允许对 std 做破坏性变更, 没法直接修改 RawFd, 所以 RFC#3128 提出的方案是加几个新的 fd 类型和 trait, 像 NonZero 那样在构造时进行保证, 然后逐步让所有库替换掉旧的 AsRawFd 实现. 可以在 #87074 中追踪进度