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
函数的用法:
- 第一个参数是要访问的 map
- 第二个参数是要访问的 map 中的 key
- 第三个参数可选,作用是设置一个默认值,如果 map 中访问的 key 不存在,则返回这个参数的值
- 正常情况下返回 key 所对应的 value
- 如果第三个参数未填写,那么一旦 map 中没有所访问的 key,则返回
nil
get 函数不仅能访问 map,有关它的其他用法请查询相关 api。
如果你使用了 冒号+关键字名
这种形式来表示一个 key,那么我们就可以更加简便的访问 map 中的 value 了。
=> (:name player-info)
"FFF团火法师"
=> (:sex player-info)
nil
=> (:sex player-info "not found")
"not found"
我们发现, 冒号+关键字名
这种形式的表示,竟然能放在括号的第一个位置,这说明它也是一个函数。所以这种奇特的函数的使用方法是这样的:
- 第一个参数是要访问的 map
- 第二个参数可选(和
get
函数的第三个参数作用一致) - 在 map 中寻找和 key 函数本身相同的 key(其它特性与
get
函数相似)
除了读取 map 中的内容,我们还可以使用 assoc
和 dissoc
来增加和删除 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 的名字一致,但顺序无要求。
还有许多解构的特殊形式,如解构剩余内容,解构的默认值,在今后的学习中在进行说明。