上一章讲了向量、哈希表等比Lisp中较常用的数据结构。就如今的Common Lisp而言,的确选择很多,而过去却只有列表(list)这一个选项,so~
当然历史原因只是其一(话说能在历史中被选择的总有其合理之处),就当前通常可见的场景下,List也是一个不错的选择,其实用性早已被时间所证明(有点像数学啊,很多公理都早已被确定~)。所以,了解List,知道选择List的优劣还是很有必要的。
跳过一段"花非花"的论断,让我们直接进入叫做"cons cells"的东东(算是list的原型吧,无需深入,现在都用新的定义方式了):
我们用cons来定义、创建这个结构:
(cons 1 2) ==> (1 . 2)
一些基本的动作:
(car (cons 1 2)) ==> 1
(cdr (cons 1 2)) ==> 2
(defparameter *cons* (cons 1 2))
*cons* ==> (1 . 2)
(setf (car *cons*) 10) ==> 10
*cons* ==> (10 . 2)
(setf (cdr *cons*) 20) ==> 20
*cons* ==> (10 . 20)
合并、嵌套:
(cons 1 nil) ==> (1)
(cons 1 (cons 2 nil)) ==> (1 2)
(cons 1 (cons 2 (cons 3 nil))) ==> (1 2 3)
用直观的图形来表示:
原子状态:
模拟个(1 2 3)列表:
当当当...隆重引出我们的列表专用key:List
(list 1) ==> (1)
(list 1 2) ==> (1 2)
(list 1 2 3) ==> (1 2 3)
对应的一些基本动作:
(defparameter *list* (list 1 2 3 4))
(first *list*) ==> 1
(rest *list*) ==> (2 3 4)
(first (rest *list*)) ==> 2
列表还能轻松hold住各种类型的元素:
(list "foo" (list 1 2) 10) ==> ("foo" (1 2) 10)
看起来就像这个样子:
原则上,如果需要,列表可以无限扩展任意深度。
关于列表,Lisp为其配套了一大堆功能性函数,在这儿就不一一阐述,取其典型:append
(append (list 1 2) (list 3 4)) ==> (1 2 3 4)
直观图:
(这图不错啊~)
细心的人可能已经发现,似乎并非是一个全新的列表啊,被你发现了~
(defparameter *list-1* (list 1 2))
(defparameter *list-2* (list 3 4))
(defparameter *list-3* (append *list-1* *list-2*))
*list-1* ==> (1 2)
*list-2* ==> (3 4)
*list-3* ==> (1 2 3 4)
(setf (first *list-2*) 0) ==> 0
*list-2* ==> (0 4) ; as expected
*list-3* ==> (1 2 0 4) ; maybe not what you wanted
似乎符合发现,但不符合预期啊。这种关联是有适用场景的,从某种程度上节约了数据空间的成本。所以,使用的时候需要小心。
其实大部分对列表的操作函数其实是不破坏原表的,除非你写成这个样子:
(setf *list* (reverse *list*))
生成新列表对于确保粒子的原子性有帮助,不过对于空间就比较糟蹋了,如果对空间有需求的话,可以使用类似上面的语句来覆盖原表,达到空间复用的目的:
(defparameter *x* (list 1 2 3))
(nconc *x* (list 4 5 6)) ==> (1 2 3 4 5 6)
*x* ==> (1 2 3 4 5 6)
下面来看一个关于循环和结构共享的例子:
(defun upto (max)
(let ((result nil))
(dotimes (i max)
(push i result))
(nreverse result)))
(upto 10) ==> (0 1 2 3 4 5 6 7 8 9)
上面的代码创建了独立无外部关联的列表。对于数据空间共享,还有个小窍门:
*list-2* ==> (0 4)
*list-3* ==> (1 2 0 4)
(setf *list-3* (delete 4 *list-3*)) ==> (1 2 0)
*list-2* ==> (0)
如果上面的delete改用remove的话就不会出现串联同步修改的问题了。
有无损的函数,自然就会有会损坏原表的,在这种情况下,如果有必要,copy-list是一个不错的选择:
CL-USER> (defparameter *list* (list 4 3 2 1))
*LIST*
CL-USER> (sort *list* #'<)
(1 2 3 4) ; looks good
CL-USER> *list*
(4) ; whoops!
因为一些历史原因,我们还可以看到一些比较奇怪的表达式:
(caar list) === (car (car list))
(cadr list) === (car (cdr list))
(cadadr list) === (car (cdr (car (cdr list))))
(caar (list 1 2 3)) ==> error
(caar (list (list 1 2) 3)) ==> 1
(cadr (list (list 1 2) (list 3 4))) ==> (3 4)
(caadr (list (list 1 2) (list 3 4))) ==> 3
目前只要知道这些表达式的意思就可以了,最好别多用~
以下是一些基本的不完全的功能列表:
Function |
Description |
LAST |
Returns the last cons cell in a list. With an integer, argument returns the last n cons cells |
BUTLAST |
Returns a copy of the list, excluding the last cons cell. With an integer argument, excludes the last n cells. |
NBUTLAST |
The recycling version of BUTLAST; may modify and return the argument list but has no reliable side effects. |
LDIFF |
Returns a copy of a list up to a given cons cell. |
TAILP |
Returns true if a given object is a cons cell that's part of the structure of a list. |
LIST* |
Builds a list to hold all but the last of its arguments and then makes the last argument the CDR of the last cell in the list. In other words, a cross between LIST and APPEND. |
MAKE-LIST |
Builds an n item list. The initial elements of the list are NIL or the value specified with the :initial-element keyword argument. |
REVAPPEND |
Combination of REVERSE and APPEND; reverses first argument as with REVERSE and then appends the second argument. |
NRECONC |
Recycling version of REVAPPEND; reverses first argument as if by NREVERSE and then appends the second argument. No reliable side effects. |
CONSP |
Predicate to test whether an object is a cons cell. |
ATOM |
Predicate to test whether an object is not a cons cell. |
LISTP |
Predicate to test whether an object is either a cons cell or NIL. |
NULL |
Predicate to test whether an object is NIL. Functionally equivalent to NOT but stylistically preferable when testing for an empty list as opposed to boolean false. |
作为一种数据结构,可遍历也是list的基本特性,所以map类函数也都有对应的解决方案:
MAPCAR
(mapcar #'(lambda (x) (* 2 x)) (list 1 2 3)) ==> (2 4 6)
(mapcar #'+ (list 1 2 3) (list 10 20 30)) ==> (11 22 33)
MAPLIST 对应cdr
MAPCAR/MAPLIST 生成新的list
MAPCAN/MAPCON 生成组合的list
MAPC/MAPL 结果覆盖第一个list
跳过了很多描述性和观点性的东西(有些是晦涩,有些是暂不认同),至于具体的函数,用到时看看help,文档应该很容易覆盖吧。
其实,就总体而言,只要coder有好的代码风格,做到数据传递不被破坏(或者是预期的破坏),一切就都是浮云了。
(未完待续)