Does `next ()` move or clone elements?
I am reading Chapter 13 of the Rust book . It says strings are clone
less efficient than accessing them through an iterator (i.e. next()
). Comparing the following examples, I have two questions:
- Does it move the string
args.next()
or clone the string toquery
andfilename
? - If it were a move, it would transfer ownership from something to
env::args()
toquery
, wouldn't this code break other code? If it was a clone, why might it be more efficient than cloning the string directly?
Definition:
struct Config {
query: String,
filename: String,
}
Ineffective version
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args)
}
impl Config {
fn new(args: &[String]) -> Result<Config, &'static str> {
// [...]
let query = args[1].clone();
let filename = args[2].clone();
// [...]
}
}
Improved version
fn main() {
let config = Config::new(env::args())
}
impl Config {
fn new(mut args: std::env::Args) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let filename = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file name"),
};
// [...]
}
}
source to share
Moving or cloning
args.next()
Start by looking at the function signature for Iterator::next
:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
next
transfers ownership of any type Self::Item
to the caller. It has no additional restrictions on Self
, but it can change the internal properties of the iterator.
Next, check the inputs and outputs of a particular iterator. For example, it always returns strings, but has no input values:
struct Greet;
impl Iterator for Greet {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
Some(String::from("hello"))
}
}
In this case, determines how , so each value from the call is . Args
Item
String
next
Option<String>
We know what String
needs distribution. However, since we cannot provide any arguments for env::args()
in order to get the selection, there are only two possibilities:
- The iterator allocates a value.
- Some global state changes behind the scenes.
Rust is generally not global state, so anything that actually changes the global state will actually be propagated (printed to stdout) or marked with Big Warning.
Checking the documentation, we don't see that much warning text, so it's safe to assume that the iterator is highlighting.
You can check this by repeating it twice; you will see duplicate values. The argument list is secretly mutating under you.
Although this iterator selects strings, it is even more efficient to use the iterator value directly. When you collect into a vector, you are allocating memory for the vector. Then you clone the value inside the vector again to use it. Both of these selections are not required.
The mid-term version would be to use references to elements in the vector, namely &str
:
let query = &args[1];
let filename = &args[2];
This still has the "overhead" of allocating the vector, which may or may not be needed outside of this function.
I like to be too quirky, so I can write something like this:
fn main() {
let config = Config::new(std::env::args().skip(1));
}
impl Config {
fn new<I, S>(args: I) -> Result<Config, &'static str>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut args = args.into_iter();
let query = args.next().ok_or("Didn't get a query string")?;
let filename = args.next().ok_or("Didn't get a file name")?;
unimplemented!()
}
}
ok_or
usually useful because it makes the iterator type generic and omits the program name outside of it Config::new
. This allows testing Config
without the actual argument string.
Into<String>
just demonstrates.
source to share