事始
wikipediaの「オブジェクト指向プログラミング」のページを見ていたら、「一般的に以下の機能や特徴を活用したプログラミング技法のことをいう」にこんな項目が挙げられていました(2016年3月28日現在)。
- カプセル化(振る舞いの隠蔽とデータ隠蔽)
- インヘリタンス(継承) -- クラスベースの言語
- ポリモフィズム(多態性、多相性) -- 型付きの言語
- ダイナミックバインディング(動的束縛) -- 動的型付言語
上2つはまぁ良いとして下2つ、これらがオブジェクト指向の「機能や特徴」に挙げられている事は別に良いんですが、その後ろの型付きとの対応付はそれで良いの?
と思ったのですが、何分知識不足で納得も反論もできなかった。
ちょっとオブジェクト指向の本読んで勉強します。
いい本教えて下さい……。
というわけで
オブジェクト指向そのものの勉強はさておき、ちょっと動的束縛について調べてみましょう。
動的束縛とは
要するにポリモフィズムなメソッド呼び出しです(だと思います)。
……ちょっと自信無いです。
とりあえずポリモフィズムについて解説します。
class Foo {
def say = println("foo!")
}
class Bar extends Foo {
override def say() = println("bar!")
}
val list = List(new Foo, new Bar) // :List[Foo]
list.foreach(_.say)
// foo!
// bar!
Foo型として扱われている時でも、実態がBar型ならBarとしての振る舞いをします。
これこそまさしくオブジェクト指向プログラミングの肝要と言っていいでしょう。
ちなみに動的型付け言語、あるいは総称型(テンプレート、ジェネリクス)の強力な静的型付け言語だと、更に汎用的な「ダックタイピング」を用いた多相性を活用する事ができます。
下の例では、2つのクラスに継承関係が無い事に注目して下さい。
class Panther
def ute
"feuer!"
end
end
class Sherman
def ute
"fire!"
end
end
[Panther.new, Sherman.new].each { |tank| puts tank.ute }
# =>
# feuer!
# fire!
クラスじゃなくて構造体だって可能です。
import std.stdio;
void aisatz(T)(T lang)
{
lang.aisatz();
}
struct English
{
void aisatz() { writeln("Hello!"); }
}
struct Latin
{
void aisatz() { writeln("Salve!"); }
}
void main()
{
aisatz(English()); // Hello!
aisatz(Latin()); // Salve!
}
ダックタイピングは強力過ぎて危ない、という方には「構造的部分型」をおすすめします。
class Hoge {
def hey = "ho!"
}
class Fuga {
def hey = "fu!"
}
def sayHey(heyer: { def hey: String }) = println(heyer.hey)
sayHey(new Hoge) // ho!
sayHey(new Fuga) // fu!
一応補足しておきますと、動的言語のダックタイピングはさておき、静的言語のジェネリックスや構造的部分型はあくまで使われる型が「静的に」決まります。ただし、実際に呼び出されるメソッドは「動的に」決まると思います。
import std.stdio;
class C
{
void say() { writeln("C"); }
}
class D : C
{
override void say() { writeln("D"); }
}
void func(T)(T t) { t.say; }
void gunc(C c) { func(c); } // <- コンパイル時での静的な型はC
void main()
{
gunc(new D()); // <- 実行時での動的な型はD
}
動的束縛の無い世界
生まれた時からオブジェクト指向言語が存在し、初めてのプログラミングはオブジェクト指向という皆さんにとって、ポリモフィズムは空気のように当たり前の存在で、それ以外の挙動をする言語なんて逆に想像がつかないかもしれません。
何型の変数に代入されてたって、メソッドは本来の型の実装を呼び出すのが当然、とお思いでしょう。
ところがそうじゃない言語もあります。
そう、C++です。
C++の場合
嘘です。
C++でもポリモフィズムはあります。
というかそうじゃないとvtableなんて必要ないですし。
しかしC++のクラスは、使い方によってはポリモフィズムを行ってくれない事があります。それはクラスを値型として使用した場合です。
文章で説明しても非常に解り難いので、次のコードを見て下さい。
#include <iostream>
using namespace std;
class A
{
public:
virtual void say(void);
};
void A::say(void)
{
cout << "from A\n";
}
class B : public A
{
public:
void say(void);
};
void B::say(void)
{
cout << "from B\n";
}
int main(void)
{
cout << "As a Pointer\n";
auto a1 = new A();
auto b1 = new B();
a1->say();
b1->say();
a1 = b1; // GC持たない言語の経験無いから普通にこういう事しちゃったけど、ヤバイのでは?
a1->say();
cout << "\nAs a Value\n";
auto a2 = A();
auto b2 = B();
a2.say();
b2.say();
a2 = b2;
a2.say();
return 0;
}
さて、これを実行するとどうなるでしょう?
このようになるんですね……。
As a Pointer
from A
from B
from B
As a Value
from A
from B
from A
比べてみてください。new
で作ったクラスはちゃんとポリモフィズムが効いているのに対して、値型としてスタックに確保したクラスは、何と今自分が代入されている変数の型のメソッドを忠実に呼び出しています!
これが動的束縛の無い世界の挙動です。
スライシング
上は、一般に「スライシング」と呼ばれる問題です。子クラスのフィールドから親クラスのフィールドだけを「スライス」してしまうからスライシングでしょうか?
どうしてこういう事が起きてしまうかといいますと、値の代入(値渡しの関数呼び出しを含む)というのは要するに「値のコピー」だからです。
a2 = b2
という式は、b2
をa2
にコピーする、という意味になります。
そして何をコピーするかは、コピー先によって決まります。今回コピー先はA型でしたね?
a2「お前の値を俺に代入してやるから値よこせ」
b2「はいはい、値ですねー! 私B型なんでB型の値も持ってるんですよ。えーとね……」
a2「いや、俺A型だから。A型の値だけでいいから」
b2「え? でも、私B型で……」
a2「B型ってA型の派生だからA型の値も持ってんだろ? そのA型の分だけしかいらねぇよ」
b2「はぁ」
こんな感じで、フィールドが「A型の分だけ」変数a2
にわたってしまっているわけです。
この挙動は決してバグなどではない、適切なものですが、オブジェクト指向のポリモフィックな力を活用しようとしてコードを書いていると面食らってしまいます。ScalaやRubyのようなモダンな言語を使い慣れた人間がこんな動きに突き当たったら理解に苦しむでしょう。
そういうわけで、一般的にオブジェクトを値として扱うのは避けた方が良いと言われています。
結び
結局動的束縛とは何なのか。
誰か教えてクレメンス……。