本帖最后由 highflybird 于 2022-9-13 08:44 编辑
版权申明:本贴首发明经通道,转载需要告知原作者、注明出处。
取这个名字,显然是对《C陷阱与缺陷》的致敬。为了使得更多LISP编程者和爱好者了解其的特点以及学习的需要,特撰写此文。
LISP是一种通用高级计算机程序语言,长期以来垄断人工智能领域的应用。LISP作为应用人工智能而设计的语言,是第一个声明式系内函数式程序设计语言,有别于命令式系内过程式的C、Fortran和面向对象的Java、C#等结构化程序设计语言。在我看来,它是一门伟大的编程语言。
本文谈到的LISP,是LISP的方言-AutoLISP,它是由Autodesk公司开发的一种LISP程序语言。通过autolisp编程,可以节省工程师很多时间。AutoLISP语言作为嵌入在AutoCAD内部的具有智能特点的编程语言,是开发应用AutoCAD不可缺少的工具。它代码简洁高效,没有CAD版本限制,开发周期短等等。
任何一门编程语言都有它的优势,也有它的缺点。Lisp也不例外,我在此分享在使用LISP中遇到的问题以及解决方法和使用经验。
一、引用中的陷阱
如果我们对一个符号a进行赋值,然后我们想要拷贝a的值,一般这么做,(setq b a),那么b的值也和a的值一样。
一直以来我以为以后a的值发生了变化,不会影响到b的值。譬如,
(seqt a 1) (setq b a) 进行拷贝。
以后如果a的值发生了变化,如(setq a 3), b的值不会因为a的改变而发生变化。
看起来好像这样没问题。然而我忽视一个lisp与其它编程语言的不同之处。
lisp中的符号是一种引用!理解这一点非常重要。当我们用setq拷贝一个符号的时候,实际上是对这个符号的添加了一个别名。
也就是换汤不换药。
譬如当我第一次踩到这个坑的时候,是一个安全数组的。代码大致是这样的:
(setq a (vlax-make-safearray vlax-vbdouble '(0 . 3))) 建立数组,
(setq b a) 进行拷贝,
然后,对安全数组进行元素赋值,
(vlax-safearray-put-element a 0 1)
(vlax-safearray-put-element a 1 2)
(vlax-safearray-put-element a 2 3)
(vlax-safearray-put-element a 3 4)
等等,进行一系列操作,然后我又对b也进行了一系列操作,完全没注意到:
我改变a的同时,也改变了b,同样改变了b的同时也改变了a,结果导致我的程序总是没有按照我预期的运行。
后来终于找到了原因,明白了像对数组,vla-object,选择集之类的,在拷贝符号的时候,要注意到它们的值是相关联的!!!
直到你对一个符号重新分配内存或者重新指向一个新的vla-object或者选择等等,它们才失去关联。
这个就有点像量子纠缠了,当一个符号改变值时候,另一个也会相应改变。
你以为仅仅是数组,vla-object,选择集这些数据类型才是这样吗。不,几乎对于LISP的所有数据类型都是这样,除了整数类型外。
但你会说为何其它数据类型没有出现这样的情况呢?譬如: (setq a "test") ,(setq b a) 然后(setq a "hello"),发现b还是 "test",这怎么解释?
注意,在这个地方(setq a "hello")实际上是重新为符号a重新分配空间并赋值了。
如果依据LISP内部的方式去改变a的值,而不是用setq ,那你就会发现b会因为a的值因为内部方法的改变而发生改变。
如果要验证这一点,不妨请你先阅读一下:Make Lisp Great Again--利用隐藏函数恢复isp的活力!
你会发现,当用内部函数修改某个符号的值时候,其复制的符号值也会发生改变。
举一个例子,字符串:(setq a "abcde") 对a赋值, (setq b a) b拷贝符号a
然后采用内部函数 (string-fill a (ascii "f")) 这样修改a的字符串,得到_$ a =>"fffff",然后你查询b 会发现_$ b =>"fffff"。
你会觉得很惊讶是不是?我对b没有操作啊,b的字符串怎么就改变了呢?
这是因为a和b在内存中指的是同一个东西!所以你修改了一个,另外一个也会发生变化。
我们再来看另外一个例子,是关于LISP中被隐藏的数据类型vector:
_$ (setq a (vector 1 2 3 4 5))
#5(1 2 3 4 5)
_$ (setq b a)
#5(1 2 3 4 5)
然后,_$ (vector-fill a 12)
#5(12 12 12 12 12)
你再查询一下b:
_$ b
#5(12 12 12 12 12)
结果b的值也变成了一样了。
另外,对表也是如此:
假如(setq a '(1 2 3))
(setq b a)
当采用内部函数(list-elt<- b 5 2)改变了b的元素的值,然后你会发现:
_$ b
(1 2 5)
_$ a
(1 2 5)
也就是a,b会同时修改。
要说明的是,对于赋值为整数类型,lisp视作为立即数,即使你用内部方式改变了某个值,复制的符号的值并不会发生改变。
也就是说(setq a 3) ,(setq b a) 当你即使用内部方法修改a的数值,b的数值依然不变。
所以我在这里的一个建议:
1、如果你要采用内部函数,复制一个符号的话,建议采用内部的一些复制函数,譬如:
copy-vector, copy-string ,copy-list等方式代替直接用 setq 复制。
2、对于LISP的数组类型,采用深度克隆方式。
3、对于vla-object之类的对象,注意拷贝符号时或者当参数时带来的变化对原对象的影响。
二、传址的陷阱
大部分的编程语言函数传参都有两种方式:传值和传址。
传值:实参把值传给形参,但没有传地址,即对实参的修改无效,生成的临时变量。函数会对形参和中间变量重新分配空间。
传址:实参把自己的内存地址传给了形参,这样对实参的修改有效,同样也生成临时变量,该临时变量为外部实参地址。
CAD的参数,大都是按传值进行的,意味着你的对实参的修改只是在函数内进行,离开了函数,实参恢复原值。
举例:
(defun swap (x y / temp)
(setq temp x x y y temp)
)
假如a=3,b=4,你(swap a b)之后,会发现 a,b的值没有改变。
怎样才能改变了,也有办法。当我们了解到符号其实是一种引用后,我们把代码修改一下:
- (defun swap (x y / temp)
- (setq temp (vl-symbol-value x))
- (set x (vl-symbol-value y))
- (set y temp)
- (princ)
- )
当你再用(swap 'a 'b)后,你就会发现,a和b的值真的交换了。
注意!这个地方a和b上面有个',没这个,程序会出错。
这里我留下一个问题给大家回答:
你发现这个交换函数,对除了符号x,y之外的都会有效,但对符号x,y无效。
譬如你(setq x 3 y 4)
然后你用(swap 'x 'y)
结果你会发现,x,y根本就没交换值。是不是又踩到了一个坑?
好吧,读者不妨跟踪一下,看看发生了什么?
再次说明的是: 函数中的参数,如果在函数里面对实参进行修改,当函数返回后,实参的值返回原来的值,举一个简单的例子:
(setq x 1) (defun test (x) (setq x 2))
然后(test x) =>2 , 检查x的值,_$ x =>1
一个建议:lisp的函数,除了参数外的,定义在函数内部的变量,应当设置为局部变量,即在函数定义部分用“/” 把局部变量指定出来。
如果不指出来,可能会引起种种意想不到的错误。因为它变成了一个全局变量,其值可能在函数外被改变,可能会带来意想不到的结果。
另外 ,要留意foreach ,mapcar之类的函数,整个应当视作为一个函数定义。
举例:
(defun test (lst / x)
(setq x 1)
(foreach x lst (setq x 2))
x
)
(test '(1 2 3)) =>1 可以看出,x的值并没有因为在foreach中重新赋值而改变。
foreach中的x已经被视作为局部变量,已经与外部的x没关系了,因而在foreach函数内部对x的修改,并不影响foreach外部x变量的值。
三、类型的陷阱
强类型语言是一种强制类型定义的语言,即一旦某一个变量被定义类型,如果不经强制转换,那么它永远就是该数据类型。而弱类型语言是一种弱类型定义的语言,某一个变量被定义类型,该变量可以根据环境变化自动进行转换,不需要经过现行强制转换。LISP是一门弱类型编程语言。而且是最弱的那种,另外LISP也是一种解释型语言,它在运行时才会检查数据类型,不像编译型语言,类型不对,编译会通不过。因此当你传送一个变量给函数时候,需要注意传进去一个正确类型的参数。
类型错误是初学者经常犯的错,譬如,(setq a 12312),然后你(read a) 时候就会报错 ; 错误: 参数类型错误: stringp 12312。
因为对read函数来说,需要提供一个字符串作为参数。再比如:
_$ (itoa "312")
; 错误: 参数类型错误: fixnump: "312"
说明itoa需要传入一个整数型参数。
而且还有可能遇到这样的情况,当你传入了一个错误类型的参数是,此时程序并未出错,但运行的结果却错了。
对于这一类型的陷阱,一句话,留心点就好了。
四、read陷阱
(read [string])
read 函数分析字符串,并将字符串中的第一个“词”转换为对应的数据类型并返回。
参数 string 字符串。string 参数不能在表或字符串外包含空格。 返回值 read 函数将其参数转换成相应的数据类型后返回。如果未指定参数,read 返回 nil。 如果字符串中包含由空格、换行符、制表符或括号等 LISP 分隔符分开的多个词,则只返回其中的第一个词。 因为read函数的返回值的多样性,另外一些用户对此函数的规则不了解,造成了诸多错误,下面链接列出了一些典型例子,解决方法也在链接里面。 [讨论]223bug?
关于 read 的一个致命的问题!!
上面的两个链接,是关于read的函数错误,这个错误与程序无关,应该是lisp的一个bug。可能在某些版本,或者某些操作系统会出现。
为何read函数读不出某些字符串
请教read函数使用的一个问题
这两个问题是read陷阱和点号陷阱的合集。
lisp中的read函数出差,求助
[求助]read问题,返回值不理解
请问用read出错,是什么问题,请大家看看!
这两个问题是read陷阱和中文陷阱的合集。
求解Read函数返回数字格式的问题
read的精度问题。
请教read的用法
read遇到空格会忽略掉后面的字符串。
[求助]read函数的问题
read遇到单撇号忽略掉后面的字符串。
[求助]read返回值的问题
如果字符串中包含由空格、制表符、换行符或括号等LISP分隔符分开的多个词,则只返回其中的第一个词。
read函数返回的是大写的,怎么办?
read函数返回值为数值或者符号,符号不区分大小写,一律为大写样式。
另外,read陷阱和和下面的中文陷阱有些它们之间有关联。
五、中文的陷阱
因为autolisp和autocad是漂亮国的,人家用的是英语,自然而然,在使用和编程时候,一些地方就会莫名其妙出现中文的问题。
其实大家可以先看看下面链接:
lisp的中文陷阱
以及下面的一些链接:
[求助]read-line怎么读取中文字符,乱码
载入lSP执行后中文提示显示是乱七八糟的汉字???
与lisp文件编码有关。
为什么中文显示不正常:閫夋嫨鏂囧瓧<閫
变量未列入函数括号内。
中文复制到 Visual LISP IDE会出现乱码。
发现个问题,dictsearch无法搜索中文条目?
参看链接解决办法。
利用vl-sort 对中文进行排序,未能出现设想结果,请各位大侠帮忙,谢谢!
中文字符排序问题。
另外还要说明的一点是:中文也是可以用来作为符号的,譬如我可以用(setq 中文 "English") ,或者定义中文函数 :
_$ (defun 中文函数 (字符串) (alert 字符串))
中文函数
_$ (中文函数 "你好,世界!") 这样也能正确运行。
但是中文符号和中文函数有可能出错。读者不放参见下面链接:
如何正确使用中文变量或者中文函数
此处讲得很好,很透彻。
六、点号的陷阱
你也许会说,点号会有什么陷阱。有的,首先符号的语法规则是不允许出现括号,双引号,单引号和点号。但其实,用点号也是可以的。
例如(setq a.b 2)这样也是可以的,但是这样做的结果就是不管点号后面接的是什么,统统都会忽略掉,这样就等同于(setq a 2)
因此,用read函数的时候需要小心点号。 譬如:
(read "a.b") => A , 只输出符号A,后面的被忽略。
(read "1. 2") =>1.0 ,这个地方多了个空格,得到了不是自己想要的结果。
$ '(1. 2)
(1.0 2) =>返回一个表
_$ '(1 .2)
; 错误: 输入中的点位置不正确
_$ '(1 . 2)
(1 . 2) =>返回点表
因而在书写代码和传入字符串的时候,要留意点号带来的影响。
七、转义符的陷阱
AutoLISP使用反斜杠\作为转义字符,所以在字符串中必须使用两个反斜杠才能表示一个反斜杠。
像下面的错误:
为何在Lisp代码中无法将"\"反斜杠,赋值给一个变量.
转义符带来的错误大多出现在路径中,譬如:
read函数结果不是想要的
在read函数或者对话框的一些字符串里面,要小心转义符。
八、对话框的陷阱
首先AutoLISP的对话框是模态对话框。意味着在程序运行的过程中,若出现了模态对话框,那么主窗口将无法发送消息,直到模态对话框退出才可以发送。退出对话框,模态对话框会被销毁。因此,在对话框运行的时候,你对cad的所有操作都不能立即反馈。如果对话框设置不当,就可能造成死锁或者假死的情况。
另外在有子对话框的时候,需要注意出错处理,子对话框出错可能会导致主对话框无法退出。
在对话框结束后,不要忘记使用done_dialog、unload_dialog等函数。另外要数值确定和取消按钮,确定按钮动作含有从对话框获取最新数据功能的表达式或者函数,单击确定,对话框消失,将数据传递给应用程序,若单击取消按钮,对话框消失,不向应用程序传递数据。
另外需要注意的是,图像控件的坐标是以像素为单位的坐标,不同于CAD里面的坐标,坐标原点以图像控件的左上角为(0,0).
在对话框活动期间,你不应当使用下面的一些函数,如命令函数,交互操作函数。
1.command 类函数。
2.交互和屏幕操作函数。
getXXX,grXXX,osnap,prompt,menucmd等等。
3.图元处理函数。
entXXX,nentsel等。
如果要使用上述函数,应先将对话框隐藏完成操作后再重新显示。
注:以上部分语句摘自《vlsual LISP程序设计(第二版)》 。
最后还要说明的一点是,如果对话框dcl文件没有写全路径,或者不在支持目录下,就会造成无法加载对话框文件错误,因而对话框不能运行。
九、捕捉的陷阱
初学者在使用LISP的时候经常犯的错误,特别是使用command 函数绘制图形时候,因为忘记关闭捕捉,导致画出来的图跑偏跑远,而得不到正确的结果。
如下面的帖子:
新手求助:对象捕捉的开关
cad中把捕捉全部关了,如果不放大视图,cad还是会画歪,这是怎么回事
如何取消捕捉功能对程序的影响?
最常见的解决方法是,一开始保存osmode,命令使用完毕后,恢复原来的osmode,(别忘了在出错处理时也要恢复)
(setq oldos (getvar "osmode"))
(setvar "osmode" 0)
....
做完后:
(setvar "osmode" oldos)
或者在命令里面加入"none" ,或者把command语句用相关的函数替换,譬如entmake,vla-move,等。
另外有捕捉有关的系统变量可能还有angbase、orthomode等。
下面的这篇帖子讲得很透:
从对象捕捉的开和关想到的
十、视图的陷阱
在CAD里面进行选择操作时候,有时候程序中明明指定的范围已经包含所想要的物体,为何结果却没有?这是因为视图在作怪。
譬如你用(ssget "w" p1 p2)选取对象时候,你要选择的物体并没出现在视图中或者只有局部出现,那么就可能导致选择失败。
因此在进行这类选择的时候,应该使用zoom等相关命令或者函数,使得要选择的物体出现在需要的视口内。
另外,也注意initget函数的用法。在函数说明里有下面的一段话:
8 (位3) 允许用户在当前图形界限之外输入一个点,即使 AutoCAD 的系统变量 LIMCHECK 当前被设置为开 (ON),本条件也照样对随后调用的用户输入函数有效。
因此设置正确的initget也可帮助我们在使用getXXX类函数时候,即使超出视口外,也能获得我们需要的结果。
十一、排序的陷阱
大看一下这个例子:
_$ (vl-sort '(2 6 2 4 5 7 5) '<)
(2 4 5 6 7)
_$ (vl-sort '(2.0 6.0 2.0 4.0 5.0 7.0 5.0) '<)
(2.0 2.0 4.0 5.0 5.0 6.0 7.0)
说明了什么,lisp的排序函数在对整数排序的时候,会消除重复的元素。
因此如果不想消除重复元素,可以先对整数转为浮点。
当然,你也可以自己写排序函数。
十二、坐标系的陷阱
初学者常犯的错误之一是,在ucs下使用getxxx类函数或者 entmake等命令,会得到不是预期的效果。因为UCS下的坐标和wcs坐标是不一样的
entmake是用wcs,而 getpoint,getdist,getangle,getcorner,getorient,grread,grdraw,grvecs等函数是和UCS相关的。因此在需要时,须进行坐标系转换。
十三、and和or的陷阱
先来看看这两个函数的说明:
(and [expr ...])参数 expr 任意表达式。 返回值
如果任何一个表达式的求值结果为 nil,本函数就停止进一步的求值并返回 nil,否则返回 T。如果不带参数调用 and,它返回 T。
(or [expr...]) or 函数对表达式表中的表达式从左到右进行求值,并查找非 nil 表达式。 参数 expr 要计算的表达式。 返回值 如果存在非 nil 表达式则返回 T,如果所有表达式均为 nil,或未提供参数,则返回 nil。 请注意 or 接受原子作为参数。如果提供原子作为参数,则返回 T。
因此对and来说,只要有一个为假了,后面的不会继续求值,对or来说,只要有一个为真了,后面的也不会继续求值。
从这个意义来说,也不能算是一种陷阱,应用得好,可以达到很好的效果。
譬如你不想对某个变量重复赋值或者某个函数不想重复运行,以防止改变,可以这样:
(or var (setq var 1))
在一些方面上,or和and比if更简洁,但是别忘了理清它们的逻辑关系。
十四、括号的陷阱
首先括号是要配对的,不能缺失半边。其次括号代表的函数或者表,progn相当于C语言的花括号 。
if函数里面如果有多条语句,需要用progn表达。 譬如 (if 条件语句 (progn 语句1 语句2 语句3...) )
(if 条件语句 语句1 语句2 语句3... )是错误的表达。
十五、路径的陷阱
因为反斜杠\ 是代表LISP的控制字符的,所以路径名不能写"c:\temp"之类,需要写成"c:\\temp" ,或者用斜杠替换,如:"c:/temp"
否则会造成无法找到路径。同样,如果字符串里面还有字符,需要 用\"表示。 例如 "This is a string:\"char\"".
十六、反应器的陷阱
下面我列举了在用反应器中一些常出现的问题。
属性块的反应器问题,求指点
[求助]反应器问题:当一个块属性更改时,另一个属性块同时更改!
求助,反应器问题,更新块属性时系统崩溃
说实在话,反应器用多了也不好,容易造成CAD崩溃,所以最好少用。
其它的还有很多我没能谈及的,此贴不再一一提及,容以后慢慢补充。
LISP有什么缺陷呢?下面我列举几条。
1、运行速度慢。
2、对话框只能是模态的,且可见可得性太差。
3、数据类型如果不采用内部函数的话,就没有指针,向量等,而采用表的话在效率上会大打折扣。
4、只能使用可以LISP定义的几种类型的回调函数,譬如反应器回调、对话框回调等,其它的则不能自定义回调。
5、在纯vlisp下不能调用系统API,需要借助其它编程语言或者第三方插件。
本帖参考了论坛中的一些内容和一些书籍,在此向他们致谢!
欢迎大家讨论!
|