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

use*_*932 7 lifetime move-semantics rust borrowing

我有一个结构Foo:

struct Foo {
    v: String,
    // Other data not important for the question
}
Run Code Online (Sandbox Code Playgroud)

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

我想使用a HashMap<&str, usize>作为索引,其中键将是&Foo::v,而值是该位置Vec<Foo>,但我对其他建议持开放态度.

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

例如,我想:

  • String每个数据流读取只分配一次
  • 不要搜索索引两次,一次检查密钥不存在,一次用于插入新密钥.
  • 不使用Rc或增加运行时间RefCell.

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

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
}
Run Code Online (Sandbox Code Playgroud)

有很多问题:

  1. hash.entry借用钥匙所以s必须有一个"更大"的寿命hash
  2. 我想s在第(b)行移动,而我在第(a)行有一个只读引用

那么在String::clone调用HashMap::get之后HashMap::insert如何在没有额外调用或调用的情况下实现这个简单的算法呢?

She*_*ter 7

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

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

  1. 有一个额外的间接 - String是堆分配的,所以将指针移动到堆分配并不是一个真正的问题.
  2. String永远不会改变.如果是,那么它可能会重新分配,使引用的地址无效.

在这种情况下,可以使用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,我的猜测是你不希望在你的main方法中使用这个代码但是想要从某个函数返回它.这将是一个问题,因为您将很快遇到为什么我不能在同一个结构中存储值和对该值的引用?.


老实说,有些代码风格不适合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"));
}
Run Code Online (Sandbox Code Playgroud)

或者,如下所示,使用项目字段作为关键字的查找表的惯用方法什么?,你可以包装你的类型并将其存储在一个集合容器中:

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"));
}
Run Code Online (Sandbox Code Playgroud)

不使用Rc或增加运行时间RefCell

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


Mat*_* M. 5

不要使用Rc或 来增加运行时间RefCell

@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);
}
Run Code Online (Sandbox Code Playgroud)


Pet*_*all 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
Run Code Online (Sandbox Code Playgroud)

note:答案就在最后。

s 必须生存下去,hash因为您正在&s使用HashMap. s当被删除时,该引用将变得无效。但是,正如注释所说,hash在 后被 s删除。一个快速解决方法是交换它们声明的顺序:

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

但现在你有另一个问题:

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

这个就比较明显了。s被 借用Entry,它将存活到块的末尾。克隆s将解决这个问题:

l.push(Foo { v: s.clone() }); //b
Run Code Online (Sandbox Code Playgroud)

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

但 的类型Foo.vString,所以无论如何它都会拥有自己的副本str。仅该类型意味着您必须复制s.

您可以将其替换为 a &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
            }
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

请注意,以前我必须将 的声明移至sbefore hash,以便它比它更长久。但现在,l持有对 的引用s,因此必须更早地声明它,以便它比 更长久l

  • @user1244932如果您说这对您不起作用,因为您的实际代码使用循环,那么在代码示例中包含循环可能会很有用。 (2认同)

归档时间:

查看次数:

936 次

最近记录:

6 年,9 月 前