【问题标题】:Using dot notation to access CLOS slots使用点符号访问 CLOS 插槽
【发布时间】:2025-12-27 22:55:17
【问题描述】:

访问类槽时,而不是写

(defmethod get-name ((somebody person) (slot-value somebody 'name))

是否可以使用点表示法,即 C++,即

(defmethod get-name ((somebody person) somebody.name) ?

否则,当一个方法中有很多槽操作时,(slot-value... 会创建很多样板代码。

我今天找到了答案,我只是将其作为问答发布,但如果有更好的解决方案或我的解决方案存在问题,请随时添加新的答案或 cmets。

【问题讨论】:

    标签: common-lisp clos


    【解决方案1】:

    access 提供了一个点符号阅读器宏,用于访问槽(以及哈希表和其他东西)。通过调用 (access:enable-dot-syntax) 启用阅读器宏后,您将能够使用#D。使用在其他语言中流行的点语法访问插槽名称。

    (defclass person ()
      ((name :initarg :name :reader name)))
    
    CL-USER> (access:enable-dot-syntax)
    ; No values
    CL-USER> (defvar *foo* (make-instance 'person :name "John Smith"))
    *FOO*
    CL-USER> #D*foo*
    #<PERSON #x302001F1E5CD>
    CL-USER> #D*foo*.name
    "John Smith"
    

    如果您不想使用阅读器宏,还有一个 with-dot

    CL-USER> (access:with-dot () *foo*.name)
    "John Smith"
    

    【讨论】:

    • 感谢您指出这个库。不幸的是,我现在不能尝试, (ql:quickload "access") 会导致错误(我很难记住上次 pacman -S 导致错误是什么时候)。解决这些问题后,我会回复您的答案。
    • 显然,前向兼容性存在一些问题。使用 (ql:update-all-dists) 更新所有软件包解决了这个问题,我现在可以尝试访问库。我喜欢它,因为它是一个库,我不需要自己编写代码和维护任何东西。我不喜欢手头的任务。该库的重点与简单地为类提供点表示法不同,它进行了大量运行时检查。访问对象的槽比我的手动方法慢 149 倍,访问对象的槽的槽慢 174 倍。
    【解决方案2】:

    您不应手动编写访问器,也不应使用slot-value(在对象生命周期函数之外,访问器可能尚未创建)。改用类槽选项:

    (defclass foo ()
      ((name :reader foo-name
             :initarg :name)
       (bar :accessor foo-bar
            :initarg :bar)))
    

    现在您可以使用命名访问器了:

    (defun example (some-foo new-bar)
      (let ((n (foo-name some-foo))
            (old-bar (foo-bar some-foo)))
        (setf (foo-bar some-foo) new-bar)
        (values n old-bar)))
    

    通常,您希望您的类是“不可变的”,然后您会使用 :reader 而不是 :accessor,这只会创建阅读器,而不是 setf 扩展。

    【讨论】:

    • 谢谢,这确实是我想要的。我想我只是没有充分阅读 Practical Common Lisp 中的相应章节 :) 所以我也不知道有 with-slots 和 with-accessors 宏。
    【解决方案3】:

    最简单的解决方案似乎是重载. 的读取器宏,以便(slot-value somebody 'name) 可以写为.somebody.name 我的策略是将somebody.name 读取为字符串(我们需要定义一个非终止宏字符,以便阅读器不会在字符串中间停止),然后处理字符串以构造适当的(slot-value...

    我需要两个辅助函数:

    (defun get-symbol (str)
      "Make an uppercase symbol"
      (intern (string-upcase str)))
    
    (defun split-string (str sep &optional (start 0))
      "Split a string into lists given a character separator"
      (let ((end (position sep str :start start)))
        (cons (subseq str start end) (if end (split-string str sep (1+ end))))))
    

    然后我可以定义我的阅读器宏:

    (defun dot-reader (stream char)
      (declare (ignore char))
      (labels ((make-query (list)
                 (let ((car (car list))
                       (cdr (cdr list)))
                   (if cdr `(slot-value ,(make-query cdr) (quote ,(get-symbol car)))
                       (get-symbol car)))))
        (make-query (nreverse (split-string (symbol-name (read stream)) #\.)))))
    

    最后,我需要注册这个阅读器宏:

    (set-macro-character #\. #'dot-reader t)
    

    现在可以写了:

    (defmethod get-name ((somebody person) .somebody.name)
    

    或者,如果name 本身就是一个类,

    (defmethod get-name ((somebody person) .somebody.name.first-name)
    

    一个限制是 s 表达式在点之间不起作用,比如说

    .(get-my-class).name
    

    不会工作。

    【讨论】:

    • 这是否仍然允许正常的 consing 点,例如(setq cons '(a . b))?
    • @Barmar 有趣的是,consing dot 仍然有效。它接缝阅读器宏在这种情况下根本不被调用。