Поскольку вы упомянули "структура, берущая на себя ответственность или что-то подобное", я собираюсь немного отклониться от вашего вопроса, чтобы немного прояснить ситуацию.
Упрощенный обзор владения
Способ ржавчина имеет дело с безопасностью памяти, не прибегая к сборке мусора, используя концепции владение и заимствование . Этим понятиям требуется время для усвоения, но некоторые аспекты этого можно увидеть в простых примерах:
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, включая объявления структур и функций, выражения и т. Д. c. В результате способ объявления функций может дать вызывающей стороне информацию о том, что вы хотите сделать с данными.
// 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 . Если значение реализует свойство копирования, ржавчина будет копировать его везде, где только можно, поэтому вам не нужно беспокоиться о том, чтобы поставить &
перед типом, как это происходит для f32
и многих других баз c типы.
Проблема под рукой
Чтобы понять, почему вы не можете делать то, что хотите (используйте &args[1]
вместо args[1].clone()
), нам нужно помнить о правила владения в ржавчине :
- Каждое значение в 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 }
}
По сути, если бы это было разрешено, у Strings
в args
было бы два владельца: переменные args
и config
, нарушающие правило № 2 владения.
Давайте предположим, что ржавчина не была настолько строгой, и правила № 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 не сможет больше гарантировать безопасность памяти.
Решения
Клонирование
Решение, которое предлагает книга, заключается в клонировании строк, что означает, что вы выделяете целый новый блок памяти, отличный от того, которым владеет args
:
let query = args[1].clone();
let filename = args[2].clone();
Теперь и config
, и args
в порядке, поскольку им принадлежат совершенно разные блоки памяти. Когда они go выйдут из области видимости, каждый уберет то, что у них есть.
Переход к функции
Этот уже упоминался. Вы можете переместить переменную args
в главном параметре функции. Поскольку теперь он владеет данными, вы можете делать с ним все, что захотите, включая передачу прав собственности на другую структуру.
fn parse_config(mut args: Vec<String>) -> Config
Это работает, потому что функция main
не использует переменную args
после parse_config
называется. Вы все равно не можете использовать индексирование, потому что вы не можете выйти из индекса, поскольку это происходит с помощью неизменного заимствования:
из документации по признаку индекса :
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
.
Другими словами, вы можете получить доступ только к config.filename
и config.query
, пока первоначальный владелец (args
в main
) все еще находится в области действия.
Я создал игровую площадку с рабочий пример и другой с другой областью для аргументов , чтобы вы могли видеть, как время жизни влияет на возвращение.
Я не профессионал Rust, и я сделал некоторые упрощения, чтобы сделать объяснение прямым. Если я скажу что-то не так, надеюсь, более опытный рустокан поправит меня.