M.Hiroi's Home Page

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

オブジェクト指向編 : 共有スロット

Copyright (C) 2010 Makoto Hiroi
All rights reserved.

はじめに

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

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

●共有スロットの設定

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

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

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

(define-class <foo> ()
  ((a :accessor foo-a :init-keyword :a :allocation :class)
   (b :accessor foo-b :init-value 1 :init-keyword :b)))

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

gosh> (define x1 (make <foo> :a 0 :b 10))
x1
gosh> (define x2 (make <foo> :b 20))
x2
gosh> (foo-a x1)
0
gosh> (foo-b x1)
10
gosh> (foo-a x2)
0
gosh> (foo-b x2)
20
gosh> (set! (foo-a x1) 100)
100
gosh> (foo-a x1)
100
gosh> (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 になります。このように、スロットが共有されていることがわかります。

また Guache の場合、共有スロットは次の関数を使うとインスタンスではなくクラスオブジェクトからでもアクセスすることができます。

class-slot-ref class slot-name
class-slot-set! class slot-name obj
class-slot-bound? class slot-name obj

簡単な使用例を示します。

gosh> (define-class <foo> () ((a :init-value 1 :allocation :class)))
<foo>
gosh> (class-slot-bound? <foo> 'a)
#t
gosh> (class-slot-ref <foo> 'a)
1
gosh> (class-slot-set! <foo> 'a 10)
10
gosh> (class-slot-ref <foo> 'a)
10

●共有スロットの継承

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

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

(define-class <foo> ()
  ((a :accessor foo-a :init-keyword :a :allocation :class)
   (b :accessor foo-b :init-value 1 :init-keyword :b)))

(define-class <foo1> (<foo>)
  ((c :accessor foo-c :init-value 2 :init-keyword :c)))

クラス <foo> を継承してクラス <foo1> を定義します。<foo1> のスロットは a, b, c の 3 つになりますが、スロット a は共有スロットになります。このとき、スロット a は <foo1> だけはなく、<foo> と <foo1> の共有スロットになります。簡単な例を示しましょう。

gosh> (define x3 (make <foo1>))
x3
gosh> (foo-a x3)
100
gosh> (foo-b x3)
1
gosh> (foo-c x3)
2
gosh> (set! (foo-a x3) 200)
200
gosh> (foo-a x1)
200
gosh> (foo-a x2)
200
gosh> (foo-a x3)
200

クラス <foo> のインスタンスが変数 x1, x2 にセットされている状態で、クラス <foo1> のインスタンスを生成して変数 x3 にセットします。x3 のスロット a は共有スロットなので、x1, x2 と同じ値になります。スロット b, c は局所スロットなので、:init-value の値で初期化されます。ここで、(set! (foo-a x3) 200) とスロット a の値を 200 に書き換えると、(foo-a x1) と (foo-a x2) の値は 200 になります。スロット a はクラス <foo> と <foo1> で共有されていることがわかります。

●共有スロットの衝突

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

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

(define-class <foo> ()
  ((a :accessor foo-a :init-keyword :a :allocation :class)
   (b :accessor foo-b :init-value 1 :init-keyword :b)))

(define-class <foo2> (<foo>)
  ((a :accessor foo2-a :init-keyword :a :allocation :class)
   (c :accessor foo2-c :init-value 3 :init-keyword :c)))

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

gosh> (define x1 (make <foo> :a 100))
x1
gosh> (define x2 (make <foo2> :a 200))
x2
gosh> (foo-a x1)
100
gosh> (foo2-a x2)
200
gosh> (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 オプションはサブクラスの指定が優先されます。次のリストを見てください。

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

(define-class <foo1> (<foo>)
  ((c :accessor foo-c :init-value 2 :init-keyword :c)))

(define-class <foo3> (<foo>)
  ((a :accessor foo3-a :init-keyword :a :allocation :instance)
   (c :accessor foo3-c :init-value 3 :init-keyword :c)))

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

gosh> (define x1 (make <foo> :a 10 :b 20))
x1
gosh> (define x2 (make <foo> :b 30))
x2
gosh> (define y1 (make <foo3> :a 100 :c 200))
y1
gosh> (define y2 (make <foo3> :a 300 :c 400))
y2
gosh> (foo-a x1)
10
gosh> (foo-a x2)
10
gosh> (foo3-a y1)
100
gosh> (foo3-a y2)
300
gosh> (foo-a y1)
100
gosh> (foo-a y2)
300

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

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

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

(define-class <bar> ()
  ((a :accessor bar-a :init-value 0 :init-keyword :a)
   (b :accessor bar-b :init-value 1 :init-keyword :b)))

(define-class <bar1> (<bar>)
  ((a :accessor bar1-a :init-keyword :a :allocation :class)
   (c :accessor bar1-c :init-value 2 :init-keyword :c)))

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

gosh> (define x1 (make <bar> :a 10 :b 20))
x1
gosh> (define x2 (make <bar> :a 30 :b 40))
x2
gosh> (define y1 (make <bar1> :a 100 :b 200 :c 300))
y1
gosh> (define y2 (make <bar1> :b 400 :c 500))
y2
gosh> (bar-a x1)
10
gosh> (bar-a x2)
30
gosh> (bar-a y1)
100
gosh> (bar-a y2)
100
gosh> (bar1-a y1)
100
gosh> (bar1-a y2)
100

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

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

(define-class <baz1> ()
  ((a :accessor baz1-a :init-keyword :a)))

(define-class <baz2> ()
  ((a :accessor baz2-a :init-keyword :a :allocation :class)))

(define-class <baz3> (<baz1> <baz2>) ())

(define-class <baz4> (<baz2> <baz1>) ())

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

gosh> (define x1 (make <baz3> :a 10))
x1
gosh> (define x2 (make <baz3> :a 20))
x2
gosh> (define y1 (make <baz4> :a 30))
y1
gosh> (define y2 (make <baz4>))
y2
gosh> (baz1-a x1)
10
gosh> (baz1-a x2)
20
gosh> (baz2-a y1)
30
gosh> (baz2-a y2)
30
gosh> (baz1-a y2)
30
gosh> (baz1-a y1)
30

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


初版 2010 年 4 月 4 日