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

时间:2017-07-20 18:45:51

标签: rust

我正在阅读Chapter 13 of the Rust book。它说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 个答案:

答案 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()提供任何参数来进行分配,因此只存在两种可能性:

  1. 迭代器分配值。
  2. 某种全球状态正在幕后进行修改。
  3. 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>纯粹是炫耀。