Menu

Category

Archive

logo


The Swift Programming Language ~ Protocols [Collection, Inheritance, Composition, Casting, Optional]

2014-06-30 18:00:00 +0900
  • このエントリーをはてなブックマークに追加

iBooks にある “The Swift Programming Language” の勉強メモ。Objective-C と C を普段書いている自分から、ちょっと馴染みがないものを特にまとめておきます。目次は こちら

今回は、Protocols の継承やキャスト等に関して。

以前の Delegate や Extension に関してはこちら

Protocols

8. Collection とプロトコル

配列や Dictionary といった Collection にプロトコルをタイプとして保存することができます。

1
2
3
4
5
6
7
8
let things: TextRepresentable[] = [game, d12, simonTheHamster]

for thing in things {
    println(thing.asText())
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon

この thing は TextRepresentable タイプです。Dice や DiceGame, Hamster ではありません。

9. プロトコルの継承

プロトコルは 1 つまたは複数のプロトコルを継承し、新しい要件を追加することもできます。プロトコルの文法は、クラスの継承と似ています。いくつかのプロトコルを継承することができるので、その場合は、カンマで区切ります。

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

先ほどの TextRepresentable を継承し、新しいプロトコルを作成します。

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

この PrettyTextRepresentable に準拠するには、TextRepresentable の要件と PrettyTextRepresentable の要件を両方を満たさなければなりません。

10. プロトコル結合 (Composition)

いくつかのプロトコルの要件を満たしたタイプを一度に定義できたら便利です。そのため Swift では、いくつかのプロトコルを 1 つにまとめることができます。基本的なシンタックスは、protocol<SomeProtocol, AnotherProtocol> と <> で囲います。複数ある場合は、<> 内でカンマで区切ります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protocol Named {
    var name: String { get }
}
protocol Aged {
    var age: Int { get }
}
struct Person: Named, Aged {
    var name: String
    var age: Int
}
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!"

この例では、2 つのプロトコルが存在しています。この 2 つのプロトコルを満たしたのが、Person 構造体です。また、このコンテキストでのグローバル関数 wishHappyBirthday は引数として結合されたプロトコルをとっています。Person 構造体は、この両者の要件を満たしているので、最後の行のようにこの関数の引数として使用することができます。

このプロトコル結合は新しいタイプを定義しているわけではありません。一時的なローカルプロトコルが生成されています。

11. プロトコルに準拠しているか確認

タイプキャスティングのように isas を使用して、あるプロコトルに準拠しているか確認、特定のプロトコルへキャストすることができます。まずは、例のプロトコルを用意していきます。

1
2
3
@objc protocol HasArea {
    var area: Double { get }
}

この HasArea プロトコルのように、@objc 属性が付いているプロトコルのみ、プロトコルに準拠しているか確認することができます。また、この @objc を付けた場合、このプロトコルは、クラスにのみ実装されます。構造体・Enumeration は使用することができません。例をみていきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}
class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

この HasArea プロトコルに準拠している 2 つのクラスと、そうでないクラスを 1 つがあります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let objects: AnyObject[] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

for object in objects {
    if let objectWithArea = object as? HasArea {
        println("Area is \(objectWithArea.area)")
    } else {
        println("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

このようにあるインスタンスがプロコトルに準拠しているかを確認し、それによって処理を変更しています。objectWithArea のタイプは、HasArea か nil です。したがって、プロトコルが定義している area プロパティにしかアクセスすることはできません。タイプが Circle や Country、Animal ではないからです。

12. 選択的プロトコル要件

選択的なプロトコルの要件を定義することができます。これらの要件は、必ずしも実装される必要はありません。これは、@optional キーワードを使用して定義されます。

選択的プロトコルの要件は、Optional Chaining により使用されます。実装されていない可能性があるため、直接アクセスするとランタイムエラーを起こす可能性があります。

この選択的なプロトコルの要件は、@objc 属性と一緒に使用されます。つまり、これはクラスでしか使えません。

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

このプロトコルは、引数を 1 つ取る incrementForCount 関数とプロパティを実装することを求めていますが、これは任意です。

そして、下記の Counter クラスをみてみます。

1
2
3
4
5
6
7
8
9
10
11
@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
        }
    }
}

このクラスは、プロパティに dataSource という Optional のプロパティを持っています。これが、上記の CounterDataSource プロトコルです。この increment メソッドにて、実際の処理を行っています。この中で、各プロトコルが実装されているかを確認し、ある場合は、それらを使用して、処理をしています。そもそも dataSource プロパティが nil の場合も考えられるので、2 段階に確認をしています。

ここから、実際に選択的な要件がどのように使われるかみていきます。

1
2
3
class ThreeSource: CounterDataSource {
    let fixedIncrement = 3
}

まずは、シンプルなクラスから。この ThreeSource は、先ほどの CounterDataSource のプロパティ要件を実装しています。

1
2
3
4
5
6
7
8
9
10
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    println(counter.count)
}
// 3
// 6
// 9
// 12

この TreeSource クラスは、プロトコルのプロパティ要件しか満たしていないので、increment メソッドの 2 つ目の処理が実行されます。この場合は、3 つずつ count が追加されていきます。

ちょっと複雑な例をみてみます。

1
2
3
4
5
6
7
8
9
10
11
class TowardsZeroSource: CounterDataSource {
    func incrementForCount(count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}

このクラスは、CounterDataSource プロトコルの incrementForCount 関数を実装しています。count が 0 になるようにする処理です。実際には、

1
2
3
4
5
6
7
8
9
10
11
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    println(counter.count)
}
// -3
// -2
// -1
// 0
// 0

このようになります。