我正在阅读Chapter 13 of the Rust book。它说clone
字符串的效率低于通过迭代器访问它们的效率(即next()
)。比较以下示例,我有两个问题:
args.next()
是否将字符串移动或克隆为query
和filename
?env::args()
转移到query
,这不会打破其他代码吗?如果它是一个克隆,为什么它比直接克隆字符串更有效?定义:
struct Config {
query: String,
filename: String,
}
效率低下
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();
// [...]
}
}
更好的版本
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"),
};
// [...]
}
}
答案 0 :(得分:4)
args.next()
移动还是克隆
首先查看Iterator::next
的函数签名:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
next
将所有类型Self::Item
的所有权转让给来电者。它对Self
没有额外的限制,但它可以修改迭代器的内部属性。
接下来,检查特定迭代器的输入和输出。例如,这个总是返回字符串,但没有输入值:
struct Greet;
impl Iterator for Greet {
type Item = String;
fn next(&mut self) -> Option<Self::Item> {
Some(String::from("hello"))
}
}
在这种情况下Args
defines Item
to be a String
,因此调用next
的每个值都是Option<String>
。
我们知道String
需要分配。但是,由于我们无法为env::args()
提供任何参数来进行分配,因此只存在两种可能性:
Rust通常憎恨全局状态,因此任何实际改变全局状态的东西都会非常普遍(打印到stdout)或标有大警告文本。
检查文档时,我们看不到这样的大警告文本,因此可以安全地假设迭代器已分配。
你可以通过两次迭代来检查它;你会看到重复的相同值。参数列表不会在你下面秘密变异。
即使这个迭代器分配了字符串,它的仍然更有效地直接使用迭代器的值。当您收集到向量时,您正在为向量分配内存。然后,您还可以再次克隆向量中的值以使用它。这两个分配都是不需要的。
中效版本将使用对向量中项目的引用,特别是&str
:
let query = &args[1];
let filename = &args[2];
这仍然有&#34;开销&#34;分配向量,在该函数之外可能需要也可能不需要。
我喜欢过于花哨,所以我可能写下这样的东西:
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
通常很有用,因为迭代器类型是通用的,并且跳过Config::new
之外的程序名称。这允许在没有实际参数字符串的情况下测试Config
。
Into<String>
纯粹是炫耀。