Rust中as_ref 和 as_deref的对比

2,358 阅读8分钟

这个程序有什么问题?

fn main() {
    let option_name: Option<String> = Some("Alice".to_owned());
    match option_name {
        Some(name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }
    println!("{:?}", option_name);
}

编译器给了我们一个很好的错误信息,包括如何修复它的提示。

error[E0382]: borrow of partially moved value: `option_name`
 --> src\main.rs:7:22
  |
4 |         Some(name) => println!("Name is {}", name),
  |              ---- value partially moved here
...
7 |     println!("{:?}", option_name);
  |                      ^^^^^^^^^^^ value borrowed here after partial move
  |
  = note: partial move occurs because value has type `String`, which does not implement the `Copy` trait
help: borrow this field in the pattern to avoid moving `option_name.0`
  |
4 |         Some(ref name) => println!("Name is {}", name),
  |              ^^^

这里的问题是,我们对option_name 的模式匹配将Option<String> 的值移到了匹配中。这样我们就不能再在match 之后使用option_name 。但这是令人失望的,因为我们在模式匹配中使用option_namename ,实际上根本不需要移动值!相反,借用就可以了。相反,借用就可以了。

而这也正是编译器中的note 所说的。我们可以使用标识符模式中的ref 关键字来改变这种行为,而且,我们将借用一个对该值的引用,而不是移动该值。现在我们可以自由地在match 之后重复使用option_name 。该版本的代码看起来像。

fn main() {
    let option_name: Option<String> = Some("Alice".to_owned());
    match option_name {
        Some(ref name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }
    println!("{:?}", option_name);
}

对于好奇的人,你可以阅读更多关于ref 关键字的内容。

更加习以为常

虽然这是工作代码,但根据我的观点和经验,这并不是成语。把借款放在option_name ,像这样更常见。

fn main() {
    let option_name: Option<String> = Some("Alice".to_owned());
    match &option_name {
        Some(name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }
    println!("{:?}", option_name);
}

我更喜欢这个版本,因为它很明显地表明我们无意在模式匹配中移动option_name 。现在name 仍然是一个引用,println! 可以把它作为一个引用,一切都很好。

然而,这段代码能够工作的事实,是该语言特别添加的功能。在RFC 2005 "匹配人体工程学 "于2016年落地之前,上面的代码会失败。这是因为我们试图将Some 构造函数与对Option引用相匹配,而这些类型并不匹配。借用RFC的术语,让这段代码工作需要 "跳一跳"。

fn main() {
    let option_name: Option<String> = Some("Alice".to_owned());
    match &option_name {
        &Some(ref name) => println!("Name is {}", name),
        &None => println!("No name provided"),
    }
    println!("{:?}", option_name);
}

现在所有的类型都明确地排列起来了:

  • 我们有一个&Option<String>
  • 因此,我们可以在一个&Some 变体或&None 变体上进行匹配。
  • &Some 变体中,我们需要确保我们借用了内部值,所以我们添加了一个ref 关键字

幸运的是,有了RFC 2005,这个额外的噪音就不需要了,我们可以像上面那样简化我们的模式匹配。Rust语言因这一变化而变得更好,大众可以欢呼雀跃。

引入as_ref

但是如果我们没有RFC 2005呢?我们是否需要永远使用上面这种笨拙的语法?多亏了一个辅助方法,不会。我们代码中的问题是,&option_name 是对Option<String> 的引用。我们想对SomeNone 构造函数进行模式匹配,并捕获一个&String 而不是String (避免移动)。RFC 2005将其作为一个直接的语言特性来实现。但在Option 上也有一个方法可以做到这一点:as_ref

impl<T> Option<T> {
    pub const fn as_ref(&self) -> Option<&T> {
        match *self {
            Some(ref x) => Some(x),
            None => None,
        }
    }
}

这是另一种避免 "跳舞 "的方式,通过在方法定义中捕获它。但值得庆幸的是,有一个很好的语言工效学特性,可以捕捉到这种模式,并自动为我们应用这一规则。这意味着as_ref ,不再是真正必要的...对吗?

题外话:Rust的人机工程学

我绝对喜欢Rust的人机工程学特性。我对RFC 2005的喜爱没有 "但是"。然而,在学习和教授具有这些工效的语言时,我有一个担忧。这类功能在99%的情况下是有效的。但是,当它们失败时,正如我们即将看到的那样,它可能会带来巨大的冲击。

Some 我猜大多数Rustaceans,至少是那些在2016年之后学习这门语言的人,从来没有考虑过这样一个事实:能够从一个&Option<String> 值中进行模式匹配,这有一些奇怪。它感觉很自然。它自然的。但是,由于你在学习语言时从未被迫面对这个问题,在遥远的未来的某个时刻,当这个符合人体工程学的功能没有启动时,你会撞到一堵墙。

我有点希望有一个--no-ergonomics ,我们可以在学习语言时打开这个标志,迫使我们面对所有这些细节。但是现在没有。我希望像这样的博文能有所帮助。无论如何,。

当RFC 2005失败时

我们可以相当容易地创造出一个假想的例子,说明匹配的人机工程学不能解决我们的问题。让我们 "改进 "我们上面的程序,把问候逻辑分解成自己的辅助函数。

fn try_greet(option_name: Option<&String>) {
    match option_name {
        Some(name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }
}

fn main() {
    let option_name: Option<String> = Some("Alice".to_owned());
    try_greet(&option_name);
    println!("{:?}", option_name);
}

这段代码不会被编译。

error[E0308]: mismatched types
  --> src\main.rs:10:15
   |
10 |     try_greet(&option_name);
   |               ^^^^^^^^^^^^
   |               |
   |               expected enum `Option`, found `&Option<String>`
   |               help: you can convert from `&Option<T>` to `Option<&T>` using `.as_ref()`: `&option_name.as_ref()`
   |
   = note:   expected enum `Option<&String>`
           found reference `&Option<String>`

现在我们已经绕过了在调用处使用匹配工效学的任何能力。以我们对as_ref 的了解,要解决这个问题很容易。但是,至少在我的经验中,第一次有人遇到这种错误时,会有点惊讶,因为我们大多数人以前从未想过Option<&T>&Option<T> 之间的区别。

这类错误往往是在结合其他辅助函数时出现的,比如map ,它规避了明确的模式匹配的需要。

顺便说一句,你可以很容易地解决这个编译错误,而不必求助于as_ref 。相反,你可以改变try_greet 的类型签名,取一个&Option<String> ,而不是Option<&String> ,然后让匹配的人机工程学在try_greet 。不这样做的一个原因是,如前所述,这只是一个被设计出来的例子,用来证明一个失败。但另一个原因更重要:&Option<String>Option<&String> 都不是好的参数类型。接下来让我们来探讨一下这个问题。

当as_ref失败时

我们在Rust职业生涯的早期就被告知,当接收一个函数的参数时,我们应该更倾向于接受对分片的引用,而不是对所有对象的引用。换句话说,

fn greet_good(name: &str) {
    println!("Name is {}", name);
}

fn greet_bad(name: &String) {
    println!("Name is {}", name);
}

事实上,如果你通过clippy 传递这段代码,它会告诉你改变greet_bad 的签名。clippy lint 的描述对此有很好的解释,但只需说greet_good 在接受的内容上比greet_bad 更通用。

同样的逻辑适用于try_greet 。为什么我们要接受Option<&String> ,而不是Option<&str> ?有趣的是,在这种情况下,clippy并没有像在greet_bad 中那样抱怨。为了了解原因,让我们这样改变我们的签名,看看会发生什么。

fn try_greet(option_name: Option<&str>) {
    match option_name {
        Some(name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }
}

fn main() {
    let option_name: Option<String> = Some("Alice".to_owned());
    try_greet(option_name.as_ref());
    println!("{:?}", option_name);
}

这段代码不再被编译了。

error[E0308]: mismatched types
  --> src\main.rs:10:15
   |
10 |     try_greet(option_name.as_ref());
   |               ^^^^^^^^^^^^^^^^^^^^ expected `str`, found struct `String`
   |
   = note: expected enum `Option<&str>`
              found enum `Option<&String>`

这是另一个人机工程学失败的例子。你看,当你调用一个参数为&String 的函数时,但该函数期望的是&strderef coercion就会启动并为你进行转换。这是我们经常依赖的Rust工效学的一个部分,但偶尔它也会完全不能帮助我们。这就是其中的一次。编译器不会自动将Option<&String> 转换为Option<&str>

(你也可以在nomicon中阅读更多关于coercions的内容)。

幸运的是,在Option 上有另一个辅助方法为我们做这件事。as_deref 的工作就像as_ref ,但另外对值进行了deref 方法调用。它在std 中的实现很有意思。

impl<T: Deref> Option<T> {
    pub fn as_deref(&self) -> Option<&T::Target> {
        self.as_ref().map(|t| t.deref())
    }
}

但我们也可以更明确地实现它,看看它的行为是什么。

use std::ops::Deref;

fn try_greet(option_name: Option<&str>) {
    match option_name {
        Some(name) => println!("Name is {}", name),
        None => println!("No name provided"),
    }
}

fn my_as_deref<T: Deref>(x: &Option<T>) -> Option<&T::Target> {
    match *x {
        None => None,
        Some(ref t) => Some(t.deref())
    }
}

fn main() {
    let option_name: Option<String> = Some("Alice".to_owned());
    try_greet(my_as_deref(&option_name));
    println!("{:?}", option_name);
}

为了让这个问题回到更接近真实世界的代码,这里有一个案例,将as_derefmap 结合起来,可以得到比原来更干净的代码。

fn greet(name: &str) {
    println!("Name is {}", name);
}

fn main() {
    let option_name: Option<String> = Some("Alice".to_owned());
    option_name.as_deref().map(greet);
    println!("{:?}", option_name);
}

现实生活中的例子

像我的大多数博客文章一样,这篇文章的灵感来自于一些真实世界的代码。为了简化这个概念,我正在解析一个配置文件,并最终得到了一个Option<String> 。我需要一些代码来提供配置中的值,或者在源代码中默认为一个静态字符串。如果没有as_deref ,我可以用STATIC_STRING_VALUE.to_string() ,让类型排成一行,但那会很难看,而且效率很低。下面是这段代码的一个有点完整的表示。

use serde::Deserialize;

#[derive(Deserialize)]
struct Config {
    some_value: Option<String>
}

const DEFAULT_VALUE: &str = "my-default-value";

fn main() {
    let mut file = std::fs::File::open("config.yaml").unwrap();
    let config: Config = serde_yaml::from_reader(&mut file).unwrap();
    let value = config.some_value.as_deref().unwrap_or(DEFAULT_VALUE);
    println!("value is {}", value);
}