【问题标题】:Why are class slots specified with keywords but accessed with symbols?为什么类槽用关键字指定但用符号访问?
【发布时间】:2021-04-22 19:11:27
【问题描述】:

我最近遇到了关于 Lisp 结构的令人困惑的二分法。

使用(defstruct) 创建结构时,我们通过关键字(:slotname)指定槽。但是在访问它时,我们使用本地符号 ('slotname)。

为什么?这对我来说毫无意义。

另外,这不是每次声明结构都会污染keyword包吗?

如果我尝试按关键字访问插槽,我会遇到以下令人困惑的错误:

当试图读取槽的值(槽值)时,槽 :BALANCE 是 对象 #S(ACCOUNT :BALANCE 1000 :CUSTOMER-NAME "John Doe") 丢失。

我不明白这条信息。它似乎在告诉我,我眼皮底下的东西并不存在。

我尝试过使用局部符号声明结构;还有未绑定的关键字 (#:balance),这些都不起作用。

【问题讨论】:

  • 你需要拿出一个实际的例子。在标准的 Common Lisp 结构中,插槽不是通过插槽名称访问的,而是通过函数访问的。
  • (slot-value 'balance some-object)
  • 直到今天我才知道结构隐式访问函数。我已经使用(slot-value) 几个月了。所以,我的问题很简单,为什么(slot-value :balance some-object) 不起作用?
  • 在 Common Lisp 标准中明确未指定 SLOT-VALUE 是否完全适用于结构对象。您的示例也没有任何意义,因为函数 SLOT-VALUE 的参数是错误的。首先是一个对象,然后是插槽名称。
  • @SodAlmighty:这是一个风格问题。许多人将访问器用于公共接口,并将插槽名称用于我怀疑的内部接口。对于用defstruct 定义的东西,只有访问器,但您可以假设访问器非常快(例如,您不能在它们上定义方法)。

标签: oop syntax common-lisp


【解决方案1】:

DEFSTRUCT 在语言标准中是这样设计的:

  • 插槽名称未公开
  • 没有指定的方法来获取结构类的槽名列表
  • 没有指定的方法可以通过插槽名称访问插槽
  • 因此在运行时可能没有插槽名称
  • 使用访问器函数优化了对插槽的访问:静态结构布局、内联访问器函数...

同样明确:

  • string= 下的插槽名称不允许重复。因此,同一结构类中的槽foo::abar::a 是不允许的

  • 重新定义结构的效果是不确定的

结构的目标是提供快速的类似记录的对象,而无需重新定义、多重继承等昂贵的功能。

因此,使用SLOT-VALUE 访问结构槽是实现的扩展,而不是定义语言的一部分。 SLOT-VALUE 是在将 CLOS 添加到 Common Lisp 时引入的。一些实现提供了一种通过SLOT-VALUE 访问结构槽的方法。这还要求实现跟踪该结构的插槽名称。

SLOT-VALUE 只是一个更新的 API 函数,来自 CLOS for CLOS。结构是一个较老的特性,它已经在 CLtL1 书定义的 Common Lisp 的第一个版本中定义。

【讨论】:

  • 我不知道这些事情。感谢您的简明细分。
  • 所以,我在这里看到的是 (with-slots) 在结构上具有“未定义的行为”,但在 SBCL 中“恰好起作用”。那么,我可以继续在 SBCL 中使用它吗?或者,编写一个发出 (let ((balance (account-balance instance) ... )-type 构造的宏 (with-struct-slots) 是否更好(并且可行)?
【解决方案2】:

您使用make-instance 创建了一个类实例,然后您显示了一个结构,我很困惑。

结构会自动构建它们的访问器函数。您使用make-account 创建它。然后你会使用account-balance 而不是slot-value

我不知道将make-instance 与结构一起使用的预期行为是什么。虽然它似乎适用于我的 SBCL,但您没有正确使用结构。

(defstruct account 
   (balance))

(make-account :balance 100)
#S(ACCOUNT :BALANCE 100)

(account-balance *)
100

使用类,您可以随意命名访问器函数。

;;(pseudocode)
(defclass bank-account () 
  ((balance :initform nil ;; otherwise it's unbound
            :initarg :balance ;; to use with make-instance :balance
            :accessor balance  ;; or account-balance, as you wish.
)))

(make-instance 'bank-account :balance 200)
#<BANK-ACCOUNT {1009302A33}>

(balance *)
200

对象 #S(ACCOUNT :BALANCE 1000 :CUSTOMER-NAME "John Doe") 中缺少插槽 :BALANCE。

插槽名称实际上是balance,表示使用生成的initargs。使用类对象,错误消息可能不会那么混乱:

当尝试读取插槽的值(插槽值)时,对象 # 中缺少插槽 :BALANCE。

【讨论】:

  • 我的问题主要是(defstruct)。我已经编辑了这个问题,以避免混淆课程的问题。
  • 等等,(defstruct) 为每个字段创建访问器函数?我不知道。有没有办法改变这种行为?
  • 是的,它创建了访问函数和创建函数!您可以要求另一个创建者功能(请参阅食谱),但 AFAIK 没有办法阻止访问器功能的创建。在CLOS你可以。
【解决方案3】:

首先,请参阅Rainer's excellent answer 结构。总结:

defstruct 定义的对象具有命名的访问器函数,而不是命名的槽。此外,defstruct 表单中提到的这些对象的字段名称必须是不同的字符串,因此关键字完全适合在构造函数中使用。在此类对象上对slot-value 的任何使用都取决于实现,实际上是否存在命名槽完全取决于实现。

您通常需要构造函数的关键字参数,原因是您在其他地方需要关键字参数:您不想痛苦地提供 49 个可选参数,因此您可以指定第 50 个。因此,defstruct 所做的默认操作是合理的。但是,如果您愿意,可以使用 BOA 构造函数完全覆盖它,defstruct 允许您这样做。你甚至可以没有构造函数!举个例子,这里有一个相当反常的结构构造函数:它确实使用关键字参数,但不是您可能天真期望的那些。

(defstruct (foo
            (:constructor
             make-foo (&key ((:y x) 1) ((:x y) 2))))
  y
  x)

所以真正的问题是围绕用defclass 定义的类,通常确实 有命名槽以及slot-value 在哪里工作。

所以在这种情况下,答案实际上有两个部分。

首先,和以前一样,关键字参数对构造函数非常有用,因为没有人愿意记住 932 个可选参数默认值。但是defclass 提供了对关键字参数和它们初始化的槽之间的映射的完全控制,或者它们是完全初始化槽还是传递给某个initialize-instance 方法。你可以在这里做任何你想做的事情。

其次,您真的希望使用defclass 定义的类的对象的插槽名称是存在于包中的符号。你绝对不希望这种情况发生:

(in-package "MY-PACKAGE")
(use-package "SOMEONE-ELSES-PACKAGE")

(defclass my-class (someone-elses-class)
  ((internal-implementation-slot ...)))

才发现你刚刚修改了someone-elses-classsomeone-elses-package::internal-implementation-slot槽的定义。那会很糟糕。所以插槽名称是存在于包中的符号,并且包周围的正常命名空间控制也适用于它们:my-package::internal-implementation-slotsomeone-elses-package::internal-implementation-slot (通常)不是一回事。

此外,整个关键字符号参数/非关键字符号变量是,好吧,让我们说完善:

(defun foo (&key (x 1))
  ... x ...)

最后请注意,当然,关键字参数实际上不一定是关键字:它们通常很方便,因为否则您需要引号,但是:

(defclass silly ()
  ((foo :initarg foo
        :accessor silly-foo)
   (bar :initarg bar
        :accessor silly-bar)))

现在

> (silly-foo (make-instance 'silly 'bar 3 'foo 9))
9

【讨论】:

  • 所以.....让我看看我是否有这个权利。结构中的槽实际上是在内部使用符号而不是关键字命名的。也就是说,如果它们在内部被命名为。上课也是一样。因此,当您(make-thing) 时,您可以使用关键字或符号,因为构造函数通过其字符串表示形式查找插槽。但是你(slot-value)的时候一定要用局部符号,因为内部槽名不是关键字?
  • 在这种情况下,为什么(make-thing) 使用字符串表示,而(slot-value) 要求精确的符号?
  • @SodAlmighty:对不起,我想我帮不了你。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-21
  • 1970-01-01
  • 2010-12-26
  • 2012-07-24
  • 1970-01-01
  • 2011-06-08
相关资源
最近更新 更多