前回まででオブジェクト指向の基本であるクラス、インスタンス、メソッドについて一通り説明しました。今回はオブジェクト指向機能の目玉ともいえる「継承」について説明します。もちろん、F# でも継承を使うことができます。まず最初に、一般的なオブジェクト指向言語で使われている継承について簡単に説明します。
「継承 (inheritance : インヘリタンス)」は、簡単に言うとクラスに「親子関係」を持たせる機能です。子供のクラスは親クラスの性質を受け継ぐことができます。プログラミング言語の場合、引き継ぐ性質は定義されたインスタンス変数やメソッドになります。プログラムを作る場合、今まで作ったプログラムと同じような機能が必要になることがありますが、継承を使うことでその機能を受け継ぎ、新規の機能や変更される機能だけプログラムする、いわゆる「差分プログラミング」が可能になります。
クラスを継承する場合、その元になるクラスを「スーパークラス」とか「ベースクラス」と呼びます。そして、継承したクラスを「サブクラス」と呼びます。この呼び方は言語によってまちまちで統一されていません。C++ の場合は、元になるクラスを基本クラスといい、継承するクラスを派生クラスとか導出クラスといいます。
たとえば、クラス Foo1 を継承してクラス Foo2 を定義したとしましょう。クラス Foo1 にはメソッド bar が定義されています。クラス Foo2 にメソッド bar は定義されていませんが、Foo2 のオブジェクトに対して bar を呼び出すと、スーパークラス Foo1 のメソッド bar が実行されるのです。
メソッドの選択は次のように行われます。最初に、オブジェクトが属するクラス Foo2 にメソッド bar が定義されているか調べます。ところが、Foo2 には bar が定義されていないので、スーパークラスである Foo1 に bar が定義されているか調べます。ここでメソッド bar が見つかり、それを実行するのです。このように、メソッドが見つかるまで順番にスーパークラスを調べていきますが、最上位のスーパークラスまで調べてもメソッドが見つからない場合はエラーとなります。
継承したクラスのメソッドとは違う働きをさせたい場合、同名のメソッドを定義することで、そのクラスのメソッドを設定することができます。これを「オーバーライド (override)」といいます。メソッドを選択する仕組みから見た場合、オーバーライドは必然の動作です。メソッドはサブクラスからスーパークラスに向かって探索されるので、スーパークラスのメソッドよリサブクラスのメソッドが先に選択されるわけです。
A /|\ / | \ B C D / \ / \ E F 図 1 : 単一継承におけるクラスの階層
継承には「単一継承」と「多重継承」の 2 種類があります。単一継承は、ただひとつのクラスからしか機能を継承することができません。したがって、クラスの階層は図 1 のような木構造で表すことができます。
継承は何段階に渡って行われてもかまいません。たとえばクラス E の場合、スーパークラスが B で、B のスーパークラスが A に設定されています。サブクラスは複数あってもかまいません。たとえば、A のサブクラスは B, C, D と 3 つ、B のサブクラスは E, F と 2 つあります。図 1 の場合、クラス A のスーパークラスはありませんが、ほかのクラスではただひとつのスーパークラスを持っています。プログラミング言語では F#, Java, Ruby, Smalltalk などが単一継承です。
これに対し多重継承は、複数のクラスを継承することができます。このため、クラスの階層は木構造ではなく、図 2 のようなグラフ [*1] で表すことができます。
A / \ / \ B C / \ / \ / \ / \ D E F 図 2 : 多重継承におけるクラスの階層
クラス E に注目してください。スーパークラスには B と C の 2 つがあります。多重継承では、単一継承と同じくサブクラスを複数持つことができ、なおかつ、スーパークラスも複数持つことができるのです。プログラミング言語では C++, Common Lisp Object System (CLOS), OCaml, Python などが多重継承をサポートしています。F# は OCaml とは異なり単一継承であることに注意してください。
実をいうと、M.Hiroi は多重継承に対してあまりいいイメージを持っていません。私見ですが、多重継承はメリットよりもプログラムを複雑にするデメリットの方が大きいのではないか、と思っています。特に、図 2 のクラス A, B, C, E のような菱形の関係を C++ でプログラムする場合、とても複雑な問題を引き起こすことが知られています。
一般的なオブジェクト指向言語の場合、継承によって引き継がれる性質は定義されたインスタンス変数やメソッドになります。図 3 を見てください。
class ┌─ Foo ─┐ ┌─ instance ─┐ ├─────┤ ├───────┤ │ 変数 a │────→│ 変数 a │ ├─────┤ ├───────┤ │ 変数 b │ │ 変数 b │ └─────┘ └───────┘ method : get_a, get_b │ 継承 ↓ ┌─ Bar ─┐ ┌─ instance ─┐ ├─────┤────→├───────┤ │ 変数 c │ │ 変数 a │ └─────┘ ├───────┤ method : get_c │ 変数 b │ ├───────┤ │ 変数 c │ └───────┘ 図 3 : 一般的なオブジェクト指向言語における継承
クラス Foo にはインスタンス変数 a, b とメソッド get_a, get_b が定義されています。次に、クラス Bar を定義します。Bar は Foo を継承し、Bar 固有のインスタンス変数 c とメソッド get_c を定義します。Foo と Bar のインスタンスを生成すると、図 3 に示したように、Bar のインスタンスにはクラス Foo で定義された変数 a, b も含まれます。このように、Foo のインスタンス変数が Bar に継承されます。
Foo のインスタンスを生成すると、もちろん変数 a, b は含まれていますが、Bar のインスタンスとメモリを共有することはありません。クラスはオブジェクトの設計図です。設計に共通な部分があったとしても、それから生み出されるインスタンスは別々の実体で、インスタンス変数を共有することはないのです。
クラス Bar にはメソッド get_c しか定義されていませんが、クラス Foo を継承することにより、メソッド get_a と get_b を利用することができます。Bar のインスタンスに対して get_a を呼び出すと、クラス Bar には get_a が定義されていないので、スーパークラス Foo を調べ、そこで定義されている get_a が呼び出されます。もちろん、取り出される値は Bar のインスタンスにある変数 a の値です。このように、Foo のメソッドが Bar に継承されます。
それでは、具体的に F# の継承を説明しましょう。スーパークラスは class ... end の中で inherit 宣言を使って指定します。
type class_name<...>(...) = class inherit super_class(...) ... end
F# は単一継承なので、inherit で指定できるスーパークラスは一つだけです。継承に必要な設定はこれだけです。
簡単な例として、図 3 のクラスを実際にプログラムしてみましょう。まずクラス Foo を定義します (リスト 1)。
リスト 1 : クラス foo の定義 type Foo<'a>(x: 'a, y: 'a) = let a = x let b = y member this.getA() = a member this.getB() = b
インスタンス変数 a, b はコンストラクタの引数 x, y で初期化します。データ型は型変数 'a で表します。メソッド getA と getB の定義は簡単です。与えられたインスタンスから値を取り出すだけです。次にクラス Bar を定義します (リスト 2)。
リスト 2 : クラス Bar の定義 type Bar<'a>(x: 'a, y: 'a, z: 'a) = inherit Foo<'a>(x, y) let c = z member this.getC() = c
inherit でスーパークラス Foo を指定します。このとき、必要な型変数や引数を指定します。これで Foo のコンストラクタを呼び出し、Foo で定義されているインスタンス変数を初期化します。あとは自分のクラスで使うインスタンス変数 c を初期化し、メソッド getC を定義します。
実際にクラス foo と bar を定義すると、次のように表示されます。
type Foo<'a> = new: x: 'a * y: 'a -> Foo<'a> member getA: unit -> 'a member getB: unit -> 'a type Bar<'a> = inherit Foo<'a> new: x: 'a * y: 'a * z: 'a -> Bar<'a> member getC: unit -> 'a
それでは実行してみましょう。
> let a = new Foo<int>(1, 2);; val a: Foo<int> > let b = new Bar<int>(10, 20, 30);; val b: Bar<int> > a.getA();; val it: int = 1 > a.getB();; val it: int = 2 > b.getA();; val it: int = 10 > b.getB();; val it: int = 20 > b.getC();; val it: int = 30
メソッド getA は Bar に定義されていませんが、スーパークラス Foo のメソッド getA が呼び出されて、インスタンス変数 a の値を求めることができます。また、Bar のインスタンスに対して getC を呼び出せば、インスタンス変数 c の値を求めることができます。
スーパークラスに do 束縛が定義されている場合、上位のスーパークラスから順番に実行されます。簡単な例を示しましょう。
リスト 3 : do 束縛の実行順序 type Foo(xi: int) = let mutable x = xi do printfn "Foo(%d)" xi member this.X with get() = x and set(a) = x <- a type Bar(xi: int, yi: int) = inherit Foo(xi) let mutable y = yi do printfn "Bar(%d, %d)" xi yi member this.Y with get() = y and set(a) = y <- a type Baz(xi: int, yi: int, zi: int) = inherit Bar(xi, yi) let mutable z = zi do printfn "Baz(%d, %d, %d)" xi yi zi member this.Z with get() = z and set(a) = z <- a
type Foo = new: xi: int -> Foo member X: int with get, set type Bar = inherit Foo new: xi: int * yi: int -> Bar member Y: int with get, set type Baz = inherit Bar new: xi: int * yi: int * zi: int -> Baz member Z: int with get, set
> let a = new Foo(10);; Foo(10) val a: Foo > let b = new Bar(1, 2);; Foo(1) Bar(1, 2) val b: Bar > let c = new Baz(100, 200, 300);; Foo(100) Bar(100, 200) Baz(100, 200, 300) val c: Baz > a.X;; val it: int = 10 > b.X;; val it: int = 1 > c.X;; val it: int = 100
Baz のインスタンスを生成すると、Foo, Bar, Baz の順番で do 束縛が評価されていることがわかります。
継承はクラスに新しい機能を追加するだけではなく、メソッドをオーバーライドすることで機能を変更することができます。簡単な例題として、前回作成したスタックを継承して、格納する要素数を制限するスタック FixedStack というクラスを作ってみましょう。リスト 4 を見てください。
リスト 4 : 制限付きスタック (stack2.fsx) // 例外 exception Empty exception Full // スタック type Stack<'a> () = // データを格納するリスト let mutable content: 'a list = [] // データを追加 member this.push x = content <- x::content // データの削除 member this.pop () = match content with [] -> raise Empty | x::xs -> content <- xs; x //データの取得 member this.top () = match content with [] -> raise Empty | x::_ -> x // スタックは空か *) member this.is_empty () = List.isEmpty content // 制限付きスタック type FixedStack<'a> (limit: int) = inherit Stack<'a>() let mutable size = 0 member this.push x = if size = limit then raise Full else ( size <- size + 1 base.push x ) member this.pop () = if size = 0 then raise Empty else ( size <- size - 1 base.pop() ) member this.is_full () = size = limit
FixedStack は指定した上限値までしか要素を格納できません。Stack で要素を追加するメソッドは push で、削除するメソッドは pop です。この 2 つのメソッドをオーバーライドすることで、FixedStack の機能を実現することができます。スタックの上限値は引数 limit で指定し、スタックの要素数はインスタンス変数 size で管理します。
FixedStack は stack を継承するので、inherit でスーパークラスに Stack を指定します。メソッドをオーバーライドするとき、スーパークラスのメソッドを呼び出すことができると便利です。これを「メソッド結合 (method combination)」といいます。
F# の場合、キーワード base を使ってスーパークラスのメソッドを呼び出します。他の言語では super というキーワードを使うことが多いようです。FixedStack では、base.push x や base.pop () とすることでスーパークラス Stack のメソッドを呼び出すことができます。
メソッド push は limit と size を比較して、size が limit と等しい場合はデータを挿入できないので例外 Full を送出します。そうでない場合はスーパークラスのメソッド push を呼び出して、データを挿入して size を +1 します。
メソッド pop の場合、size が 0 であればデータを削除できないので例外 Empty を送出します。size が 0 よりも大きいときにスーパークラスのメソッド pop を呼び出して、size を -1 します。これで、スタックに格納される要素数を管理することができます。
簡単な実行例を示しましょう。
> #load "stack2.fsx";; [読み込み中 /home/mhiroi/fsharp/stack2.fsx] namespace FSI_0002.Stack2 exception Empty exception Full type Stack<'a> = new: unit -> Stack<'a> member is_empty: unit -> bool member pop: unit -> 'a member push: x: 'a -> unit member top: unit -> 'a type FixedStack<'a> = inherit Stack<'a> new: limit: int -> FixedStack<'a> member pop: unit -> 'a member push: x: 'a -> unit member is_full: bool
> open Stack2;; > let a = new FixedStack<int>(5);; val a: FixedStack<int> > for i = 0 to 4 do a.push i done;; val it: unit = () > a.is_full();; val it: bool = true > a.push 5;; FSI_0002.Stack2+Full: Exception of type 'FSI_0002.Stack2+Full' was thrown. ... 略 ... > while not (a.is_empty()) do printfn "%d" (a.pop()) done;; 4 3 2 1 0 val it: unit = () > a.is_empty();; val it: bool = true > a.is_full();; val it: bool = false
このように Stack を継承することで、FixedStack を簡単にプログラムすることができます。
クラスでメソッドを定義するとき、キーワード abstract を付けるとメソッドの型だけを宣言することができます。これを「抽象メソッド」といいます。そして、抽象メソッドを持つクラスを「抽象クラス」といい、new でインスタンスを生成することはできません。抽象クラスと抽象メソッドは次のように定義します。
[<AbstractClass>] type クラス名<型, ...> (引数, ...) = class ... abstract メソッド名 : 型式 end
抽象クラスには AbstractClass 属性が必要です。抽象クラスは継承されることを前提としたクラスで、抽象メソッドはサブクラスで具体的に定義します。抽象クラスでは、サブクラス共通のメソッドを定義します。このとき、抽象クラスのメソッドは抽象メソッドを使って定義することができます。サブクラスのインスタンスが生成されるとき、そのサブクラスでは抽象メソッドが具体化されているはずなので、抽象クラスのメソッドからサブクラスのメソッドが呼び出されることになります。
それでは簡単な例題として、図形の面積を求めるプログラムを作ってみましょう。次のリストを見てください。
リスト 5 : 図形のクラス let pi = 3.14159265 [<AbstractClass>] type Figure() = abstract kind_of : unit -> string abstract area : unit-> float member this.print () = printfn "%s: area = %f\n" (this.kind_of()) (this.area())
クラス figure は抽象クラスです。メソッド kind_of と area が抽象メソッドで、kind_of は図形の種類を文字列で返し、area は図形の面積を計算して返します。print は図形の種別と面積を表示するメソッドです。ここで、抽象メソッド kind_of と area を呼び出しています。kind_of と area はサブクラスで定義します。
実際に figure を定義すると、次のように表示されます。
type Figure = new: unit -> Figure abstract area: unit -> float abstract kind_of: unit -> string member print: unit -> unit
次は図形を表すサブクラスを定義します。
リスト 6 : サブクラスの定義 // 三角形 type Triangle(altitude: float, base_: float) = inherit Figure() member this.getAltitude () = altitude member this.getBase () = base_ override this.kind_of () = "Triangle" override this.area () = altitude * base_ / 2.0 // 四角形 type Rectangle(width: float, height: float) = inherit Figure() member this.getWidth () = width member this.getFeight () = height override this.kind_of () = "Rectangle" override this.area () = width * height // 円 type Circle(radius: float) = inherit Figure() member this.getRadius () = radius override this.kind_of () = "Circle" override this.area () = radius * radius * pi
Triangle, Rectangle, Circle は Figure を継承します。サブクラス固有のメソッドも定義されていますが、どのクラスも抽象メソッド kind_of と area を具体化しています。この場合、キーワードは member ではなく override を使います。なお、スーパークラスの抽象メソッドをすべて具体化しないと、そのサブクラスも抽象クラスになるため、インスタンスを生成することができません。ご注意ください。
実際にクラスを定義すると、次のように表示されます。
type Triangle = inherit Figure new: altitude: float * base_: float -> Triangle override area: unit -> float member getAltitude: unit -> float member getBase: unit -> float override kind_of: unit -> string type Rectangle = inherit Figure new: width: float * height: float -> Rectangle override area: unit -> float member getFeight: unit -> float member getWidth: unit -> float override kind_of: unit -> string type Circle = inherit Figure new: radius: float -> Circle override area: unit -> float member getRadius: unit -> float override kind_of: unit -> string
それでは簡単な実行例を示します。
> let a = new Triangle(2.0, 2.0);; val a: Triangle > let b = new Rectangle(2.0, 2.0);; val b: Rectangle > let c = new Circle(2.0);; val c: Circle > a.print();; Triangle: area = 2.000000 val it: unit = () > b.print();; Rectangle: area = 4.000000 val it: unit = () > c.print();; Circle: area = 12.566371 val it: unit = ()
正常に動作していますね。
F# では、サブクラスのインスタンスをスーパークラスの型に変換することができます。型変換のことを「キャスト」といいます。特に、サブクラスをスーパークラスに変換することを「アップキャスト」といい、スーパークラスのインスタンスをサブクラスの型に変換することを「ダウンキャスト」といいます。
F# の場合、サブクラスのインスタンスをスーパークラスの変数 (メソッドの引数) やリストなどに代入するとき、自動的にアップキャストが行われます。コンパイルエラーは発生しません。それ以外のアップキャストや、スーパークラスのインスタンスをサブクラスの変数などに代入するダウンキャストの場合、プログラマが明示的にキャストしないとコンパイルエラーになります。
F# にはアップキャストとダウンキャストを行う演算子が用意されています。
subClassObj :> superClass superClassObj :?> subClass
演算子 :> は左辺のインスタンス subClassObj を右辺で指定した superClass にアップキャストします。演算子 :?> は左辺のインスタンス superClassObj を右辺の subClass にダウンキャストします。
簡単な例を示しましょう。
リスト : キャスト (1) [<AbstractClass>] type Foo() = abstract display: unit -> unit type Bar1() = inherit Foo() override this.display() = printfn "Bar1" type Bar2() = inherit Foo() override this.display() = printfn "Bar2" type Baz() = member this.display() = printfn "Baz"
> let b = new Bar1();; val b: Bar1 > let c = new Bar2();; val c: Bar2 > let d = new Baz();; val d: Baz > let a1: Foo = b;; val a1: Foo > let a2: Foo = c;; val a2: Foo > a1.display();; Bar1 val it: unit = () > a2.display();; Bar2 val it: unit = () > let b1: Bar1 = a1;; => エラー (キャストが必要) > let b1: Bar1 = a1 :?> Bar1;; val b1: Bar1 > b1.display();; Bar1 val it: unit = () > let a3: Foo = d :> Foo;; => エラー (キャストしてもエラー, Baz は Foo を継承していない) > let b2: Bar2 = a1 :?> Bar2;; => エラー (間違ったダウンキャスト)
各クラスのインスタンスを生成して変数 b, c, d にセットします。Bar1, Bar2 は Foo のサブクラスなので、Foo の変数 a1 に b を、a2 に c を代入することができます。これがアップキャストです。そして、a1, a2 から display() を呼び出すと、ポリモーフィズムが働いて Bar1, Bar2 の display() が呼び出され、Bar1 と Bar2 が表示されます。
逆に、Bar1 の変数 b1 に a1 を代入するには明示的にキャストする必要があります。これがダウンキャストです。ダウンキャストは演算子 :?> を使います。a1 :?> Bar1 とすれば、a1 をダウンキャストして変数 b1 に代入することができます。そして、b1 からメソッド display() を呼び出すと Bar1 と表示されます。
また、a1 を Bar2 に間違えてダウンキャストすると、コンパイルは成功しますがプログラムの実行時にエラーが発生します。それから、Foo と継承関係のない Baz のインスタンスは、キャストしても Foo や Bar の変数に代入することはできません。ご注意くださいませ。
次に、図形のオブジェクトをリストにまとめて格納することを考えます。Triangle, Rectangle, Circle は型が違うので、同じリストに格納することはできません。この場合、Figure に型変換すると同じリストに格納することができます。次の例を見てください。
リスト : 合計値を求める let sum_of_figure xs = List.fold (fun a (x: Figure) -> a + x.area()) 0.0 xs
> let a = new Triangle(2.0, 2.0);; val a: Triangle > let b = new Rectangle(2.0, 2.0);; val b: Rectangle > let c = new Circle(2.0);; val c: Circle > sum_of_figure [a; b; c];; val it: float = 18.5663706
関数 sum_of_figure は図形の面積の合計値を求めます。リスト xs の要素はアップキャストされるので Figure のインスタンスになります。型変換した場合、サブクラスの情報は失われるため、サブクラス独自のメソッドを呼び出すことはできません。型変換したスーパークラスのメソッドしか利用できませんが、ポリモーフィズムによりサブクラスのメソッドが呼び出されるため、図形の面積を正しく計算することができます。
インスタンス変数は個々のインスタンス (オブジェクト) に割り当てられる変数です。その値はインスタンスによって変わります。クラスで共通の変数や定数を使いたい場合は、let の前にキーワード static を付けます。これを「クラス変数」といいます。簡単な例を示しましょう。
リスト : クラス変数 type Foo() = let mutable x = 1 static let mutable y = 2 member this.X with get() = x and set(v) = x <- v member this.Y with get() = y and set(v) = y <- v
type Foo = new: unit -> Foo member X: int with get, set member Y: int with get, set
> let a = new Foo();; val a: Foo > let b = new Foo();; val b: Foo > a.X;; val it: int = 1 > a.X <- 10;; val it: unit = () > a.X;; val it: int = 10 > b.X;; val it: int = 1 > a.Y;; val it: int = 2 > a.Y <- 20;; val it: unit = () > a.Y;; val it: int = 20 > b.Y;; val it: int = 20
インスタンスを生成して変数 a, b にセットします。x はインスタンス変数なので、a.X の値を書き換えても b.X の値は変わりません。変数 y は static 宣言されているのでクラス変数になります。a.Y と b.Y とすると、クラス変数 y にアクセスします。そして、a.Y の値を 20 に書き換えると、b.Y の値も 20 になります。
メソッドは個々のインスタンスを操作する関数です。一般に、ユーザが定義するメソッドは引数のインスタンスを操作対象とし、クラスの動作にかかわることはありません。インスタンスを操作するメソッドを「インスタンスメソッド」といいます。これに対し、クラスの動作にかかわるメソッドを考えることができます。これを「クラスメソッド」といいます。F# は member の前にキーワード static を付けるとクラスメソッドになります。
たとえば、クラス Foo のクラス変数 y を操作するクラスメソッド getY(), setY() を作りましょう。次のリストを見てください。
リスト : クラスメソッド type Foo() = let mutable x = 1 static let mutable y = 2 member this.X with get() = x and set(v) = x <- v // クラスメソッド static member getY() = y static member setY(v) = y <- v
クラスメソッドを定義するとき、自己修飾子は付けないでください。クラスメソッドの呼び出しは次のように行います。
class.method(args, ...)
class はクラス名、method() はクラスメソッドを表します。クラスメソッドはインスタンスを生成しなくても呼び出すことができます。逆にいえば、インスタンスからクラスメソッドを呼び出すことはできません。簡単な例を示しましょう。
type Foo = new: unit -> Foo static member getY: unit -> int static member setY: v: int -> unit member X: int with get, set
> let a = new Foo();; val a: Foo > a.getY();; => エラー > Foo.getY();; val it: int = 2 > Foo.setY(20);; val it: unit = () > Foo.getY();; val it: int = 20
クラス変数とクラスメソッドはインスタンス変数とインスタンスメソッドと同様に継承されます。簡単な例を示しましょう。
リスト : クラス変数とクラスメソッドの継承 type Foo() = static let mutable x = 1 static member getX() = x static member setX(v) = x <- v type Bar() = inherit Foo()
type Foo = new: unit -> Foo static member getX: unit -> int static member setX: v: int -> unit type Bar = inherit Foo new: unit -> Bar
> Foo.getX();; val it: int = 1 > Bar.getX();; val it: int = 1 > Foo.setX(10);; val it: unit = () > Foo.getX();; val it: int = 10 > Bar.getX();; val it: int = 10 > Bar.setX(100);; val it: unit = () > Foo.getX();; val it: int = 100 > Bar.getX();; val it: int = 100
クラス Foo にはクラス変数 x とクラスメソッド getX(), setX() が定義されています。クラス Bar はクラス Foo を継承しているので、クラスメソッド getX(), setX() は、Foo と Bar どちらからでも呼び出すことができます。
一般的なオブジェクト指向言語の場合、継承はインスタンス変数やメソッドに作用するだけではなく、型にも作用します。サブクラスに属するインスタンスは型も継承されるため、スーパークラスにアップキャストして取り扱うことができるのです。F# にはインスタンスをダウンキャストできるか判定する演算子 :? が用意されています。
object :? type
:? は object を type にダウンキャストできるならば真を返します。簡単な例を示しましょう。
> type Foo() = class end;; type Foo = new: unit -> Foo > type Bar() = class inherit Foo() end;; type Bar = inherit Foo new: unit -> Bar > type Baz() = class inherit Bar() end;; type Baz = inherit Bar new: unit -> Baz > let a = new Foo();; val a: Foo > let b: Foo = new Bar();; val b: Foo > let c: Foo = new Baz();; val c: Foo > a :? Bar;; val it: bool = false > a :? Baz;; val it: bool = false > b :? Bar;; val it: bool = true > b :? Baz;; val it: bool = false > c :? Bar;; val it: bool = true > c :? Baz;; val it: bool = true
クラス Bar はクラス Foo を継承し、クラス Baz はクラス Bar を継承します。変数 a には Foo のインスタンスをセットします。Bar, Baz のインスタンスはアップキャストして、型 Foo の変数 b, c にセットすることができます。a は Foo のインスタンスなので、Bar, Baz にダウンキャストすることはできません。b は Bar のインスタンスなので、Bar にダウンキャストできますが、Baz にダウンキャストはできません。c は Baz のインスタンスなので、Bar, Baz どちらにでもダウンキャストすることができます。
このように、クラスを単一継承してサブクラスを作ると、サブクラスはスーパークラスの部分集合として考えることができます。下図を見てください。サブクラス Baz は Bar や Foo に含まれているので、そのインスタンスに Bar や Foo のメソッドを適用することができるわけです。
┌──────────┐ │ Foo │ │ ┌──────┐ │ │ │ Bar │ │ │ │ ┌──┐ │ │ │ │ │Baz │ │ │ │ │ │ │ │ │ │ │ └──┘ │ │ │ │ │ │ │ └──────┘ │ │ │ └──────────┘ 図 4 : クラスとサブクラスの関係
なお、演算子 :? はパターンマッチングでも使用することができます。
?: type [as identifier] -> 式
これを「型テストパターン」といいます。入力データが型 type にダウンキャストできれば式を実行します。
簡単な例を示しましょう。
リスト : 型テストパターン let check (x: Foo) = match x with :? Baz -> printfn "Baz" | :? Bar -> printfn "Bar" | _ -> ()
> check a;; val it: unit = () > check b;; Bar val it: unit = () > check c;; Baz val it: unit = ()
変数 a, b, c の型は Foo ですが、実体は a が Foo のインスタンス、b が Bar のインスタンス、c が Baz のインスタンスです。check a を実行すると、a は Bar, Baz ともにダウンキャストできないので、最後の節が実行されます。check b は Baz にダウンキャストできませんが、Bar にダウンキャストできるので "Bar" と表示されます。同様に、check c は "Baz" と表示されます。
なお、1 番目と 2 番目の節を逆にすると、check c は "Bar" と表示されます。c は Bar と Baz のどちらでもダウンキャストできるので、型テスト :? Bar が :? Baz よりも先に行われると、:? Bar のテストが必ず成功するので、:? Baz のテストは行われないことになります。下位のサブクラスからチェックするよう注意してください。