Swiftのクラス継承 | オーバーライド(Overriding)

Swiftのクラス継承(Inheritance)

オーバーライド(overriding)です。クラス特有の機能で、派生先のサブクラスでプロパティやメソッドに独自の実装を加えたい時に必要になります。そもそも継承出来ないと使えませんので、構造体や列挙型等では使用出来ません。

ここでは、オーバーライドとは何か?という定義から始めて、メソッドのオーバーライド、プロパティのオーバーライドに関して説明します。また、プロパティ監視のオーバーライドについても紹介して、最後にオーバーライドを防ぐ(オーバーライドさせない)ためにはどうするば良いか?という点に簡単に触れて、まとめます。

オーバーライド(overriding)って何?

同じインターフェイスで異なる実装

オーバーライド(overriding)というのは、一言で表現すると「機能の上書き」です。Override自体の意味は「無視する」、「優先する」、「再定義する」という訳が割り当てられますが、ここでは「再定義する」が一番しっくり来ます。「上書きする」とか「置換する」という意味で、replaceが結構近いと思います。もうちょっと具体的に定義すると、

スーパークラス(superclass)から継承したメソッド(instance method, type method)、プロパティ(instance property, type property)、サブスクリプト、をサブクラス(subclass)の独自の実装で置き換える(上書きする)こと

をオーバーライドと呼びます。この定義から自明ですが、オーバーライドはクラス(classes)特有の機能で、継承の出来ない構造体(structures)や列挙型(enumerations)では使えません。

オーバーライドを「スーパークラスの実装を無視して、サブクラス独自の実装をする」という風に捉えれば、「無視する」とか「優先する」という意味も悪くないと思います。

overrideキーワードでオーバーライド

Swiftでは、オーバーライドを実装する場合はoverrideキーワードを使います。後で具体例を示しつつ詳しく説明しますが、overrideキーワードを使用せずにオーバーライドを実装しようとすると、コンパイルエラーを起こします。これは意図せずにオーバーライドしてしまうことを防ぐための措置で、コードを書く側としては「オーバーライドするよ」と宣言してオーバーライドすることになるので、分かりやすいですね。

以下、具体的なオーバーライドの実装方法について説明していきます。オーバーライドは

に実装できます。

スーパークラス(Superclass)のメソッド、プロパティ、サブスクリプトへのアクセス

オーバーライドの具体的な実装方法に入る前に、1つ補足です。

サブクラスでオーバーライドを実装した機能(メソッド、プロパティ、またはサブスクリプト)でも、スーパークラス(superclass)の機能を使いたいケースがあるかもしれません。そのような場合には、スーパークラスのメソッド等の前にsuper前置詞を付けます。具体例を使った方が分かりやすいので、次のセクションで説明します。

メソッドのオーバーライド

先ずはメソッドのオーバーライドです。インスタンスメソッド(instance methods)タイプメソッド(type methods)両方でオーバーライドが使えます。ここでは、インスタンスメソッドでオーバーライドを適用した場合の具体例です。

インスタンスメソッドのオーバーライドの具体例

// 名前付き形クラス
class NamedShape: Shape {
    var name: String = ""

    override func description() -> String {
        return "名前は\(name)、" + super.description()
    }
}

一つ前の継承のページで作ったNamedShapeを再利用しています。ここでは、メソッドdescription()をオーバーライドして、名前nameの表示を追加しています。

繰り返しになりますが、オーバーライドしたい場合は、上記のようにoverrideキーワードを使います。こうすることで、サブクラス先での独自機能を実装できます。サブクラスでオーバーライドしたメソッドdescription()は、サブクラスで宣言したプロパティnameを含めた文字列を返すように変更されています。

スーパークラスのインスタンスメソッドにはsuper.method()でアクセス

また、description()内部では、スーパークラスの同じメソッドsuper.description()を呼び出しています。メソッドの名前は同じですが、スーパークラスの機能が必要な際には、superを付けることでアクセス可能です。これは、メソッドに限らずプロパティやサブスクリプトに関しても同様です。

// スーパークラスのメソッドへのアクセスにはsuperを使う        
return "名前は\(name)、" + super.description()

明示的なオーバーライドの指定をしないとコンパイルエラー

実際に、サブクラスのインスタンスを作って、メソッドdescription()を呼び出すと

let namedShape = NamedShape()
namedShape.name = "named shape"
print(namedShape.description())
//"名前はnamed shape、0個の頂点を持っています。面積は0.0です。"

という具合に、ちゃんとオーバーライドしたメソッドがコールされているのが分かります。

もしoverrideキーワードを使わずに、メソッドのオーバーライドを実装しようとすると

//error: overriding declaration requires an 'override' keyword

このような感じで怒られます。この仕様によって、意図しないメソッド(またはプロパティなど)のオーバーライドを未然に防ぐことが出来ます。

プロパティのオーバーライドに関して

プロパティのオーバーライドで追加できる機能は、

になります。オーバーライドする対象となるプロパティに関しての制限はありません。例えば、スーパークラスの格納プロパティ(stored property)に対して、サブクラスでgetterとsetterを追加する、という実装も可能です。

試しにやってみましょう。

// スーパークラスの格納プロパティをオーバーライドして、getterとsetter(計算プロパティの機能)をサブクラスで追加
// スーパークラス
class SuperClass {
    var propertySuperClass = "Hello world!"
}

// サブクラス
class SubClass: SuperClass {
    override var propertySuperClass: String {
        get {
            return super.propertySuperClass + " (サブクラス)"
        }
        set {
            super.propertySuperClass = newValue
        }
    }
}

実用性の無い具体例ですが、上記の例のように、スーパークラスでは格納プロパティでサブクラスでは計算プロパティ、という実装も可能です。

以下、計算プロパティとプロパティ監視のオーバーライドに関して、具体例を使って詳しく説明していきます。

プロパティGetter & Setterのオーバーライド

オーバーライドでは名前と型だけが重要

計算プロパティの機能(getterとsetter)は、オーバーライドによって実装することが可能です。その際、オーバーライドの対象となるプロパティは何でも構いません。つまり、

スーパークラスの格納プロパティ(stored properties)や計算プロパティ(computed properties)を継承して、サブクラスでgetterやsetterを実装出来る、

ということになります。

公式マニュアルにも書いてありますが、プロパティに関してサブクラスが把握しているのは、プロパティの名前と型だけです。別の言い方をすると、サブクラスにとっては、スーパークラスのプロパティが格納プロパティだろうが計算プロパティだろうが関係ありません。「名前と型だけが重要」なので、オーバーライドでは、スーパークラスの持つプロパティの「名前と型」を一致させなければコンパイルエラーになります。

先程の具体例で言うと、var propertySuperClass: Stringというプロパティの「名前と型」を一致させておかないといけません。例えば、型を変えてみると、

override var propertySuperClass: Int {
....
// コンパイルエラー
//error: property 'propertySuperClass' with type 'Int' cannot override a property with type 'String'

という具合に怒られます。

読み出し専用(Read-only)と読み書き両用(read-write) propertiesの違い

スーパークラスで読み出し専用(read-only)のプロパティ(つまりgetterだけの計算プロパティ)は、サブクラスで「読み出し専用」もしくは「読み書き両用」を選べます。すなわち、サブクラスでsetterを実装することができます。一方で、スーパークラスで読み書き両用
(read-write)のプロパティ(getterとsetterを持つ計算プロパティ)は、サブクラスでも読み書き両用でなければコンパイルエラーになります。

スーパークラス(superclass) サブクラス(subclass)でオーバーライド
Read-Only ◯ Read-Only
◯ Read-Write
Read-Write × Read-Only
◯ Read-Write

表にすると少し分かりやすいと思います。「Read-writeプロパティはread-onlyにできない」という例外だけ覚えておけば良いかもしれません。

プロパティgetterとsetterのオーバーライドの具体例

これも一つ前の継承のページで使った「円」クラスですが、プロパティgetterとsetterを追加して、面積を実装しています。

import Darwin

// 円
class Circle: NamedShape {
    var radius: Double = 0.0

    // Area = pi * r^2
    override var area: Double {
        get {
            return 3.14 * radius * radius
        }
        set {
            radius = sqrt(newValue/3.14)
        }
    }
}

let circle = Circle()
circle.name = "test circle"
circle.radius = 2.0
print(circle.description())
//"名前はtest circle、0個の頂点を持っています。面積は12.56です。半径=2.0。"
circle.area = 5.0
print(circle.description())
//"名前はtest circle、0個の頂点を持っています。面積は5.0です。半径=1.26188616281267。"

面積の実装を見ると分かると思いますが、プロパティgetterでは、格納プロパティradiusを使って面積を計算しています。一方、setterではインプット(パラメータ)が面積になるので、面積から逆算して半径を計算しています。インスタンスを作って検算していますが、ちゃんと動いているのが分かります。

ここで、面積から半径を計算するのに平方根を取っているので、import Darwinを、上記コードの前に追加しておかないとコンパイルエラーになります。

プロパティ監視(property observers)のオーバーライド

実用性はないですが、プロパティ監視を使うために、頂点の数によって名前が変わる形クラスを作ってみます。

// 頂点の数によって名前を変える
class AnyShape: NamedShape {
    override var numberOfVertices: Int {
        didSet {
            switch numberOfVertices {
                case 1: name = "point"
                case 2: name = "line"
                case 3: name = "triangle"
                case 4: name = "square"
                default: name = "nothing"
            }
        }
    }
}

let triangle = AnyShape()
triangle.numberOfVertices = 3
print(triangle.description())
//"名前はtriangle、3個の頂点を持っています。面積は0.0です。"
triangle.numberOfVertices = 4
print(triangle.description())
//"名前はsquare、4個の頂点を持っています。面積は0.0です。"

プロパティ監視のdidSetを使って、頂点の数に応じて名前を変えています。インスタンスを作って、頂点が3つの場合と4つの場合でテストしていますが、ちゃんと動いていますね。

プロパティ監視をオーバーライドで実装する場合には、色々と制約があります。

  1. 定数の格納プロパティ(constant stored properties)と読み出し専用計算プロパティ(read-only computed properties)には、プロパティ監視を追加できない
  2. プロパティsetterとプロパティ監視は同時に実装できない

1つ目に関しては自明だと思いますが、プロパティ監視内ではプロパティの値の変更が可能です。したがって、それが不可能な定数プロパティと読み出し専用の計算プロパティには実装できません。

また、2番目に関しては、例えば既にプロパティsetterが実装されているなら、setter内部に「監視機能」を追加することは可能なので、プロパティ監視を実装する意味がありません(おそらく逆もしかりですが、ちゃんと検証していません)。したがって、これら2つの機能は同時に実装できないようになっています。

オーバーライドを防ぐには? | finalキーワード

オーバーライドしてほしくないプロパティやメソッドがある場合、その定義構文の前にfinalを付けることで、サブクラスでのオーバーライドを防いでくれます。具体的には、

  • final var(プロパティ)
  • final func(メソッド)
  • final class func(型メソッド)
  • final subscript(サブスクリプト)

という感じで定義します。

試しに格納プロパティでやってみると、

// スーパークラス
class SuperClass {
    final var propertySuperClass = "Hello world!"
}

// 子クラス(サブクラス)
class SubClass: SuperClass {
    override var propertySuperClass: String {
        get {
            return super.propertySuperClass + " (サブクラス)"
        }
        set {
            super.propertySuperClass = newValue
        }
    }
}

という感じです。

このプロパティをサブクラスでオーバーライドしようとすると、

// finalキーワード付きだとオーバーライド出来ない
let subClass = SubClass();
print(subClass.propertySuperClass)
// コンパイルエラー
//error: var overrides a 'final' var
//        override var propertySuperClass: String {
//                     ^
// ....

こんな感じで怒られます。

クラス全体に適用したい場合には、クラスの前にfinalを付けると良いみたいです。上記の例ですと、

// クラス全体をオーバーライド不可にしたい場合
final class SuperClass {
    var propertySuperClass = "Hello world!"
}
....

となります。この場合、クラスが持つ全てのプロパティやメソッドがfinalキーワードの対象となります。

まとめ

  • オーバーライド(overriding)は、スーパークラスから継承したメソッド、プロパティ、サブスクリプトの「機能を上書き」すること
  • オーバーライドを実装したい場合はoverrideを定義構文の前に置く
  • サブクラスでスーパークラスの機能(メソッドやプロパティなど)を使用したい場合は、スーパークラスを明示的に示すsuperを使ってアクセスする
  • オーバーライドを防ぐには、スーパークラスでfinalキーワードを使う
  • クラス全体をオーバーライド出来ないようにしたい場合は、クラスの前にfinalキーワードを置く(final class