既然你提到了“结构取得所有权或其他东西”,我将稍微偏离你的问题来澄清一下。
所有权的简化概述
Rust 在不求助于垃圾回收的情况下处理内存安全的方式是使用ownership 和borrowing 的概念。这些概念需要时间来内化,但其中的某些方面可以从简单的示例中看出:
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
这也适用于结构声明,如果你声明一个属性后面没有&,它表示它想要拥有数据**,所以Bike结构表示它想要拥有品牌String数据。好吧,自行车类比在这里有点分崩离析,所以让我们回到你的问题。
** 这里有一个小不一致适用于实现Copy trait 的值。如果一个值实现了 Copy trait,rust 会在所有可能的地方复制它,因此您无需费心在类型前加上 &,就像 f32 和许多其他基本类型一样。
手头的问题
要了解为什么你不能做你想做的事(使用&args[1] 而不是args[1].clone()),我们需要记住rules of ownership in rust:
- Rust 中的每个值都有一个称为其所有者的变量。
- 一次只能有一个所有者。
- 当所有者超出范围时,该值将被删除。
考虑到这一点,让我们看看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 将有两个所有者,args 和 config 变量违反了所有权规则 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();
现在config 和args 都可以了,因为它们拥有完全不同的内存块。当他们超出范围时,每个人都会清理他们拥有的东西。
进入函数
这个已经提到了。您可以将 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_string对String的实现如下:
// 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.filename 和config.query。
我用working example 和a different scope for args 做了一个游乐场,这样你就可以看到生命周期如何影响回报。
我不是 Rust 专业人士,我做了一些简化以使解释简单明了。如果我说错了,希望更有经验的 rustacean 能纠正我。