`next()` 方法是移动还是克隆元素?

3

我正在阅读Rust书籍第13章。它说clone字符串比通过迭代器(即next())访问它们的效率要低。比较以下示例,我有两个问题:

  • args.next()会将字符串移动或克隆到queryfilename中吗?
  • 如果是移动操作,它会从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"),
        };
        // [...]
    }
}
1个回答

4

args.next()是移动还是克隆?

首先看一下Iterator::next的函数签名:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
}

nextSelf::Item类型的所有权转移给调用者。它对Self没有额外的限制,但可以修改迭代器的内部属性。

接下来,检查特定迭代器的输入和输出。例如,这个迭代器总是返回字符串,但没有输入值:

struct Greet;

impl Iterator for Greet {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        Some(String::from("hello"))
    }
}

在这种情况下,Args定义ItemString,因此调用next的每个值都是一个Option<String>
我们知道String需要分配内存。然而,由于我们无法为env::args()提供任何参数来获取内存分配,只有两种可能性存在:
  1. 迭代器分配值。
  2. 某种全局状态在幕后被修改。
Rust通常不喜欢全局状态,因此任何实际更改全局状态的内容都将非常常见(如打印到stdout)或标有大警告文本。
检查文档,我们没有看到这样的大警告文本,因此可以安全地假设迭代器进行了分配。
您可以通过两次迭代来检查这一点;您将看到重复的相同值。参数列表不会在您底下秘密突变。
即使这个迭代器分配字符串,直接使用迭代器的值仍然更高效。当您收集到向量中时,您正在为向量分配内存。您还要再次克隆向量中的值以使用它。这两个分配都是不必要的。
中等效率的版本将使用向量中项目的引用,特别是&str
let query = &args[1];
let filename = &args[2];

这仍然有向向量分配内存的“开销”,这可能或可能不需要在此函数之外使用。


我喜欢过度花哨,因此我可能会写出这样的代码:

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> 纯粹是炫耀。


两个后续问题:1)如何知道函数没有从签名内部进行复制?因为它不需要 Item 被复制吗?2)移动所有权意味着 Args 不再拥有参数。这不是一个很大的副作用,我必须在整个项目中记住吗?我想我只是在验证我是否理解了它的工作原理,暂且不考虑它是否是一个好的实践。 - qweruiop
@qweruiop 啊,我稍微读错了你的问题。让我修正一下我的回答。 - Shepmaster
但是为什么仅使用引用只是中等效率呢?当引用项目时,根本不需要为项目分配任何空间,不是吗? - qweruiop
2
@qweruiop 因为你仍然没有理由分配向量。 - Shepmaster

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接