M.Hiroi's Home Page

Common Lisp Programming

お気楽 CLOS プログラミング入門

[ PrevPage | CLOS | NextPage ]

メソッド結合

今回は「メソッド結合 (method combination)」について説明します。メソッド結合とは、複数のメソッドを組み合わせて使うための機能です。継承 で説明しましたが、スーパークラスのメソッドは call-next-method で呼び出すことができます。一般的なオブジェクト指向言語の場合、「メソッド結合」はスーパークラスのメソッドを呼び出す機能のことを指すようです。

CLOS の場合、メソッド結合は call-next-method だけではありません。実は、CLOS には「基本メソッド」と「補助メソッド」という 2 種類のメソッドがあります。補助メソッドは基本メソッドを補助するためのメソッドで、基本メソッドが評価される前後で自動的に補助メソッドが評価されるのです。この機能には M.Hiroi も驚きました。

補助メソッドは defmethod で「メソッド修飾子」 :before, :after, :around のどれかひとつを指定すると、そのメソッドは補助メソッドになります。メソッド修飾子の指定がない場合は基本メソッドになります。まず最初に、:before メソッドと :after メソッドについて説明します。

●:before メソッドと :after メソッド

:before メソッドは基本メソッドの前に評価される補助メソッドで、:after メソッドは逆に基本メソッドのあとに評価される補助メソッドです。簡単な例を示しましょう。次のリストを見てください。

リスト : :before メソッドと :after メソッド

;;; クラス定義
(defclass foo1 () ())
(defclass foo2 (foo1) ())
(defclass foo3 (foo2) ())

;;; 基本メソッド
(defmethod bar ((x foo1)) (format t "foo1 bar~%"))
(defmethod bar ((x foo2)) (format t "foo2 bar~%"))
(defmethod bar ((x foo3)) (format t "foo3 bar~%"))

;;; :after メソッド
(defmethod bar :after ((x foo1)) (format t "foo1 bar after~%"))
(defmethod bar :after ((x foo2)) (format t "foo2 bar after~%"))
(defmethod bar :after ((x foo3)) (format t "foo3 bar after~%"))

;;; :before メソッド
(defmethod bar :before ((x foo1)) (format t "foo1 bar before~%"))
(defmethod bar :before ((x foo2)) (format t "foo2 bar before~%"))
(defmethod bar :before ((x foo3)) (format t "foo3 bar before~%"))

クラス FOO1, FOO2, FOO3 と基本メソッド bar を定義します。補助メソッドの定義は、defmethod でメソッド名の後ろにメソッド修飾子を指定するだけです。このとき、複数のメソッド修飾子を指定することはできません。ご注意ください。これで各クラスの基本メソッド bar に :after メソッドと :before メソッドが定義されます。

それでは実際に実行してみましょう。次の例を見てください。

* (setq x1 (make-instance 'foo1))

#<FOO1 {1001EB2513}>
* (bar x1)
foo1 bar before
foo1 bar
foo1 bar after
NIL

クラス FOO1 のインスタンスを生成して、メソッド bar を評価します。最初に :before メソッドが評価され、次に基本メソッド、最後に :after メソッドが評価されます。この場合、基本メソッドの返り値が (bar x1) の返り値になります。このとき、:before メソッドと :after メソッドの返り値は無視されます。

次はクラス FOO2, FOO3 のインスタンスを生成して、メソッド bar を評価してみましょう。結果は次のようになります。

* (setq x2 (make-instance 'foo2))

#<FOO2 {1001FA7F03}>
* (bar x2)
foo2 bar before
foo1 bar before
foo2 bar
foo1 bar after
foo2 bar after
NIL
* (setq x3 (make-instance 'foo3))

#<FOO3 {1001FAC243}>
* (bar x3)
foo3 bar before
foo2 bar before
foo1 bar before
foo3 bar
foo1 bar after
foo2 bar after
foo3 bar after
NIL

:before メソッドと :after メソッドは、自動的にスーパークラスの :before メソッドと :after メソッドを呼び出すことに注意してください。したがって、:before メソッドや :after メソッド内で call-next-method を使う必要はありません。実際に call-next-method を呼び出すとエラーが通知されます。

クラス FOO2 のインスタンスを生成してメソッド bar 呼び出すと、最初に FOO2 の :before メソッドが評価され、次に FOO1 の :before メソッドが評価されます。つまり、「クラス優先順位リスト」に従って特定的なメソッドから順番に評価されます。

:after メソッドは、評価順序が :before メソッドの逆になることに注意してください。つまり、クラス優先順位リストの逆順に評価されます。したがって、FOO1 の :after メソッドが評価されてから、次に FOO2 の :after メソッドが評価されます。

クラス FOO3 のインスタンスを生成してメソッド bar を評価すると、:before メソッドは FOO3 -> FOO2 -> FOO1 の順番で評価されます。:after メソッドは逆に FOO1 -> FOO2 -> FOO3 の順番で評価されます。

●:around メソッド

基本メソッドを評価するとき、:before メソッドと :after メソッドが自動的に評価されるという機能はとてもユニークだと思います。AWK や Perl をご存知の方であれば、BEGIN と END という機能を思い浮かべたかもしれませんね。最後の :around メソッドも、非常に面白い機能です。次のリストを見てください。

リスト : :around メソッド

;;; クラス定義
(defclass foo1 () ())
(defclass foo2 (foo1) ())
(defclass foo3 (foo2) ())

;;; 基本メソッド
(defmethod bar ((x foo1)) (format t "foo1 bar~%") 'foo1)
(defmethod bar ((x foo2)) (format t "foo2 bar~%") 'foo2)
(defmethod bar ((x foo3)) (format t "foo3 bar~%") 'foo3)

;;; :after メソッド
(defmethod bar :after ((x foo1)) (format t "foo1 bar after~%"))
(defmethod bar :after ((x foo2)) (format t "foo2 bar after~%"))
(defmethod bar :after ((x foo3)) (format t "foo3 bar after~%"))

;;; :before メソッド
(defmethod bar :before ((x foo1)) (format t "foo1 bar before~%"))
(defmethod bar :before ((x foo2)) (format t "foo2 bar before~%"))
(defmethod bar :before ((x foo3)) (format t "foo3 bar before~%"))

;;; :around メソッド
(defmethod bar :around ((x foo1)) (format t "foo1 bar around~%") (call-next-method))
(defmethod bar :around ((x foo2)) (format t "foo2 bar around~%") (call-next-method))
(defmethod bar :around ((x foo3)) (format t "foo3 bar around~%") (call-next-method))

クラス定義、:before メソッド、:after メソッドは最初の例題と同じですが、基本メソッド bar はクラス名のシンボルを返すように変更しています。:around メソッドは :before メソッドよりも先に評価される補助メソッドです。:around メソッドは :before メソッドと :after メソッドとは違い、自動的にスーパークラスの :around メソッドや基本メソッドを呼び出すことはしません。次の例を見てください。

* (setq x1 (make-instance 'foo1))

#<FOO1 {100224F603}>
* (bar x1)
foo1 bar around
foo1 bar before
foo1 bar
foo1 bar after
FOO1

クラス FOO1 のインスタンスを生成してメソッド bar を評価します。:around メソッドが定義されている場合、まず最初に :around メソッドが評価されます。:around メソッド内で call-next-method が評価されると、クラス優先順位リストに従って次の :around メソッドが評価されます。

もしも、call-next-method で次に評価する :around メソッドがない場合は、基本メソッドが評価されます。このとき、:before メソッドや :after メソッドが定義されていれば、いままでと同様に :before メソッドが先に評価され、次に基本メソッド、最後に :after メソッドが評価されます。

FOO1 の :around メソッドで call-next-method が評価されると、次に評価する :around メソッドはもうありません。そこで、bar の :before メソッドが評価され、その次に基本メソッド、そして最後に :after メソッドが評価されます。

それから、(bar x1) の返り値は基本メソッドの返り値ではなく、:around メソッドの返り値になります。この場合は call-next-method の値が返されるので、call-next-method によって評価された基本メソッドの返り値 FOO1 になります。

もしも、:around メソッドで call-next-method の評価が行われないと、そのメソッドを評価するだけで他のメソッドは評価されないことに注意してください。たとえば、FOO1 の :around メソッドで call-next-method を削除すると、基本メソッドの呼び出しは行われません。:around メソッドには call-next-method が必要であることを覚えておいてください。

それでは、クラス FOO2 と FOO3 のインスタンスを生成して、メソッド bar を評価してみましょう。結果は次のようになります。

* (setq x2 (make-instance 'foo2))

#<FOO2 {10023388B3}>
* (bar x2)
foo2 bar around
foo1 bar around
foo2 bar before
foo1 bar before
foo2 bar
foo1 bar after
foo2 bar after
FOO2
* (setq x3 (make-instance 'foo3))

#<FOO3 {100233D163}>
* (bar x3)
foo3 bar around
foo2 bar around
foo1 bar around
foo3 bar before
foo2 bar before
foo1 bar before
foo3 bar
foo1 bar after
foo2 bar after
foo3 bar after
FOO3

クラス FOO2 のインスタンスを生成してメソッド bar を呼び出します。最初に、FOO2 の :around メソッドが評価され、call-next-method により FOO1 の :around メソッドが評価されます。そして、FOO1 の :around メソッドで call-next-method が評価され、基本メソッドが評価されます。このとき、:before メソッドと :after メソッドが定義されているので、:before メソッド -> 基本メソッド -> :after メソッドの順番で評価されます。

(bar x2) の返り値は FOO2 の :around メソッドの返り値になります。この場合、最後に評価された call-next-method の値になります。この値は FOO1 の :around メソッドの返り値なので、けっきょく基本メソッドの返り値 FOO2 が (bar x2) の返り値になります。

クラス FOO3 の場合は foo3 -> foo2 -> foo1 の順番で :around メソッドが評価され、そのあとで基本メソッドが評価されます。:before メソッドと :after メソッドが定義されているので、:before メソッド -> 基本メソッド -> :after メソッドの順番で評価されます。

●補助メソッドはアクセスメソッドにも定義できる

ところで、スロットにアクセスするため defclass の :accessor でメソッドを指定しますが、このメソッドにも補助メソッドを定義することができます。次のリストを見てください。

リスト : アクセスメソッドの定義

;;; クラス定義
(defclass foo () ((a :accessor foo-a :initform 1 :initarg :a)))

;;; 読み込み用補助メソッド
(defmethod foo-a :after ((x foo)) (format t "foo-a after~%"))
(defmethod foo-a :before ((x foo)) (format t "foo-a before~%"))

;;; 書き込み用補助メソッド
(defmethod (setf foo-a) :after (data (x foo)) (format t "setf foo-a after~%"))
(defmethod (setf foo-a) :before (data (x foo)) (format t "setf foo-a before~%"))

foo-a はスロット a にアクセスするメソッドです。メソッド foo-a の補助メソッドを定義するのは簡単ですね。これで (foo-a x) のようにメソッドが評価されると、:before メソッドと :after メソッドが評価されます。

ところが、(setf (foo-a x) 10) のようにスロットに値をセットする場合、メソッド foo-a ではなく書き込み用のメソッドが評価されます。このメソッドの名前は (setf foo-a) という特別な形式で、第 1 引数が書き込むデータ、第 2 引数がインスタンスになります。このメソッドに補助メソッドを定義すると、(setf (foo-a x) 10) を評価したときに、:before メソッドと :after メソッドが評価されます。

それでは、実際に試してみましょう。

* (setq x (make-instance 'foo))

#<FOO {1001D22C93}>
* (foo-a x)
foo-a before
foo-a after
1
* (setf (foo-a x) 10)
setf foo-a before
setf foo-a after
10

クラス FOO のインスタンスを生成して変数 X にセットします。(foo-a x) を評価すると、:before メソッドと :after メソッドが評価され、スロットの値 1 が返されます。次に、(setf (foo-a x) 10) を評価すると、:before メソッドと :after メソッドが評価され、スロットに書き込んだ値 10 が返されます。このように、アクセスメソッドにも補助メソッドを定義することができます。


インスタンスの初期化

CLOS は make-instance でクラスのインスタンスを生成します。スロットの初期値は make-instance でセットすることができますが、このほかに総称関数 initialize-instance を使ってインスタンスの初期化を行うことができます。今回はインスタンスの初期化について説明します。

●initialize-instance

initialize-instance は make-instance から呼び出される総称関数です。インスタンスを生成するとき、独自の初期化処理を行いたい場合は initialize-instance を使うと便利です。

initialize-instance instance &rest initargs

initialize-instance はインスタンスが生成されてから呼び出されます。このとき、スロットは未束縛であることに注意してください。スロットの初期値は :initform や :initarg を使って設定することができますが、この処理は CLOS が提供する initialize-instance の基本メソッドで行われます。したがって、単純に initialize-instance をオーバーライドすると、スロットは未束縛のままになってしまいます。次の例を見てください。

リスト : initialize-instance の例 (1)

;;; クラス定義
(defclass foo () ((a :accessor foo-a :initform 1 :initarg :a)))

;;; initialize-instance をオーバーライドする
(defmethod initialize-instance ((x foo) &rest initargs)
  (format t "init-args ~S~%" initargs)
  (if (slot-boundp x 'a)
      (format t "slot a is ~S~%" (foo-a x))
      (format t "slot a is unbound~%")))

この例はクラス foo の initialize-instance をオーバーライドしています。slot-boundp はスロットが束縛されているかチェックする関数です。

slot-boundp instance slot-name

slot-name はスロット名を表すシンボルです。instance のスロット slot-name が束縛されていれば T を、未束縛であれば NIL を返します。

ここで、スロットを操作するときに便利な関数を紹介しましょう。スロットを未束縛にする関数が slot-makunbound です。

slot-makunbound instance slot-name

slot-makunbound は instance のスロット slot-name を未束縛の状態にします。slot-makunbound は instance を返します。

任意のオブジェクトにスロットがあるかチェックする関数が slot-exists-p です。

slot-exists-p object slot-name

slot-exists-p はオブジェクト object にスロット slot-name が存在すれば T を、なければ NIL を返します。

それでは、クラス foo のインスタンスを生成してみましょう。

* (setq z (make-instance 'foo :a 10))
init-args (:A 10)
slot a is unbound
#<FOO {1001D3F3E3}>
* (slot-boundp z 'a)

NIL

make-instance からオーバライドした initialize-instance が呼び出されますが、このときスロット A は未束縛の状態です。このあと、生成されたインスタンスのスロット A を slot-boundp でチェックすると NIL が返ってきます。このように、initialize-instance をオーバーライドすると、:initform や :initarg で指定したスロットの初期化処理が行われないのです。

この場合、基本メソッドをオーバーライドするのではなく、次のように :after メソッドとして定義するとよいでしょう。

リスト : initialize-instance の例 (2)

;;; :after メソッド
(defmethod initialize-instance :after ((x foo) &rest initargs)
  (format t "init-args ~S~%" initargs)
  (if (slot-boundp x 'a)
      (format t "slot a is ~S~%" (foo-a x))
      (format t "slot a is unbound~%")))

initialize-instance は通常のメソッドと同様に補助メソッド (:before, :after, :around) を定義することができます。実行例は次のようになります。

* (setq z (make-instance 'foo :a 10))
init-args (:A 10)
slot a is 10
#<FOO {1001DB8C63}>
* (foo-a z)

10

:after メソッドは基本メソッドのあとで呼び出される補助メソッドです。したがって、:after メソッドが評価されるときには、スロット A の値は既に :A で指定した 10 に初期化されているのです。

●ベクタによるキューの実装

それでは簡単な例題として、ベクタを使って「キュー (queue)」を実装してみましょう。Common Lisp 入門 : 構造体 ではキューの定義に構造体を使いましたが、今回は CLOS でプログラムを作ってみます。

最初に、キューを操作するためのメソッドを示します。

次に、キューを表すクラスを定義します。

リスト : クラス QUEUE の定義

(defclass queue ()
  ((front :accessor queue-front :initform 0)
   (rear  :accessor queue-rear  :initform 0)
   (count :accessor queue-count :initform 0)
   (size  :accessor queue-size  :initform 16 :initarg :size)
   (buffer :accessor queue-buffer)))

;;; 初期化
(defmethod initialize-instance :after ((x queue) &rest initargs)
  (setf (queue-buffer x) (make-array (queue-size x))))

スロット COUNT はキューに格納されたデータ数をカウントします。この変数を用意することで、キューの状態を簡単にチェックすることができます。スロット SIZE はキューの大きさを表し、スロット BUFFER にはベクタをセットします。ベクタは initialize-instance で生成してスロット BUFFER にセットします。これで make-instance で :size を指定するだけで、その大きさのキューを簡単に作ることができます。

あとのメソッドは簡単なので説明は省略いたします。詳細は プログラムリスト をお読みくださいませ。

それでは簡単な実行例を示します。

* (setq z (make-instance 'queue))

#<QUEUE {1001EAFE63}>
* (queue-size z)

16
* (dotimes (x 10) (enqueue z x))

NIL
* (dotimes (x 10) (print (dequeue z)))

0
1
2
3
4
5
6
7
8
9
NIL
* (emptyp z)

T
* (fullp z)

NIL

正常に動作していますね。この実行例では make-instance で :size を指定していないので、キューの大きさはスロット SIZE の :initform の値 (16) になります。


●プログラムリスト

;;;
;;; queue_clos.lisp : 簡単なリングバッファの実装 (CLOS バージョン)
;;;
;;;                   Copyright (C) 2003-2020 Makoto Hiroi
;;;

;;; クラス定義
(defclass queue ()
  ((front :accessor queue-front :initform 0)
   (rear  :accessor queue-rear  :initform 0)
   (count :accessor queue-count :initform 0)
   (size  :accessor queue-size  :initform 16 :initarg :size)
   (buffer :accessor queue-buffer)))

;;; 初期化
(defmethod initialize-instance :after ((x queue) &rest initargs)
  (declare (ignore initargs))
  (setf (queue-buffer x) (make-array (queue-size x))))

;;; キューに追加
(defmethod enqueue ((q queue) data)
  (when (< (queue-count q) (queue-size q))
    (setf (aref (queue-buffer q) (queue-rear q)) data)
    (incf (queue-count q))
    (incf (queue-rear q))
    (if (= (queue-size q) (queue-rear q))
        (setf (queue-rear q) 0))
    t))

;;; キューから取り出す
(defmethod dequeue ((q queue))
  (when (plusp (queue-count q))
    (prog1
      (aref (queue-buffer q) (queue-front q))
      (decf (queue-count q))
      (incf (queue-front q))
      (if (= (queue-size q) (queue-front q))
          (setf (queue-front q) 0)))))

;;; データをリード(削除しない)
(defmethod front ((q queue))
  (when (plusp (queue-count q))
    (aref (queue-buffer q) (queue-front q))))

;;; キューが空か
(defmethod emptyp ((q queue))
  (zerop (queue-count q)))

;;; キューが満杯か
(defmethod fullp ((q queue))
  (= (queue-count q) (queue-size q)))

;;; キューを空にする
(defmethod clear ((q queue))
  (setf (queue-rear q)  0
        (queue-front q) 0
        (queue-count q) 0))

共有スロット

CLOS の場合、インスタンス中のスロットは同じクラスのインスタンスでも別々のメモリ領域に割り当てられます。たとえば、クラス FOO にスロット A, B がある場合、make-instance でインスタンス X1, X2 を生成すると、X1 と X2 のスロット A, B は異なるメモリ領域に割り当てられます。CLOS では、これを「局所スロット」といいます。オブジェクト指向プログラミングの場合、インスタンスは個々のオブジェクトを表しているので、スロットがインスタンスごとに別々のメモリ領域に割り当てられるのは当然といえるでしょう。

ところが、プログラムによっては、同じクラスのインスタンスで共通の変数や定数を使いたい場合があります。つまり、インスタンスごとにスロットを用意するのではなく、クラス単位でスロットを用意するのです。CLOS では、これを「共有スロット」といいます。今回は共有スロットについて説明します。

●共有スロットの設定

共有スロットの設定は、スロットオプション :allocation にキーワード :class を指定します。:allocation の指定がない場合、もしくはキーワード :instance を指定すると、スロットは共有されず「局所スロット」になります。

簡単な例を示しましょう。次のリストを見てください。

リスト : 共有スロットの定義

(defclass foo ()
  ((a :accessor foo-a :initarg :a :allocation :class)
   (b :accessor foo-b :initform 1 :initarg :b)))

クラス FOO にはスロット A, B がありますが、スロット A が共有スロットで、スロット B が局所スロットになります。局所スロット B はインスタンスごとにメモリ領域が割り当てられますが、共有スロット A のメモリ領域はクラスでひとつしかありません。次の例を見てください。

* (setq x1 (make-instance 'foo :a 0 :b 10))

#<FOO {1001BE2233}>
* (setq x2 (make-instance 'foo :b 20))

#<FOO {1001BE4BA3}>
* (foo-a x1)

0
* (foo-a x2)

0
* (foo-b x1)

10
* (foo-b x2)

20
* (setf (foo-a x1) 100)

100
* (foo-a x2)

100

最初に、インスタンス X1 を生成します。ここでスロット A を 0 に、B を 10 に初期化します。次にインスタンス X2 を生成し、スロット B を 20 に初期化します。スロット A は共有スロットなので、A の値は X1 を生成したときの値 0 になります。当然ですが、(foo-a x1) と (foo-a x2) は同じ値になり、(foo-b x1) と (foo-b x2) は異なる値になります。

また、(setf (foo-a x1) 100) のように共有スロットの値を書き換えると、(foo-a x2) の値は書き換えた値 100 になります。このように、スロットが共有されていることがわかります。

●共有スロットの継承

ところで、CLOS の継承はスロットやメソッドだけではなく、:accessor, :initform, :initarg などのスロットオプションも継承されますが、:allocation オプションも継承されることに注意してください。次のリストを見てください。

リスト : 共有スロットの継承

(defclass foo ()
  ((a :accessor foo-a :initarg :a :allocation :class)
   (b :accessor foo-b :initform 1 :initarg :b)))

(defclass foo1 (foo)
  ((c :accessor foo-c :initform 2 :initarg :c)))

クラス FOO を継承してクラス FOO1 を定義します。FOO1 のスロットは A, B, C の 3 つになりますが、スロット A は共有スロットになります。このとき、スロット A は FOO1 だけはなく、FOO と FOO1 の共有スロットになります。簡単な例を示しましょう。

* (setq x3 (make-instance 'foo1))

#<FOO1 {1001D3B8A3}>
* (foo-a x3)

100
* (foo-b x3)

1
* (foo-c x3)

2
* (setf (foo-a x3) 200)

200
* (foo-a x1)

200
* (foo-a x2)

200
* (foo-a x3)

200

クラス FOO のインスタンスが変数 X1, X2 にセットされている状態で、クラス FOO1 のインスタンスを生成して変数 X3 にセットします。X3 のスロット A は共有スロットなので、X1, X2 と同じ値 (100) になります。スロット B, C は局所スロットなので、:initform の値で初期化されます。ここで、(setf (foo-a x3) 200) とスロット A の値を書き換えると、(foo-a x1) と (foo-a x2) の値は 200 になります。スロット A はクラス FOO と FOO1 で共有されていることがわかります。

●共有スロットの衝突

それでは、サブクラスにスーパークラスと同じ名前の共有スロットを定義したらどうなるのでしょうか。次のリストを見てください。

リスト : 同名の共有スロットがある場合

(defclass foo ()
  ((a :accessor foo-a :initarg :a :allocation :class)
   (b :accessor foo-b :initform 1 :initarg :b)))

(defclass foo2 (foo)
  ((a :accessor foo2-a :initarg :a :allocation :class)
   (c :accessor foo2-c :initform 3 :initarg :c)))

クラス FOO を継承してクラス FOO2 を定義します。FOO2 でも共有スロット A を定義していることに注意してください。この場合、FOO のスロット A と FOO2 のスロット A は共有されません。つまり、FOO のスロット A は FOO の共有スロットであり、FOO2 のスロット A は FOO2 の共有スロットになるのです。次の例を見てください。

* (setq x1 (make-instance 'foo :a 100))

#<FOO {1001C265E3}>
* (setq x2 (make-instance 'foo2 :a 200))

#<FOO2 {1001C2B263}>
* (foo-a x1)

100
* (foo2-a x2)

200
* (foo-a x2)

200

FOO のインスタンスを生成して変数 X1 にセットします。このとき、共有スロット A を 100 に初期化しています。次に、FOO2 のインスタンスを生成して変数 X2 にセットします。共有スロット A は 200 に初期化していることに注意してください。そして、インスタンス X1 と X2 のスロット A の値を求めるてみると 100 と 200 になります。

このように、FOO と FOO2 のスロット a は共有されません。FOO2 のインスタンス中のスロット A は FOO2 の共有スロットであり、(foo-a x2) としても FOO の共有スロット A にアクセスすることはできません。つまり、FOO の共有スロット A はサブクラス FOO2 の共有スロット A に「隠蔽 (シャドウ)」されるわけです。

●局所スロットと共有スロットの衝突

今度は、スーパークラスの共有スロットと同じ名前の局所スロットがある場合を考えてみます。この場合、:allocation オプションはサブクラスの指定が優先されます。次のリストを見てください。

リスト : 局所スロットと共有スロットの衝突 (1)

(defclass foo ()
  ((a :accessor foo-a :initarg :a :allocation :class)
   (b :accessor foo-b :initform 1 :initarg :b)))

(defclass foo3 (foo)
  ((a :accessor foo3-a :initarg :a :allocation :instance)
   (c :accessor foo3-c :initform 3 :initarg :c)))

クラス FOO を継承してクラス FOO3 を定義します。FOO のスロット A は共有スロットですが、FOO3 のスロット A は局所スロットであることに注意してください。この場合、クラス FOO のインスタンスのスロット A は共有スロットになりますが、クラス FOO3 のインスタンスのスロット A は局所スロットになります。簡単な例を示しましょう。

* (setq x1 (make-instance 'foo :a 10 :b 20))

#<FOO {1001C26A53}>
* (setq x2 (make-instance 'foo :b 30))

#<FOO {1001C293E3}>
* (setq y1 (make-instance 'foo3 :a 100 :c 200))

#<FOO3 {1001C2D6D3}>
* (setq y2 (make-instance 'foo3 :a 300 :c 400))

#<FOO3 {1001C30A23}>
* (foo-a x1)

10
* (foo-a x2)

10
* (foo-a y1)

100
* (foo-a y2)

300

クラス FOO のインスタンスを生成して変数 X1, X2 にセットし、クラス FOO3 のインスタンスを生成して変数 Y1, Y2 にセットします。FOO のスロット A は共有スロットなので、(foo-a x1) と (foo-a x2) は同じ値 (10) になります。ところが、FOO3 のスロット A は局所スロットになるので、make-instance で指定した値に初期化されます。したがって、(foo-a y1) は 100 になり、(foo-a y2) は 300 になります。

逆に、スーパークラスのスロット A が局所スロットで、サブクラスのスロット A が共有スロットの場合、サブクラスのスロット A は共有スロットになります。次のリストを見てください。

リスト : 局所スロットと共有スロットの衝突 (2)

(defclass bar ()
  ((a :accessor bar-a :initform 0 :initarg :a)
   (b :accessor bar-b :initform 1 :initarg :b)))

(defclass bar1 (bar)
  ((a :accessor bar1-a :initarg :a :allocation :class)
   (c :accessor bar1-c :initform 2 :initarg :c)))

クラス BAR のスロット A, B は局所スロットです。BAR を継承してクラス BAR1 を定義します。このとき、スロット A を共有スロットとして定義します。BAR のインスタンス中のスロット A は局所スロットになりますが、BAR1 のスロット A は共有スロットになります。簡単な実行例を示しましょう。

* (setq x1 (make-instance 'bar :a 10 :b 20))

#<BAR {1001D549B3}>
* (setq x2 (make-instance 'bar :a 30 :b 40))

#<BAR {1001D56B13}>
* (setq y1 (make-instance 'bar1 :a 100 :b 200 :c 300))

#<BAR1 {1001D5AA73}>
* (setq y2 (make-instance 'bar1 :b 400 :c 500))

#<BAR1 {1001D5D1F3}>
* (bar-a x1)

10
* (bar-a x2)

30
* (bar-a y1)

100
* (bar-a y2)

100
* (bar1-a y2)

100

クラス BAR のインスタンス X1, X2 のスロット A は局所スロットで、BAR1 のインスタンス Y1, Y2 のスロット A は共有スロットになっていることがわかります。ようするに、スロットオプション :allocation の設定は「クラス優先順位リスト」に従って決定されるのです。これは多重継承でも同じです。次のリストを見てください。

リスト : 局所スロットと共有スロットの衝突 (3)

(defclass baz1 ()
	  ((a :accessor baz1-a :initarg :a)))

(defclass baz2 ()
	  ((a :accessor baz2-a :initarg :a :allocation :class)))

(defclass baz3 (baz1 baz2) ())

(defclass baz4 (baz2 baz1) ())

クラス BAZ1 のスロット A は局所スロットで、BAZ2 のスロット A は共有スロットです。この 2 つのクラスを多重継承して、クラス BAZ3 と BAZ4 を作成します。この場合、クラス優先順位リスト(この場合は左優先則)に従って、BAZ3 のスロット A は局所スロット、BAZ4 のスロット A は共有スロットになります。実行例は次のようになります。

* (setq x1 (make-instance 'baz3 :a 10))

#<BAZ3 {1001E0AA73}>
* (setq x2 (make-instance 'baz3 :a 20))

#<BAZ3 {1001E0D783}>
* (setq y1 (make-instance 'baz4 :a 30))

#<BAZ4 {1001E102B3}>
* (setq y2 (make-instance 'baz4))

#<BAZ4 {1001E11903}>
* (baz1-a x1)

10
* (baz1-a x2)

20
* (baz1-a y1)

30
* (baz1-a y2)

30

BAZ3 のインスタンス X1, X2 のスロット A の値は 10, 20 になるので、局所スロットであることがわかります。次に、BAZ4 のインスタンス Y1, Y2 を生成します。Y1 のスロット A の値は 30 で、Y2 のスロット A も 30 なので、共有スロットであることがわかります。このように、スロット名が衝突した場合、:allocation の設定は「クラス優先順位リスト」に従って決定されます。


Copyright (C) 2003-2020 Makoto Hiroi
All rights reserved.

[ PrevPage | CLOS | NextPage ]