kentana20 技忘録

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

詳解Swift#7(Chapter10 プロトコル)

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

詳解 Swift 改訂版

詳解 Swift 改訂版

chapter10 プロトコル

コレより前の章でもちょこちょこ出てきていたプロトコルについての話。プロトコル実装を伴わない宣言の集合 と書いている。インタフェースによく似ている。インタフェースの継承のようにインスタンスがどのような機能を持つか、ということをSwiftではプロトコルで実現していて

  • クラス
  • 構造体
  • 列挙型

のそれぞれにプロトコルを使って標準ライブラリが持つ機能を継承できるらしい。

プロトコルの宣言

プロトコル名はキャメルケースの大文字開始で宣言する。

///   `CustomStringConvertible`
public protocol CustomDebugStringConvertible {
  /// A textual representation of the instance, suitable for debugging.
  var debugDescription: String { get }
}
protocol Personal {
    var name: String { get }
    init(name: String)
    func sayHelloto(p:Personal)
    func saySomething() -> String
}

メソッドは宣言だけして、コードブロック {} は記述しない。

  • イニシャライザ

プロトコルのイニシャライザは、継承するクラスでは必須イニシャライザとなるので、 required 修飾子を付ける必要がある。

protocol Personal {
    var name: String { get }
    init(name: String)
    func sayHelloTo(p:Personal)
    func saySomething() -> String
}

class Citizen: Personal {
    let name: String
    required init(name: String) {
        self.name = name
    }
    func sayHelloTo(p: Personal) {
        print("どうも、 \(p.name) さん")
    }
    func saySomething() -> String {
        return "なるほど"
    }
}

var ct = Citizen(name: "kentana")
var ct2 = Citizen(name: "tanaken")
ct.saySomething() // なるほど
ct.sayHelloTo(ct2) // どうも、 tanaken さん
ct2.sayHelloTo(ct) // どうも、 kentana さん
  • クラス定義のためのプロトコル
    • 用途がクラスに限定されているプロトコルprotocol プロトコル名: class { と書く

プロトコルと型

プロトコルは型としても使える。(というか、↑の例で既に引数の型としてPersonalを使ってる)

enum Sex { case Male, Female } 
protocol HealthInfo: Personal { // Personalを継承
    var weight: Double { get set }
    var height: Double { get set }
    var sex: Sex? { get }
}
// 新しく合成したプロトコルを定義
protocol HelthyPerson: Healthy, Personal {}

// 定義せずに合成して型とする
var teller: protocol<Healthy, Personal>
exp is P   // 適合している場合はtrue, していない場合はfalse
exp as P   // プロトコルP型にキャストされる。適合してない場合は実行時エラー
exp as? P  // プロトコルP型にダウンキャストされる。適合してない場合はnil
exp as! P  // プロトコルP型にダウンキャストされる。適合してない場合は実行時エラー
@objc protocol PersonalData {
    var name: String { get }
    optional avr bloodtype: String { get }
    optional func age() -> Int
}

var p: PersonalData = ...
if let btype = p.bloodtype {
    print(btype)
}

if let a = p.age?() {
    print("hoge")
}

プロトコルと付属型

  • プロトコル内で付属型を定義することで、そのプロトコルを継承した構造体/クラスが付属型を任意に扱うことができる
    • 付属型にはプロトコル名を記述することで、適合する型を制限することもできる
protocol VectorType {
    typealias Element
    var x : Element { get set }
    var y : Element { get set }
}

struct VectorFloat : VectorType {
    var x, y: Float
    func convToDouble() -> VectorDouble {
        return VectorDouble(
            x: VectorDouble.Element(x),
            y: VectorDouble.Element(y))
    }
}
struct VectorDouble : VectorType, CustomStringConvertible {
    var x, y: Double
    var description: String { return "[\(x), \(y)]" }
}
struct VectorGrade : VectorType {
    enum Element { case A, B, C, D, X }
    var x, y: Element
}

var a = VectorFloat(x: 10.0, y: 12.0)
print("x:\(a.x), y:\(a.y)")
let b = a.convToDouble()
print(b) // [10.0, 12.0]
var g = VectorGrade(x: .A, y: .C)
print(g) // VectorGrade(x: VectorGrade.Element.A, y: VectorGrade.Element.C)

プロトコルに適合する型の定義方法

Swiftのデータ型の多くは構造体で定義されているが、この構造体も標準のプロトコルを使っている。ここに合わせる形で定義するデータ型も標準のプロトコルに適合させると、既存のデータ型と同じように使える。これは確かによいかもしれない。

  • Comparable / Equatable
  • ForwardIndexType
    • for-in で繰り返しできるように範囲を作れる
enum WeekDay: Int, ForwardIndexType {
    case Sun, Mon, Tue, Wed, Thu, Fri, Sat
    case _dummy
    func successor() -> WeekDay {
        if self == ._dummy {
            return ._dummy
        }
        return WeekDay(rawValue: self.rawValue + 1)!
    }
}

var d = WeekDay.Sun
print(d.rawValue) // 0
print(d.successor().rawValue) // 1

var r = Range<WeekDay>(start:.Mon, end:.Fri)
for i in WeekDay.Sun ... WeekDay.Sat {
    print(i.rawValue, terminator:" ") // 0 1 2 3 4 5 6
}

for i in WeekDay.Mon ... .Sat {
    print(i.rawValue, terminator:" ") // 1 2 3 4 5 6
}
protocol Copyable {
    func copy() -> Self
}

class GeoPoint : Copyable, Equatable, CustomStringConvertible {
    let latitude: Double                    // 緯度
    let longitude: Double                   // 経度
    required init(lat:Double, lon:Double) { // 必須イニシャライザ
        self.latitude = lat
        self.longitude = lon
    }
    func copy() -> Self {
        let nw = self.dynamicType.init(lat:latitude, lon:longitude)
        return nw
    }
    var description: String {
        return "GeoPoint:\(latitude), \(longitude)"
    }
}

func == (lhs:GeoPoint, rhs:GeoPoint) -> Bool {
    return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude
}

継承が絡んで来る場合、Selfではなく self.dynamicType を使わないとサブクラス側から呼び出された場合にスーパークラスインスタンスを参照してしまうので注意が必要。

所感・雑感

  • プロトコルC#, Javaなどでいうとこのインタフェース。特に違和感なく使えそう
  • Swift標準を意識してプログラムを書くとプロトコルに適合するようになるのかもしれない
  • プロトコルの合成はインタフェースの組み合わせを自在に定義できるので、シンプルなプロトコルをいくつか定義していおいて、それらを組み合わせてかなり便利に使えそう
  • @objc 修飾子は Objective-C と組み合わせる意図はなく、オプショナルの項目を作りたい場合でも定義できるのは名前が直感的でないのでイマイチ感ある。そして @objc 修飾したプロトコルはクラスでしか使えない。これは覚えられる気がしないので、実戦で掴んでいかないといかん。
  • 付属型はちょっとよくわからない。というか、複雑なコードになったときに活躍しそうなので、いまはまだ評価できないという印象
  • Self難しい。