読者です 読者をやめる 読者になる 読者になる

【Swift】Appleの新言語「Swift」のリファレンスを読む(16) - Protocols

注意

  • あくまでメモ書きなので細かい部分を端折りますし、色々間違ってるかもしれません。ちゃんとした内容は原文を読んでね。
  • コード例は基本的に原文からそのまま引用していますが、ちょっとした注釈をつけたり、統合したりしています。
  • SyntaxHighlighterが対応してないので微妙に読みにくいです。SyntaxHighlighterはこちらのものを使用させて頂いてます。
  • 他言語にそっくりな部分でも指摘しない。(自戒)

Protocolsプロトコル

俗に言うインターフェースと言うやつです。Objective-Cでもその手の機構はプロトコルと呼ばれていたみたいなので、そのまま引き継いだのでしょう。

普段はJavaとかC#とかをやってるので、どうしてもこの手のものを使用することに対して「実装する」(implement)って言ってしまうんですが、SwiftObjective-C)では「適合する」(conform to、adapt to)と呼んでいるそうです。個人的には非常に違和感があるんですが、仕方ないのでこの章ではそう呼びます。

また、オブジェクト指向におけるインターフェースの役割やメリットだとかポリモーフィズムの話とかをしてもいいんですが、そんな普遍的なことをここで話しても仕方ないので、文法中心にささっと見ていきたいと思います。

Protocol Syntax

プロトコルの宣言方法は以下の通りです。

protocol SomeProtocol {
    // protocol definition goes here
}

ある型にプロトコルを適合させる場合は、以下のようになります。

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}

// クラスの継承を同時に行なう場合は親クラスの後に記述する
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

例にはありませんが、enumプロトコルを適合させることは可能です。

Property Requirements

プロトコルを適合させる型に計算型プロパティの宣言を強制させることができます。

ReadWriteならgetとsetを、ReadOnlyならgetだけ書いておけばOKです。

protocol SomeProtocol {
    // 計算型プロパティなのでletで宣言するとかは無理
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

クラスプロパティ(静的プロパティ)でもいけます。

protocol AnotherProtocol {
    // 値型に適合させるならclassをstaticにする必要がある
    class var someTypeProperty: Int { get set }
}

実際に適合させてみましょう。まずは構造体。

protocol FullyNamed {
    var fullName: String { get }
}

struct Person: FullyNamed {
    var fullName: String
}

let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"

FullyNamedではfullNameをReadOnlyと定めていますが、あくまで「fullNameと言うプロパティを呼び出すと何かしらStringの値が返ってくる」までが適合側の責任の範疇になるので、ReadWrite(もしくはstored property)にしても問題なく責任を果たしていることになる…んだと思います。(明示的にそう書いてあるわけではない。)

これはプロパティのオーバーライドと同じルールですね。

じゃあ次はクラス。

class Starship: FullyNamed {
    var prefix: String?
    var name: String

    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    
    var fullName: String {
        return (prefix ? prefix! + " " : "") + name
    }
}

var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"

Starship.fullNameはやはりFullyNamedによってReadOnlyであることを強制されていますが、getの中身(どのような値を返すのか)についてはStarship(適合側の型)に委譲されています。

Method Requirements

同じ要領でメソッドもやってみましょう。

基本的に普通のメソッドの文法と一緒ですが、デフォルト引数は設定できないらしいです。

protocol SomeProtocol {
    // クラスメソッド
    class func someTypeMethod()
}

protocol RandomNumberGenerator {
    func random() -> Double
}

じゃあRandomNumberGeneratorを適合させてみましょう。

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0

    func random() -> Double {
        lastRandom = ((lastRandom * a + c) % m)
        return lastRandom / m
    }
}

let generator = LinearCongruentialGenerator()
println("Here's a random number: \(generator.random())")
// prints "Here's a random number: 0.37464991998171"
println("And another one: \(generator.random())")
// prints "And another one: 0.729023776863283"

うーん。特に説明することがないですね。

Mutating Method Requirements

値型で自身のプロパティの値を書き換えるメソッドの場合はmutatingキーワードを付けなくてはいけないんですが、プロトコルでもこれは例外ではありません。

protocol Togglable {
    mutating func toggle()
}

enum OnOffSwitch: Togglable {
    case Off, On

    mutating func toggle() {
        switch self {
        case Off:
            self = On
        case On:
            self = Off
        }
    }
}

var lightSwitch = OnOffSwitch.Off
lightSwitch.toggle()
// lightSwitch is now equal to .On

じゃあmutatingキーワードが含まれているプロトコルは参照型(クラス)に適合できないのかと言うとそんなことはなく、mutatingキーワードを外した形で宣言する必要があります。

// 例がなかったので自分で書く 全くもって良い例ではないけど
class TogglableClass: Togglable {
    var isOn = false
    
    func toggle() {
        self.isOn = !self.isOn
    }
}

Protocols as Types

ポリモーフィズムの話です。(完)

class Dice {
    let sides: Int
    let generator: RandomNumberGenerator

    init(sides: Int, generator: RandomNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }
    
    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}

Dice.generatorにはRandomNumberGeneratorに適合する何かなら何でも渡せるとかそう言うアレです。

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())

for _ in 1...5 {
    println("Random dice roll is \(d6.roll())")
}
// Random dice roll is 3
// Random dice roll is 5
// Random dice roll is 4
// Random dice roll is 5
// Random dice roll is 4

Delegation

単なるデザインパターンAdapterパターン)の話なので割愛。

Adding Protocol Conformance with an Extension

前章で軽く触れましたが、拡張(Extension)を使用することで既存の型にプロトコルと実装しなければならない諸々を追加することができます。

protocol TextRepresentable {
    func asText() -> String
}

extension Dice: TextRepresentable {
    func asText() -> String {
        return "A \(sides)-sided dice"
    }
}

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
println(d12.asText())
// prints "A 12-sided dice"

拡張であってもプロトコルに適合していることは間違いないので、上記の例で言えばDiceをTextRepresentableと言う型として扱うことも可能です。

また、既にプロトコルに宣言されている諸々を実装済みの場合は、「プロトコルに適合している」と言う事実だけを拡張できます。

ちょっとわかりにくいかもしれませんがコードを見ればすぐわかると思います。

struct Hamster {
    var name: String
    
    func asText() -> String {
        return "A hamster named \(name)"
    }
}

extension Hamster: TextRepresentable {}

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
println(somethingTextRepresentable.asText())
// prints "A hamster named Simon"

Collections of Protocol Types

またポリモーフィズムの話です。

let things: TextRepresentable[] = [d12, simonTheHamster]

for thing in things {
    println(thing.asText())
}
// A 12-sided dice
// A hamster named Simon

Protocol Inheritance

プロトコルは他のプロトコルを継承できます。でしょうね、って感じです。

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // protocol definition goes here
}

protocol PrettyTextRepresentable: TextRepresentable {
    func asPrettyText() -> String
}

PrettyTextRepresentableに適合させる型は「func asText() -> String」と「func asPrettyText() -> String」を実装しないといけませんとか、そう言う話です。

既にTextRepresentableに適合する型であれば、拡張を使ってasPrettyTextのみを実装してもOKです。

extension Hamster: PrettyTextRepresentable {
    func asPrettyText() -> String {
        return "○" + asText() "○"
    }
}

Protocol Composition

中々面白い機能として、プロトコルの合成があります。

まずはプロトコルとそれに適合する構造体を作成しましょう。

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

struct Person: Named, Aged {
    var name: String
    var age: Int
}

型名をprotocol<Named, Aged>とすることで、「NamedとAgedに適合する何か」と言う引数を設定することができます。

func wishHappyBirthday(celebrator: protocol<Named, Aged>) {
    println("Happy birthday \(celebrator.name) - you're \(celebrator.age)!")
}

let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(birthdayPerson)
// prints "Happy birthday Malcolm - you're 21!"

ファンクションの引数以外でも使えるのかとかプロトコルを3つ以上指定しても問題ないのかとか、そう言ったことは全然書かれてないのがアレなんですが、そもそも「そう言えばこんな機能あったね…」ってなりそうですね。

Checking for Protocol Conformance

asやisを使ってプロトコルにキャストしたりプロトコルに適合しているかどうかを調べることができます。

このセクションに書いてあることはキャストの章に全部書いてあるので割愛します。

Optional Protocol Requirements

「実装してもいいけど実装しなくてもいい」と言うなんじゃそりゃなものをプロトコルに定義することができます。

そう言ったものを宣言する場合は@optionalと言うAttributeをつけます。

@objc protocol CounterDataSource {
    @optional func incrementForCount(count: Int) -> Int
    @optional var fixedIncrement: Int { get }
}

この機能は主にObjective-Cとの互換のためにあるらしいです。これを使う場合は必ず@objcと言うAttributeをつける必要があります。

実際に適合させてみましょう。CounterDataSourceで宣言されているincrementForCountは実装していませんが、fixedIncrementの方は実装しているThreeSourceと言うプロトコルを作成します。

class ThreeSource: CounterDataSource {
    let fixedIncrement = 3
}

@optionalがついているものはオプションの連鎖が発生します。実装されているかわからない以上、必ずnilチェックしましょう。

@objc class Counter {
    var count = 0
    var dataSource: CounterDataSource?

    func increment() {
        if let amount = dataSource?.incrementForCount?(count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement? {
            count += amount
        }
    }
}

var counter = Counter()
counter.dataSource = ThreeSource()

for _ in 1...4 {
    counter.increment()
    println(counter.count)
}
// 3
// 6
// 9
// 12

今度はincrementForCountだけを実装してみましょう。

class TowardsZeroSource: CounterDataSource {

    func incrementForCount(count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}

counter.count = -4
counter.dataSource = TowardsZeroSource()

for _ in 1...5 {
    counter.increment()
    println(counter.count)
}
// -3
// -2
// -1
// 0
// 0

私信

流石に20日も経つと周りの状況が変わってクソ忙しくなってしまったので、次回はちょっと期間が開くかもしれません。

残っているのは楽しい楽しいジェネリクスの話と楽しい楽しい演算子オーバーロードの話なので、頑張って今月中には終わらせたいんですが…。