このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

クラス継承、リスコフの置換原則、部分集合の型

「クラス、オブジェクト、型; なんだか変じゃない?」に、まだチラホラとコメントが付いているようです。んじゃ、少し補足しておきましょう。

何人かの方が「リスコフの置換原則」に言及しているので、その話。それと、列挙型や部分範囲型についても触れます。「クラス、オブジェクト、型; なんだか変じゃない?」に挙げておいた事例(Point3D extends Point2Dとか)は断りなしに引用します。

継承では、受け継ぐ財産を拒否できない

「クラスはデータと手続きを一緒に定義したものだ」とか言われますね。データはヒープ上のメモリブロックで、手続きはサブルーチンのセットです。クラス継承によって、データも手続きもいやおうなくサブクラスへと押しつけられます。要らないと言ってもダメです。

例えば、bonotake方式で UserHandle extends UserID とした場合、UserHandleでは ID which, int number は要らないと思ってもメモリが取られてしまいます。一般に、B extends A のとき、B(のデータ)のメモリレイアウトは:


 先頭
 ↓
 |Aのデータ|B固有のデータ|
  \___Bのデータ___/

Bで新しく定義したデータ(フィールド達と思ってよい)がお尻に追加されるのです。

手続き(メソッドですね)も同様。Point3D extends Point2D のとき、moveTo(double, double)、moveBy(double, double), distanceFrom(Point2D) とか受け継いでもショーモナイのだけど、とにかく全部のメソッドがサブクラスにも自動的/強制的に備わります。オーバライドはできても、メソッドの存在そのもを打ち消すことはできません。

リスコフの置換原則とは、プログラマを100%だますこと

次の例を考えましょう。


Point2D p, q;

/* オブジェクトの生成と初期化は謎 */

// でも、ここからp, qを使ってもいいよ
double x = p.getX();
if (x < 0.0) p.moveBy(100, 0);
Point2D org = new Point2D(0, 0);
if (q.distanceFrom(org) <= 10.0) q.moveTo(0, 0);
// ...
// ナニヤラ、カニヤラ
// ゴニョゴニョ、ゴチャゴチャ

「でも、ここからp, qを使ってもいいよ」と書いてある場所から下に、プログラマは「変数pとqにPoint2Dのオブジェクトが入っていると思い込んでコーディングする」としましょう。しかし実際は、ColoredPoint2DとかPoint3Dのオブジェクトが代入されていたとします。さて、何が起きるか?

ColoredPoint2D、Point3Dを使う限り特に何も起きません。プログラマは、p, qはPoint2D型だと思い込んだまま人生を終えます(いや、別に人生まで終えなくてもいいが)。だまされたまま。知らぬが仏(って、やっぱり成仏するのか)。

p, qがPoint3Dだったケースを考えると、Point3Dにとってはクソ迷惑なだけの moveTo(double, double)、moveBy(double, double), distanceFrom(Point2D) などが「Point2Dのふりをする」ときは役に立っているのです。継承メカニズムを使う限り、データ領域が足りない(アクセスエラーやオーバーランが発生する)とか、メソッドが存在しない、という破綻<はたん>は避けられます。ただし、不注意な(あるいは故意に動作を変える)オーバライドをすれば、プログラマをだまし続ける(Point2Dのふりを続ける)ことはできません。例えば、次のオーバライドは気付かれます。


// 2D関係は使わないから、零でも返すことにしよう。
double distanceFrom(Point2D pt) {
return 0.0;
}

表題のリスコフの置換原則ですけど、これは、サブクラスはスーパークラスのふりをして、プログラマを100%だますべし、というリスコフ女史からのお達しです。

部分集合を表す型は難しい

整数型を表すクラスがあったとします。JavaのアノInteger型とは区別したいので、class IntNumber としましょう。x, y, zがIntNumberのとき、次のようなことができるとします。そして、これが演算メソッドの全てです。


z = x.plus(y); // z = x + y
z = x.minus(y); // z = x - y
z = x.times(y); // z = x * y

さてと、NonNegativeInteger extends IntNumber, Score100 extends IntNumberとしたとき、サブクラスであるNonNegativeIntegerとScore100は、リスコフ女史の教えに従ってプログラマをだますことができるでしょうか? -- NonNegativeIntegerでは、メソッドminusによっていずれ馬脚をあらわします。Score100は、plus, minus, timesのどれであれ、IntNumberとは食い違った挙動を示すでしょう。

というわけで、部分集合を表す型を継承を使って、しかもリスコフの置換原則を破らずに実現することは困難です。でも、いつでも不可能というわけでもありません。偶数を表すクラスを EvenNumber extends IntNumber と定義して、plus, minus, times の挙動を IntNumberにあわせることはできます(コンストラクタやリテラルが登場しない範囲での“なりすまし”)。

IntNumberのような算術演算を備えたクラスを継承してNonNegativeNumberやScore100を定義するのは無理がある話で、演算(メソッド)の一部を削除(忘却;forgetful操作)したり、メソッド仕様を変更したりする必要があります。しかし継承は、財産を拒否できないので、多くの場合は部分集合型の定義に向かないってことですね。

ユニオン(バリアント)型、TextEditor/TextViewerの例も、リスコフの置換原則を基準として議論することができますよ。考えてみてください。