符号是有名字的对象,这么说可能有点抽象。我们先来回忆一下C/C++中关于符号的内容。
C/C++ 最终被编译成机器码直接执行,在机器码中不存在变量名称,函数名称等字符,它只有一串地址。但是在写C/C++代码的时候有变量名,函数名,类名,对象名等等名称。编译器是如何做到将符号和地址关联起来的呢?答案是,编译器在编译阶段会提供一个符号表,符号表如何实现的我也不太清楚,但是它做到了关联一个地址和字符串符号的作用。在Windows平台的 vs下,debug版本一般会生成与exe同名的pdb文件,这个是调试文件,它里面保存了一些调试信息,包括符号表。这里的符号与C/C++中的符号表中的符号类似,可以通过它来找到具体的变量。可以理解成elisp提供了这么一种操作符号表的功能。
首先必须知道的是符号的命名规则。符号名字可以含有任何字符。与之对应的,一般的编程语言在定义变量的时候有些特殊符号不能用,而且不能以数字开头,有些关键字也不能作为变量名。而elisp中没有这些限制,只是在使用特殊符号的时候需要使用转义字符。
(symbolp '+1) ;; ==> nil
(symbolp '\+1) ;; ==> t
(symbol-name '+1) ;; error
(symbol-name '\+1) ;; ==> "+1"
上面的代码 symbolp
是在判断一个对象是否是一个符号的函数,symbol-name
用于取符号的名称,它会返回给定符号的名称的字符串。上面的代码说明了,elisp中符号没有什么特别要求,只是对于特定的字符需要使用转义字符。
与c/c++中类似,elisp中的符号名也是区分大小写的。
符号创建
在C/C++中符号表由编译器来创建和操作。而elisp中则提供了操作符号表的方式。符号名要有唯一性,所以一定会有一个表与名字关联,这个表在 elisp 里称为 obarray。从这个名字可以看出这个表是用数组类型,事实上它是一个向量。对于一个新的符号,解释器会首先取字符串的hash值,根据hash值来放入数组对应的位置。同时我们也将这种保存符号的数据结构称之为obarray。也就是说obarray不仅是一个保存符号的变量,也是一种结构。我们也可以在符号上建立这么一个结构用来保存符号对应的属性。也可以作为参数传入emacs 相关函数中
当 elisp 读入一个符号时,通常会先查找这个符号是否在 obarray 里出现过,如果没有则会把这个符号加入到 obarray 里。这样查找并加入一个符号的过程称为是 intern。intern 函数可以查找或加入一个名字到 obarray 里,返回对应的符号。默认是全局的obarray,也可以指定一个 obarray。intern-soft 与 intern 不同的是,当名字不在 obarray 里时,intern-soft 会返回 nil,而 intern 会加入到 obarray里。
(setq foo (make-vector 10 0)) ;; ==> [0 0 0 0 0 0 0 0 0 0]
(intern-soft "abc" foo) ;; ==> nil
(intern "abc" foo) ;; ==> abc
(intern-soft "abc") ;; ==> abc
(intern-soft "abc") ;; ==> nil
lisp 每读入一个符号都会 intern 到 obarray 里,如果想避免,可以用在符号名前加上 #:
(intern-soft "abc") ;; ==> nil
'abc
(intern-soft "abc") ;; ==> abc
'#:abcd ;; ==> abcd
(intern-soft "abcd") ;; ==> nil
可以使用 untern 从obarray中删除对应的符号,如果成功删除,会返回t,如果没有对应的符号,则会返回 nil
(intern "abc") ;; ==> abc
(intern-soft "abc") ;; ==> abc
(unintern "abc") ;; ==> t
(intern-soft "abc") ;; ==> nil
(setq foo 1)
(intern-soft "foo")
(unintern "foo")
(intern-soft "foo")
(1+ foo) ;; error
通过setq,我们让elisp将foo这个变量放入到obarray中,后续使用unintern 删除这个变量后再使用foo的时候就会报错,foo是一个空变量
与hash-table一样,obarray 也提供一个mapatom 函数来遍历整个obarray,例如下面是一个计算所有符号数量的例子
(setq count 0)
(defun count-sys(s)(setq count (1+ count)))(mapatoms 'count-sys)
count ;; ==> 95733
(length obarray) ;; ==> 15121
这里我们看到,数组的长度小于符号的数量。这根hash-table的实现是一样的。各位读者在学习hash-table的时候应该了解过,hash-table 中 hash值不同的元素存储在数组的不同位置,相同的元素通过链表进行串联,一般的hash-table在内存中的结构如下图.
符号的组成
在计算机中,所有的内容都是使用二进制来进行存储的,我们人为的将二进制数据划分为代码和数据。如果单纯的给出一个内存的地址,如何知道它是数据还是代码呢?例如在C/C++中定义了一个int
类型的变量a
,为什么在后续使用a(1)
这样的语句会报错呢?又例如一个函数指针 pfn
,使用 *pfn = 1
这样也会报错呢?编译器怎么知道哪个地址存的是变量,哪个地址存的是函数指针呢?还是通过符号表来解决,符号表中针对每个符号名称都会给定它的类型,例如这个符号对应的地址是一个整数,或者指针,又或者是函数指针。
符号名称
类比C/C++ 中的符号表,elisp中每个符号都可以有4个组成部分,一个是符号名称,它可以类比到符号表中的名称,可以用symbol-name 来访问。它返回一个符号的字符串名称,关于使用的例子在最开始已经给出了。
符号值
第二个组成部分是符号值,可以类比成普通变量的值,可以通过set函数来设置,通过 symbol-value 来获取。
(set 'abc "i am abc") ;; ==> "i am abc"
(symbol-value 'abc) ;; ==> "i am abc"
abc ;; ==> "i am abc"
set 以及 symbol-value 需要提供一个符号,表示对哪个符号进行操作。解释器执行第一行代码的时候未发现 abc 这个符号,那么它会将abc放入符号表中,然后这个符号就可以作为普通变量来使用了。最后一行代码我们直接将它作为普通变量那样使用,直接对它进行求值,发现它也可以获取到具体的值
我们使用 setq 也可以达到这样的效果
(setq val 123) ;; ==> 123
(symbol-value 'val) ;; ==> 123
val ;; ==>123(set 'val 1234) ;; ==> 1234
(symbol-value 'val) ;; ==> 1234
val ;; ==> 1234
从上面的代码中发现,setq
直接使用变量名来对变量进行赋值,而set
则需要对符号进行quote
操作。我们可以将setq
看做是一个宏(至于宏是什么,会在后面进行介绍),也就是 set quote
,自动将后面的符号进行quote
操作。
但是setq
只能对全局的obarray
中的符号进行赋值,如果我们想放到指定的obarray
中进行,此时就不能使用setq
了
(setq foo (make-vector 10 0))
(set (intern "value" foo) 123)
(symbol-value 'value) ;; ==> error
(symbol-value (intern-soft "value" foo)) ;; ==> 123
set
和 symbol-value
没有直接的参数来指定符号所在的obarray
,如果想要使用自定义的obarray
,那么就需要借助 intern
、intern-soft
、这样可以指定obarray
的函数来进行辅助操作。
如果一个符号的值已经有设置过的话,则 boundp
测试返回 t
,否则为 nil
。对于 boundp
测试返回 nil 的符号,使用符号的值会引起一个变量值为 void 的错误
(intern "null")
(boundp 'null) ;; ==> nil
null ;; error
(set 'null 123)
(boundp 'null) ;; ==> t
null ;;==> 123
函数
第三个组成部分是函数,它可以类比成函数指针, 它可以用 symbol-function
来访问,用 fset
来设置。在之前一篇文章中,有知乎大牛指出我的问题,根据大牛的描述,在绑定lambda表达式时将函数部分绑定到符号的函数部分,使用funcall 调用的时候是在取函数部分的内容执行。这里详细了解符号相关的知识之后上述表达就很容易理解了。
(setq foo (make-vector 10 0))
(fset (intern "abc" foo) (lambda (name)message "hello,%s" name))(funcall (intern-soft "abc" foo) "Emacs") ;; ==> error
上述的代码会报告一个错误,因为这里我们使用的obarray 是自定义的foo,它里面没有message这个符号,当然我们可以使用 intern来获取全局的 message 函数,并将它放入到foo中。这里的代码可以这么改
(fset (intern "abc" foo) (lambda (name)(funcall (intern-soft "message") "hello,%s" name)))(funcall (intern-soft "abc" foo) "Emacs") ;; ==> "hello,Emacs"
类似的,可以用 fboundp 测试一个符号的函数部分是否有设置。
(fboundp 'message)
(fboundp (intern-soft "message" foo)) ;; ==> nil(fset (intern "message" foo) (symbol-function 'message))
(fboundp (intern-soft "message" foo)) ;; ==>t
属性列表
第4个组成部分是属性列表,关于这部分我暂时还没想到该怎么用C/C++进行类比,如果非要一个类比的话,可以用这个类比。
C/C++的编译器在看待变量的时候是将变量转变成对应的内存地址,操作变量实际上就是在操作变量所对应的内存。从CPU的角度来讲,CPU并没有规定哪些内存是只读的,哪些是数据,哪些是代码。编译器是如何做的呢?答案应该是编译器会在符号表中对各个符号做一些标记,例如const型变量所对应的内存不能修改。具体编译器是如何实现我也不太清楚,先这么生搬硬套吧,至少在了解elisp的符号这块,这么理解可能会稍微具体一点
elisp中的属性列表,用于存储和符号相关的信息,比如变量和函数的文档,定义的文件名和位置,语法类型。属性名和值可以是任意的 lisp 对象,但是通常名字是符号,可以用 get 和 put 来访问和修改属性值,用 symbol-plist 得到所有的属性列表:
(put (intern "abc" foo) 'doc "this is abc")
(get (intern-soft "abc" foo) 'doc) ;; ==> "this is abc"
(symbol-plist (intern-soft "abc" foo)) ;; ==> (doc "this is abc")
符号的属性列表在内部表示上是用(prop1 value1 prop2 value2 ...) 的形式, 在存取上有点像C/C++中的map,但是在elisp中并不是所谓的map结构。
另外还可以用 plist-get 和 plist-put 的方法来访问和设置属性列表,在上一段代码的基础之上(也就是设置了符号 abc 的 doc 属性的前提下),使用如下代码来进行测试
(plist-get (symbol-plist (intern-soft "abc" foo)) 'doc) ;; ==> "this is abc"
(plist-put (symbol-plist (intern-soft "abc" foo)) 'foo 69)
(get (intern-soft "abc" foo) 'foo) ;; ==> 69
(setq my-plist '(doc "this is abc")) ;; ==> "this is abc"
(plist-put my-plist 'foo 89)
(plist-get my-plist 'doc) ;; ==> "this is abc"
(plist-get my-plist 'foo)
(get (intern-soft "abc" foo) 'foo) ;; ==> 69
从上面的代码来看,plist-get 和 plist-put 需要一个额外的属性列表的操作表示要操作的属性列表,但是它也可以通过传入符号的真实属性列表直接来操作符号的属性列表。