如何在处理数据流时有效地构建向量和该向量的索引?

时间:2017-04-17 22:13:13

标签: rust move-semantics lifetime borrowing

我有一个结构Foo

struct Foo {
    v: String,
    // Other data not important for the question
}

我想处理数据流并将结果保存到Vec<Foo>,并在字段Vec<Foo>上为此Foo::v创建索引。

我想使用HashMap<&str, usize>作为索引,其中键为&Foo::v,值为Vec<Foo>中的位置,但我对其他人开放建议。

我希望尽可能快地处理数据流,这需要两次不做明显的事情。

例如,我想:

  • 每个数据流只读一次String
  • 不搜索索引两次,一次检查密钥不存在,一次用于插入新密钥。
  • 使用RcRefCell
  • 不会增加运行时间

借阅检查器不允许使用此代码:

let mut l = Vec::<Foo>::new();
{
    let mut hash = HashMap::<&str, usize>::new();
    //here is loop in real code, like: 
    //let mut s: String; 
    //while get_s(&mut s) {
    let s = "aaa".to_string();
    let idx: usize = match hash.entry(&s) { //a
        Occupied(ent) => {
            *ent.get()
        }
        Vacant(ent) => {
            l.push(Foo { v: s }); //b
            ent.insert(l.len() - 1);
            l.len() - 1
        }
    };
    // do something with idx
}

存在多个问题:

  1. hash.entry借用了密钥,因此s必须有一个更大的&#34;寿命比hash
  2. 我想在第(b)行移动s,而我在第(a)行有一个只读参考
  3. 那么,在调用String::clone后,如何在没有额外调用HashMap::get或致电HashMap::insert的情况下实施这个简单算法?

3 个答案:

答案 0 :(得分:7)

一般来说,你想要完成的是不安全的,Rust正确地阻止你做一些你不应该做的事情。举一个简单的例子,考虑Vec<u8>。如果向量具有一个项并且容量为1,则向向量添加另一个值将导致重新分配和复制向量中的所有值,从而使对向量的任何引用无效。这会导致索引中的所有键都指向任意内存地址,从而导致不安全的行为。编译器会阻止它。

这个的情况下,有两个额外的信息,编译器不知道,但程序员不是:

  1. 有一个额外的间接 - String是堆分配的,所以将指针移动到该堆分配并不是真正的问题。
  2. String 永远不会更改。如果是,那么它可能会重新分配,使引用的地址无效。
  3. 在这种情况下,只要您正确记录为什么它不安全,就可以使用unsafe代码。

    use std::collections::HashMap;
    use std::mem;
    
    #[derive(Debug)]
    struct Player {
        name: String,
    }
    
    fn main() {
        let names = ["alice", "bob", "clarice", "danny", "eustice", "frank"];
    
        let mut players = Vec::new();
        let mut index = HashMap::new();
    
        for &name in &names {
            let player = Player { name: name.into() };
            let idx = players.len();
    
            // INSERT REASON WHY THIS CODE IS NOT UNSAFE
            let stable_name: &str = unsafe { mem::transmute(&*player.name) };
    
            players.push(player);
            index.insert(idx, stable_name);
        }
    
        for (k, v) in &index {
            println!("{:?} -> {:?}", k, v);
        }
    
        for v in &players {
            println!("{:?}", v);
        }
    }
    

    但是,我的猜测是您不希望在main方法中使用此代码,而是希望从某个函数返回它。这将是一个问题,因为您将很快遇到Why can't I store a value and a reference to that value in the same struct?

    老实说,有些代码风格不符合Rust的限制。如果碰到这些,你可以:

    • 认定Rust不适合您或您的问题。
    • 使用unsafe代码,最好经过全面测试,并且只展示安全的API。
    • 调查其他陈述。

    例如,我可能会重写代码以使索引成为密钥的主要所有者:

    use std::collections::BTreeMap;
    
    #[derive(Debug)]
    struct Player<'a> {
        name: &'a str,
        data: &'a PlayerData,
    }
    
    #[derive(Debug)]
    struct PlayerData {
        hit_points: u8,
    }
    
    #[derive(Debug)]
    struct Players(BTreeMap<String, PlayerData>);
    
    impl Players {
        fn new<I, S>(iter: I) -> Self
            where I: IntoIterator<Item = S>,
                  S: AsRef<str>,
        {
            let players = iter.into_iter()
                .map(|name| (name.as_ref().to_string(), PlayerData { hit_points: 100 }))
                .collect();
            Players(players)
        }
    
        fn get<'a>(&'a self, name: &'a str) -> Option<Player<'a>> {
            self.0.get(name).map(|data| {
                Player {
                    name: name,
                    data: data,
                }
            })
        }
    }
    
    fn main() {
        let names = ["alice", "bob", "clarice", "danny", "eustice", "frank"];
    
        let players = Players::new(&names);
    
        for (k, v) in &players.0 {
            println!("{:?} -> {:?}", k, v);
        }
    
        println!("{:?}", players.get("eustice"));
    }
    

    或者,如What's the idiomatic way to make a lookup table which uses field of the item as the key?所示,您可以将您的类型换行并将其存储在set容器中:

    use std::collections::BTreeSet;
    
    #[derive(Debug, PartialEq, Eq)]
    struct Player {
        name: String,
        hit_points: u8,
    }
    
    #[derive(Debug, Eq)]
    struct PlayerByName(Player);
    
    impl PlayerByName {
        fn key(&self) -> &str {
            &self.0.name
        }
    }
    
    impl PartialOrd for PlayerByName {
        fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
            Some(self.cmp(other))
        }
    }
    
    impl Ord for PlayerByName {
        fn cmp(&self, other: &Self) -> std::cmp::Ordering {
            self.key().cmp(&other.key())
        }
    }
    
    impl PartialEq for PlayerByName {
        fn eq(&self, other: &Self) -> bool {
            self.key() == other.key()
        }
    }
    
    impl std::borrow::Borrow<str> for PlayerByName {
        fn borrow(&self) -> &str {
            self.key()
        }
    }
    
    #[derive(Debug)]
    struct Players(BTreeSet<PlayerByName>);
    
    impl Players {
        fn new<I, S>(iter: I) -> Self
            where I: IntoIterator<Item = S>,
                  S: AsRef<str>,
        {
            let players = iter.into_iter()
                .map(|name| PlayerByName(Player { name: name.as_ref().to_string(), hit_points: 100 }))
                .collect();
            Players(players)
        }
    
        fn get(&self, name: &str) -> Option<&Player> {
            self.0.get(name).map(|pbn| &pbn.0)
        }
    }
    
    fn main() {
        let names = ["alice", "bob", "clarice", "danny", "eustice", "frank"];
    
        let players = Players::new(&names);
    
        for player in &players.0 {
            println!("{:?}", player.0);
        }
    
        println!("{:?}", players.get("eustice"));
    }
    
      

    使用RcRefCell

    不会增加运行时间

    在不执行分析的情况下猜测性能特征绝不是一个好主意。老实说,我不相信当克隆或删除某个值时递增一个整数会有明显的性能损失。如果问题同时需要索引和向量,那么我会达成某种共享所有权。

答案 1 :(得分:4)

  

不使用RcRefCell来增加运行时间。

@Shepmaster已经证明使用unsafe完成此操作,一旦你有了,我会鼓励你检查Rc实际上会花多少钱。以下是Rc的完整版:

use std::{
    collections::{hash_map::Entry, HashMap},
    rc::Rc,
};

#[derive(Debug)]
struct Foo {
    v: Rc<str>,
}

#[derive(Debug)]
struct Collection {
    vec: Vec<Foo>,
    index: HashMap<Rc<str>, usize>,
}

impl Foo {
    fn new(s: &str) -> Foo {
        Foo {
            v: s.into(),
        }
    }
}

impl Collection {
    fn new() -> Collection {
        Collection {
            vec: Vec::new(),
            index: HashMap::new(),
        }
    }

    fn insert(&mut self, foo: Foo) {
        match self.index.entry(foo.v.clone()) {
            Entry::Occupied(o) => panic!(
                "Duplicate entry for: {}, {:?} inserted before {:?}",
                foo.v,
                o.get(),
                foo
            ),
            Entry::Vacant(v) => v.insert(self.vec.len()),
        };
        self.vec.push(foo)
    }
}

fn main() {
    let mut collection = Collection::new();

    for foo in vec![Foo::new("Hello"), Foo::new("World"), Foo::new("Go!")] {
        collection.insert(foo)
    }

    println!("{:?}", collection);
}

答案 2 :(得分:1)

错误是:

error: `s` does not live long enough
  --> <anon>:27:5
   |
16 |         let idx: usize = match hash.entry(&s) { //a
   |                                            - borrow occurs here
...
27 |     }
   |     ^ `s` dropped here while still borrowed
   |
   = note: values in a scope are dropped in the opposite order they are created

最后的note:就是答案所在。

s 必须hash更长,因为您使用&s作为HashMap中的密钥。删除s时,此引用将无效。但是,正如笔记所说,hash将在 s之后删除。快速解决方法是交换声明的顺序:

let s = "aaa".to_string();
let mut hash = HashMap::<&str, usize>::new();

但现在你还有另外一个问题:

error[E0505]: cannot move out of `s` because it is borrowed
  --> <anon>:22:33
   |
17 |         let idx: usize = match hash.entry(&s) { //a
   |                                            - borrow of `s` occurs here
...
22 |                 l.push(Foo { v: s }); //b
   |                                 ^ move out of `s` occurs here

这个更明显。 s借用Entry,它将一直存在到块的末尾。克隆s将解决这个问题:

l.push(Foo { v: s.clone() }); //b
  

我只想分配一次,而不是克隆它

Foo.v的类型为String,因此无论如何它都拥有自己的str副本。只是那种类型意味着您必须复制s

您可以将其替换为&str,这样就可以将其作为对s的引用:

struct Foo<'a> {
    v: &'a str,
}

pub fn main() {
    // s now lives longer than l
    let s = "aaa".to_string();
    let mut l = Vec::<Foo>::new();
    {
        let mut hash = HashMap::<&str, usize>::new();

        let idx: usize = match hash.entry(&s) {
            Occupied(ent) => {
                *ent.get()
            }
            Vacant(ent) => {
                l.push(Foo { v: &s });
                ent.insert(l.len() - 1);
                l.len() - 1
            }
        };
    }
}

请注意,之前我必须将s的声明移到hash之前,以便它会比它更长久。但现在,l包含对s的引用,因此必须更早地声明它,以便它比l更长。