如何简洁地填充一个二维HashMap?

7
有没有一种简单的方法来填充一个二维 HashMap?在 C++ 中,我可以做一些类似于以下方式的操作:
std::unordered_map<int,std::unordered_map<int,int>> 2dmap;
//...
2dmap[0][0] = 0;
2dmap[0][1] = 1;
//...

在我的Rust项目中,我尝试填充一个类似的映射表:
let mut map: HashMap<i32, HashMap<i32, i32>> = HashMap::new();
//...fill the hash map here.

我能想到的唯一方法是构建每个子地图,然后将它们移动到超级地图中,类似于以下代码:

let mut sub1: HashMap<i32, i32> = HashMap::new();
sub1.insert(0, 0);
sub1.insert(1, 1);
let mut map: HashMap<i32, HashMap<i32, i32>>::new();
map.insert(0, sub1);

有没有更简洁的方法来完成这个任务?

以上代码是我实际使用情况的简化版本,其中使用枚举作为 HashMap 的索引:

enum Example {
    Variant1,
    Variant2,
    //...
}

所有变量都没有值。我正在使用这种语法从我的HashMap中查找:

let value = map[Example::Variant1][Example::Variant2];

3
既然在Rust中元组表现得很好,为什么不使用HashMap<(i32,i32),i32>呢? - Alec
在您进行编辑后,仍然可以使用 HashMap<(example, example),i32>。您真的需要仅使用第一个 example 进行查找吗?如果不需要,应该使用元组... - Alec
@Alec是指使用元组会提供更紧凑的表示还是更快的查找?还是两者都有?这是什么原理?我不反对这个想法,我只是想了解推理而不仅仅是解决方案。 - Alex Zywicki
1
这个参数更多地涉及语义问题。顺便说一下,表示将会更加紧凑和快速。不需要额外的HashMap开销,每个都分配比它们需要的更多的内存,并且只需要在表中查找一次,而不是两次。 - Alec
3个回答

11

Rust中,元组非常好用。您可能应该只使用 HashMap<(i32, i32), i32>。这样,您最终得到的代码将非常接近于C++代码。

let mut map: HashMap<(i32, i32), i32> = HashMap::new();
sub1.insert((0, 0), 0);
sub1.insert((0, 1), 1);
// ...

自然地,如果我们能有一个像vec!这样的宏就好了,而且RFC正在进行中。使用这个答案中的宏,您可以编写以下代码:

let map = hashmap![(0,0) => 0, (0,1) => 1, ...];

如果您在使用枚举,那么不需要进行任何更改 - 只要确保对其进行EqPartialEqHash的派生即可。

let mut sub1: HashMap<(Example, Example), i32> = HashMap::new();
sub1.insert((Example::Variant1, Example::Variant1), 0);
sub1.insert((Example::Variant1, Example::Variant2), 1);
// ...

查找语法是什么? - Alex Zywicki
1
@AlexZywicki map.get(&(0,1)) - Alec
没有必要指定HashMap的类型,类型推断会处理它。 - Shepmaster
1
@Shepmaster 当然。但是,作为一个大部分编程生涯都在支持类型推断的语言(Haskell和Scala)中工作的人来说,我仍然喜欢在定义时添加注释,这样可以让类型不会_立即_变得明显。未来的我对现在的我更加满意。 - Alec
请注意,如果您想从地图中不安全地获取值,也可以这样做:map[&(0,1)] - Alec
2
@ Alec,“unsafe”一词在Rust中具有特定的含义。 map [&(0,1)]是安全的(在Rust意义下),但可能会出现panic情况。 - user4815162342

7
使用entry API:
use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.entry(0).or_insert_with(HashMap::new).insert(0, 0);
    map.entry(0).or_insert_with(HashMap::new).insert(1, 1);

    println!("{:?}", map);
    println!("{}", map[&0][&1]);
}

与C++不同,构建嵌套的HashMap并不是隐式的 - 你必须非常明确地表达你希望创建一个新的HashMap
另一个答案不同,这样可以保留原始数据结构,并能够根据初始键获取整个映射的子集:
println!("{:?}", map.get(&0));
println!("{:?}", map.get(&0).and_then(|m| m.get(&1)));

如果您始终提供两个数字索引,那么元组是更好的解决方案,因为它更准确地模拟了问题——只有一个真正的键,只是它有两个部分。还可能具有更好的数据局部性,因为有一个大内存块,而不需要哈希嵌套的额外指针级别。

2
该 C++ 代码片段之所以简洁,是因为方便的 operator [] 会在映射中缺少值时自动默认构造值。虽然 Rust 默认情况下不会这样做,但可以很容易地告诉它这样做,就像 Shepmaster 的回答所示。
为了避免拼写 entry(key).or_insert_with(ValueType::new),可以将类似于 C++ 中 operator [] 的方法添加到 Rust 的 HashMap 中。毕竟,Rust 有必要的工具-它支持使用 traits 向现有类型添加方法,并且它有一个大致相当于 C++ 默认构造函数的 trait。
以下是 C++ 表达式:
map[0][0] = 0;
map[0][1] = 1;

使用返回引用的方法,将以下内容编写成Rust代码,代替operator []:

*map.ensure(0).ensure(0) = 0;
*map.ensure(0).ensure(1) = 1;

ensure将被声明在一个trait中,只有导入该trait的代码才能获得这个方法:

use std::collections::HashMap;
use std::hash::Hash;

trait MapDefault<K, V: Default> {
    fn ensure(&mut self, key: K) -> &mut V;
}

...并且定义如下:

impl<K: Eq + Hash, V: Default> MapDefault<K, V> for HashMap<K, V> {
    fn ensure(&mut self, key: K) -> &mut V {
        self.entry(key).or_insert_with(V::default)
    }
}

如果我们能够为HashMap定义IndexMut,那么表达式*map[0][0] = 0就可以缩短了。这与C++的原始代码几乎完全一致。但不幸的是,Rust不允许实现其他模块类型的运算符。


你可以实现一个包装类型来实现IndexMut,不是吗? - Shepmaster
1
实际上,也许不行。HashMap 没有实现 IndexMut 的原因是有道理的,所以你真的做不到。目前来说,使用该方法是更好的选择。 - Shepmaster
@Shepmaster 感谢你的提示,我将尝试实验在包装类型上实现类似于 ensureIndexMut::get_mutHashMap 本身无法实现答案中所提供的 IndexMut 类型,因为其值不需要 Default 特性(也不应该有,我的看法是这个特性不值得)。之前存在但已被删除的 IndexMut 只会在不存在的键上 panic,这绝对不是程序员期望的 map[k] = v 的语义。 - user4815162342

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