符号
符号 是有 名字 的对象
作个不恰当的比方,符号可以看作是 C 语言里的指针 通过符号可以得到和这个符号相关联的信息,比如值,函数,属性列表等等
命名规则
符号名字可以含有任何字符。大多数的符号名字只含有字母、数字和标点 -+=*/ 。这样的名字不需要其它标点。名字前缀要足够把符号名和数字区分开来,如果需要的话,可以在前面用 \ 表示为符号:
(symbolp '+1) ; => nil (symbolp '\+1) ; => t (symbol-name '\+1) ; => "+1"
其它字符 _~!@$%^&:<>{}? 用的比较少,但是也可以直接作为符号的名字 任何其它字符都可以用 \ 转义后用在符号名字里。但是和字符串里字符表示不同,\ 转义后只是表示其后的字符,比如 \t 代表的字符 t,而不是制表符 如果要在符号名里使用制表符,必须在 \ 后加上制表符本身
符号名是区分大小写的。这里有一些符号名的例子:
foo ; 名为 `foo' 的符号 FOO ; 名为 `FOO' 的符号,和 `foo' 不同 char-to-string ; 名为 `char-to-string' 的符号 1+ ; 名为 `1+' 的符号 (不是整数 `+1') \+1 ; 名为 `+1' 的符号 (可读性很差的名字) \(*\ 1\ 2\) ; 名为 `(* 1 2)' 的符号 (更差劲的名字). +-*/_~!@$%^&=:<>{} ; 名为 `+-*/_~!@$%^&=:<>{}' 的符号. ; 这些字符无须转义
创建
一个名字如何与数据对应上呢?这就要了解一下符号是如何创建的了
符号名要有唯一性,所以一定会有一个表与名字关联,这个表在 elisp 里称为 obarray
从这个名字可以看出这个表是用数组类型,事实上是一个向量
当 emacs 创建一个符号时:会对这个名字求 hash 值以得到一个在 obarray 这个向量中查找值所用的下标
这里强调的是obarray 不是一个特殊的数据结构,就是一个一般的向量
全局变量 obarray里 emacs 所有变量、函数和其它符号所使用的 obarray
注意不同语境中obarray 的含义不同。前一个 obarray 是变量名,后一个 obarray 是数据类型名
也可以自己建立向量,把这个向量作为 obarray 来使用。这是一种代替散列的一种方法。它比直接使用散列有这样一些好处:
- 符号不仅可以有一个值,还可以用属性列表,后者又可以相当于一个关联列表。这样有很高的扩展性,而且可以表达更高级的数据结构
- emacs 里有一些函数可以接受 obarray 作为参数,比如补全相关的函数
当 lisp 读入一个符号时,通常会先查找这个符号是否在 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" foo) ; => abc
为了不污染 obarray,例子中尽量在 foo 这个 obarray 里进行 一般来说,去了 foo 参数,则会在 obarray 里进行。其结果应该是相同的
lisp 每读入一个符号都会 intern 到 obarray 里,如果想避免,可以用在符号名前加上 #: :
(intern-soft "abc") ; => nil 'abc ; => abc (intern-soft "abc") ; => abc (intern-soft "abcd") ; => nil '#:abcd ; => abcd (intern-soft "abcd") ; => nil
如果想除去 obarray 里的符号,可以用 unintern 函数。unintern 可以用符号名或符号作参数在指定的 obarray 里去除符号:
- 成功去除则返回 t
- 如果没有查找到对应的符号则返回 nil
(intern-soft "abc" foo) ; => abc (unintern "abc" foo) ; => t (intern-soft "abc" foo) ; => nil
和 hash-table 一样,obarray 也提供一个 mapatoms 函数来遍历整个 obarray。比如要计算 obarray 里所有的符号数量:
(setq count 0) ; => 0 (defun count-syms (s) (setq count (1+ count))) ; => count-syms (mapatoms 'count-syms) ; => nil count ; => 70395 (length obarray) ; => 15121
obarry实现
由前面的例子可以看出elisp 中的向量长度都是有限的,而 obarray 里的符号有成千上万个 那这些符号是怎样放到 obarray 里的呢?
其实这和散列的的实现是一样的。obarray 里的每一个元素通常称为 bucket 。 一个 bucket 是可以容纳多个相同 hash 值的字符串和它们的数据。可以用 这样的方法来模拟一下:
(defun hash-string (str) (let ((hash 0) c) (dotimes (i (length str)) (setq c (aref str i)) (if (> c #o140) (setq c (- c 40))) (setq hash (+ (setq hash (lsh hash 3)) (lsh hash -28) c))) hash)) (let ((len 10) str hash) (setq foo (make-vector len 0)) (dotimes (i (1+ len)) (setq str (char-to-string (+ ?a i)) hash (% (hash-string str) len)) (message "I put %s in slot %d" str hash) (if (eq (aref foo hash) 0) (intern str foo) (message "I found %S is already taking the slot: %S" (aref foo hash) foo) (intern str foo) (message "Now I'am in the slot too: %S" foo))))
在我这里的输出:
I put a in slot 7 I put b in slot 8 I put c in slot 9 I put d in slot 0 I put e in slot 1 I put f in slot 2 I put g in slot 3 I put h in slot 4 I put i in slot 5 I put j in slot 6 I put k in slot 7 I found a is already taking the slot: [d e f g h i j a b c] Now I’am in the slot too: [d e f g h i j k b c]
当然,这个 hash-string 和实际 obarray 里用的 hash-string 只是算法上是相同的,但是由于数据类型和 c 不是完全相同,所以对于长一点的字符串结果 可能不一样,只好用单个字符来演示一下
组成
名字和值
每个符号可以对应四个组成部分:
- 符号的名字 ,可以用 symbol-name 访问
- 符号的值 。符号的值可以通过 set 函数来设置,用 symbol-value 来访问
- 如果一个符号的值已经有设置过的话,则 boundp 测试返回 t,否则为 nil
- 对于 boundp 测试返回 nil 的符号,使用符号的值会引起一个变量值为 void 的错误
(set (intern "abc" foo) "I'm abc") ; => "I'm abc" (symbol-value (intern "abc" foo)) ; => "I'm abc"
可能大家最常见到 setq ,而 set 函数确很少见到 setq 可以看成是一个宏,它可以让你用 (setq sym val) 代替 (set (quote sym) val),事实上这也是它名字的来源 (q 代表 quoted) 但是 setq 只能设置 obarray 里的变量,前面这个例子中就只能用 set 函数
函数
符号的第三个组成部分是 函数 :它可以用 symbol-function 来访问,用 fset 来设置
(fset (intern "abc" foo) (symbol-function 'car)) ; => #<subr car> (funcall (intern "abc" foo) '(a . b)) ; => a
类似的,可以用 fboundp 测试一个符号的函数部分是否有设置
属性列表
符号的第四个组成部分是 属性列表 。通常属性列表用于 存储 和 符号相关的信息 ,比如变量和函数的 文档 ,定义的 文件名 和 位置 ,语法类型
属性名和值可以是任意的 lisp 对象,通常名字是符号,可以用 get 和 put 来访问和修改属性值,用 symbol-plist 得到所有的属性列表:
(put (intern "abc" foo) 'doc "this is abc") ; => "this is abc" (get (intern "abc" foo) 'doc) ; => "this is abc" (symbol-plist (intern "abc" foo)) ; => (doc "this is abc")
关联列表和属性列表很相似。符号的属性列表在内部表示上是用(prop1 value1 prop2 value2 ...) 的形式,和关联列表也是很相似的 属性列表在查找和这个符号相关的信息时,要比直接用关联列表要简单快捷的多,所以变量的文档等信息都是放在符号的属性列表里 但是关联表在头端加入元素是很快的,而且它可以删除表里的元素,而属性列表则不能删除一个属性
如果已经把属性列表取出,那么还可以用 plist-get 和 plist-put 的方法来访问和设置属性列表
(plist-get '(foo 4) 'foo) ; => 4 (plist-get '(foo 4 bad) 'bar) ; => nil (setq my-plist '(bar t foo 4)) ; => (bar t foo 4) (setq my-plist (plist-put my-plist 'foo 69)) ; => (bar t foo 69) (setq my-plist (plist-put my-plist 'quux '(a))) ; => (bar t foo 69 quux (a))
plist-get 和 plist-put 的实现
(defun my-plist-get (plist prop) (cadr (memq plist prop))) (defun my-plist-put (plist prop val) (let ((tail (memq prop plist))) (if tail (setcar (cdr tail) val) (setcdr (last plist) (list prop val)))) plist)
my-plist-put 函数没有 plist-put 那样 robust,如果属性列表是 '(bar t foo) 这样的话,这个函数就会出错 而且加入一个属性的时间复杂度比 plist 更高(memq 和 last 都是 O(n)),不过可以用循环来达到相同的时间复杂度