3.6. サブクラスと継承 (2)

今度は次の例を見てください。

CL-USER 40 > (defclass figurine ()
               ((potter :accessor made-by :initarg :made-by)
                (comes-from :initarg :made-in)))
#<STANDARD-CLASS FIGURINE 205FBD1C>
 
CL-USER 41 > (defclass figurine-aardvark (aardvark figurine)
               ((name :reader aardvark-name :initarg :aardvark-name)
                (diet :initform nil)))
#<STANDARD-CLASS FIGURINE-AARDVARK 205FF354>
 
CL-USER 42 >

figurine-aardvark クラスは二つのダイレクトスーパークラス(直接継承を指定するスーパークラス)から振る舞いを継承しています。当然、このクラスのインスタンスはこれらのスーパークラスと、さらにそのスーパークラスインスタンスになります。


図 3-3. 二つのダイレクトスーパークラスを継承する figurine-aardvark

このような継承を「多重継承」と呼びます。CLOSの非常に便利な機能で、他のどんなオブジェクトシステムでもサポートしていません*1。例えばJavaでは、継承できるスーパークラスは一つであり、それ以外の方法での継承は厳しく制限されています。CLOSの多重継承では、好きなだけスーパークラスを指定しても均等に継承されます。今あなたが目にしているオブジェクトシステムは、多重継承を完全にサポートしているのです。
ここで気をつけなければならないことがあります。CLOSのクラスがすべて standard-object を継承しているということは、多重継承の仕方によっては「継承のループ」に陥ります。継承階層は一本道にはならず(トポロジカルソート)、継承した結果は次の要素に依存します。

figurine-aardvark に戻りましょう。次の例はクラス継承階層を示します。

CL-USER 42 > (class-precedence-list (find-class 'figurine-aardvark))
(#<STANDARD-CLASS FIGURINE-AARDVARK 2150938C> #<STANDARD-CLASS AARDVARK 2150A5D4>
 #<STANDARD-CLASS MAMMAL 2150A894> #<STANDARD-CLASS ANIMAL 2150BA0C>
 #<STANDARD-CLASS FIGURINE 2150A06C> #<STANDARD-CLASS STANDARD-OBJECT 20305B4C>
 #<BUILT-IN-CLASS T 20305AEC>)
 
CL-USER 43 >

figurine-aardvark のスロットを見てみましょう。

legs
animal から継承。
comes-from
animalfigurine から継承。
diet
mammal から継承し、figurine-aardvark のダイレクトスロット(ダイレクトスーパークラスから継承したスロット)。
cute-p
aardvark から継承。
potter
figurine から継承。
name
figurine-aardvark のダイレクトスロット。

同名のスロットが複数のクラス継承階層に現れると何が起きるのでしょうか?答えは、同名のスロットが一つだけサブクラスに定義されます。このスロットのプロパティは、クラス継承階層に登場した複数のスロットのすべてのプロパティを組み合わせたものになります。各オプションの多重継承の規則を次に示します。

:accessor, :reader
アクセサとリーダはすべてのスロットから継承されます。実際の挙動は4章で説明します。
:initarg
初期化キーワード引数名もすべてのスロットから継承されます。例えば figurine-aardvark のスロット comes-from:initargs は、:comes-from:made-in になります。
:initform
最も自明の初期値、つまりクラス継承階層の中で最初に現れた :initform が継承されます。例えば、figurine-aardvark のスロット diet:initformnil になります。
:allocation
継承されません。このプロパティはスロットの定義されたクラスでのみ有効です。サブクラスでのデフォルトは :instance になります。

例:

CL-USER 43 > (setf Eric (make-instance 'figurine-aardvark
                                       :legs 4
                                       :made-by "Jen"
                                       :made-in "Brittany"
                                       :aardvark-name "Eric"))
#<FIGURINE-AARDVARK 206108BC>
 
; cute-p の初期値は nil
CL-USER 44 > (shiftf (cute-p Eric) t)
NIL
 
; diet の初期値も nil
CL-USER 45 > (slot-value Eric 'diet)
NIL
 
CL-USER 46 >

継承は間違えた使い方も簡単にできます。多重継承ならなおさらです。foo が本当に bar を継承するよう求めているのか、fooインスタンスbar に含まれるスロットを求めているのかをよく考えることです。一般的な指針として、 foobar が「同類」であれば、継承して仕様を混合させるのが妥当です。ただし、両者のコンセプトがまったく異なるのであれば、foo にスロットを定義して両者を分離させておくべきです。

具体的な例として、交通信号(traffic light)を描画するアプリケーションを考えます。drawable-traffic-light クラスはおそらく drawable を継承し、各インスタンスには traffic-light のスロットが必要になるでしょう。しかし、これらのクラスを全部一緒くたにして多重継承すると、スパゲッティのような継承図が出来上がってしまいます。もしトポロジカルソートを十分に理解していて、:initform で済むところが使わない理由を説明できるほど多くのクラスで試したというのなら、明らかにやり過ぎです。やめておきましょう。

練習問題

  • 今までの記述で、CLOSの機能のうち defstruct に相当するものすべてに言及してきましたか?
  • 構造体を使っているアプリケーションを取ってきて、defclass を使って書き直しなさい。
  • 今使っている処理系で、nil のクラス継承階層( class-precedence-list)を調べなさい。

わだばLisperになるさんがこの問題に挑戦されています。


3.7. クラスの変更 (1)

*1:C++とかPythonとかあるけど…