avatar Nihil

Nichts Hsu

  • 首页
  • 子域
  • 分类
  • 标签
  • 归档
  • 关于
首页 Rust 不透明类型上的生命周期
文章

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

发表于 2024/02/18 更新于 2024/03/27
作者 Nichts Hsu
13 分钟阅读
Rust 不透明类型上的生命周期
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;

fn do_sth<F, Ret>(f: F)
where
    F: Fn(&i32) -> Ret,
    Ret: Debug,
{
    let a = 1;
    f(&a);
}

fn foo(t: &i32) -> impl Debug + '_ {
    t
}

fn do_foo() {
    do_sth(foo);
}

尝试编译得到报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
error[E0308]: mismatched types
  --> src/main.rs:17:5
   |
12 | fn foo(t: &i32) -> impl Debug + '_ {
   |                    ---------------
   |                    |
   |                    the expected opaque type
   |                    the found opaque type
...
17 |     do_sth(foo);
   |     ^^^^^^^^^^^ one type is more general than the other
   |
   = note: expected opaque type `impl for<'a> Debug + '_`
              found opaque type `impl Debug + '_`
   = note: distinct uses of `impl Trait` result in different opaque types
note: the lifetime requirement is introduced here
  --> src/main.rs:5:20
   |
5  |     F: Fn(&i32) -> Ret,
   |                    ^^^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `fuck_rust` (bin "fuck_rust") due to 1 previous error

HRTBs

阅读本文之前,你需要了解什么是 HRTBs(Higher-Rank Trait Bounds),由于我之前已经写过两篇文章讲解 HRTBs,因此不再重新介绍一次:

从实例看 Rust 的 HRTBs

从另一个视角看 Rust HRTBs

不透明类型与隐含的生命周期

什么是不透明类型(Opaque Types)?不透明类型是 Rust 底层的一种概念,它表示这里存在某个特定的类型,我们不知道或者说不关心它具体是啥,但是我们知道它的一些性质。不透明类型的最典型应用就是 RPIT(Return Position impl Trait),它允许使用者在函数签名中不写明返回值的真正类型,而是使用 impl Trait 代替它,这样,我们可以返回一些无法写出具体类型的值,例如闭包和 async block:

1
2
3
fn test_rpit(a: i32) -> impl Fn(i32) -> i32 {
    move |x| x + a
}

但是不透明类型有一个问题:如果原始类型携带生命周期,那么它可能会隐含一个生命周期参数,就像我们上面的 foo 函数那样:

1
2
3
fn foo(t: &i32) -> impl Debug + '_ {
    t
}

该 impl Trait + '_ 是一种语法糖,脱糖后为:

1
2
3
fn foo<'a>(t: &'a i32) -> impl Debug + 'a {
    t
}

那么在泛型中呢?泛型类型也是事实上的不透明类型,Rust 并不知道它实际对应的是什么类型,包不包含生命周期,因此,Rust 对所有泛型类型 T 都假设为 for<'a> T: 'a。若是将 Rust 隐含实现的 HRTBs 展开,那么上面的 do_sth 函数长这样:

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

这很好,事实上对于大部分类型来说都没有什么问题。如果类型实际不包含生命周期,那么 for<'a> 的引入不会有任何影响;而对于自带生命周期类型的类型,由于 HRTBs 的存在,它的生命周期也会自动匹配上 'a。

如果不透明类型的生命周期不依赖其他类型的生命周期,那么它就能够满足 for<'a> T: 'a 的约束,因此,将 foo 函数改成下面这样是可以通过编译的,因为此时不透明类型的生命周期与 &i32 无关:

1
2
3
fn foo(_t: &i32) -> impl Debug + 'static {
    &1
}

那么原本的 foo 有啥问题呢?问题就出在一个不透明类型它不仅包含一个生命周期,而且它的生命周期还依赖另一个类型的生命周期。事实上,我们回看展开了 HRTBs 的 do_sth 函数,我们就会发现一些端倪。具体来说,对于 F 和 Ret,它们由 HRTBs 引入的生命周期是互相独立的,两个类型之间没有产生生命周期上的联系。但实际上,我们希望 Ret 的生命周期 'a 与 F 的生命周期 'b 是同一个生命周期。(即使你把 'a 和 'b 换个位置,变成 for<'a, 'b> F: (Fn(&'a i32) -> Ret) + 'b,也无法和 for<'a> Ret 的生命周期关联在一起,这两个 'a 是互相独立的。)

那么要如何解决这个问题呢?当然,一个最简单的办法就是让 Rust 支持下面这种写法:

1
2
3
4
5
6
7
fn do_sth<F>(f: F)
where
    for<'a> F: Fn(&'a i32) -> (impl Debug + 'a),
{
    let a = 1;
    f(&a);
}

遗憾的是,即使是使用 nightly 版本,也无法编译这种代码,关于该语法的讨论,见 rust-lang/rust#93082。但是这给我们提供了一种思路:如果我们有办法去掉 Ret 泛型,将两个约束压缩为一个约束,那么我们或许就有办法将二者的生命周期联系在一起。

TAIT

第一种方法是 TAIT(Type Alias impl Trait),它允许我们为不透明类型定义别名:

1
2
#![feature(type_alias_impl_trait)]
type S = impl Trait;

此 S 类型并不是泛型,它会根据第一次使用它的地方自动推导为某个特定类型。因此,当我们使用 TAIT 替换掉 Ret 泛型时,需要将 foo 函数的返回值类型改成该 TAIT 类型名,这样才能让 Rust 推导其类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#![feature(type_alias_impl_trait)]

use std::fmt::Debug;

type DebugOpaque<'a> = impl Debug + 'a;

fn do_sth<F>(f: F)
where
    for<'a> F: Fn(&'a i32) -> DebugOpaque<'a>,
{
    let a = 1;
    f(&a);
}

fn foo(t: &i32) -> DebugOpaque<'_> {
    t
}

fn do_foo() {
    do_sth(foo);
}

然而,正如前面所说,类型 DebugOpaque 并不是泛型,而是由 foo 函数的实际返回值类型推导而来,这也导致了 DebugOpaque 被固定推导为了 &i32,从而令 do_sth 函数无法接收返回其他类型的函数作为参数。

因此,更推荐使用下面的方法。

Unboxed Closure

这种方法依赖 Unboxed Closures 特性,具体来说,有了这个特性,我们可以手动脱糖 Fn Traits:

1
2
3
4
Fn(A, B, C) -> D
// 可手动脱糖为:
#![feature(unboxed_closures)]
Fn<(A, B, C), Output = D>

脱糖后,我们可以将返回值类型的语法 -> D 换成关联类型的语法 Output = D。

使用该特性的好处是,Rust 允许我们为关联类型指定约束而不需要写一个新的泛型类型,这样我们就可以将二者的生命周期关联起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#![feature(unboxed_closures)]

use std::fmt::Debug;

fn do_sth<F>(f: F)
where
    for<'a> F: Fn<(&'a i32,)>,
    // 需要注意,`Output` 是 `FnOnce` 的关联类型而不是 `Fn` 的
    for<'a> <F as FnOnce<(&'a i32,)>>::Output: Debug + 'a,
{
    let a = 1;
    f(&a);
}

fn foo(t: &i32) -> impl Debug + '_ {
    t
}

// 这种情况下,`do_sth` 能够接受返回值类型不同的函数作为参数
fn foo2(t: &i32) -> impl Debug + '_ {
    unsafe { std::mem::transmute::<&i32, &[u8; 4]>(t) }
}

fn do_foo() {
    do_sth(foo);
    do_sth(foo2);
}

ATB

最后一种方法是 ATB(Associated Type Bounds),它允许我们约束关联类型(Associated Types)。

associated_type_bounds 特性即将在 Rust 1.79.0 稳定。

这种方法是第二种方法的改进版,让我们可以直接在同一行中约束 Output,不需要把它单独拆出来约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#![feature(associated_type_bounds)]
#![feature(unboxed_closures)]

use std::fmt::Debug;

fn do_sth<F>(f: F)
where
    for<'a> F: Fn<(&'a i32,), Output: Debug>,
{
    let a = 1;
    f(&a);
}

fn foo(t: &i32) -> impl Debug + '_ {
    t
}

// 这种情况下,`do_sth` 能够接受返回值类型不同的函数作为参数
fn foo2(t: &i32) -> impl Debug + '_ {
    unsafe { std::mem::transmute::<&i32, &[u8; 4]>(t) }
}

fn do_foo() {
    do_sth(foo);
    do_sth(foo2);
}

这里 rust 自动为关联类型 Output 添加了 'a 约束,因此可以不写 Output: Debug + 'a,当然写了也行,没有区别。

后注

需要注意的是,Rust 当前还支持另一种比较相似的语法,即在关联类型处使用等于号而不是冒号:

1
2
3
4
5
6
7
#![feature(unboxed_closures)]

fn do_sth(f: impl for<'a> Fn<(&'a i32,), Output = impl Debug>)
{
    let a = 1;
    f(&a);
}

这种写法看起来很美好,但是与使用冒号作为约束不同的是,使用等号令 Output 等于一个不透明类型,该不透明类型隐式实现了 HRTBs,大致相当于:

1
2
3
4
5
6
7
8
#![feature(anonymous_lifetime_in_impl_trait)]
#![feature(unboxed_closures)]

fn do_sth(f: impl for<'a> Fn<(&'a i32,), Output = impl for<'b> Debug + '_>)
{
    let a = 1;
    f(&a);
}

这种写法令 Output 类型实际并没有与 'a 产生联系。而若你想要手动添加生命周期标注时:

1
2
3
4
5
6
7
#![feature(unboxed_closures)]

fn do_sth(f: impl for<'a> Fn<(&'a i32,), Output = impl Debug + 'a>)
{
    let a = 1;
    f(&a);
}

Rust 会说:

1
2
3
4
5
error: `impl Trait` can only mention lifetimes from an fn or impl
 --> src/lib.rs:5:64
  |
5 | fn do_sth(f: impl for<'a> Fn<(&'a i32,), Output = impl Debug + 'a>)
  |                       -- lifetime declared here              

这意味着当前版本的 Rust 并不支持不透明类型从 HRTBs 中捕获生命周期参数。关于此语法的讨论以及为什么不被支持,参见 rust-lang/rust#96194。关于此语法未来可能的发展,参见 rust-lang/rust#104288。需要注意的一点是,从讨论中可以看出,该语法期望解糖为 TAIT,因此,即使该语法最终得到稳定,也很有可能像 TAIT 那样不能视为泛型返回值类型。

杂记, 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/02

从另一个视角看 Rust HRTBs

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

2023/11/07

Rust: Generator 已死,Coroutine 当立,Generator 又活了

参考文献: Generators are dead, long live coroutines, generators are back Generalized coroutines 在曾经,Rust 通过不稳定特性 generators 和 generator_trait 提供 Generator 功能,大体使用方法如下: #![feature(generators, genera...

从另一个视角看 Rust HRTBs

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

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

本站采用 Jekyll 主题 Chirpy

热门标签

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

发现新版本的内容。