4.6 结构 (Structures)

结构可以想成是豪华版的向量。假设你要写一个程序来追踪长方体。你可能会想用三个向量元素来表示长方体:高度、宽度及深度。与其使用原本的 svref ,不如定义像是下面这样的抽象,程序会变得更容易阅读,

  1. (defun block-height (b) (svref b 0))

而结构可以想成是,这些函数通通都替你定义好了的向量。

要想定义结构,使用 defstruct 。在最简单的情况下,只要给出结构及字段的名字便可以了:

  1. (defstruct point
  2. x
  3. y)

这里定义了一个 point 结构,具有两个字段 xy 。同时隐式地定义了 make-pointpoint-pcopy-pointpoint-xpoint-y 函数。

2.3 节提过, Lisp 程序可以写出 Lisp 程序。这是目前所见的明显例子之一。当你调用 defstruct 时,它自动生成了其它几个函数的定义。有了宏以后,你将可以自己来办到同样的事情(如果需要的话,你甚至可以自己写出 defstruct )。

每一个 make-point 的调用,会返回一个新的 point 。可以通过给予对应的关键字参数,来指定单一字段的值:

  1. (setf p (make-point :x 0 :y 0))
  2. #S(POINT X 0 Y 0)

存取 point 字段的函数不仅被定义成可取出数值,也可以搭配 setf 一起使用。

  1. > (point-x p)
  2. 0
  3. > (setf (point-y p) 2)
  4. 2
  5. > p
  6. #S(POINT X 0 Y 2)

定义结构也定义了以结构为名的类型。每个点的类型层级会是,类型 point ,接着是类型 structure ,再来是类型 atom ,最后是 t 类型。所以使用 point-p 来测试某个东西是不是一个点时,也可以使用通用性的函数,像是 typep 来测试。

  1. > (point-p p)
  2. T
  3. > (typep p 'point)
  4. T

我们可以在本来的定义中,附上一个列表,含有字段名及缺省表达式,来指定结构字段的缺省值。

  1. (defstruct polemic
  2. (type (progn
  3. (format t "What kind of polemic was it? ")
  4. (read)))
  5. (effect nil))

如果 make-polemic 调用没有给字段指定初始值,则字段会被设成缺省表达式的值:

  1. > (make-polemic)
  2. What kind of polemic was it? scathing
  3. #S(POLEMIC :TYPE SCATHING :EFFECT NIL)

结构显示的方式也可以控制,以及结构自动产生的存取函数的字首。以下是做了前述两件事的 point 定义:

  1. (defstruct (point (:conc-name p)
  2. (:print-function print-point))
  3. (x 0)
  4. (y 0))
  5. (defun print-point (p stream depth)
  6. (format stream "#<~A, ~A>" (px p) (py p)))

:conc-name 关键字参数指定了要放在字段前面的名字,并用这个名字来生成存取函数。预设是 point- ;现在变成只有 p 。不使用缺省的方式使代码的可读性些微降低了,只有在需要常常用到这些存取函数时,你才会想取个短点的名字。

:print-function 是在需要显示结构出来看时,指定用来打印结构的函数 ── 需要显示的情况比如,要在顶层显示时。这个函数需要接受三个实参:要被印出的结构,在哪里被印出,第三个参数通常可以忽略。 [2] 我们会在 7.1 节讨论流(stream)。现在来说,只要知道流可以作为参数传给 format 就好了。

函数 print-point 会用缩写的形式来显示点:

  1. > (make-point)
  2. #<0,0>