avatar Nihil

Nichts Hsu

  • 首页
  • 子域
  • 分类
  • 标签
  • 归档
  • 关于
首页 从实例看 Rust 的 HRTBs
文章

从实例看 Rust 的 HRTBs

发表于 2022/01/05
作者 Nichts Hsu
8 分钟阅读
从实例看 Rust 的 HRTBs
从实例看 Rust 的 HRTBs

问题

这一天,我像往常一样快乐地水群,一位群友抛出了一张编译报错求解。原版的代码内容太多,这里我先把最简化后的版本放出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
trait MyTrait<T> {
    fn do_sth(&self, r: T);
}

struct MyStruct;

impl<T> MyTrait<T> for MyStruct {
    fn do_sth(&self, r: T) {
        todo!();
    }
}

fn main() {
    let my_obj: Box<dyn MyTrait<&i32>> = Box::new(MyStruct);
    let num = 1;
    my_obj.do_sth(&num);
}

尝试编译,得到报错:

1
2
3
4
5
6
7
8
9
10
11
12
error[E0597]: `num` does not live long enough
  --> src/main.rs:16:19
   |
16 |     my_obj.do_sth(&num);
   |                   ^^^^ borrowed value does not live long enough
17 | }
   | -
   | |
   | `num` dropped here while still borrowed
   | borrow might be used here, when `my_obj` is dropped and runs the destructor for type `Box<dyn MyTrait<&i32>>`
   |
   = note: values in a scope are dropped in the opposite order they are defined

分析

对于 Box<dyn MyTrait<&i32>>,实际上它隐含了一个生命周期参数 Box<dyn MyTrait<&'a i32>>,这个生命周期参数由 Rust 自动推导。在本例中,Rust 实际推断其与 my_obj 相同,而不是与 num 相同,因此对生命周期小于 my_obj 的 num 重拳出击。

当然,这种情况也有一个很简单的解决方案,只需要稍微挪一下位置,让 num 的生命周期大于 my_obj 就可以通过编译了:

1
2
3
let num = 1;
let my_obj: Box<dyn MyTrait<&i32>> = Box::new(MyStruct);
my_obj.do_sth(&num);

然而,这固然是个简单的方法,但他真的是一个好办法吗?想象一个简单的应用场景:

1
2
3
4
5
fn do_i32(my_obj: Box<dyn MyTrait<&i32>>) {
    let num = value_from_other_function();
    // 我们要如何把 `&num` 传递给 my_obj.do_sth()?
    // my_obj.do_sth(&num);
}

有些人这时候可能就要说了,为什么非得用 &i32 不可呢,我用 i32 不就好好的吗?

然而事实往往并不是这么简单。我之前说过,这是我简化后的代码,实际在群友给出的原始代码中,传递的参数并非是 &i32,而是从 <&str>.as_byte() 中得到的 &[u8],并且 MyTrait<T> 限定了 T 要实现 std::io::Read,再者,[u8] 字节流本身也是 DST,无法实例化。

那么,在限定使用引用的前提下,还有什么办法能够让它通过编译呢?有,HRTBs。

高阶 Trait 约束

高阶 Trait 约束(HRTBs, Higher-Rank Trait Bounds),社区也有人称高阶生命周期约束,大部分人接触它看到的例子应该都是 Fn 作为泛型参数的例子。从未接触过 HRTBs 的看客也无需慌张,如果你能理解这个例子,对于 Fn 的例子也可以简单的举一反三。

在上面的例子中,我们的需求和 Rust 编译器的理解是有出入的。实际上,我们想要的是,Box<dyn MyTrait<&i32>> 在调用 do_sth(&num) 时能够主动适配 &num 的生命周期,而不是让 &num 来满足自己的生命周期。

而高阶 Trait 边界,就是我们传达我们真实意图的工具。我们可以通过它的语法,告诉 Rust 编译器应该主动适配入参的生命周期。它的语法是将类型的定义修改为:

1
Box<dyn for<'a> MyTrait<&'a i32>>

for<'a> S<&'a T> 这个结构,我们可以从字面上理解,将 for 翻译为“对于”,将该语法解释为“对于所有所选择的生命周期,动态地产生对应的引用”。

该语法可以解决上述例子中的所有问题:

1
2
3
4
5
fn main() {
    let my_obj: Box<dyn for<'a> MyTrait<&'a i32>> = Box::new(MyStruct);
    let num = 1;
    my_obj.do_sth(&num);
}

以及

1
2
3
4
fn do_i32(my_obj: Box<dyn for<'a> MyTrait<&'a i32>>) {
    let num = value_from_other_function();
    my_obj.do_sth(&num);
}

Fn 泛型中的 HRTBs

照顾到没有学习过 HRTBs 的看客,我们再来看看传统的 Fn 泛型作为例子的 HRTBs。

众所周知,在 Rust 中,如果入参是两个及以上引用,返回值也是一个引用,那么:

1
2
3
fn do_it(a: &i32, b:&i32) -> &i32 {
    todo!();
}

不标注任何生命周期是无法通过编译的,Rust 不能确定返回值的生命周期要依赖于哪个入参。因此我们可以修改为:

1
2
3
fn do_it<'a>(a: &'a i32, b:&'a i32) -> &'a i32 {
    todo!();
}

但是,如果我们把这一套引入到 Fn 泛型之中:

1
2
3
4
5
6
7
8
fn do_fn<'a, F>(f: F)
where
    F: Fn(&'a i32, &'a i32) -> &'a i32,
{
    let a = 1;
    let b = 1;
    let c = f(&a, &b);
}

就不行了,Rust 编译器会告诉我们 a 和 b 的生命周期没有 'a 那么长。其原因和我们最初的例子是相似的,Rust 定义 'a 是整个 do_fn 的生命周期,在其中的 a 和 b 自然不可能满足它的生命周期要求。同样的,我们也可以用 HRBTs 来告诉 Rust 编译器,我们想要它根据 f 的入参 a 和 b 动态地适应生命周期:

1
2
3
4
5
6
7
8
fn do_fn<F>(f: F)
where
    F: for<'a> Fn(&'a i32, &'a i32) -> &'a i32,
{
    let a = 1;
    let b = 1;
    let c = f(&a, &b);
}

Fn 类型的隐式绑定

事实上,我们常写的 Fn(Args) -> R 类型实质是 Fn<(Args,), Output=R> 类型的语法糖(注意后者需要 #![feature(unboxed_closures)] 才能使用)。在 Rust 中规定,当使用括号语法(即前者)时默认引入 HRTBs,这也就意味着 Fn(&i32) -> &i32 等效于 for<'a> Fn(&'a i32) -> &'a i32 等效于 for<'a> Fn<(&'a i32,), Output=&'a i32>。而使用尖括号语法(后者)不引入 HRTBs。对于 fn 类型也是同理。

参考

Higher-Rank Trait Bounds (HRTBs) - The Rustonomicon

谈一谈rust里的一个黑魔法语法HRTBs

0387-higher-ranked-trait-bounds - The Rust RFC Book

杂记, Rust
rust 编程语言
本文由作者按照 CC BY 4.0 进行授权
分享

最近更新

  • 『I Wanna』 Best Bye To 2016
  • [译] Rust 中的内联
  • [Rust] 幽灵索引类型与匿名结构体
  • [C++] 深入了解左值与右值
  • Android.bp 中启用 openmp
外部链接
  • 996.icu
  •  此博客的 Github 仓库
  •  Olimi 的个人博客

文章内容

相关文章

2024/04/26

[Rust] 幽灵索引类型与匿名结构体

幽灵索引类型 假设我们有一个这样的类型: 1 2 #[derive(Debug, Clone, Copy)] struct Pair&lt;T, U&gt;(T, U); 并且,该类型保证 T 与 U 始终不会是相同的类型。那么,我们要如何设计一个统一的 get() 方法,使得下面的代码可以实现: 1 2 3 let pair = Pair(1, "hello"); let fi...

2024/02/18

Rust 不透明类型上的生命周期

此文撰写于 Rust stable 1.76.0(即 nightly 1.78.0)版本,由于 nightly 特性不受到 Rust 开发团队的保证,请谨慎甄别本文内容是否仍然适用。 抛出问题 在最前面,我们首先抛出一个问题,为什么下面的代码无法编译? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use std::fmt::Debug; ...

2024/02/02

从另一个视角看 Rust HRTBs

HRTBs,即高阶 Trait 约束(Higher-Rank Trait Bounds),在 Rust 中是令很多初学者感到莫名其妙的概念,一手 for&lt;'a&gt; S&lt;'a&gt; 的语法更是使得原本就复杂的生命周期更加吓人。 但是,如果从另一个角度对 HRTBs 进行解剖,或许我们能看到不一样的东西。 首先,让我们考虑一个泛型和闭包的应用: 1 2 3 4 5 6 7...

USB 2.0 与 USB 3.2

Git 从基础到进阶

© 2024 Nichts Hsu. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

热门标签

编程语言 教程 rust c++ android c++20 usb 翻译 linux qt

发现新版本的内容。