< 返回版块

Aaron009 发表于 2021-10-08 15:28

大佬Potato TooLarge, 这个文章,把我搞懵逼了

https://zhuanlan.zhihu.com/p/194156624

fn foobar<'a, 'b, 'k>(set: &'b HashSet<&'a str>, key: &&'k str) -> Option<&'b &'a str>
    where 'a: 'b, 'b: 'k
{
    set.get(key)
}

问题一:

key: &&'k str 为什么有两个&&, 不应该只有一个&

问题二:

Option<&'b &'a str>多个生命周期不应该是,分割的嘛,这里怎么是空格?

问题三:

我还是看不懂为什么去掉&,它就能跑起来了,我再理解理解。

评论区

写评论
Grainspring 2021-10-15 12:04

推荐两个Rust生命周期相关的中文资料,希望对理解生命周期有所帮助。

LRFRC系列:Rust生命周期基础 https://mp.weixin.qq.com/s?__biz=MzIxMTM0MjM4Mg==&mid=2247483819&idx=1&sn=777ae853e12ed9259f6e0b6de9e9c34c

LRFRC系列:全面理解Rust生命周期 https://mp.weixin.qq.com/s?__biz=MzIxMTM0MjM4Mg==&mid=2247483826&idx=1&sn=5b3fe0e8b51d5cafb01db1f4a441be66

苦瓜小仔 2021-10-09 17:16

&'k &'a str 是引用的引用啊,描述的是一个在 'a 作用域内的共享引用基础上,产生或者说需要一个 `'k 作用域的共享引用。

  • 根据引用的规则 2,引用必须有效,所以必须有 'a: 'k 关系才能通过编译。
  • 根据我上面一直强调的 coercion 性质,如果一个作用域(块、函数、方法等等)为 'k,那么你传在 'a 作用域有效的引用也行。

当然,&'k &'a str 也是一种类型,而且它与 &'a str 是不同的类型。这一点在我给的例子里已经验证过了。

Rust Book 讲得很清楚

src: Validating References with Lifetimes

  • 生命周期就是引用保持有效的作用域
  • 在函数中使用生命周期注解的具体意义是:将函数的多个 参数 与其 返回值 的生命周期进行关联的。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。
  • 在函数签名中指定生命周期参数时,我们 并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。

所以,一个 &'a str 并不能说明什么,重要的是 函数签名中生命周期定义/指明的各种引用关系

作者 Aaron009 2021-10-09 06:30
fn foobar3<'a, 'b, 'k>(set: &'b HashSet<&'a str>, key: &'k &'a str) -> Option<&'b &'a str>

key: &'k &'a str是什么意思啊,是表示key要同时满足k, a着两个生命周期的str类型么?

Option<&'b &'a str> 也是这个意思么?

我晕的原因是看到的所有资料里都是 key: &'a str,没见过这样写的

--
👇
苦瓜小仔: 这要从生命周期、函数签名一起分析:

#![allow(dead_code)]

use std::collections::HashSet;

fn main() {
    let hashset: HashSet<&'static str> = "a b c".split(' ').collect();

    println!("{:?}", hashset.get("a"));

    // println!("{:?}", foobar_(&hashset, "a"));
    println!("{:?}", foobar_(&hashset, &"a"));

    println!("{:?}", foobar(&hashset, "a"));
    println!("{:?}", foobar(&hashset, &"a"));
}

// HashSet 的 get 方法签名为:
// pub fn get<Q: ?Sized>(&self, value: &Q) -> Option<&T> where T: Borrow<Q>, Q: Hash + Eq 
// 根据生命周期省略规则 3,有:
// pub fn get<'q, Q: ?Sized>(&'q self, value: &'q Q) -> Option<&'q T> where T: Borrow<Q>, Q: Hash + Eq
// T 在这里用 &'t str 描述;而 T: Borrow<Q> 指明 (&'t T).borrow() -> &'t T -> &'t Q 关系,即 T 的借用必须至少比 Q 的借用长
// 那么不难写出这样的函数:
fn foobar_<'t, 'q>(set: &'q HashSet<&'t str>, key: &'q &'t str) -> Option<&'q &'t str>
where
    't: 'q,
{
    set.get(key)
}

// 但是我这里写了一个非常具体而特殊的情况:T 为 &'static str
// 所以函数 foobar_ 有一个限制,参数 key 不得使用 "a",因为:
// key: &'q &'t str,而我们知道,"a" 的类型是 &'static str,显然不满足签名,
// 所以必须 foobar_(&hashset, &"a") 才能满足签名
// 可以利用 共享借用具有长的生命周期自动转换成短生命周期的性质
// 见 https://doc.rust-lang.org/rust-by-example/scope/lifetime/lifetime_coercion.html
// 这条性质不起眼,但是很重要,因为这表明可以把 &'static str 转换成 &'q str
// 从而修改成函数 foobar,让调用方式更简洁 foobar(&hashset, "a") 

// foobar 也符合 HashSet::get 要求的 T: Borrow<Q>
// 因为 &'b &'t str 是 &'t str 的一个借用,所以 &'t str 一定至少比 &'b &'t str 长
fn foobar<'t, 'q>(set: &'q HashSet<&'t str>, key: &'q str) -> Option<&'q &'t str>
where
    't: 'q,
{
    set.get(key)
}

回到你的问题:

问题 1 和 3

key: &&'k str 的形式是不正确的,因为编译器没法推断生命周期时,你需要指明所有生命周期

fn foobar3<'a, 'b, 'k>(set: &'b HashSet<&'a str>, key: &'k &'a str) -> Option<&'b &'a str>
where
    'a: 'b,
    'b: 'k,
{
    set.get(key)
}

而且可以只有一个 &,因为共享借用具有长的生命周期自动转换成短生命周期的性质;

此外,你会发现,你传给函数 &&&&&&&&&&&&&& something&something 都可以通过编译,因为 Coercion types 第 1-2 条规则:

T to U if T is a subtype of U (reflexive case)

T_1 to T_3 where T_1 coerces to T_2 and T_2 coerces to T_3 (transitive case)

典型的:共享借用具有长的生命周期自动转换成短生命周期的性质

'a: 'b 条件中,'a 是 'b 的 subtype,表示 'a 生命周期至少比 'b 长 ('a outlives 'b);'static 比所有生命周期长,故 'static 是所有生命周期的子类型,故 'static 可以强制转换成各种小作用域里的生命周期。

所以,你写各种形式的借用有时都没问题: playground

见:

  • https://doc.rust-lang.org/nightly/reference/type-coercions.html#coercion-types
  • https://doc.rust-lang.org/nightly/reference/subtyping.html

问题 2

多个共享借用符号 & 无需分隔 &&,多个生命周期(以及独占借用符号 &mut &mut&'a mut)应该空格分隔的,如果逗号分隔,怎么和函数参数区别开来呢?

&'b &'a str 写成 &'b, &'a str 吗?

yuikonnu 2021-10-08 20:45

我也晕了,我贴一下我的理解吧,望路过的大大们斧正。

首先是失败的情况:

fn foobar<'a, 'b, 'k>(set: &'b HashSet<&'a str>, key: &&'k str) -> Option<&'b &'a str>
    where 'a: 'b
{
    // pub fn get<Q: ?Sized>(&self, value: &Q) -> Option<&T> where T: Borrow<Q>, ...
    // Self: &'b HashSet<&'a str>
    // Q: &'k str
    // T: &'a str
    // T: Borrow<Q> 在这里等价于:&'a str: Borrow<&'k str>,这个约束不可能被满足(因为生命周期之间没有子类型关系),标准库中也没有对应的实现。
    set.get(key)// 约束得不到满足,编译失败了。
}

然后是成功的情况:

fn foobar<'a, 'b, 'k>(set: &'b HashSet<&'a str>, key: &'k str) -> Option<&'b &'a str>
    where 'a: 'b
{
    // pub fn get<Q: ?Sized>(&self, value: &Q) -> Option<&T> where T: Borrow<Q>, ...
    // Self: &'b HashSet<&'a str>
    // Q: str
    // T: &'a str
    // T: Borrow<Q> 在这里等价于:&'a str: Borrow<str>,这个约束是合理的,标准库中有对应的实现:https://doc.rust-lang.org/std/borrow/trait.Borrow.html#impl-Borrow%3CT%3E
    set.get(key) // 约束得到满足,编译成功了。
}

如果只想通过编译的话,下面这种方式也可以。但这种方式在实践中是不合理的,对key的生命周期过度约束了。

fn foobar<'a, 'b, 'k>(set: &'b HashSet<&'a str>, key: &&'k str) -> Option<&'b &'a str>
    where 'a: 'b, 'k: 'a
{
    // pub fn get<Q: ?Sized>(&self, value: &Q) -> Option<&T> where T: Borrow<Q>, ...
    // Self: &'b HashSet<&'a str>
    // Q: &'k str
    // T: &'a str
    // T: Borrow<Q> 看起来是 &'a str: Borrow<&'k str>,但因为存在`'k: 'a`的约束,通过型变能够得到:&'a str: Borrow<&'a str>,这个约束在标准库中有对应的实现:https://doc.rust-lang.org/std/borrow/trait.Borrow.html#impl-Borrow%3CT%3E-4
    set.get(key)// 约束得到满足,编译通过了。
}
苦瓜小仔 2021-10-08 18:01

这要从生命周期、函数签名一起分析:

#![allow(dead_code)]

use std::collections::HashSet;

fn main() {
    let hashset: HashSet<&'static str> = "a b c".split(' ').collect();

    println!("{:?}", hashset.get("a"));

    // println!("{:?}", foobar_(&hashset, "a"));
    println!("{:?}", foobar_(&hashset, &"a"));

    println!("{:?}", foobar(&hashset, "a"));
    println!("{:?}", foobar(&hashset, &"a"));
}

// HashSet 的 get 方法签名为:
// pub fn get<Q: ?Sized>(&self, value: &Q) -> Option<&T> where T: Borrow<Q>, Q: Hash + Eq 
// 根据生命周期省略规则 3,有:
// pub fn get<'q, Q: ?Sized>(&'q self, value: &'q Q) -> Option<&'q T> where T: Borrow<Q>, Q: Hash + Eq
// T 在这里用 &'t str 描述;而 T: Borrow<Q> 指明 (&'t T).borrow() -> &'t T -> &'t Q 关系,即 T 的借用必须至少比 Q 的借用长
// 那么不难写出这样的函数:
fn foobar_<'t, 'q>(set: &'q HashSet<&'t str>, key: &'q &'t str) -> Option<&'q &'t str>
where
    't: 'q,
{
    set.get(key)
}

// 但是我这里写了一个非常具体而特殊的情况:T 为 &'static str
// 所以函数 foobar_ 有一个限制,参数 key 不得使用 "a",因为:
// key: &'q &'t str,而我们知道,"a" 的类型是 &'static str,显然不满足签名,
// 所以必须 foobar_(&hashset, &"a") 才能满足签名
// 可以利用 共享借用具有长的生命周期自动转换成短生命周期的性质
// 见 https://doc.rust-lang.org/rust-by-example/scope/lifetime/lifetime_coercion.html
// 这条性质不起眼,但是很重要,因为这表明可以把 &'static str 转换成 &'q str
// 从而修改成函数 foobar,让调用方式更简洁 foobar(&hashset, "a") 

// foobar 也符合 HashSet::get 要求的 T: Borrow<Q>
// 因为 &'b &'t str 是 &'t str 的一个借用,所以 &'t str 一定至少比 &'b &'t str 长
fn foobar<'t, 'q>(set: &'q HashSet<&'t str>, key: &'q str) -> Option<&'q &'t str>
where
    't: 'q,
{
    set.get(key)
}

回到你的问题:

问题 1 和 3

key: &&'k str 的形式是不正确的,因为编译器没法推断生命周期时,你需要指明所有生命周期

fn foobar3<'a, 'b, 'k>(set: &'b HashSet<&'a str>, key: &'k &'a str) -> Option<&'b &'a str>
where
    'a: 'b,
    'b: 'k,
{
    set.get(key)
}

而且可以只有一个 &,因为共享借用具有长的生命周期自动转换成短生命周期的性质;

此外,你会发现,你传给函数 &&&&&&&&&&&&&& something&something 都可以通过编译,因为 Coercion types 第 1-2 条规则:

T to U if T is a subtype of U (reflexive case)

T_1 to T_3 where T_1 coerces to T_2 and T_2 coerces to T_3 (transitive case)

典型的:共享借用具有长的生命周期自动转换成短生命周期的性质

'a: 'b 条件中,'a 是 'b 的 subtype,表示 'a 生命周期至少比 'b 长 ('a outlives 'b);'static 比所有生命周期长,故 'static 是所有生命周期的子类型,故 'static 可以强制转换成各种小作用域里的生命周期。

所以,你写各种形式的借用有时都没问题: playground

见:

  • https://doc.rust-lang.org/nightly/reference/type-coercions.html#coercion-types
  • https://doc.rust-lang.org/nightly/reference/subtyping.html

问题 2

多个共享借用符号 & 无需分隔 &&,多个生命周期(以及独占借用符号 &mut &mut&'a mut)应该空格分隔的,如果逗号分隔,怎么和函数参数区别开来呢?

&'b &'a str 写成 &'b, &'a str 吗?

1 共 5 条评论, 1 页