【问题标题】:How do I create a HashMap literal?如何创建 HashMap 文字?
【发布时间】:2023-03-29 02:50:02
【问题描述】:

如何在 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 {}
            }
        }
    }
}

【问题讨论】:

    标签: hashmap rust


    【解决方案1】:

    Rust 中没有映射字面量语法。我不知道确切的原因,但我预计有多种数据结构表现出类似地图的作用(例如BTreeMapHashMap)这一事实会让人很难选择一个.

    锈 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);
    }
    

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

    另见:

    以前的版本

    您可以创建一个宏来为您完成这项工作,如Why does this rust HashMap macro no longer work? 中所示。这是宏简化了一点,并具有足够的结构使其成为runnable in the 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。计算值的更复杂版本的宏是可能的,但性能优势可能不是大多数宏的使用会受益的东西。

    【讨论】:

    【解决方案2】:

    我推荐maplit crate。

    引用文档:

    具有特定类型的容器文字的宏。

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

    maplit crate 为映射宏使用=&gt; 语法。由于常规 macro_rules! 宏的语法限制,无法使用 : 作为分隔符。

    请注意,rust 宏在调用时使用的括号是灵活的。您可以将它们用作hashmap!{}hashmap![]hashmap!()。这个 crate 建议 {} 作为映射 &amp; 设置宏的约定,它匹配它们的调试输出。

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

    【讨论】:

      【解决方案3】:

      documentation for HashMap中有一个如何实现的例子:

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

      【讨论】:

      • 虽然它在这种情况下有效,但在使用 String 而不是 &amp;str 时会变得不必要地昂贵。
      • 当然有运行时成本,但也有与添加另一个 crate 依赖项或定义难以理解的宏相关的成本。大多数代码不是性能关键,我发现这个版本非常易读。
      • 这个例子可以通过使用vec![ (name, value) ].into_iter().collect()来调整以避免这些问题
      • @Stargateur 因为我不做过早的优化,你也不应该。您的程序可能会受益于更优化的 HashMap 文字初始化,但可能不会。如果您想获得最佳性能,请测量和优化程序中关键路径上的部分。随机优化东西只是一个糟糕的策略。我的回答对 99.9999% 的人都适用。它可读、编译速度快、不会引入增加复杂性和引入潜在安全漏洞的依赖项。
      • 是的,像 hashmap![("Norway",100)] 这样的语法会使事情更具可读性,绝对不会是一种“过早的优化”。写的更短!我认为它真的应该在标准库中。
      【解决方案4】:

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

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

      【讨论】:

        【解决方案5】:

        正如@Johannes 在 cmets 中指出的那样,可以使用 vec![],因为:

        • Vec&lt;T&gt; 实现了 IntoIterator&lt;T&gt; 特征
        • HashMap&lt;K, V&gt; 实现 FromIterator&lt;Item = (K, V)&gt;

        这意味着你可以这样做:

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

        您可以使用&amp;str,但如果不是'static,您可能需要注释生命周期:

        let map: HashMap<&str, usize> = vec![("one", 1), ("two", 2)].into_iter().collect();
        

        【讨论】:

        • 这不代表二进制中的向量文字然后在运行时将其收集到哈希图中吗?
        【解决方案6】:

        从 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&lt;[(K, V); N]&gt; 实现,它更加简洁:

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

        【讨论】:

          【解决方案7】:

          您可以使用velcro crate*。它类似于maplit,正如其他答案中所推荐的那样,但具有更多的集合类型、更好的语法(至少在我看来!)和更多功能。

          假设您想使用Strings 而不是&amp;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!{}
                      }
                  }
              }
          };
          

          这有点难看,因为Strings 的构造方式。但是可以通过使用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 版本更详细


          *完全披露:我是这个箱子的作者。

          【讨论】:

            【解决方案8】:

            对于一个元素

            如果您希望在一行中仅使用一个元素来初始化地图(并且代码中没有可见的突变),您可以这样做:

            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() 之类的操作,但这更长,可读性更低,并且可能存在一些优化问题,因此我建议不要这样做。

            【讨论】:

              【解决方案9】:

              我见过很多奇特的解决方案,但我只是想要一些简单的东西。到 最后,这是一个特质:

              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)
              }
              

              我认为这是一个不错的选择,因为它本质上是 Ruby 已经使用的,并且 尼姆:

              【讨论】:

              • “我认为它是一个不错的选择,因为它本质上已经被 Ruby 和 Nim 使用了”我不明白这是怎么回事?为什么 ruby​​ 或 Nim 这样做会是一个更好的论点,而不是“他们这样做,所以这很好”
              • 没有论据的意见在 SO 答案中没有用
              • 一个细节:将 trait 称为“Hash”是一个坏主意:已经有一个具有该名称的 trait,在 rust 中使用散列时你会很快遇到它。
              • 我还建议更喜欢 copied() 禁止不实现复制的类型,避免无用克隆,或者至少使用 cloned_to_map() 明确表示如果你也想能够只用克隆类型来做到这一点。
              猜你喜欢
              • 2014-01-17
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2014-03-27
              • 2018-02-17
              • 2016-10-20
              • 2023-04-03
              • 1970-01-01
              相关资源
              最近更新 更多