【问题标题】:I don't understand why the variable has to be cloned and not referenced我不明白为什么必须克隆而不引用变量
【发布时间】:2020-07-22 14:17:44
【问题描述】:

我正在阅读 Rust 编程语言。在这段代码中,我不明白为什么必须是args[1].clone(),为什么不能是&args[1]

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = parse_config(&args);

    println!("Searching for {}", config.query);
    println!("In file {}", config.filename);

    let contents = fs::read_to_string(config.filename)
        .expect("Something went wrong reading the file");

    println!("With text:\n{}", contents);
}

struct Config {
    query: String,
    filename: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = args[1].clone();
    let filename = args[2].clone();

    Config { query, filename }
}

这本书解释了它,但我仍然不明白。它说明了结构获得所有权。

这和上面的代码一样吗?这就是我将args[1].clone 更改为&amp;args[1] 时编译器所说的操作

fn parse_config(args: &[String]) -> Config {
    let query = &args[1];
    let filename = &args[2];

    Config { query: query.to_string(), filename: filename.to_string() }
}

【问题讨论】:

  • 你的背景是什么?在某些语言中,例如 Java,所有对象都是引用,并且不存在像 &amp;String 这样的类型。另一方面,在 C++ 中,编译器有时会自动调用特殊的构造函数来将 string&amp; 转换为 string,因此当事物是引用时,它可能有点晦涩难懂。在 Rust 中,&amp;StringString 是不同的类型,您必须显式调用 .clone() 等方法才能将引用转换为拥有的类型。
  • 我相信他不明白的是移动/转移所有权与复制内存的概念。也许可以接受一个解释他为什么不能直接从args 参考以及这如何影响main 中的实际变量的答案。不幸的是,我对 Rust 没有足够的信心来回答它。我建议阅读this articlethis other one

标签: rust


【解决方案1】:

std::ops::Index 返回对容器中类型的引用,在本例中为切片。

您有多种选择来获取工作代码。其中最好的是将parse_config 重写为Vec

fn parse_config(mut args: Vec<String>) -> Config {
    let filename = args.remove(2);
    let query = args.remove(1);

    Config { query, filename }
}

【讨论】:

  • 我认为remove 会改变向量的内容,所以我认为第二次调用会给你第三个参数...doc.rust-lang.org/std/vec/struct.Vec.html#method.remove
  • ... 或使用 remove 向后分配参数也可以工作...
  • 你是对的。它应该是remove(1) 或者你建议的倒数。
【解决方案2】:

既然你提到了“结构取得所有权或其他东西”,我将稍微偏离你的问题来澄清一下。

所有权的简化概述

Rust 在不求助于垃圾回收的情况下处理内存安全的方式是使用ownershipborrowing 的概念。这些概念需要时间来内化,但其中的某些方面可以从简单的示例中看出:

struct Bike {
   brand: String;
   tire: f32;
}

impl Bike {
   fn ride_bike(&self) {
       println!("Goin' where the weather suits my clothes!")
   }
}

fn main() {
   // You are the owner of the bike
   let rafaels_bike = Bike { 
      brand: String::from("Bike Friday"), 
      tire: 28.0 
   }

   // You are giving your bike to me
   let eduardos_bike = rafaels_bike

   // Since you gave the bike to me, you cannot ride it anymore
   // rafaels_bike.ride_bike()    // It will give a borrow after move error, if uncommented

   // The bike is mine now, so I can ride it.
   eduardos_bike.ride_bike()
}

Rust 这样做是因为它需要知道何时清理内存。为此,它查看所有者的范围:当所有者超出范围时,是时候清理其资源了。在这种情况下,Bike 结构会分配内存,因此需要对其进行清理。

这种所有权概念渗透到 Rust 中的每个操作中,包括结构和函数声明、表达式等。因此,您声明函数的方式可以向调用者提供有关您希望如何处理数据的信息。

// Immutable borrow: You're giving me permission to use your data (Bike), 
// but I can't change it
fn have_a_look(bike: &Bike) 

// Mutable borrow: You're giving me permission to modify your data (Bike)
fn install_modifications(bike: &mut Bike) 

// Moving operation: You're giving the bike away, you cannot use it anymore
// after you call the function
fn give_to_charity(bike: Bike)     // Bike is still available inside the function

这也适用于结构声明,如果你声明一个属性后面没有&amp;,它表示它想要拥有数据**,所以Bike结构表示它想要拥有品牌String数据。好吧,自行车类比在这里有点分崩离析,所以让我们回到你的问题。

** 这里有一个小不一致适用于实现Copy trait 的值。如果一个值实现了 Copy trait,rust 会在所有可能的地方复制它,因此您无需费心在类型前加上 &amp;,就像 f32 和许多其他基本类型一样。

手头的问题

要了解为什么你不能做你想做的事(使用&amp;args[1] 而不是args[1].clone()),我们需要记住rules of ownership in rust

  1. Rust 中的每个值都有一个称为其所有者的变量。
  2. 一次只能有一个所有者。
  3. 当所有者超出范围时,该值将被删除。

考虑到这一点,让我们看看parse_config 函数是否按照您想要的方式实现:

// The Config struct wants to own both the query and filename strings
struct Config {
    query: String,
    filename: String,
}

// You're saying that you want to look at the args data and nothing else
fn parse_config(args: &[String]) -> Config {
    let query = &args[1];
    let filename = &args[2];

    // Then you're trying to give it away to Config behind the caller's back
    // You broke the contract
    Config { query, filename }
}

基本上,如果允许这样做,args 中的 Strings 将有两个所有者,argsconfig 变量违反了所有权规则 n°2。

让我们假设 rust 没有那么严格,并且不存在第 2 条规则。在这个假设中,这是可能的:

// This is a world without rule n° 2, this code will not compile.
use std::env;
use std::fs;

fn main() {
    // args own the String inside the Vec
    let args: Vec<String> = env::args().collect();

    let contents = {
        // config will also own the Strings at index 1 and 2
        let config = parse_config(&args);

        println!("Searching for {}", config.query);
        println!("In file {}", config.filename);

        fs::read_to_string(config.filename)
            .expect("Something went wrong reading the file")

        // config goes out of scope, cleaning the memory for strings it owns
    };

    // The strings at index 1 and 2 don't exist anymore
    // This is known as a dangling pointer
    println!("{:?}", args);

    println!("With text:\n{}", contents);

    // End of scope: Will try to clean args memory, but some of it was already cleaned
    // This is known as a double-free
}

struct Config {
    query: String,
    filename: String,
}

fn parse_config(args: &[String]) -> Config {
    let query = &args[1];
    let filename = &args[2];

    // Give the strings to Config anyway
    Config { query, filename }
}

如您所见,一切都会中断,Rust 将无法再保证内存安全。

解决方案

克隆

本书提供的解决方案是将字符串分配给clone,这意味着您分配一个与args 所拥有的不同的全新内存块:

    let query = args[1].clone();
    let filename = args[2].clone();

现在configargs 都可以了,因为它们拥有完全不同的内存块。当他们超出范围时,每个人都会清理他们拥有的东西。

进入函数

这个已经提到了。您可以将 main 中的 args 变量移动到函数参数中。由于它现在拥有数据,因此您可以对它做任何您想做的事情,包括将所有权授予另一个结构。

fn parse_config(mut args: Vec<String>) -> Config

这是因为main 函数在调用parse_config 后不使用args 变量。但是你仍然不能使用索引,因为你不能从索引中移出,因为它是通过不可变借用发生的:

来自Index trait documentation

fn index(&self, index: Idx) -> &Self::Output

使用 to_string()

这个是你自己拿到的,是的,它在功能上等同于书中提供的那个。

to_stringString的实现如下:

// to_string is implemented by means of to_owned
#[stable(feature = "string_to_string_specialization", since = "1.17.0")]
impl ToString for String {
    #[inline]
    fn to_string(&self) -> String {
        self.to_owned()
    }
}

// to_owned uses clone when the type implements the Clone trait
[stable(feature = "rust1", since = "1.0.0")]
impl<T> ToOwned for T
where
    T: Clone,
{
    type Owned = T;
    fn to_owned(&self) -> T {
        self.clone()
    }

    fn clone_into(&self, target: &mut T) {
        target.clone_from(self);
    }
}

// And String implements Clone
#[stable(feature = "rust1", since = "1.0.0")]
impl Clone for String {
    fn clone(&self) -> Self {
        String { vec: self.vec.clone() }
    }

    fn clone_from(&mut self, source: &Self) {
        self.vec.clone_from(&source.vec);
    }
}

使用生命周期

您还可以通过在 Config 结构中使用生命周期来解决此问题,如下所示:

struct Config<'a> {
    query: &'a String,
    filename: &'a String,
}

fn parse_config(args: &[String]) -> Config {
    let query = &args[1];
    let filename = &args[2];

    // Now config is not trying to own the Strings
    // So you can pass your references to it
    Config { query, filename }
}

这允许您以您想要的方式实现代码,但它将Config 结果的生命周期与args 的生命周期联系起来,在本例中为main 函数范围。

换言之,您只能在初始所有者(main 中的args)仍在范围内时访问config.filenameconfig.query

我用working examplea different scope for args 做了一个游乐场,这样你就可以看到生命周期如何影响回报。


我不是 Rust 专业人士,我做了一些简化以使解释简单明了。如果我说错了,希望更有经验的 rustacean 能纠正我。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-06
    • 2013-04-20
    • 2021-11-08
    • 1970-01-01
    • 2019-04-21
    • 1970-01-01
    相关资源
    最近更新 更多