2010年9月10日金曜日

クラスとインスタンスの関係 これだけは知っておきたい

意外と勘違いしている人が多い、クラスとインスタンスの関係をまとめておく。


◆ スーパークラスはサブクラスのフィールド・メソッドが見えているか?
見えていない。
スーパクラス内でサブクラスのメソッドを利用するとエラーになる。



◆ サブクラスはスーパークラスのフィールド・メソッドが見えているか?
見えていない。



◆ ではなぜ、サブクラスからスーパークラスのフィールド・メソッドが使えるのか?
使えない。スーパークラスとサブクラスは別物である。
ただし、サブクラスのインスタンスはスーパークラスのインスタンスを含むため、
サブクラスのインスタンスがスーパークラス側のフィールド・メソッドを利用できる。



◆ アップキャストとは何か?
スーパークラスの参照型変数へ、サブクラスの参照を入れることである。



◆ アップキャストした時のインスタンスの型はどう変化するか?
参照をどの参照型変数に入れようと、それによってインスタンスの型が変化することはない。



◆ スーパークラスの参照型変数からサブクラスのフィールド・メソッドが使えるか?
スーパークラスの参照型変数を通してサブクラスのフィールド・メソッドは使えない。
どのクラスのフィールド・メソッドが使えるかは参照型変数の型で決まるためである。
インスタンスの型には関係がない。
参照型変数の型がスーパークラスの場合、
スーパークラスのフィールド・メソッドしか使えない。
ただし・・・


◆ スーパークラスのメソッドがサブクラスでオーバーライドされていた場合はどうなるか?
サブクラスの、オーバーライドしたメソッドが呼び出される。



◆ 何のためにアップキャストがあるのか?
ポリモフィズム(多態性)を実現するためにある。
アップキャストとオーバーライドがこの基本原理である。



◆ アップキャストしたあとに、サブクラスにしかないメソッドを使いたい場合どうするか?
スーパークラスからサブクラスへとキャストする。
つまり、インスタンスの型の参照型変数に入れ直せばいい。
これをダウンキャストという。

アップキャストした時と同じく、ダウンキャストをしても、
参照型変数の型が変わるだけでインスタンスは一切変化しない。



◆ ダウンキャストの可否をどう判断するか?
アップキャストができたのは、サブクラスのインスタンスが
スーパークラスのインスタンスを含んでいたからである。

インスタンスの中にある型にのみキャストできるということである。

そのため、その逆の場合もダウンキャスト可能である。

しかし、アップキャスト後、そのスーパークラスを継承する別の
クラスへのダウンキャストは可能である。
スーパークラスを通せば、そのキャスト元の参照型変数の型で
キャストできるかどうかの判断がされるため
実際のインスタンスの型がどうであれできてしまう。
インスタンスの中にキャスト先の型のインスタンスは含まれていないので、
コンパイルエラーにならないが、当然実行時にはエラーになる。


【補足】
これまでの理解ができれば下のコードの結果も分かるだろう。
bが出力される。
#include <iostream>
using namespace std;

class A{
  public:
  void result(){cout << "a" << endl;}
};

class B{
  public:
  void result(){cout << "b" << endl;}
};

int main(){
  B* b = reinterpret_cast<B*> (new A());
  b->result();
  return 0;
}


ではaを出力させる(継承させる)にはどうすればいいだろうか。
継承機能を擬似的にまねてみる。
class A{
  private:
  char mType;

  public:
  A() : mType('A'){}
  void result(){cout << "a" << endl;}
};

class B{
  private:
  char mType;

  public:
  B() : mType('B'){}

  void result(){
    if(mType == 'A') {
      A* a = reinterpret_cast<A*>(this);
      a->result();
    }else{
      cout << "b" << endl;
    }
  }
};

int main(){
  B* b = reinterpret_cast<B*> (new A());
  b->result();
  return 0;
}

泥臭く手書きで継承らしいコードを書いてみた。
これはA、B共にインスタンスのメモリ領域の最初に
mTypeがあるからうまくいくだけであるので注意しておくこと。
ただし実際の仮想関数もコンパイラが
上のコードのような分岐をやってくれる機能であると考えて大きな間違いはない。



◆ instanceofについて
instanceof演算子を使えばダウンキャスト可能か判定できる。

スーパークラスであるShape、
そのサブクラスのCircleとTriangle、
があるとする。

Circleのオブジェクトcircleがあった時、
circle instanceof Shape 
は、常に真である。

逆に、Shapeクラスのオブジェクトshapeがあったとき、
shape instanceof Circle
という式は、真になる可能性がある。

(真になる場合)
Circle circle = new Circle()
Shape shape = (Shape) circle

アップキャスト後に元の型にダウンキャストも可能
Circle circle = (Circle) shape

(真にならない場合)
Triangle triangle = new Triangle()
Shape shape = (Shape) triangle

アップキャスト後にCircle型へのダウンキャストは不可である。
Circle circle = (Circle) shape



◆ オーバーライド時の注意
まず、共変(covariant)、反変(contravariant)の意味をおさええおく。

共変 : 広い型から狭い型へ変換すること
反変 : 狭い型から広い型へ変換すること

アップキャウト、ダウンキャストの話を思い出してほしい。
安全と非安全な場合を考えてみる。

見かけの型    実際の型
Shape        Shape   ・・・(1)
Shape        Circle  ・・・(2)
Circle       Circle  ・・・(3)
Circle       Shape   ・・・(4)

安全
(1)型が一致
(2)アップキャスト
(3)型が一致

非安全
(4)ダウンキャスト


メソッド実行の前後で成立しなくてはならない条件が見えてくるだろう。
安全であるコード例を書いてみた。

class Shape{
  Shape get(int index);    ・・・(a) 安全な戻り値の型
  void set(Circle circle); ・・・(b) 安全な引数の型
};

class Circle extends Shape{
  Circle get(int index);   ・・・(a) 安全な戻り値の型
  void set(Shape shape);   ・・・(b) 安全な引数の型
};


Shape型の変数に、Circleのオブジェクトを参照させるようなポリモフィズムを行う様子をイメージをして考えると理解しやすい。

(a)の戻り値の型は、共変であるべきである。
参照変数の型がShapeであり、実態がCircleであった場合、アップキャストすればよいだけである。問題ない。
当然(a)部分は下でもよい。
Shape get(int index)

一方、
(b)の引数の型は、反変であるべきである。
参照型はCircle型の引数を受け、ただ実際はShape型であれば、この場合もアップキャストになる。

ちなみに、Javaはこれを認めていない。
オーバライドにはならないで、オーバーロードになる。
引数型が違う同名の別メソッドが増えただけと見なすことでガードしている。
というよりも、オーバロードの機能を使っていたのでそちらをとったか、仕様の変更ができなかっただけかもしれない。


リスコフの置換原則(LSP(Liskov Substitution Principle))とはこれら派生型の置換原則のことである。



◆ Generics型
共変、反変の話をしたので、Genericsについても見てみる。Genericsは共変、反変を許しておらず、不変である。

不変(invariant) : 型を変換できないこと

例えば、ListとListの継承関係はない。
ただし、配列に関しては共変である。

【Java】ジェネリックス型の不変、共変、反変とは何か
【参考】
http://www.kab-studio.biz/Programing/OOPinJava/
http://itpro.nikkeibp.co.jp/article/COLUMN/20061128/255060/