highflybird 发表于 2022-9-11 12:00:18

LISP陷阱与缺陷

本帖最后由 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 x1) (defun test (x)   (setq x 2))
然后(test x) =>2, 检查x的值,_$ x =>1
一个建议:lisp的函数,除了参数外的,定义在函数内部的变量,应当设置为局部变量,即在函数定义部分用“/” 把局部变量指定出来。
如果不指出来,可能会引起种种意想不到的错误。因为它变成了一个全局变量,其值可能在函数外被改变,可能会带来意想不到的结果。
另外 ,要留意foreach ,mapcar之类的函数,整个应当视作为一个函数定义。
举例:
(defun test (lst / x)
(setqx 1)
(foreach xlst (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 )
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任意表达式。返回值
如果任何一个表达式的求值结果为 nil,本函数就停止进一步的求值并返回 nil,否则返回 T。如果不带参数调用 and,它返回 T。
(or ) or 函数对表达式表中的表达式从左到右进行求值,并查找非 nil 表达式。参数expr要计算的表达式。返回值如果存在非 nil 表达式则返回 T,如果所有表达式均为 nil,或未提供参数,则返回 nil。请注意 or 接受原子作为参数。如果提供原子作为参数,则返回 T。
因此对and来说,只要有一个为假了,后面的不会继续求值,对or来说,只要有一个为真了,后面的也不会继续求值。
从这个意义来说,也不能算是一种陷阱,应用得好,可以达到很好的效果。
譬如你不想对某个变量重复赋值或者某个函数不想重复运行,以防止改变,可以这样:
(orvar(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,需要借助其它编程语言或者第三方插件。

本帖参考了论坛中的一些内容和一些书籍,在此向他们致谢!
欢迎大家讨论!

llsheng_73 发表于 2022-10-17 18:23:02

yjtdkj 发表于 2022-10-17 09:46
“二、传址的陷阱”这部分没太看懂,实参、形参是什么意思,楼主能否再讲明白一些呢?

函数定义过程列出的参数均为形参(形式意义上的参数,没有值)
在调用有参函数时所用的的参数为“实际参数”。它包含了实实在在的数据,会被函数内部的代码使用。与形参一一对应,否则会发生错误
一般情况下,实参传递值给形参,在被调用的函数内部不管对形参怎么修改,实参不会改变
但特殊情况下,修改形参会导致实参被修改,如果实参不是以值的形式传递给形参而是以地址方式传递的话,比如
(vla-GetBoundingBox obj 'a 'b)会修改a和b,这里参数传递方式是明确了按地址传递的,此处没有陷阱。
另ssdel和ssadd会对实参进行修改,但传递参数的时候并没有向上边一样明确指定按址传递(ssdel e ss)(ssadd e ss)都会把返回值修改到ss变量,此处可以认为是有益的陷阱,因为可以省去一个setq而达到修改变量为函数返回值的目的
一般说传址的陷阱是指,传参过程并没有明确哪个参数需要按址传递,但函数返回值的同时,会对某些实参进行修改,修改的结果还不一定是所调用的函数的返回值,可能只是那个函数执行过程的一些过程数据,所以对于不太熟悉的函数需要多测试它,才可能发现它的问题,使用过程中避免它

highflybird 发表于 2022-9-11 16:27:16

tigcat 发表于 2022-9-11 14:15
十一条排序消除重复的陷阱是否可以用vl-sort-i函数解决。

可以,但不直接,有些不便

不死猫 发表于 2022-9-11 12:20:18

本帖最后由 不死猫 于 2022-9-11 12:28 编辑

中文变量遇到第一个双字节开始的第一个字节会忽略判定,其余字节相同就认为是相同变量
中文字符的妥善使用可以有抗反编译的效果
http://bbs.mjtd.com/forum.php?mo ... 4990&fromuid=332981
(出处: 明经CAD社区)
中文字符冲突原理视频讲解:
https://www.toutiao.com/article/7083878031474852383/

llsheng_73 发表于 2022-9-11 17:49:08

本帖最后由 llsheng_73 于 2022-9-11 17:50 编辑

这一篇把lisp的陷阱讲得很透彻,认真研读理解后可以避免踩很多不必要的雷。

此外曾碰到过两类陷阱:
大坐标陷阱:目前发现主要是vlax-curve-系列函数,产生的原因初步估计是因为数值过大,而实现那些功能的函数,多半需要进行乘方计算,而这个计算过程不可避免的可能产生数据溢出,最终返回错误的计算结果,比如数据精度明显不正确,交点个数明显不正确等。我是碰到过多次,于是在调用它们的时候,自己加了个“壳”(先取对象任意一点坐标作为基点,然后把所有点以此为基准改为0,0,然后再调用需要的vlax-curve-来处理,最后把处理结果全部加上基点),虽然比较啰嗦一些,但一般情况下确实能解决问题;
版本陷阱:通常我们认为,lisp函数是不受CAD版本限制的,但同样在vlax-curve-getpointatparam上发现过问题,假设一条多段线e,param为2的点和param为的3点完全重合,在低版本下,如果取param大于2小于3,那么返回nil,但在高版本上它不会返回nil,它会返回param为2的坐标。曾经在低版本下利用这一特点在取点时去除重复坐标,结果在高版本的时候不适用了。
个人的建议是,一方面对于已知的陷阱,认真理解,实在不行先记住它,避免踩雷;
另一方面是任何成品发布前需要测试:局部测试、整体测试、海量测试、换版本测试,当然测试简单,麻烦的是准备测试数据,这一点可能比实现功能本身更高麻烦。

轮回 发表于 2022-9-11 12:07:31

先赞后读!!

bonny 发表于 2022-9-11 12:08:52

:victory::victory::victory::victory::victory:

自贡黄明儒 发表于 2022-9-11 12:24:20

我使用bricscad,画出的图是歪的,要(Setvar "angbase" 0)(Setvar "angbase" 0)运行两次,画出来的图才是正的。

guosheyang 发表于 2022-9-11 12:37:57

感谢高大师的系统总结!

自贡黄明儒 发表于 2022-9-11 13:15:03

本帖最后由 自贡黄明儒 于 2022-9-11 13:23 编辑

1 还有一个选择集的问题,既使以局部变量的形式写入函数,如果函数内部删除了一个成员,选择集照样改变
2 (command "._Select" ss1 "_Remove" ss1 "")后,选择集数量为1

tigcat 发表于 2022-9-11 14:15:06

十一条排序消除重复的陷阱是否可以用vl-sort-i函数解决。

baitang36 发表于 2022-9-11 16:32:21

lisp有点歧视汉字,用保留函数计算hash值时,并未把汉字计算在内。因此你改了汉字并不会影响hash值。
页: [1] 2 3 4 5
查看完整版本: LISP陷阱与缺陷