Как я могу передать объект суперкласса конструктору подкласса?

Допустим, у меня есть класс A с парой слотов:

(defclass a ()
  ((a-1 :initarg :a-1)
   (a-2 :initarg :a-2)))

И класс B, наследуемый от A:

(defclass b (a)
  ((b-1 :initarg :b-1)))

Если я хочу создать экземпляр B, make-instance предложит мне слоты :a-1, :a-2 и :b-1.

Вот сумасшедшая идея: что, если я хочу создать экземпляр B, используя существующий экземпляр A и заполнив только слот b-1?

PS. Почему это может быть полезно: если A реализует какие-то общие методы, которые B наследует напрямую, не добавляя ничего нового. В альтернативном подходе, делая экземпляр A слотом в B, мне нужно было бы написать тривиальные оболочки методов для вызова этих методов в этом слоте.

Единственный способ, который я могу придумать: во вспомогательном конструкторе разложить объект A и передать соответствующие слоты make-instance для B, то есть:

(defun make-b (b-1 a-obj)
  (with-slots (a-1 a-2) a-obj
    (make-instance 'b :b-1 b-1 :a-1 a-1 :a-2 a-2)))

Есть ли лучшие способы сделать это? (а может быть, такой подход приводит к очень плохому дизайну, и мне следует вообще его избегать?)


person mobiuseng    schedule 03.02.2016    source источник
comment
Вы также можете взять экземпляр A и изменить его класс на B, а затем инициализировать добавленные слоты. Обратите внимание, что вам действительно нужно избавиться от мышления «A реализует методы». Это CLOS, где классы/слоты и общие функции/методы немного более независимы.   -  person Rainer Joswig    schedule 03.02.2016
comment
@RainerJoswig Интересная идея изменить класс. Но я не уверен, что полностью понимаю вас в отношениях между классами/слотами и универсальными функциями/методами. Я понимаю, что они вполне независимые сущности. Что может быть альтернативой мышлению «метод внедрения»?   -  person mobiuseng    schedule 03.02.2016
comment
@mobiuseng в clos, дело не в том, что реализует метод f, а в том, что есть метод, определенный для универсальной функции f, специализированной для a. Это более заметная разница, когда вы специализируете методы на нескольких аргументах.   -  person Joshua Taylor    schedule 03.02.2016
comment
Джошуа прав. Плюс: существуют методы, определенные для универсальной функции f, специализированной для a'. Их может быть больше одного.   -  person Rainer Joswig    schedule 03.02.2016
comment
@RainerJoswig Хорошо, понятно. Просто в данном конкретном случае я специализировался только на одном аргументе, так что это выглядело как реализация метода. Путь CLOS немного смещает фокус.   -  person mobiuseng    schedule 03.02.2016


Ответы (2)


Я не думаю, что есть общее решение. Подумайте: что должно произойти, например, если в классе A есть какие-то слоты, которые инициализируются не просто из какого-то :initarg, а, скажем, во время initialize-instance или shared-initialize?

Тем не менее, пока вы контролируете все задействованные классы, вы можете попробовать

  • сделать протокол, реализованный A, что-то вроде строк

    (defgeneric initargs-for-copy (object)
      (:method-combination append)
      (:method append (object) nil))
    
    (defmethod initargs-for-copy append ((object a))
      (list :a-1 (slot-value object 'a-1) :a-2 (slot-value object 'a-2)))
    
    (defun make-b (b-1 a-obj)
      (apply #'make-instance 'b :b-1 b-1 (initargs-for-copy a-obj)))
    
  • используйте MOP для извлечения слотов во время выполнения (для этого могут потребоваться знания о реализации Lisp по вашему выбору или помощь какой-либо библиотеки, например closer-mop, доступной через быстрый ответ)

    (defun list-init-args (object)
      (let* ((class (class-of object))
             (slots (closer-mop:class-slots class)))
        (loop
          for slot in slots
          for name = (closer-mop:slot-definition-name slot)
          for keyword = (closer-mop:slot-definition-initargs slot)
          when (and keyword (slot-boundp object name))
            nconc (list (car keyword) (slot-value object name)))))
    
    (defun make-b (b-1 a-obj)
       (apply #'make-instance 'b :b-1 b-1 (list-init-args a-obj)))
    
  • используйте change-class, чтобы преобразовать экземпляр A в B экземпляр разрушительно.

В любом случае: я не уверен, действительно ли ваш вариант использования требует наследования. Композиционный подход кажется (с точки зрения дизайна) здесь более понятным. Помимо того, что B наследует некоторые общие реализации методов через A: действительно ли экземпляры B считаются правильными экземплярами A в вашем реальном приложении (т. е. существует ли отношение is-a?)? Или вы просто пытаетесь избежать необходимости предоставлять здесь обертки?

person Dirk    schedule 03.02.2016
comment
Спасибо за ответ! Я не думал о нетривиальной инициализации... Я пытаюсь определить отношения между строительными блоками, чтобы правильно их составить. Таким образом, я подумал об этой идее. Пока кажется, что композиция будет работать лучше, чем наследование. - person mobiuseng; 03.02.2016

То, что вы пытаетесь сделать, можно сделать, используя композицию как форму прототипного наследования, когда объект «наследуется» от другого экземпляра.

(defclass prototype-mixin ()
  ((parent :initarg :parent :initform nil :accessor parent)))

(defmethod slot-unbound (c (p prototype-mixin) slot)
  (declare (ignore c))
  (let ((parent (parent p)))
    (if parent
      (slot-value parent slot)
      (call-next-method))))

Теперь вы определяете два класса:

(defclass a ()
  ((slot :initarg :slot)))

(defclass b (a prototype-mixin) 
  ((other :initarg :other)))

Когда вы создаете b из существующего экземпляра a, вы устанавливаете слот parent b на a. Поскольку b также является a, в b есть несвязанный slot. Когда вы пытаетесь получить доступ к этому слоту, вы получаете доступ к слоту, присутствующему в «родительском» объекте, который является экземпляром a. Но если вы хотите, вы можете переопределить значение в b.

Этот подход вдохновлен публикацией Эрика Наггума на comp. яз.лисп.

person coredump    schedule 03.02.2016