我该如何创建一个HashMap字面量?

166

在Rust中如何创建HashMap字面值?在Python中我可以这样做:

hashmap = {
   'element0': {
       'name': 'My New Element',
       'childs': {
           'child0': {
               'name': 'Child For Element 0',
               'childs': {
                   ...
               }
           }
       }
   },
   ...
}

在 Go 语言中是这样的:

type Node struct {
    name string
    childs map[string]Node
}

hashmap := map[string]Node {
    "element0": Node{
        "My New Element",
        map[string]Node {
            'child0': Node{
                "Child For Element 0",
                map[string]Node {}
            }
        }
    }
}
9个回答

203

Rust没有字面量语法来表示map。我不知道确切的原因,但我认为有多个类似map的数据结构(例如BTreeMapHashMap)会使选择一个变得困难。

Rust 1.56

现在许多集合都提供了使用FromInto从数组参数进行转换的功能:

use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};

fn main() {
    let s = Vec::from([1, 2, 3]);
    println!("{:?}", s);

    let s = BTreeSet::from([1, 2, 3]);
    println!("{:?}", s);

    let s = HashSet::from([1, 2, 3]);
    println!("{:?}", s);

    let s = BTreeMap::from([(1, 2), (3, 4)]);
    println!("{:?}", s);

    let s = HashMap::from([(1, 2), (3, 4)]);
    println!("{:?}", s);
}

这个逻辑可以封装回宏中,以实现某些语法糖:

use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};

macro_rules! collection {
    // map-like
    ($($k:expr => $v:expr),* $(,)?) => {{
        core::convert::From::from([$(($k, $v),)*])
    }};
    // set-like
    ($($v:expr),* $(,)?) => {{
        core::convert::From::from([$($v,)*])
    }};
}

fn main() {
    let s: Vec<_> = collection![1, 2, 3];
    println!("{:?}", s);

    let s: BTreeSet<_> = collection! { 1, 2, 3 };
    println!("{:?}", s);

    let s: HashSet<_> = collection! { 1, 2, 3 };
    println!("{:?}", s);

    let s: BTreeMap<_, _> = collection! { 1 => 2, 3 => 4 };
    println!("{:?}", s);

    let s: HashMap<_, _> = collection! { 1 => 2, 3 => 4 };
    println!("{:?}", s);
}

Rust 1.51

从 Rust 1.51 开始,您可以使用按值数组迭代器和FromIterator来收集多种类型的集合:

use std::array::IntoIter;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::iter::FromIterator;

fn main() {
    // Rust 1.53
    let s = Vec::from_iter([1, 2, 3]);
    println!("{:?}", s);

    // Rust 1.51
    let s = Vec::from_iter(IntoIter::new([1, 2, 3]));
    println!("{:?}", s);

    let s = BTreeSet::from_iter(IntoIter::new([1, 2, 3]));
    println!("{:?}", s);

    let s = HashSet::<_>::from_iter(IntoIter::new([1, 2, 3]));
    println!("{:?}", s);

    let s = BTreeMap::from_iter(IntoIter::new([(1, 2), (3, 4)]));
    println!("{:?}", s);

    let s = HashMap::<_, _>::from_iter(IntoIter::new([(1, 2), (3, 4)]));
    println!("{:?}", s);
}

请注意,在Rust 1.53中,std :: array :: IntoIter并不总是需要的。

这个逻辑可以包装到一个宏中,以获得一些语法糖:

use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};

macro_rules! collection {
    // map-like
    ($($k:expr => $v:expr),* $(,)?) => {{
        use std::iter::{Iterator, IntoIterator};
        Iterator::collect(IntoIterator::into_iter([$(($k, $v),)*]))
    }};
    // set-like
    ($($v:expr),* $(,)?) => {{
        use std::iter::{Iterator, IntoIterator};
        Iterator::collect(IntoIterator::into_iter([$($v,)*]))
    }};
}

fn main() {
    let s: Vec<_> = collection![1, 2, 3];
    println!("{:?}", s);

    let s: BTreeSet<_> = collection! { 1, 2, 3 };
    println!("{:?}", s);

    let s: HashSet<_> = collection! { 1, 2, 3 };
    println!("{:?}", s);

    let s: BTreeMap<_, _> = collection! { 1 => 2, 3 => 4 };
    println!("{:?}", s);

    let s: HashMap<_, _> = collection! { 1 => 2, 3 => 4 };
    println!("{:?}", s);
}

这些解决方案避免了不必要的分配和重新分配。

另请参阅:

以前的版本

您可以创建一个宏来为您完成这项工作,就像在为什么这个Rust HashMap宏不再起作用?中所示。以下是该宏的简化版本,并具有足够的结构使其在playground中运行

macro_rules! map(
    { $($key:expr => $value:expr),+ } => {
        {
            let mut m = ::std::collections::HashMap::new();
            $(
                m.insert($key, $value);
            )+
            m
        }
     };
);

fn main() {
    let names = map!{ 1 => "one", 2 => "two" };
    println!("{} -> {:?}", 1, names.get(&1));
    println!("{} -> {:?}", 10, names.get(&10));
}

这个宏避免了分配不必要的中间 Vec,但它没有使用HashMap::with_capacity,因此在添加值时可能会有一些无用的 HashMap 重新分配。该宏的更复杂版本可以计算值,但是性能收益可能对大多数宏的使用者没有益处。


6
grabbag_macros crate 中也有一个类似的东西。你可以在这里看到源代码: https://github.com/DanielKeep/rust-grabbag/blob/master/grabbag_macros/src/lib.rs#L3-L64。 - DK.
5
啊,遗憾。我喜欢 Swift 对此的处理方式,它将字面量与其类型分离。DictionaryLiteral 可用于初始化任何符合 ExpressibleByDictionaryLiteral 的类型(尽管标准库已经提供了一个这样的类型——Dictionary)。 - Alexander
随着 Rust 1.51 中 std::array::IntoIter 的稳定,您不再需要使用 nightly 来使用第二种方法。 - Alex
2
值得一提的是 Rust 1.56 的 HashMap::from([ ("k1",1), ("k2",2) ]) - Haider

73

我推荐使用maplit crate。

引用自文档:

带有特定类型的容器字面值宏。

use maplit::hashmap;

let map = hashmap!{
    "a" => 1,
    "b" => 2,
};

maplit crate使用=>语法来映射宏。由于在常规的macro_rules!宏中存在语法限制,因此无法使用:作为分隔符。

请注意,Rust宏在调用时使用的括号很灵活。您可以将它们用作hashmap!{}hashmap![]hashmap!()。该crate建议对于映射和集合宏使用{}作为约定,以匹配它们的Debug输出。

  • btreemap 从键-值对列表创建BTreeMap
  • btreeset 从元素列表创建BTreeSet
  • hashmap 从键-值对列表创建HashMap
  • hashset 从元素列表创建HashSet

2
自 Rust 1.56 起,您可以在不使用外部创建的情况下完成此操作。 let hm = HashMap::from([(1, "2"), (3, "4")]); - Westy92

53

HashMap的文档中有一个实现此功能的示例:

let timber_resources: HashMap<&str, i32> = [("Norway", 100), ("Denmark", 50), ("Iceland", 10)]
    .iter()
    .cloned()
    .collect();

5
在这种情况下它能够工作,但是当使用String代替&str时就会变得不必要地昂贵。 - Shepmaster
8
当然存在运行时成本,但添加另一个 crate 依赖或定义难以理解的宏也会带来成本。大多数代码不是性能关键的,我认为这个版本非常易读。 - jupp0r
17
这个例子可以进行调整以避免那些问题,方法是使用 vec![ (name, value) ].into_iter().collect() - Johannes
3
因为我不进行过度优化,你也不应该这样做。也许你的程序会从更优化的HashMap字面初始化中受益,但很可能不会。如果你想获得最佳性能,那么测量和优化关键路径上的部分是必要的。随意优化往往是一种糟糕的策略。我的答案对99.9999%的人来说都很好。它易于阅读,编译速度快,不引入增加复杂性和潜在安全漏洞的依赖项。 - jupp0r
3
是的,像hashmap![("Norway",100)]这样的语法可以使内容更易读,绝对不会是一种“过早优化”的形式。而且它的书写长度更短!我认为它真的应该在标准库中。 - avl_sweden
显示剩余6条评论

30

从 Rust 1.56 开始,你可以使用 from() 初始化一个 HashMap,这有点像拥有一个 HashMap 字面量。 from() 接受一个键值对数组。你可以像这样使用它:

use std::collections::HashMap;

fn main() {
    let hashmap = HashMap::from([
        ("foo", 1),
        ("bar", 2)
    ]);
}

7

正如评论中@Johannes所指出的,使用vec![]是可能的,因为:

  • Vec<T>实现了IntoIterator<T> trait
  • HashMap<K, V>实现了FromIterator<Item = (K, V)>

这意味着你可以这样做:

let map: HashMap<String, String> = vec![("key".to_string(), "value".to_string())]
    .into_iter()
    .collect();

您可以使用&str,但如果它不是 'static,您可能需要注释生命周期:
let map: HashMap<&str, usize> = vec![("one", 1), ("two", 2)].into_iter().collect();

这样做不会在二进制中表示向量文字,而是在运行时将其收集到哈希映射中吗? - alex elias

4
您可以使用velcro工具包*。与其他答案中推荐的maplit类似,但它包含更多的集合类型、更好的语法(至少我认为如此!)和更多的功能。
假设您想要使用String而不是&str,您的确切示例将如下所示:
use std::collections::HashMap;
use velcro::hash_map;

struct Node {
    name: String
    children: HashMap<String, Node>,
}

let map = hash_map! {
    String::from("element0"): Node {
        name: "My New Element".into(),
        children: hash_map! {
            String::from("child0"): Node {
                name: "child0".into(),
                children: hash_map!{}
            }
        }
    }
};

这看起来有点丑陋,因为它是基于如何构造String的。但可以通过使用hash_map_from!来使其更加简洁,而不改变键类型,hash_map_from!将自动进行转换:

use std::collections::HashMap;
use velcro::{hash_map, hash_map_from};

let map: HashMap<String, Node> = hash_map_from! {
    "element0": Node {
        name: "My New Element".into(),
        children: hash_map_from! {
            "child0": Node {
                name: "child0".into(),
                children: hash_map!{}
            }
        }
    }
};

这并不比 Go 版本更冗长。


*完全公开透明:我是这个 crate 的作者。


4
从 Rust 1.51 开始,数组实现了 IntoIterator,因此你可以使用 from_iter 方法创建一个 HashMap 而不需要克隆:
use std::collections::HashMap;
use std::iter::FromIterator;

// note that this type annotation is required
let mut map: HashMap<_, _> = HashMap::from_iter([("a", 1), ("b", 2), ("c", 3)]);

截至 Rust 1.56(目前为夜间版),你可以使用 From<[(K, V); N]> 实现,这是更加简洁的方式:

let mut map = HashMap::from([
    ("a", 1),
    ("b", 2),
    ("c", 3),
]);

1

针对一个元素

如果你希望在一行中仅使用一个元素来初始化地图(并且在代码中不显示任何可见的变化),你可以这样做:

let map: HashMap<&'static str, u32> = Some(("answer", 42)).into_iter().collect();

这要归功于Option的实用性,它可以通过into_iter()变成一个Iterator

在真正的代码中,你通常不需要帮助编译器确定类型:

use std::collections::HashMap;

fn john_wick() -> HashMap<&'static str, u32> {
    Some(("answer", 42)).into_iter().collect()
}

fn main() {
    let result = john_wick();

    let mut expected = HashMap::new();
    expected.insert("answer", 42);

    assert_eq!(result, expected);
}

还有一种方法可以链接多个元素,像这样进行操作:Some(a).into_iter().chain(Some(b).into_iter()).collect(),但是这种方式更长、可读性更差,可能存在一些优化问题,所以我建议不要使用。


-3

我看到了很多花哨的解决方案,但我想要简单的东西。为此,这里有一个特质:

use std::collections::HashMap;

trait Hash {
    fn to_map(&self) -> HashMap<&str, u16>;
}

impl Hash for [(&str, u16)] {
    fn to_map(&self) -> HashMap<&str, u16> {
        self.iter().cloned().collect()
    }
}

fn main() {
    let m = [("year", 2019), ("month", 12)].to_map();
    println!("{:?}", m)
}

我认为这是一个不错的选择,因为它基本上是RubyNim已经在使用的。


2
我认为这是一个不错的选择,因为它本质上就是 Ruby 和 Nim 已经在使用的。但我不明白这怎么成为了一个论点?为什么 Ruby 或 Nim 这样做会比只是说“他们这样做所以很好”更有说服力呢? - Stargateur
一个没有论据的观点在 Stack Overflow 答案中是没有用的。 - Stargateur
2
一个细节:将特质命名为“Hash”是个坏主意:已经有一个同名的特质,当你在 Rust 中处理哈希时会快速遇到它。 - Denys Séguret
2
我建议使用copied()来禁止不实现复制的类型,避免无谓的克隆,或者至少使用cloned_to_map()来显式表达你也想要只用克隆类型做到这一点。 - Stargateur

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