kentana20 技忘録

技術ネタを中心に、セミナー、勉強会、書籍、会社での出来事を綴っていきます。不定期更新。

詳解Swift#5(Chapter8 クラス)

前回に続き、詳解Swiftの写経を続ける。

詳解 Swift 改訂版

詳解 Swift 改訂版

chapter8 クラス

クラス定義

構造体とは異なり、クラスでは 全項目イニシャライザを使えない(カプセル化の思想と相性悪)。

class Time {
    var hour = 0, min = 0
    init(hour:Int, min:Int) {
        self.hour = hour; self.min = min
    }
    func add(min:Int) {             // mutatingは不要
        let m = self.min + min
        if m >= 60 {
            self.min = m % 60
            let t = self.hour + m / 60
            self.hour = t % 24
        }else{
            self.min = m
        }
    }
    func inc() {                    // mutatingは不要
        self.add(1);
    }
    func toString() -> String {
        let h = hour < 10 ? " \(hour)":"\(hour)"
        let m = min < 10 ? "0\(min)":"\(min)"
        return h + ":" + m
    }
}

let t1 = Time(hour:13, min:20)
let t2 = t1
print(t1.toString())   // 13:20 を出力
t1.inc()
print(t2.toString())   // 13:21 を出力
  • クラスの継承
    • クラスを継承した場合、メソッドやプロパティなど殆どのものは引き継ぐが、イニシャライザは継承しない
    • override キーワードでスーパークラスメソッドをオーバーライドできる
    • サブクラスの定義は self. , スーパークラスの定義は super. キーワードを使う
class クラス名: スーパークラス名 {

}

で書ける。

// Timeを継承、プロトコルを採用
class Time12 : Time, CustomStringConvertible {
    var pm:Bool                       // 新しいインスタンス変数。午後なら真
    init(hour:Int, min:Int, pm:Bool) {// 新しいイニシャライザ
        self.pm = pm
        super.init(hour:hour, min:min)
    }   // スーパークラスのイニシャライザを呼び出して使う
    override convenience init(hour:Int, min:Int) { // 24時制
        let isPm = hour >= 12
        self.init(hour:isPm ? hour - 12 : hour, min:min, pm:isPm)
    }   // スーパークラスのイニシャライザを上書きする
    override func add(min:Int) {    // メソッドを上書き
        super.add(min)              // スーパークラスのメソッド
        while hour >= 12 {          // hourはスーパークラスの定義
            hour -= 12
            pm = !pm
        }
    }
    var description : String {// スーパークラスのメソッドを利用
        return toString() + " " + (pm ? "PM" : "AM")
    }
}

convenience は他のイニシャライザ(この例だと直上のイニシャライザ)に処理を委譲する場合に使う。 CustomStringConvertibleプロトコルdescription をつけて print(Time12型のインスタンス) で標準出力を可能にする。 これを除くと書いてることは結構シンプル。

  • 動的結合とキャスト

  • クラスメソッド / クラスプロパティ

    • class キーワードで書くメソッドとプロパティ
    • タイププロパティとは異なり、サブクラスでの上書きができる(タイプメソッド・タイププロパティはサブクラスでは上書きできない)
    • クラスプロパティは計算型に限定されていて、格納型は定義できない
class A : CustomStringConvertible {
    static var className : String { return "A" }  // 計算型タイププロパティ
    static var total = 0                          // 格納型タイププロパティ
    class var level : Int { return 1 }            // 計算型クラスプロパティ
    static func point() -> Int { return 1000 }    // タイプメソッド
    class func say() -> String { return "Ah." }   // クラスメソッド
    init() { ++A.total }
    var description: String {
        return "\(A.total): \(A.className), "
            + "Level=\(A.level), \(A.point())pt, \(A.say())"
    }
}

class B : A {
    // override static var className : String { return "B" }
    // 定義不可. error: class var overrides a 'final' class var
    // static var total = 0
    // 定義不可. error: cannot override with a stored property 'total'
    override class var level : Int { return 2 }
    // override static func point() -> Int { return 2000 }
    // 定義不可. error: class method overrides a 'final' class method
    override class func say() -> String { return "Boo" }
    override init() { super.init(); ++B.total }
    override var description: String {
        return "\(B.total): \(B.className), "
            + "Level=\(B.level), \(B.point())pt, \(B.say())"
    }
}

let a = A()
print(a)
let b = B()
print(b)

こんな感じ。

クラスのイニシャライザ

Swiftでは、ベースクラスを継承して作られているクラスのインスタンスを正しくつくるために、イニシャライザにはいくつかのルールがある。

用語の話

  • 指定イニシャライザ
  • 簡易イニシャライザ
    • ほかのイニシャライザも使ってインスタンスの初期化をする
    • ほかのイニシャライザを使って初期化を任せることをデリゲートと呼ぶ
    • convenience というキーワードを付ける

どのクラスも指定イニシャライザを最低1つは備えている必要がある。


これは↓の画像を見るのが一番理解が早い。

  1. 簡易イニシャライザ
  2. 指定イニシャライザ
  3. スーパークラスの指定イニシャライザ
  4. ...

という順序でイニシャライズされる。

initialize.jpg

イニシャライズ時の制約も結構ある。

  1. 指定イニシャライザはクラスで追加されている変数、定数の初期化をしなければいけない
  2. サブクラスのイニシャライザはスーパークラスのイニシャライズが終わるまではスーパークラスで定義されている変数、定数を扱うことはできない
  3. 簡易イニシャライザは指定イニシャライザの初期化処理が終わるまで変数、定数を扱うことはできない

  4. 継承しているクラスのイニシャライザのサンプル

// Zellers congruence
func dayOfWeek(var m:Int, let _ d:Int, var year y:Int) -> Int {
    if m < 3 {
        m += 12; --y
    }
    let leap = y + y / 4 - y / 100 + y / 400
    return (leap + (13 * m + 8) / 5 + d) % 7
}

// base class
class DayOfMonth : CustomStringConvertible {
    var month, day: Int
    init(month:Int, day:Int) {
        self.month = month; self.day = day
    }
    var description: String {
        return DayOfMonth.twoDigits(month)
            + "/" + DayOfMonth.twoDigits(day)
    }
    class func twoDigits(n:Int) -> String {
        let i = n % 100
        if (i < 10) { return "0\(i)" }
        return "\(i)"
    }
}

// sub class1
class Date: DayOfMonth {
    var year: Int
    var dow: Int
    init(_ y:Int, _ m:Int, _ d:Int) {
        year = y
        dow = dayOfWeek(m, d, year: y)
        super.init(month: m, day: d)
    }
}

// sub class2
class DateW : Date {
    static let namesOfDay = [
        "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ]
    var dweek = String()
    
    // 指定イニシャライザ(スーパークラスのイニシャライザと引数が同じなのでoverrideする)
    override init(_ y:Int, _ m:Int, _ d:Int) {
        super.init(y, m, d)
        dweek = DateW.namesOfDay[dow]
    }
    
    // 簡易イニシャライザ
    convenience init(_ m: Int, _ d:Int, year:Int = 2016) {
        self.init(year, m, d)
    }
    
    // プロパティオブザーバ(didSetなので、dayの値が変更した直後に発動する)
    override var day:Int {
        didSet {
            dow = dayOfWeek(month, day, year:year)
            dweek = DateW.namesOfDay[dow]
        }
    }
    
    // printで呼ばれるプロパティの上書き(本質ではない)
    override var description: String {
        return "\(year)/" + super.description + " (\(dweek))"
    }
}

var d = DateW(1991, 5, 8)
print(d, terminator:"") // 1991/05/08(Wed)
d.day++
print(d, terminator:"") // 1991/05/09(Thu)

ここまで書いたところで、初期化についてのまとめ。

  • パターン共通(すべてのケースに当てはまる)
    • プロパティの値がすべて設定されるまで、処理はできない
    • プロパティに初期値がある場合はイニシャライザで初期化しなくてもよい
  • パターン1: スーパークラスがない場合
    • イニシャライザで各プロパティの値を設定すればOK。シンプル
  • パターン2: 同じクラスの別のイニシャライザを使う(デリゲートする)場合
    • デリゲートするイニシャライザの呼出し後に変数の設定をし直せる
  • パターン3: スーパークラスがある場合
    • スーパークラスのイニシャライザを呼び出す前に自身のクラスで定義している変数・定数の初期化が済んでいる必要がある

少し端折ってるけど、こんな感じだと思います。

  • イニシャライザの継承

  • サブクラスで指定イニシャライザを定義していない

  • サブクラスがスーパークラスの指定イニシャライザをすべて持っている

この2つの条件のどちらかを満たす場合にサブクラスでスーパークラスのイニシャライザを継承できるらしい。「2.」の意味がよくわからない。。

  • 必須イニシャライザ
class ICCard {
    static let Deposit = 500
    var money = 0
    required init(charge:Int) {
        money = charge - ICCard.Deposit
    }
}

class Icocai : ICCard {
    static var serial = 0
    let idnumber: Int
    init(id:Int, money:Int) {
        idnumber = id
        super.init(charge:money)
    }
    required init(charge:Int) {
        idnumber = ++Icocai.serial
        super.init(charge:charge)
    }
}

class Suiica : ICCard {
    var name = String()
}

var card = Suiica(charge:2000)
print(card.money) // 1500

var mycard = Icocai(charge:2000)
print(mycard.money) // 1500

required なイニシャライザを持つクラスを継承したサブクラスは、同じイニシャライザの実装が必須となる。

  • 失敗のあるイニシャライザ

指定イニシャライザの場合、すべての変数・定数の値が設定されるまで return nil できないが、簡易イニシャライザの場合はいつでも return nil できる。

通常、失敗のあるイニシャライザを使ってクラスのインスタンスを作るとオプショナル型になるが、構造体と同じく、 ! で開示すると、オプショナル型ではなくなる。

この辺りは基本的に構造体と同じ設計思想。

継承とサブクラスの定義

  • プロパティの継承

スーパークラスのプロパティ(格納型、計算型どちらも)を継承して上書きできる。参照するときは super.プロパティ名 と書く。

  • 継承とプロパティオブザーバ

スーパークラスのプロパティを監視できる。計算型、格納型の制限はない。

class Propα  {
    var attr = 0
}

class Propβ : Propα  {
    override var attr : Int {
        willSet{ print("β: will set") }
        didSet{ print("β: did set") }
    }
}

class Propγ : Propβ {
    override var attr : Int {
        willSet{ print("γ: will set") }
        didSet{ print("γ: did set") }
    }
}

var g = Propγ()
g.attr = 1
  • 継承をさせない

サブクラスでの継承をさせたくないメソッド、プロパティは final 修飾子を定義の先頭に付ける。final 修飾子がついたメソッド、プロパティを継承/overrideしようとするとコンパイル時にエラーとなる。

  • 継承と型推論 / クラスのメタタイプ

クラスAと、クラスAを継承したクラスBのそれぞれのインスタンスを含む配列をつくると、その配列の型はスーパークラスであるクラスAとなる。

class Hoge {
    var text: String
    init() {
        text = "hoge"
    }
}

class Fuga: Hoge {
    init(msg: String) {
        super.init()
        super.text = msg
    }
}

var h1 = Hoge()
var f1 = Fuga(msg: "fuga")

var array = [h1, f1] // [Hoge]
array.dynamicType // Array<Hoge>.Type

xxxx.dynamicType で実際に属するクラスを表すオブジェクトを返す。コレ欲しかったやつだ。

解放時処理

  • Swiftではインスタンスは自動的に消去される
  • クラスのインスタンスは参照型なので、Swiftが内部に持っているリファレンスカウンタで管理している
  • いつ消去されるかは実行時までわからない
  • 明示的に消すことで、ファイルのクローズやメモリの解放を行うことができる

解放処理を デイニシャライザ とよぶ。

deinit {
    
}

で解放処理を書く。

遅延格納型プロパティ

初期化の際には値を決めずに、必要になったタイミングで初めて値を決めるプロパティのこと。

ex. 画像情報の読み込み

- 画像は使われるかもしれないし、使われないかもしれない
- パスだけは初期化時に指定しておく
- 画像が必要になったタイミングで遅延読み込みを行うことで、画面初期化時の処理を減らせる

こんなイメージ。 lazy キーワードをプロパティの宣言の先頭に書く。

import Foundation

class FileAttr {
    let filename: String
    // 遅延格納型プロパティ
    lazy var size:Int = self.getFileSize()
    init(file: String) {
        filename = file
    }
    func getFileSize() -> Int {
        var buffer = stat()
        stat(filename, &buffer)
        print("[getFileSize]")
        return Int(buffer.st_size)
    }
}

let d = FileAttr(file:"/Users/kentana20/Desktop/363.png")
print(d.filename)
print(d.size) // getFileSize() が呼ばれる
print(d.size)
  • いくつかの制限がある
    • クラス / 構造体定義の内部でのみ使用可能
    • プロパティは定数ではなく変数とする
    • 構造体の場合で、遅延評価を引き起こす可能性のあるメソッドには mutating が必要となる
    • プロパティオブザーバは指定できない

所感・雑感

クラス

  • イニシャライザ

    • ひととおりの把握はしたものの、奥が深すぎて実戦経験積まないと手に馴染まなそう
    • イニシャライザの継承は正直あまり使わずに(というか継承自体そんなに使わずに) super.init を基本形とするべきだと思った
    • イニシャライザはそんなにバンバン増やすものではないのでは。なるべく増やさないほうがインスタンスの見通しが明るくなりそうだと感じた。
  • 継承

    • オブジェクト指向言語の継承そのままといったイメージ
    • クラス自体、プロパティ、メソッドの継承や final での制限など、すんなり使えそう
    • 継承の文脈ではないが、 Any, AnyObject 型はどれくらい使うのだろう。型に厳密なSwiftのよさをスポイルしてしまいそう。
  • 解放処理

    • ファイルI/O、メモリ大食いなどの処理を行うクラスについては deinit 処理を明示的に書いていったほうがよい
    • ネイティブアプリのメモリ管理はシビアそうなので、チューニングやコーナーケースのデバッグなどではこの対応を行うケースがありそう
  • 遅延格納型プロパティ

    • 使えそう
    • ネイティブアプリにおいては遅延評価をするとパフォーマンスを意識したプログラミングができそう
  • ほか

    • ここまで読んで xxxx.dynamicType の存在を知った。ほっとした。