Common Lisp 包的傻瓜指南

1.介绍

当多个程序员编写大型项目时,两个不同的程序员通常可能会为两个不同的目的使用相同的名字。可以使用命名约定来解决这个问题,例如 Bob 在他所有的名字前加上“BOB-”,而 Jane 在她所有的名字前加上“JANE-”。这实际上就是 Scheme 解决这个问题的办法。(R6RS之后的 Scheme 标准也已经有了“模块”的标准了,所以这个问题已经不存在了。)

Common Lisp 提供了一个语言机制来隔离不同的命名空间。下面是包如何工作的示例:

? (make-package :bob :use '(common-lisp))
#<Package "BOB">

? (make-package :jane :use '(common-lisp))
#<Package "JANE">

? (in-package :bob)
#<Package "BOB">

? (defun foo () "This is Bob's foo")
FOO

? (in-package :jane)
#<Package "JANE">

? (defun foo () "This is Jane's foo")
FOO

? (foo)
"This is Jane's foo"

? (in-package :bob)
#<Package "BOB">

? (foo)
"This is Bob's foo"

(注意:代码示例是从 Macintosh Common Lisp (MCL) 中剪切和粘贴的。MCL 的命令提示符是一个问号。)

(注意:make-package中的参数:use '(common-lisp)是译者加上去的,sbcl 在定义新的包时不会自动:use :common-lisp这个包,导致进入新的包以后,连defun都无法执行,也无法使用in-package切换到别的包。所以,原来的代码在sbcl上是无法运行的。考虑到目前sbcl基本上是使用人数最多的实现,所以译者作了一点修改,使得代码可以在sbcl上跑起来.)

如果 Bob 想要使用 Jane 编写的函数怎么办呢?有几种方法可以做到。一种是使用特殊语法来指定要使用不同的包:

? (in-package :bob)
#<Package "BOB">

? (jane::foo)
"This is Jane's foo"

另一个办法是将他想要使用的东西导入到自己的包中。当然,Bob 可能不会想要导入 Jane 的 foo 函数,因为会和他自己的 foo 函数发生冲突。如果 Bob 想要导入 Jane 的 baz函数,他希望简单地通过 (baz) 去调用该函数,而不是输入(jane::baz),他可以像这样做:

? (in-package :jane)
#<Package "JANE">

? (defun baz () "This is Jane's baz")
BAZ

? (in-package :bob)
#<Package "BOB">

? (import 'jane::baz)
T

? (baz)
"This is Jane's baz"

可惜,事情并不总是那么顺利:

? (in-package :jane)
#<Package "JANE">

? (defun bar () "This is Jane's bar")
BAR

? (in-package :bob)
#<Package "BOB">

? (bar)
> Error: Undefined function BAR called with arguments () .

;; 哦,忘了 import

? (import 'jane::bar)
> Error: Importing JANE::BAR to #<Package "BOB"> would conflict with
symbol BAR .

;; 咦, 又出错了?怎么回事?

要了解为什么会发生这种情况,如何处理,以及包的许多其他微妙之处以及惊喜,重要的是要了解包的实际作用以及它们是如何工作的。例如,重要的是要理解,当您键入 (import ‘jane::foo) 时,您导入的是符号JANE::FOO,而不是与该符号关联的函数。理解这个差异很重要,因此我们必须从回顾一些基本的 Lisp 概念开始。

2.符号,值,以及 REPL

Lisp 在 READ-EVAL-PRINT 循环中运行。大多数有趣的事情发生在 EVAL 阶段,但是当谈到包时,有趣的事情发生在所有三个阶段,重要的是要了解什么时候发生。特别是,一些与包相关的处理会在 READ 时改变 Lisp 系统的状态,这反过来又会导致一些令人惊讶(常常是令人讨厌的)的行为,就像上一节中的最后一个例子。

包是 Lisp 符号的集合,因此要了解包,您首先必须了解符号。符号是一个非常普通的 Lisp 数据结构,就像列表、数字、字符串等一样。有用于创建和操作符号的内置 Lisp 函数。例如,有一个名为 gentemp 的函数可以创建新符号:

? (gentemp)
T1

? (setq x (gentemp))
T2

? (set x 123) ;; 注意,这里使用的是 set, 不是 setq 或 setf
123

? x
T2

? t2
123

(如果您不熟悉 SET 函数,现在就是查文档的好时机,因为如果您不搞清楚,您很快就会迷失方向。)

GENTEMP 创建的符号在所有方面的行为与你通过直接输入而产生的符号相当类似。

您只能有限地控制由 GENTEMP 创建的符号的名字。您可以向它传递一个可选的前缀字符串,但系统会自动 添加一个后缀,您必须接受它给您的任何内容。如果要使用特定的名字创建符号,则必须使用另外的函数 MAKE-SYMBOL:

(make-symbol "MY-SYMBOL")
#:MY-SYMBOL

嗯,这很奇怪。那个看起来很有趣的前缀#:在做什么?

要理解这一点,我们必须更深入地挖掘符号的内涵。

? (setq symbol1 (make-symbol "MY-SYMBOL"))
#:MY-SYMBOL

? (setq symbol2 (make-symbol "MY-SYMBOL"))
#:MY-SYMBOL

? (setq symbol3 'my-symbol)
MY-SYMBOL

? (setq symbol4 'my-symbol)
MY-SYMBOL

? (eq symbol1 symbol2)
NIL

? (eq symbol3 symbol4)
T

如您所见,MAKE-SYMBOL 可以生成多个具有相同名字的不同的符号,而直接输入相同的('my-symbol)两次而得到的符号是相同的符号。

符号的这种特性非常重要。它确保了您在一个地方输入的FOO与其它地方输入的FOO是相同的符号,否则,就会得到一些非常奇怪的结果:

? (set symbol1 123)
123

? (set symbol2 456)
456

? (setq code-fragment-1 (list 'print symbol1))
(PRINT #:MY-SYMBOL)

? (setq code-fragment-2 (list 'print symbol2))
(PRINT #:MY-SYMBOL)

? (eval code-fragment-1)
123
123

? (eval code-fragment-2)
456
456

对比一下:

? (set symbol3 123)
123

? (set symbol4 456)
456

? (setq code-fragment-3 (list 'print symbol3))
(PRINT MY-SYMBOL)

? (setq code-fragment-4 (list 'print symbol4))
(PRINT MY-SYMBOL)

? (eval code-fragment-3)
456
456

? (eval code-fragment-4)
456
456

symbol1-symbol4 都具有名字“MY-SYMBOL”,但符号 1 和 2 是不同的符号,而符号 3 和 4 是相同的符号。这怎么发生的?嗯,一个明显的区别是我们调用函数 MAKE-SYMBOL 来生成符号 1 和 2,而符号 3 和 4 是由 Lisp reader(读取器)为我们生成的。也许 Lisp 读取器生成符号的方式与调用 MAKE-SYMBOL 生成符号并不相同.我们可以检验这个假设:

? (trace make-symbol)
NIL

? 'foobaz
 Calling (MAKE-SYMBOL "FOOBAZ")
 MAKE-SYMBOL returned #:FOOBAZ
FOOBAZ

[注:reader 读取到一个新符号时会调用 make-symbol 来产生新符号这种行为仅仅在 MCL & Clozure CL 测试中发生,其它实现可能采取不一样的实现方式。]

显然 reader 通过在内部调用 MAKE-SYMBOL 以与我们直接调用MAKE-SYMBOL相同的方式产生符号。但是等等,MAKE-SYMBOL 返回的符号前面有那个有趣的#: 前缀,但是最后,那个前缀消失了。这是如何发生的?

我们可以在对MAKE-SYMBOL保持跟踪的情况下再次尝试相同的实验来找出答案:

? 'foobaz
FOOBAZ

啊哈!我们第二次输入 'FOOBAZ 时,reader 不再调用 MAKE-SYMBOL。因此,reader显然保留了它所创建的所有符号的集合,并且在创建新符号之前,它会检查该集合以查看是否已经存在同名的符号。如果有,那么它只是返回相同的符号而不是创建一个新的。作为此类集合成员的符号会丢失其神秘的 #: 前缀。

这个符号的集合就是包(package)。

包是 Lisp 符号的集合,其特性是集合中不可能有两个名字相同的符号。

不幸的是,这或多或少是包简单明了的最后一个方面。从这里开始,事情变得更加棘手。

3.驻留(Interning)

将符号存入包中的操作称为符号的驻留(interning)。属于某个包的符号被称为已驻留(interned)符号,而不属于任何包的符号则被称为未驻留(uninterned)符号。当输出未驻留符号时,系统会为其添加#:前缀以区别于已驻留符号,同时提醒使用者:尽管某个符号看起来与之前见过的符号完全相同,但实际上可能并非同一个符号。

[译注] intern 实际上是 internal 的简写

现在,事情开始变得有点混乱。

这里存在一个容易混淆的概念:有一个名为INTERN的Lisp函数,您可能以为它的功能是将符号添加到包中,但实际上并非如此。这个添加符号的功能是由另一个名为IMPORT的函数来实现的。

? symbol1
#:MY-SYMBOL

? (import symbol1)
T

? symbol1
MY-SYMBOL

? (eq symbol1 'my-symbol)
T

如您所见,symbol1 从未驻留符号转变为了已驻留符号。它失去了原有的 #: 前缀,并且现在与 Lisp 读取器生成的 MY-SYMBOL 符号是相同的。

现在,你可能想调用UNIMPORT来撤销掉IMPORT的效果,但是并没有这样的函数。(我提醒过你,事情没有那么简单),要从包中删除一个符号,你可以调用UNINTERN:

? (unintern symbol1)
T

? symbol1
#:MY-SYMBOL

? (eq symbol1 'my-symbol)
NIL

现在情况已恢复原状。symbol1 现在是一个未驻留符号,并且与读取器生成的符号不再是同一个。让我们将 symbol1 重新放入包中:

? (import symbol1)
> Error: Importing #:MY-SYMBOL to #<Package "COMMON-LISP-USER"> would
conflict with symbol MY-SYMBOL .

又发生了什么?(在进一步阅读之前尝试弄清楚这里发生了什么是一个很好的练习。你已经有足够的信息来弄清楚事实。提示:你可以尝试使用 trace make-symbol 的实验)

这就是所发生的事情:

? (unintern 'my-symbol)
T
? (eq symbol1 'my-symbol
 Calling (MAKE-SYMBOL "MY-SYMBOL")
 MAKE-SYMBOL returned #:MY-SYMBOL
)
NIL

当我们输入 (eq symbol1 'my-symbol) 时,读取器已经驻留了 MY-SYMBOL 这个符号。因此当我们尝试导入 symbol1 时,包中已存在同名符号。请记住,包始终维持着"任何时刻包内不允许存在同名符号"这个不变条件(这正是包机制的核心意义)。所以当我们尝试导入同样命名为 MY-SYMBOL 的 symbol1 时,包中早已存在由读取器静默驻留的同名符号。这种情况被称为符号冲突,遗憾的是,这种现象在实践中相当常见。

请注意,顺便说一下,上面 MAKE-SYMBOL 跟踪的输出出现在 S 表达式(eq symbol1 ‘my-symbol)中。这是因为 MCL 重新排列了屏幕上的文本以反映事件的真实顺序。因为 MAKE-SYMBOL 在处理字符串“my-symbol”时被读取器调用,但在处理关闭括号之前,输出出现在那个点。如果我们输入: (list 'x1 'x2 'x3 'x4) 结果输出如下所示:

? (list 'x1
 Calling (MAKE-SYMBOL "X1")
 MAKE-SYMBOL returned #:X1
'x2
 Calling (MAKE-SYMBOL "X2")
 MAKE-SYMBOL returned #:X2
'x3
 Calling (MAKE-SYMBOL "X3")
 MAKE-SYMBOL returned #:X3
'x4
 Calling (MAKE-SYMBOL "X4")
 MAKE-SYMBOL returned #:X4
)

现在我们可以继续介绍几个有用的函数。SYMBOL-NAME 函数能以字符串形式返回符号的名称;FIND-SYMBOL 函数接收字符串参数并查询是否已有同名符号被驻留;最后是 INTERN 函数,其功能可以用以下方式定义:

(defun intern (name)
  (or (find-symbol name)
      (let ((s (make-symbol name)))
        (import s)
        s)))

换句话说,INTERN 与 SYMBOL-NAME 相反。 SYMBOL-NAME 接受一个符号并返回该符号名字的字符串。 INTERN 接受字符串并返回名字为该字符串的符号。INTERN 是 READ 用来制作符号的函数。

intern string &optional package => symbol, status

参数和返回值:

string - 一个字符串。

package - 包描述符,默认为当前包。

symbol - 一个符号.

status - 下列之一 :inherited, :external, :internal, or nil.

可以把 intern理解为UNIX命令 touch,当文件不存在时,touch只修改文件的时间戳,当文件不存在时,它会自创建一个新文件。而intern在指定的符号不存在时会自动创建该符号,当符号存在时,则什么也不做,只是返回该符号。

4.哪一个包

诸如 FIND-SYMBOL、IMPORT 和 INTERN 等对包进行操作的函数默认的操作对象是全局变量*PACKAGE* 所标识的包,也称为当前包。和符号一样,包也有名字,系统中不可能同时存在两个相同名字的包。包和符号一样,也是普通的 Lisp 数据结构。有一些函数 PACKAGE-NAME 和 FIND-PACKAGE 的操作类似于 SYMBOL-NAME 和 FIND-SYMBOL,不过FIND-PACKAGE不接受 PACKAGE 参数。 (就好像有一个全局的元包一样。)

正如我们已经看到的,我们可以通过调用 MAKE-PACKAGE 来创建新包,并通过调用 IN-PACKAGE 设置当前包。 (请注意, IN-PACKAGE 不是函数,而是不对其参数进行求值的宏。)

您可以通过特殊语法访问不存在于当前包中的符号:包名后跟两个冒号,再跟上符号名。如果您只想使用某个符号而不想将它导入,这种方法很方便。例如,如果 Bob 想使用 Jane 的 FOO 函数,他可以输入 JANE::FOO。

4.1 home 包

Common Lisp 试图维护的不变性原则之一就是"打印-读取一致性"。这个特性指的是,当您打印一个符号,稍后再读取该符号的打印表示,结果应该得到同一个符号,但有两个前提条件:1)这不适用于未驻留的符号,2)仅当您避免某些“危险”的行为的时候。我们稍后会介绍有哪些操作会影响到“打印-读取一致性”。

为了保持打印读取的一致性,一些符号需要使用它们的包限定符来打印。例如:

? (in-package jane)
#<Package "JANE">

? 'foo
FOO

? 'jane::foo
FOO

? (in-package :bob)
#<Package "BOB">

? 'foo
FOO

? 'jane::foo
JANE::FOO

? 'bob::foo
FOO

显然,可能影响到"打印读取一致性"的“危险操作”之一就是调用 IN-PACKAGE。

现在,思考以下情况:

? (in-package 'bob)
#<Package "BOB">

? (unintern 'foo)
T

? (import 'jane::foo)
T

? (make-package :charlie)
#<Package "CHARLIE">

? (in-package :charlie)
#<Package "CHARLIE">

? 'bob::foo
JANE::FOO

这里我们有一个符号 FOO,它同时存在于 JANE 和 BOB 包中。因此,该符号可以同时作为 JANE::FOO 和 BOB::FOO 访问。当从 CHARLIE 包中打印符号时,系统如何决定使用哪种打印表示来打印非驻留的符号?

事实上,每个符号都会记录一个称为其"主包"的特定包——通常这是该符号首次被驻留的包(但也存在例外情况)。当需要带包限定符打印符号时,系统会使用其主包的限定符。您可以通过 SYMBOL-PACKAGE 函数查询符号的主包信息。

? (symbol-package 'bob::foo)
#<Package "JANE">
?

需要注意的是,存在没有主包的符号。例如未驻留符号就没有主包。但也可以创建没有主包的已驻留符号,比如:

? (in-package :jane)
#<Package "JANE">

? 'weird-symbol
WEIRD-SYMBOL

? (in-package :bob)
#<Package "BOB">

? (import 'jane::weird-symbol)
T

? (in-package :jane)
#<Package "JANE">

? (unintern 'weird-symbol)
T

? (in-package 'bob)
#<Package "BOB">

? 'weird-symbol
#:WEIRD-SYMBOL

? (symbol-package 'weird-symbol)
NIL

? (in-package :jane)
#<Package "JANE">

? 'bob::weird-symbol
#:WEIRD-SYMBOL

这其实是需要避免发生的事情。

5. export 和 use

假设 Jane 和 Bob 正在合作进行一个软件开发项目,每个人都在自己的包中工作以避免冲突。简写道:

? (in-package 'jane)
#<Package "JANE">

? (defclass jane-class () (slot1 slot2 slot3))
#<STANDARD-CLASS JANE-CLASS>

现在,假设 Bob 想要使用 JANE-CLASS,他写道:

? (in-package :bob)
#<Package "BOB">

? (import 'jane::jane-class)
T

? (make-instance 'jane-class)
#<JANE-CLASS #x130565E>

到目前为止没什么问题。现在他尝试:

? (setq jc1 (make-instance 'jane-class))
#<JANE-CLASS #x130BA96>

? (slot-value jc1 'slot1)
> Error: #<JANE-CLASS #x130BA96> has no slot named SLOT1.

发生了什么?好吧,JANE-CLASS是在 JANE包中定义的,所以,槽的名字是该包中的符号,但是 Bob 尝试使用 BOB 包中的符号访问该类的实例。换句话说,JANE-CLASS 有一个名为 JANE::SLOT1的槽,而 Bob 尝试访问一个名为 BOB::SLOT1的槽,然而并不存在这样的槽。

Bob 真正想做的是导入与 JANE-CLASS 有“关联”的所有符号,即所有的槽名、方法名等。他怎么知道所有这些符号是什么?他可以检查 Jane 的代码并尝试自己弄清楚,但这会导致许多问题,尤其是他可能决定导入一个 Jane 不想让 Bob 乱搞的符号(记住,将 Jane 的符号与 Bob 的瞎搞可能造成的影响隔离开就是包存在的全部意义)。

更好的解决方案是让 Jane 收集起一个 Bob 应该导入的符号列表,以方便 Bob 使用她的软件。然后 Jane 可以执行以下操作:

? (defvar *published-symbols* '(jane-class slot1 slot2 slot3))
*PUBLISHED-SYMBOLS*

然后 Bob 可以 (import jane::*published-symbols*).

Common Lisp 提供了一个标准的机制来实现这一点。每个包都维护着一个供其他包使用的符号列表。此列表称为该包的导出符号列表,要将符号添加到该列表中,请使用 EXPORT 函数。要从包的导出符号列表中删除符号,请使用UNEXPORT(你可能猜对了)。要导入包中的所有导出符号,请调用 USE-PACKAGE。 反导入它们你可以使用UNUSE-PACKAGE。

关于导出符号有两点需要注意。

首先,一个符号可以从驻留了该符号的任意包中导出,而不仅仅是它的主包。一个符号也不必非要从它的主包导出,就可以从该符号所在的其他包中导出。[注:绕来绕去,说的是:p2 导入了 p1 中的符号,甭管是 use 还是 import 导入的,p2 都可以将它 export, 然后,p3 可以 use p2, 间接地导入 p1 中的符号]

其次,当从主包导出的符号带有包限定符时,打印时只会使用一个冒号而非两个。这是为了提醒您该符号已从其主包导出(因此您可能更希望使用 USE-PACKAGE 来引用该符号所在包,而不是通过 IMPORT 单独导入该符号),同时通过强制您多输入一个冒号来避免您直接使用未导出的符号。(我并没有在开玩笑。)

6.遮蔽

最后还有一个问题:使用 USE-PACKAGE 与 IMPORT包中的所有导出符号略有不同。当您IMPORT一个符号时,您可以使用 UNINTERN 取消导入的效果。但是您不能使用 UNINTERN 来(部分)撤销 USE-PACKAGE 的效果。例如:

? (in-package :jane)
#<Package "JANE">

? (export '(slot1 slot2 slot3))
T

? (in-package :bob)
#<Package "BOB">

? (use-package 'jane)
T

? (symbol-package 'slot1)
#<Package "JANE">

? (unintern 'slot1)
NIL

? (symbol-package 'slot1)
#<Package "JANE">

这确实是个问题,因为看起来将无法同时使用两个导出了同名符号的不同包。例如,假设您想在名为 MYPACKAGE 的第三个包中同时使用 P1 和 P2 这两个包,而 P1 和 P2 都导出了名为 X 的符号。如果您直接尝试对 P1 和 P2 都执行 USE-PACKAGE,就会发生名称冲突——因为 Lisp 无法判断 MYPACKAGE 中的 X 应该指向 P1:X 还是 P2:X。

为了解决这种名字冲突,每个包都维护了一个所谓的 遮蔽符号列表 。遮蔽符号列表是一个符号列表,它会遮蔽或覆盖掉由 USE-PACKAGE 导入的外部符号。

有两种方法可以向包的遮蔽符号列表中添加符号:SHADOW 和 SHADOWING-IMPORT。SHADOW 用于将包内已有的符号添加到遮蔽列表,而 SHADOWING-IMPORT 则用于引入其他包中的符号并将其加入遮蔽列表。

例如:

? (in-package :p1)
#<Package "P1">

? (export '(x y z))
T

? (in-package :p2)
#<Package "P2">

? (export '(x y z))
T

? (in-package :bob)
T

? (use-package :p1)
T

? (use-package :p2)
> Error: Using #<Package "P2"> in #<Package "BOB">
>        would cause name conflicts with symbols inherited
>        by that package:
>        Z  P2:Z
>        Y  P2:Y
>        X  P2:x
...
1 >
Aborted
? (unuse-package :p1)
T

? (shadow 'x)
T

? (shadowing-import 'p1:y)
T

? (shadowing-import 'p2:z)
T

? (use-package :p1)
T

? (use-package :p2)
T

? (symbol-package 'x)
#<Package "BOB">

? (symbol-package 'y)
#<Package "P1">

? (symbol-package 'z)
#<Package "P2">

要撤销 SHADOW 或 SHADOWING-IMPORT 的效果,请使用 UNINTERN。

请注意,UNINTERN(以及许多其他令人惊讶的事情)可能会导致意外地出现名字冲突。意外的名字冲突的最常见原因通常是通过在 reader 中键入它而无意中将符号插入包中,而没有密切注意您当时处在哪个包中。

7.DEFPACKAGE

现在您已经了解了无数用于操作包的函数和宏,但是您不应该真正直接使用它们中的任何一个。相反,IMPORT、EXPORT、SHADOW 等的所有功能都集中在一个名为 DEFPACKAGE 的宏中,您应该将其用于实际(非原型)代码。

我不打算在这里解释 DEFPACKAGE,因为现在您了解了包的基本概念,您应该能够阅读 hyperspec 中的文档并理解它。 (在 hyperspec 中还有很多其他好东西,例如 DO-SYMBOLS 和 WITH-PACKAGE-ITERATOR,您现在应该能够自己理解了。)

不过,关于使用 DEFPACKAGE 的一个警告:请注意,DEFPACKAGE 的大多数参数是字符串,而不是符号。这意味着虽然它们可以是符号,但是如果您选择使用符号,那么在读取 DEFPACKAGE 表单时,无论它是什么,这些符号都将被驻留在当前包中。这通常会导致不良后果,因此养成在 DEFPACKAGE 表单中使用关键字参数或字符串的习惯是个好主意。

8.最后的想法

理解包的关键在于,它们本质上是Lisp读取器的一部分,而非求值器。一旦您真正理解了这个核心理念,其他所有内容都会迎刃而解。包的作用是控制读取器如何将字符串映射到符号(以及PRINT如何将符号映射回字符串),仅此而已。特别需要注意的是,包与可能(或可能不)与任何特定符号关联的函数、值、属性列表等都毫无关系。

特别要注意(这与包无关,但无论如何新手经常对此感到困惑): 符号和函数对象都可以被用作函数,但它们的行为略有不同。例如:

? (defun foo () "Original foo")
FOO

? (setf f1 'foo)
FOO

? (setf f2 #'foo)
#<Compiled-function FOO #xEB3446>

? (defun demo ()
    (list (funcall f1) (funcall f2)
          (funcall #'foo) (funcall #.#'foo)))
;Compiler warnings :
;   Undeclared free variable F1, in DEMO.
;   Undeclared free variable F2, in DEMO.
DEMO

? (demo)
("Original foo" "Original foo" "Original foo" "Original foo")

? (defun foo () "New foo")
FOO

? (demo)
("New foo" "Original foo" "New foo" "Original foo")

在此示例中,我们有两个变量:F1 和 F2。F1 的值是符号 FOO,而 F2 的值则是在对其赋值时,符号 FOO 的symbol-function槽中所存放的函数对象。

请注意,当重新定义 FOO 时,调用符号 FOO 的效果是获得新行为,而调用函数对象会产生旧行为。

解释列表中的第二个和第三个结果留给读者练习(此处双关,并非故意)。

评论

暂无评论