Rust中克隆引用和方法调用的语法教程

366 阅读5分钟

在我最近进行的一些Rust培训中,出现了这个半意外的角落案例。我想写一篇简短的文章可能会对将来的一些人有所帮助。

Rust的语言设计着重于人体工程学。其目标是使常见的模式易于经常性地编写。这在总体上是非常好的。但偶尔也会出现令人惊讶的结果。而我认为这种情况就是一个很好的例子。

首先,让我们假装方法语法根本不存在。假设我有一个String ,并且我想克隆它。我知道有一个Clone::clone 方法,它接收一个&String 并返回一个String 。我们可以这样利用它。

fn uses_string(x: String) {
    println!("I consumed the String! {}", x);
}

fn main() {
    let name = "Alice".to_owned();
    let name_clone = Clone::clone(&name);
    uses_string(name);
    uses_string(name_clone);
}

请注意,我需要把&name 传给clone ,而不是简单的name 。如果我使用后者,我就会出现类型错误。

error[E0308]: mismatched types
 --> src\main.rs:7:35
  |
7 |     let name_clone = Clone::clone(name);
  |                                   ^^^^
  |                                   |
  |                                   expected reference, found struct `String`
  |                                   help: consider borrowing here: `&name`

这是因为Rust不会自动从函数参数中借用一个引用。你需要明确地说,你想借用这个值。很好。

但是现在我想起来了,方法语法其实一个东西。所以,让我们继续使用它吧!

let name_clone = (&name).clone();

记住clone 需要一个&String ,而不是一个String ,在调用clone 方法之前,我已经提前从name 借用了,很有帮助。而且我需要用圆括号把整个表达式包起来,否则会被编译器错误地解析出来。

这一切都很有效,但这显然不是我们一般想要写代码的方式。相反,我们想放弃小括号和& 符号。幸运的是,我们可以这样做。大多数Rustaceans在早期就学会了你可以简单地这样做。

let name_clone = name.clone();

换句话说,当我们使用方法语法时,我们可以在一个String 一个&String 上调用.clone() 。这是因为在方法调用表达式中,"为了调用一个方法,接收器可以自动被解除引用或借用。" 基本上,编译器会遵循这些步骤:

  • name 的类型是什么?好的,它是一个String
  • 是否有一个以String 为接收方的方法可用?没有。
  • 好吧,试着借用它。是否有一个以&String 为接收方的方法?是的。那就用它吧!

而且,在大多数情况下,这与你所期望的完全一样。直到它失效。让我们从一个令人困惑的错误信息开始。假设我有一个辅助函数来大声地克隆一个String

fn clone_loudly(x: &String) -> String {
    println!("Cloning {}", x);
    x.clone()
}

fn uses_string(x: String) {
    println!("I consumed the String! {}", x);
}

fn main() {
    let name = "Alice".to_owned();
    let name_clone = clone_loudly(&name);
    uses_string(name);
    uses_string(name_clone);
}

看看clone_loudly ,我意识到我可以很容易地将其泛化为不仅仅是一个String 。唯一的两个要求是,该类型必须实现Display (用于println! 的调用)和Clone 。让我们继续实现,不小心忘记了Clone

use std::fmt::Display;
fn clone_loudly<T: Display>(x: &T) -> T {
    println!("Cloning {}", x);
    x.clone()
}

正如你所期望的那样,这不会被编译。然而,给出的错误信息可能让人吃惊。如果你像我一样,你可能以为会有一条错误信息,说是缺少一个与Clone 绑定的T 。事实上,我们得到的是完全不同的东西。

error[E0308]: mismatched types
 --> src\main.rs:4:5
  |
2 | fn clone_loudly<T: Display>(x: &T) -> T {
  |                 - this type parameter - expected `T` because of return type
3 |     println!("Cloning {}", x);
4 |     x.clone()
  |     ^^^^^^^^^ expected type parameter `T`, found `&T`
  |
  = note: expected type parameter `T`
                  found reference `&T`

奇怪的是,.clone() 似乎已经成功了,但是返回的是&T 而不是T 。这是因为方法调用表达式与上面的String 的步骤相同,即。

  • x 的类型是什么?好的,它是一个&T
  • 是否有一个以&T 为接收方的clone 方法?没有,因为我们不知道T 是否实现了Clone 的特性。
  • 好吧,试着借用它。有没有一个以&&T 为接收方的方法?有趣的是,有。

让我们深入了解一下那个Clone 的实现。去除一些噪音,这样我们就可以把注意力放在重要的部分。

impl<T> Clone for &T {
    fn clone(self: &&T) -> &T {
        *self
    }
}

由于引用是Copy,将一个引用取消引用的结果就是复制内部的引用值。我发现令人着迷的是,我们的语言中有两个正交的特性,而且还有点令人担忧。

  • 方法调用语法自动导致借用
  • 为一个类型和对该类型的引用实现特性的能力

当两者结合在一起时,对于哪种特质的实现最终会被使用,存在一定程度的模糊性。

在这个例子中,我们很幸运的是,代码没有被编译。我们最后只得到了一个令人困惑的错误信息。我还没有遇到过这样的实际问题,即这种行为会导致代码被编译,但却做错了事情。这在理论上是可能的,但似乎不太可能无意间发生。也就是说,如果有人被这种情况咬过,我很想听听细节。

所以,我们的收获是:作为方法调用语法的一部分,自动借用和取消引用是该语言的一个伟大功能。如果没有它,使用Rust将是一个很大的痛苦。我很高兴它的存在。为引用实现特质是一项伟大的功能,如果没有它,我也不想使用这种语言。

但每隔一段时间,这两样东西就会咬我们一口。谨慎行事。