Clojure 学习笔记 :7 map --- 可能是最有用的数据结构

Clojure 零基础 学习笔记 map


map 是一种映射关系

map 长啥样

这次介绍的 map 不是高阶函数 map ,而是我们在第二篇文章《你好,集合》里面曾经提到名字的一种 Clojure 提供的数据结构。高阶函数 map 像是一个动词,表示一种函数和一系列参数之间的“映射”动作。而数据结构 map 是一个名词,它表示“关键字”和“值”之间的对应关系。也就是业界通常简称的“键值对”、key-value。

那到底长啥样呢?我们来写几个 map

{"键" "值"}
{"关键字1" "值1" "关键字2" "值2"} ;不推荐这种使用空格来间隔的写法
{:name "blindingdark", :age "不告诉你"} ;如果写在一行,每一组键值对之间用逗号间隔
{:name "FFF团火法师"
 :level 32
 :coins 128
 :items ["生锈的普通匕首" "火把" "汽油"]} ;不过我们更建议在每组键值对之间换行

接触过 JSON 的同学应该非常熟悉这种形式。
使用花括号 {} 来包围一组键值对,键和值可以是任意表达式(如字符串,数字,list,vector)。每一对键值对中,键和值用空格隔开,不同的键值对之间使用逗号或者空格或者换行隔开。

上面的例子中也对间隔符号进行了说明,虽然 Clojure 允许多种个性化的风格,但无论哪种风格,总是要保持良好的可读性。

不过通常我们使用一种特别的形式来表示“键”,即使用 冒号+关键字名 的形式来表示。你马上就会在接下来的内容中看到,如何使用这种形式更方便的访问 map 中的元素。

map 中的 key 必须保证唯一性!
如果你在一个 map 里使用了重名的 key,那么就会报错。

=> {"键" "值1"
    "键" "值2"}
IllegalArgumentException Duplicate key: 键  clojure.lang.PersistentArrayMap.createWithCheck (PersistentArrayMap.java:70)

操作 map 元素

首先我们假设有一名玩家的信息是这样的

 (def player-info {:name "FFF团火法师"
                    :level 32
                    :coins 128
                    :items ["生锈的普通匕首" "火把" "汽油"]})

那么我们怎么去得到这个用户的信息呢?

我们可以使用 get 函数来得到某个 key 的 value。

小贴士:使用英文可以有效提高逼格

=> (get player-info :items)
["生锈的普通匕首" "火把" "汽油"]

=> (get player-info :level)
32

=> (get player-info :sex)
nil

=> (get player-info :sex "没有找到这个属性")
"没有找到这个属性"

很容易就可以总结出 get 函数的用法:

  1. 第一个参数是要访问的 map
  2. 第二个参数是要访问的 map 中的 key
  3. 第三个参数可选,作用是设置一个默认值,如果 map 中访问的 key 不存在,则返回这个参数的值
  4. 正常情况下返回 key 所对应的 value
  5. 如果第三个参数未填写,那么一旦 map 中没有所访问的 key,则返回 nil

get 函数不仅能访问 map,有关它的其他用法请查询相关 api。

如果你使用了 冒号+关键字名 这种形式来表示一个 key,那么我们就可以更加简便的访问 map 中的 value 了。

=> (:name player-info)
"FFF团火法师"

=> (:sex player-info)
nil

=> (:sex player-info "not found")
"not found"

我们发现, 冒号+关键字名 这种形式的表示,竟然能放在括号的第一个位置,这说明它也是一个函数。所以这种奇特的函数的使用方法是这样的:

  1. 第一个参数是要访问的 map
  2. 第二个参数可选(和 get 函数的第三个参数作用一致)
  3. 在 map 中寻找和 key 函数本身相同的 key(其它特性与 get 函数相似)

除了读取 map 中的内容,我们还可以使用 assocdissoc 来增加和删除 map 中的元素。

比如我们的火法师成功的烧烧烧了一对情侣,获得 100 金币,获得称号:大师级火焰掌控

首先,我们使用 assoc 函数向 player-info 添加一个键值对 称号-称号名

=> (assoc player-info :achieve "大师级火焰掌控")
{:name "FFF团火法师", :level 32, :coins 128, :items ["生锈的普通匕首" "火把" "汽油"], :achieve "大师级火焰掌控"}

还记得我们之前说过的使用 def 声明的值是不可变的 么?所以此时我们访问 player-info 会发现它并没有改变。

=> player-info
{:name "FFF团火法师", :level 32, :coins 128, :items ["生锈的普通匕首" "火把" "汽油"]}

所以我们得重新声明:

=> (def player-info (assoc player-info :achieve "大师级火焰掌控"))
#'user/player-info

如果 assoc 函数所添加的 key 已经存在,就会使用新值来覆盖。所以我们在此使用它来改变金币的数量。

=> (assoc player-info :coins (+ 100 (:coins player-info)))
{:name "FFF团火法师", :level 32, :coins 228, :items ["生锈的普通匕首" "火把" "汽油"], :achieve "大师级火焰掌控"}

当然,它还是没有改变 player-info 的值。

我们还可以一次性对 map 进行多个值的修改,只需要把需要添加的键值对依次写上:

=> (assoc player-info
  :coins (+ 100 (:coins player-info))
  :achieve "大师级火焰掌控")
{:name "FFF团火法师", :level 32, :coins 228, :items ["生锈的普通匕首" "火把" "汽油"], :achieve "大师级火焰掌控"}

如果要删除某个键值对,我们可以使用 dissoc 函数。比如我们删除称号:

=> (dissoc player-info :achieve)
{:name "FFF团火法师", :level 32, :coins 228, :items ["生锈的普通匕首" "火把" "汽油"]}

如果我们想得到所有的 key 或者得到所有的 value,可以用 keys 函数和 vals 函数:

=> (keys player-info)
(:name :level :coins :items)

=> (vals player-info)
("FFF团火法师" 32 128 ["生锈的普通匕首" "火把" "汽油"])

map 嵌套

map 中可以嵌套 map(或者嵌套任何你想要的表达式),这是非常自然的做法,你可以一层一层的来组装你的数据,以便更好的描述你所需要的内容。

比如我们可以给我们的 player-info 添加一条信息,来表示已装备的护甲:

=> (assoc player-info :armor {:head "巫师帽子", :body "黑色法袍"})
{:name "FFF团火法师",
 :level 32,
 :coins 228,
 :items ["生锈的普通匕首" "火把" "汽油"],
 :achieve "大师级火焰掌控",
 :armor {:head "巫师帽子", :body "黑色法袍"}}

访问它的方式大家就开动脑筋吧。


map 解构

还记得之前讲到的顺序解构么?map 的解构也差不多:

=> (let [{n :name,coins :coins} player-info]
  (println n)
  (println coins))
FFF团火法师
228
nil

只不过要注意,键值对解构要把 key 写在后面,而给 key 的 value 取的新名字写在前面。通常我们把 value 的新名字取的和 key 一样,当然也可以不一样。

再来看看 defn 函数中的解构样式(点这里可以复习如何解构参数列表)

让我们写一个函数,来显示我们的火法师的姓名和等级:

=> (defn show-level-and-name
  [{name :name,coins :coins}]
  (println "昵称:" name)
  (println "金币:" coins))
#'user/show-level-and-name

=> (show-level-and-name player-info)
昵称: FFF团火法师
金币: 228
nil

同样我们可以使用嵌套的 map 解构:

=> (defn show-armors
    [{{head :head,body :body} :armor}]
    (println "头部:" head)
    (println "身体:" body))
 #'user/show-armors
 
=> (show-armors player-info)
头部: 巫师帽子
身体: 黑色法袍
nil

我们甚至可以把顺序解构和 map 解构结合起来,显示我们道具栏中的物品:

=> (let [{name :name,coins :coins,[i1 i2 i3] :items} player-info]
  (println name)
  (println coins)
  (println i1 i2 i3))
FFF团火法师
228
生锈的普通匕首 火把 汽油
nil

上面我们说了,解构的时候我们习惯把解构之后的名字取成和 key 一样,这样一来,我们就要把一个名字写两遍,当元素增多的时候,重复劳动的负担就无法忍受了。

如果你的 key 都使用 冒号+名字 的格式,你就可以使用 Clojure 提供的另一种方法:

=> (let [{:keys [coins name]} player-info]
          (println "昵称:" name)
         (println "金币:" coins))
昵称: FFF团火法师
金币: 128
nil

对比以前 [{name :name,coins :coins} player-info] ,新形式使用 :keys 放在了 map 解构头部,然后跟上一个 vector ,vector 里面的名字要和 key 的名字一致,但顺序无要求。

还有许多解构的特殊形式,如解构剩余内容,解构的默认值,在今后的学习中在进行说明。


你可能感兴趣的:(Clojure 学习笔记 :7 map --- 可能是最有用的数据结构)