Clojure是一门动态类型的语言,运行时才会做类型检查。它也不会像java这种面向对象编程语言,在调用一个对象的函数时,首先这个函数必须是属于该对象的函数,否则检查报错。所以对于clojure中的数据类型为Vector或者List的变量,总是会让初学者在选择时比较发愁,不知道什么情况下使用vector好?什么情况下使用list好?哪些函数是接收list作为参数的?而哪些函数又是接收vector作为参数的?它们的区别又是什么?本博客关于近期对clojure的list和vector的学习做一个总结:
1.List与Vector的特点:
Clojure中既有列表(list),也有向量(vector),两者都是序列数据结构,它们的特点对比如下:
存储类型: list是链式存储结构,而vector是顺序存储类型数据插入: list支持高效的前端插入,而vector在尾端插入会更高效数据查找: 在数据查找方面,因为存储类型的不同,vector的时间复杂度会明显低于list,因为vector采取的理想b树的存储结构(http://blog.csdn.net/zdplife/article/details/52138512),所以其查找效率接近常数时间(O(log32n)),而list的查找操作需要O(N)的时间复杂度,所以在存在大量查找操作时,尽量选择vector。2.创建相关函数:
vector的相关创建函数: ;;方法1:直接使用字面值常量表示法 [1 2 3] ;;=> [1 2 3] ;;方法2:使用vector,接收n个元素 (vector 1 2 3) ;;=> [1 2 3] ;;方法3:在已有结构的基础上创建向量 (vec '(1 2 3)) ;;=> [1 2 3] ;;map中的元素转换为序列时,是以键值对的形式作为元素返回的 (vec {:a 1 :b 2}) ;;=> [[:a 1] [:b 2]] ;;方法4:使用into函数在已有结构基础上创建向量 (into [] {:a 1 :b 2}) ;;=> [[:a 1] [:b 2]] (into [] '(1 2 3)) ;;=> [1 2 3]以上是vector四个创建函数,经常用到的是最后两个,我在博客(http://blog.csdn.net/zdplife/article/details/52138512)中讲到,因为into使用了transient(瞬态存储结构),所以其效率会vec函数快30%,vec的可读性比较强,而into函数的效率更大,所以在使用时可以折中选择。 还有就是map被转换为序列时,是按照键值对返回的,这个在写代码时需要注意。
list的相关创建函数: list的创建函数与vector的类似,如下: ;;由于()在clojue中会被当作函数操作符,所以这里在用字面值常量创建列表时,需要加单引号,阻止求求值 '(1 2 3) ;;=> (1 2 3) ;;该list与vector的用法类似 (list 1 2 3) ;;=> (1 2 3) ;;该into用法与上述介绍的一致 (into '() [1 2 3]) ;;=> (3 2 1)into在效率上依然是首要的选择,而使用单引号创建list时,有一点缺点就是它不会解释里面的变量或者函数调用,所以这种方法要尽量少用:
(def a 4) ;;=> #'insight.main/a ;;单引号会阻止对a求值,返回字面值 '(1 2 a) ;;=> (1 2 a) ;;而list函数则会对a进行解析,所以尽量选择list函数 (list 1 2 a) ;;=> (1 2 4)其中在使用into函数时,vector和list中元素的序列是不一样的,这是因为into函数会以此调用conj函数将后者的元素一个一个加入到前面的序列中,但由于这两个存储结构不同,conj加入的位置正好相反,conj函数会在后面介绍到。
3.插入元素相关函数: 在vector和list中插入元素经常用到的两个函数是conj和cons,这两个函数的参数位置刚刚好相反,conj是被插入的元素放在后面,而cons是将被插入的元素放在前面。需要注意的是,conj返回的是一个新的具体类对象,原有序列是什么类型,它便返回什么类型,而cons则会返回一个序列(clojure.lang.Cons),也就是会改变原有对象的类型:
;;对于conj函数返回的总是原有序列的类型 (class (conj [1 2] 3)) ;;=> clojure.lang.PersistentVector (class (conj '(1 2) 3)) ;;=> clojure.lang.PersistentList ;;而cons函数则永远返回的是一个惰性序列clojure.lang.Cons (class (cons 3 '(1 2))) ;;=> clojure.lang.Cons (class (cons 3 [1 2])) ;;=> clojure.lang.Conscons函数不管是向什么类型的序列中插入元素都会放在序列的头上,而conj函数在插入元素时,会考虑到原有序列类型的插入效率,因为list支持头部高速插入,所以会放在list头部,而如果是vector则会放到其尾部:
;;conj函数对于list和vector的插入位置刚好相反,使用时一定要注意 (conj [1 2] 3) ;;=> [1 2 3] (conj '(1 2) 3) ;;=> (3 1 2) ;;cons不管什么类型插入位置总是相同 (cons 3 [1 2]) => (3 1 2) (cons 3 '(1 2)) => (3 1 2)这两个函数还有一个区别就是,conj支持一次调用连续插入多个元素,而cons则只支持插入一个元素:
(conj [1 2 3] 4 5) ;;=> [1 2 3 4 5] (cons 4 5 [1 2 3]) ;;ArityException Wrong number of args (3) passed to: core/cons--4331最后我们讨论一下这两个函数在对于map操作时的区别,虽然不建议这样使用:
;;conj对于map依然保持原有类型,可以既可以接收map,也可以接收键值对,或者两种类型同时接受 ;;插入map (conj {:a 1 :b 2} {:c 3 :d 4}) ;;=> {:a 1, :b 2, :c 3, :d 4} ;;插入键值对 (conj {:a 1 :b 2} [:c 3]) ;;=> {:a 1, :b 2, :c 3} ;;同时插入map和键值对 (conj {:a 1 :b 2} {:c 3 :d 4} [:e 5]) ;;=> {:a 1, :b 2, :c 3, :d 4, :e 5} ;;cons函数返回的永远是一个序列,而且插入map和键值对的情况不同,所以建议尽量少用 (cons {:c 3} {:a 1 :b 2}) ;;=> ({:c 3} [:a 1] [:b 2]) (cons [:c 3] {:a 1 :b 2}) ;;=> ([:c 3] [:a 1] [:b 2])4.删除元素相关函数: 删除序列中的元素常用的函数有rest,pop,subvec,其中rest函数类似cons是序列操作函数,返回的是一个序列类型,而pop函数会保持原有数据类型不变,subvec函数对vector操作,返回原有类型,它创建的向量与原来的内部结构一样,非常有效,执行时间为常数,建议对于vector使用subvec函数:
pop函数: ;;pop函数 ;;和conj函数类似,pop函数的删除元素的位置对于vector和list恰好相反 (pop [1 2 3]) ;;=> [1 2] (pop '(1 2 3)) ;;=> (2 3) ;;对于空的序列,pop函数会抛出异常,所以使用一定要谨慎!!!!! (pop []) ;;IllegalStateException Can't pop empty vector clojure.lang.PersistentVector.pop rest函数: ;;rest函数与cons函数类似,都是删除序列头部元素,对于空序列,依然返回为空 (rest [1 2 3]) ;;=> (2 3) (rest '(1 2 3)) ;;=> (2 3) (rest '()) ;;=> () ;;这里想说一点,rest函数还可以用于map,而pop则不能,注意注意!!!! (rest {:a 1 :b 2 :c 3}) ;;=> ([:b 2] [:c 3]) subvec函数 ;;subvec函数只能使用在vector中,它会获取从第m个到第n个的元素,但是不包括第n元素 (subvec [1 2 3] 0 2) ;;=> [1 2] ;;subvec不能越界操作,或者m大于n操作,会抛出异常 (subvec [1 2 3] 2 5) ;;IndexOutOfBoundsException5.获取元素相关函数: 在序列中获取元素常用的函数nth,take,take-nth,take-last,get函数。
nth函数: nth函数用于获取vector或者list中某个序号的元素: ;;这里的vector和list用法一样 (nth [1 2 3] 2) ;;=> 3 ;;如果序号不在vector的范围内,会抛出异常 (nth [1 2 3] 4) ;;IndexOutOfBoundsException ;;如果序号不在范围内,可以给nth函数指定默认值,下面是指定默认值-1,这样我们可以对返回结果进行判断 (nth [1 2 3] 4 -1) ;;=> -1 get函数: get函数我们在clojure中的关联数据结构(http://blog.csdn.net/zdplife/article/details/52104888)这一文中已经讲过,get是个万能的函数,可以用于各种数据结构,而且不会报错,但是get用于list中是没有意义的,返回结果是nil,所以不要将get函数用于list: ;;get函数的用法在vector中与nth用法类似,只是如果下标超出范围,get函数不会报错,而是会返回nil, (get [1 2 3] 1) ;;=> 2 ;;当然,get函数也可以在找不到元素的情况下给其赋值默认值 (get [1 2 3] 4 -1) ;;=> -1 (get [1 2 3] 4) ;;=> nil ;;get用在list时,总会返回nil,所以不要使用 (get '(1 2 3) 1) ;;=> nil take/take-nth/take-last函数: 这三个函数也是获取序列中的元素,但是它们返回的是一个序列,take函数返回序列的前n个函数,take-nth是从序列的第一个元素起,每间隔k个元素顺序返回新的序列,take-last是返回序列最后几个元素: (take 2 [1 2 3]) ;;=> (1 2) (take-nth 2 [1 2 3 4 5]) ;;=> (1 3 5) (take-last 2 [1 2 3 4 5]) ;;=> (4 5) ;;这三个函数是序列化函数,所以返回的都是seq,即使在空的情况下也不会抛出异常 (take-last 2 []) ;;=> nil (take-nth 2 []) ;;=> () (take 2 []) ;;=> () vector作为函数获取元素: 当然,因为vector是关联数据结构,它可以像map一样当作函数,获取其中某个位置的元素: ([1 2 3] 2) ;;=> 3 对于vector三种获取元素函数的区别:6.更改元素值相关函数: 目前还没有发现可以更新list中某个位置元素的函数,因为vector是关联数据结构,可以使用map中的函数assoc/update来更新vector中的元素(key就是元素的index):
;;update函数第二个参数的范围必须从0到2 (update [1 2 3] 2 str) ;;=> [1 2 "3"] ;;assoc函数的第二个参数范围可以从0到3,如果是3,就相当于在末尾增加一个元素 (assoc [1 2 3] 2 "hello") ;;=> [1 2 "hello"] ;;这时的assoc与conj类似 (assoc [1 2 3] 3 "yes") ;;=> [1 2 3 "yes"]7.判断序列相关函数: 有时候我们需要判断某个序列是什么类型,可以使用vector?/list?/seq?这三个谓词函数进行判断,vector函数用于判断某个变量是不是clojure.lang.PersistentVector类型,而list?函数用于判断某个变量是不是clojure.lang.PersistentList类型,seq?函数用于是不是clojure.lang.PersistentList或者cons,lazyseq等类型,所以如果我们不关心其底层实现的话,使用seq?判断几乎总是比list?要好:
(vector? [1 2 3]) ;;=> true ;;list?函数只用于判断list类型 (list? '(1 2 3)) ;;=> true ;;因为rest函数返回惰性序列,所以使用list?会判断失败 (list? (rest [1 2 3])) ;;=> false ;;但是seq?对于惰性序列和list都作判断,所以seq?是很好的选择。 (seq? (rest [1 2 3])) ;;=> true (seq? '(1 2 3)) ;;=> true最后讲一下contain?函数,这个函数看上去貌似是判断某个元素是否存在序列中,这里大部分人会被误解,其实该函数只是针对关联数据结构中判断key是否存在,所以对于vector来说用处基本为零,只是为了判断vector的某个index是否存在而已。
8.函数效率问题: 最后想要谈到的就是函数效率问题,在transient(http://blog.csdn.net/zdplife/article/details/52138512)那篇文章中介绍了,clojure中有些函数使用该数据特性可以提高效率,所以我们也尽量选择使用一些利用了transient特性的函数,尤其在效率要求比较高的工程中:
;;因为pop函数使用了transient数据结构,所以尽量使用第二种写法: (vec (take 2 [1 2 3])) ;;=> [1 2] (pop [1 2 3]) ;;=> [1 2] ;;因为into函数使用了transient数据结构,所以尽量使用第二种写法: (vec (concat [1 2] [3 4])) ;;=> [1 2 3 4] (into [1 2] [3 4]) ;;=> [1 2 3 4]