【问题标题】:Does `next()` move or clone elements?`next()` 是否移动或克隆元素?
【发布时间】:2017-07-22 01:17:48
【问题描述】:

我正在阅读Chapter 13 of the Rust book。它说cloneing 字符串的效率低于通过迭代器访问它们(即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"),
        };
        // [...]
    }
}

【问题讨论】:

    标签: rust


    【解决方案1】:

    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 defines Item to be a String,因此调用next 的每个值都是Option&lt;String&gt;

    我们知道String 需要分配。但是,由于我们无法为env::args() 提供任何参数来进行分配,因此只有两种可能性:

    1. 迭代器分配值。
    2. 某种全局状态正在幕后修改。

    Rust 通常厌恶全局状态,所以任何实际改变全局状态的东西都会很常见(打印到标准输出)或标有大警告文本。

    检查文档,我们没有看到这样的大警告文本,因此可以安全地假设迭代器分配了。

    您可以通过迭代两次来检查它;您会看到重复的相同值。参数列表并没有在你下面秘密变异。


    即使这个迭代器分配字符串,仍然直接使用迭代器的值更有效。当您收集到向量中时,您正在为向量分配内存。然后,您还可以再次克隆向量内的值以使用它。这两种分配都是不需要的。

    中等效率的版本是使用对向量中项目的引用,特别是&amp;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&lt;String&gt; 纯属炫耀。

    【讨论】:

    • 两个后续:1)如何知道函数不是从签名内部复制?因为它不需要 Item 是 Copy? 2) 移动所有权意味着 Args 不再拥有这些论点。这不是我在整个项目中必须记住的一个很大的副作用吗?我想我只是在验证我是否理解它是如何工作的,不管这是否是一个好的做法。
    • @qweruiop 啊,我有点误读了你的问题。让我修正一下我的答案。
    • 但是为什么只使用引用是中等效率的呢?参考物品时,根本不需要分配物品,不是吗?
    • @qweruiop 因为您仍在无缘无故地分配向量。
    猜你喜欢
    • 2015-04-23
    • 1970-01-01
    • 2015-06-13
    • 1970-01-01
    • 2021-12-31
    • 1970-01-01
    • 2014-06-07
    • 2013-01-26
    • 1970-01-01
    相关资源
    最近更新 更多