UP | HOME

符号

Table of Contents

符号 是有 名字 的对象

作个不恰当的比方,符号可以看作是 C 语言里的指针

通过符号可以得到和这个符号相关联的信息,比如值,函数,属性列表等等

命名规则

符号名字可以含有任何字符。大多数的符号名字只含有字母、数字和标点 -+=*/ 。这样的名字不需要其它标点。名字前缀要足够把符号名和数字区分开来,如果需要的话,可以在前面用 \ 表示为符号:

(symbolp '+1)                           ; => nil
(symbolp '\+1)                          ; => t
(symbol-name '\+1)                      ; => "+1"
其它字符 _~!@$%^&amp;:<>{}? 用的比较少,但是也可以直接作为符号的名字

任何其它字符都可以用 \ 转义后用在符号名字里。但是和字符串里字符表示不同,\ 转义后只是表示其后的字符,比如 \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 对象,通常名字是符号,可以用 getput 来访问和修改属性值,用 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-getplist-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)),不过可以用循环来达到相同的时间复杂度

Previous:数组

Top:数据类型