今天一个下午都在试图定位一个问题:在循环体中生成列表,并且每轮循环都向列表追加数据,最后把列表输出.
结果一直输出为 nil,几经周折,才发现原来问题出在循环语句的使用方式上,自己对 Common Lisp 的循环语句没有透彻理解掌握,只是凭着印象使用.下面详细道来.
一般的 Common Lisp 语句,都是把形式体最后一条语句作为返回值,但是形如 dolist 和 dotimes 的循环语句,却不返回,换句话说它们的返回值为 nil, 其实仔细想想就明白为什么要这么规定了, 先看看 dolist 和 dotimes 的语法结构,如下:
(dolist (循环变量 列表-形式)
循环体)
(dotimes (循环变量 数字-形式)
循环体)
我们发现 dolist 和 dotimes 都是把循环体放在最后面,那么如果要返回值,就涉及一个问题:
返回语句放在哪里? 究竟放在循环体内好呢?还是放在循环体外好?
如果放在循环体内,就会出现每循环一次就返回一个值,如果循环次数很多,就会返回很多值,显然不太合理,如果放在循环体,那它就跟循环语句没有任何联系了.
所以如果想使用 dolist 和 dotimes 来返回某些需要累积的值,就先把这些值在循环体内设置为自我追加的形式,然后保存到一个变量中,最后再用这个变量传递这些值,具体语句可以写成这样:
(let* ((返回列表 nil))
(dotimes (循环变量 10)
(setq 返回列表 (append 返回列表 (list 循环变量))))
返回列表)
执行后返回结果如下:
CL-USER> (let* ((返回列表 nil))
(dotimes (循环变量 10)
(setq 返回列表 (append 返回列表 (list 循环变量))))
返回列表)
(0 1 2 3 4 5 6 7 8 9)
CL-USER>
do 这个函数的返回值可以自己设置,不过比较特别的是:它的返回值也不是最后一句,而是在结束条件判断语句后面,具体先看一下 do 的语法形式:
(do ((循环变量1 初值1 步长形式体) (循环变量2 初值2 步长形式体2) …)
(结束条件形式体 返回结果形式体)
循环体)
最初我就是没仔细看,所以没注意到它这种比较独特的返回位置,所以花了不少时间来定位,现在清楚了,继续用我们刚才的那个例子,改写为 do 版本如下:
(let* ((返回列表 nil))
(do ((循环变量 0 (incf 循环变量)))
((>= 循环变量 10) 返回列表)
(setq 返回列表 (append 返回列表 (list 循环变量)))))
执行结果如下:
CL-USER> (let* ((返回列表 nil))
(do ((循环变量 0 (incf 循环变量)))
((>= 循环变量 10) 返回列表)
(setq 返回列表 (append 返回列表 (list 循环变量)))))
(0 1 2 3 4 5 6 7 8 9)
CL-USER>
append 可以把一个列表追加到另一个列表之后,返回值为新列表,语法如下:
(append list1 list2)
但是 append 不会修改旧列表,如下代码:
CL-USER> (defparameter l1 `(1 2 3 4))
L1
CL-USER> (defparameter l2 `(5 6 7 8))
L2
CL-USER> (append l1 l2)
(1 2 3 4 5 6 7 8)
CL-USER> l2
(5 6 7 8)
CL-USER> l1
(1 2 3 4)
CL-USER>
所以如果想把后一个列表追加到前一个列表,还需要手工"赋值"一次,如下:
(setq l1 (append l1 l2))
执行效果如下:
CL-USER> (setq l1 (append l1 l2))
(1 2 3 4 5 6 7 8)
CL-USER> l1
(1 2 3 4 5 6 7 8)
CL-USER> l2
(5 6 7 8)
CL-USER>
结论: 一定要清楚你所用的语句究竟有没有返回值(也就是返回值是 nil 还是 非 nil), 如果有返回值,那么这条返回语句应该放在什么位置.
可见初学者遇到的很多编程问题都出在对编程语言的误解上,也就是对工具的错误使用上,所以古人说 “工欲善其事,必先利其器”, 我们则提倡"多实践,多试错”.