プロトコルとはJavaやC#でいうインターフェースに似たもので、簡単に言うとクラスの挙動を決めた設計図みたいなものになります。クラス自体にも設計図という側面がありますが、クラスがその属性や挙動を細部に渡って記述するのに対して、プロトコルでは外部へのインターフェースのみを定義します。そして実際の挙動はそのプロトコルを採用するクラスで記述することになります。
クラスの利用者から見た場合、そのクラスがあるプロトコルに適合していると宣言されていれば、そのプロトコルで定義されているインターフェースを使えるということが保証されていることになります。
また、Swiftのプロトコルはクラスだけでなく、構造体や列挙型にも採用することもできます。
プロトコルの定義の仕方は、クラスや構造体の場合と似ています。
protocol SomeProtocol {
// インターフェースの定義
func someMethod()
:
}
ブロトコルの名前はクラスや構造体と同様、大文字で始めます。
プロトコルに適合させるには、型名の後に:(コロン)をつけて宣言します。次は構造体をプロトコルに適合させる場合です。
struct SomeStructure: SomeProtocol {
func someMethod()
:
}
複数のプロトコルに適合させる場合は、プロトコルを,(カンマ)で区切って並べて記述します。
struct SomeStructure: SomeProtocol, AnotherProtocol {
func someMethod()
:
}
また、クラスにプロトコルを適合させる場合、そのクラスがスーパークラスを持っている場合は、スーパークラス名の後にプロトコル名を書きます。
class SomeClass: SuperClass, SomeProtocol, AnotherProtocol {
func someMethod()
:
}
プロトコルへの適合を宣言した型が、そのプロトコルの要求を満たしていない場合コンパイルエラーになります。
プロパテイの要求
プロトコルにインスタンスプロパティや型プロパティを定義できます。実装する側では保持型プロパティと計算型プロパティのどちらで実装しても構いません。
プロパティの宣言時には、getterとsetterのどちらか、或いは両方を指定します。
次の例では、プログレスバーに進捗状況を表示させるためのプロトコルを定義しています。
// 進捗表示項目プロトコル
protocol Progressing {
var total: Int { get } // 総量
var completed: Int { get set } // 完了した量
}
型プロパティを定義する場合は、classをつけて定義します。これは、構造体や列挙型でプロトコルに適合させる場合でも同じです。実際に構造体で実装する場合は、staticをつけて型プロパティとして実装することになります。
protocol Progressing {
class var numberOfProcessing: Int { get set }// 処理項目数
:
}
次の例では、構造体をProgressingプロトコルに適合させています。totalプロバティは定数になっていますが、プロトコルでgetterのみ指定されているので適合します。
struct Music: Progressing {
let total: Int
var completed: Int = 0
:
}
let music = Music(total: 1230, completed: 0)
次の例では、ダウンロード可能なファイルのクラスをProgressingプロトコルに適合させています。
/* ダウンロード可能な音楽ファイルクラス */
class DownloadableFile: Progressing {
let title: String // タイトル
let filePath: String // ファイルのバス
let fileSize: Int = 0 // ファイルサイズ
var downloaded: Int = 0 // ダウンロード済みサイズ
var total: Int {
return fileSize
}
var completed: Int {
get {
return downloaded
}
set {
downloaded = newValue
}
}
init(title: String, filePath: String) {
self.title = title
self.filePath = filePath
// ファイルからファイルサイズを取得
let fileManager = NSFileManager.defaultManager()
if let attr = fileManager.attributesOfItemAtPath(self.filePath, error: nil) {
if let fileSize: AnyObject = attr[NSFileSize] {
self.fileSize = Int((fileSize as NSInteger).value)
}
}
}
:
}
let tango = DownloadableMusic(title: "黒猫のタンゴ", filePath: "/var/www/contents/musics/tango.mp3")
print(tango.total)
ここではイニシャライザで受け取ったファイルのサイズを取得してそれを総量として使用しています。また、ダウンロードされたサイズを完了した量としています。
メソッドの要求
次はメソッドの定義の例をみてみます。
protocol SomeProtocol {
// インスタンスメソッド
func someInstanceMethod() -> String
// 型メソッド
class func someTypeMethod() -> Int
:
}
メソッド宣言の書き方は、通常のメソッドの宣言と同様ですが、{と}で囲まれた実装の部分は書きません。また、型メソッドは型プロパテイの宣言の場合と同様、classをつけて宣言します。構造体や列挙型の型メソッドの宣言ではstaticをつけますが、プロトコルはその場合でもclassをつけて宣言します。
プロトコルのメソッドの宣言にデフォルト値を与えることはできません。可変個引数を使うことはできます。
次の例では、テキスト形式との相互変換が可能なことを示すプロトコルを作成しています。
protocol TextSerializable {
// テキスト形式へ変換
func serialize() -> String
// テキスト形式から復元
mutating func desirialize(text: String)
}
テキスト形式へ変換するメソッドと、テキスト形式から復元するメソッドを宣言したプロトコルです。
復元メソッドはインスタンスの値を変更するため、mutatingを指定しています。クラスのメソッドはmutatingは不要ですが、値を変更する可能性のあるメソッドのプロトコルの宣言にはmutatingが必要です。
このプロトコルに構造体を適合させてみます。
// 書籍
struct Book: TextSerializable {
var title: String // タイトル
var author: String // 著者
// テキスト形式へ変換
func serialize() -> String {
return "タイトル:\"\(title)\", 著者:\(author)"
}
// テキスト形式から復元
mutating func desirialize(text: String) {
let elements = text.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: ","))
for element in elements {
let e = element.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: ":"))
if countElements(e) == 2 {
switch e[0].stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) {
case "タイトル":
self.title = e[1]
case "著者":
self.author = e[1]
default:
break
}
}
}
}
}
ここではBook構造体の内容を、タイトル:"〜", 著者:〜という形式のテキストへ変換しています。
Book構造体は、TextSerializableプロトコルに適合したので、TextSerializable型の変数へ代入し、メソッドを呼び出すことができます。
var ts: TextSerializable = Book(title: "坊っちゃん", author: "夏目漱石")
let text = ts.serialize()
print(text) // "タイトル:"坊っちゃん", 著者:夏目漱石"
ts.desirialize(text)
次は、テキスト形式から復元したインスタンスを返す型メソッドをプロトコルに追加します。
protocol TextSerializable {
// テキスト形式へ変換
func serialize() -> String
// テキスト形式から復元
mutating func desirialize(text: String)
// テキスト形式から復元したインスタンスを返す
class func deserialize(text: String) -> TextSerializable?
}
インスタンスを生成出来ない場合を考慮してオプショナル型を返すようにしています。
このプロトコルを今度はクラスに適合させてみます。
// パーソンクラス
class Person: TextSerializable {
var name: String // 名前
var age: Int? // 年齢
// イニシャライザ
init(name: String, age: Int) {
self.name = name
self.age = age
}
// テキスト形式へ変換
func serialize() -> String {
return "名前:\"\(name)\", 年齢:\(age!)"
}
// テキスト形式から復元
func desirialize(text: String) {
if let p = Person.deserialize(text) as? Person {
self.name = p.name
self.age = p.age
}
}
// テキスト形式から復元したインスタンスを返す
class func deserialize(text: String) -> TextSerializable? {
let elements = text.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: ","))
var name: String?, age: Int?
for element in elements {
let e = element.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: ":"))
if countElements(e) == 2 {
switch e[0].stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) {
case "名前":
name = e[1]
case "年齢":
age = e[1].toInt()
default:
break
}
}
}
return name == nil ? nil : Person(name: name!, age: age!)
}
}
テキストへの変換方法は上の構造体の場合と同様です。クラスの場合はプロパティを変更する場合でもメソッドにmutatingをつける必要はありません。コードの重複を避けるため、インスタンスメソッドのdesirialize()内から、クラスメソッドのdesirialize()を呼び出すようにしています。
Book構造体とPersonクラスはどちらもTextSerializableプロトコルに適合するので、[TextSerializable]型の配列に格納することができます。
let botchan = Book(title: "坊っちゃん", author: "夏目漱石")
let natume = Person(name: "夏目漱石", age: 60)
let maihime = Book(title: "舞姫", author: "森鴎外")
let mori = Person(name: "森鴎外", age: 49)
let items: [TextSerializable] = [botchan, natume, maihime, mori]
for item in items {
print(item.serialize())
}
/* 実行結果
タイトル:"坊っちゃん", 著者:夏目漱石
名前:"夏目漱石", 年齢:39
タイトル:"舞姫", 著者:森鴎外
名前:"森鴎外", 年齢:28
*/
デリゲーション
デリゲーション(delegation)とは、委譲という意味で、あるクラスから処理の一部を他のクラスに任せたり、他のクラスへメッセージを送る等の目的でよく使われるデザインパターンです。CocoaやObjective-Cでもよく使われています。
プロトコルはこのデリゲーションでよく使われます。プロトコルで処理呼び出し方法を決めておいて、そのプロトコルに適合するクラスのインスタンスを渡すことで必要に応じてプロトコルで決めたメソッドを呼び出してもらいます。
次の例では、ファイルのダウンロードを行うDownloadProcessクラスと、その処理経過をこのクラスの利用者に通知するためのDownloadProcessDelegateというプロトコルを定義しています。
/* ダウンロードプロセス */
class DownloadProcess {
var file: DownloadableFile
var delegate: DownloadProcessDelegate?
init(file: DownloadableFile) {
self.file = file
}
// ダウンロード処理
func download() {
var data: [Byte] = []
self.openStream()
delegate?.processDidStart(self)
dispatch_async(dispatch_get_main_queue(), {
var received = self.receiveData()
while countElements(received) > 0 {
data += received
self.delegate?.processDidReceiveData(self, data: data)
}
self.closeStream()
self.delegate?.processDidEnd(self, data: data)
})
}
// ネットワークストリームのオープン
func openStream() {
:
}
// データの受信
func receiveData() -> [Byte] {
// 受信したデータを返す
var data: [Byte]
:
return data
}
// ネットワークストリームのクローズ
func closeStream() {
:
}
}
/* ダウンロードプロセスデリゲート */
protocol DownloadProcessDelegate {
func processDidStart(process: DownloadProcess)
func processDidReceiveData(process: DownloadProcess, data: [Byte])
func processDidEnd(process: DownloadProcess, data: [Byte])
}
DonloadProcessのプロパティとしてDownloadProcessDelegateのオプショナル型を保持しています。このdelegateプロパティは設定されなくてもDownloadProcessクラス自体の処理には支障は無いのでオプショナル型にしてあります。初期値はnilになるのでイニシャライザの中で初期する必要もありません。
デリゲートメソッドの呼び出しには、オプショナルの連鎖を使っています。これにより、delegateプロバティがnilだった場合は、単にメソッドが呼び出されず、エラーにはなりません。
DownloadProcessDelegateの3つのメソッドが、downloadメソッドの中で呼び出されています。これにより、デリゲートへダウンロード処理の状況が通知されることになります。
実際の使い方は以下のようになります。
/* ダウンロードマネージャ */
class DownloadManager: DownloadProcessDelegate {
var files: [DownloadableFile] = [] // ダウンロードするファイルの配列
var downloadedSize: Int = 0 // ダウンロード済みサイズ
// ファイルのダウンロード
func downloadFiles() {
downloadedSize = 0
for file in files {
let process = DownloadProcess(file: file)
process.delegate = self
process.download()
}
}
// DownloadProcessからの通知
func processDidStart(process: DownloadProcess) {
print("\(process.file.name)のダウンロード開始")
}
func processDidReceiveData(process: DownloadProcess, data: [Byte]) {
let size = countElements(data)
downloadedSize += size
print("\(process.file.name)のダウンロード中...\(size)バイト")
}
func processDidEnd(process: DownloadProcess, data: [Byte]) {
print("\(process.file.name)のダウンロード終了")
}
}
let manager = DownloadManager()
manager.files += [DownloadableFile(name: "黒猫のタンゴ", filePath: "/var/www/contents/musics/tango.mp3"),
DownloadableFile(name: "猫踏んじゃった", filePath: "/var/www/contents/musics/nekofunda.mp3"),
DownloadableFile(name: "犬のおまわりさん", filePath: "/var/www/contents/musics/inu.mp3")]
manager.downloadFiles()
/*
黒猫のタンゴのダウンロード開始
猫踏んじゃったのダウンロード開始
犬のおまわりさんのダウンロード開始
黒猫のタンゴのダウンロード中...1024バイト
猫踏んじゃったのダウンロード中...1024バイト
犬のおまわりさんのダウンロード中...1024バイト
:
黒猫のタンゴのダウンロード終了
猫踏んじゃったのダウンロード終了
犬のおまわりさんのダウンロード終了
*/
DownloadManagerクラスをDownloadProcessDelegateプロトコルに適合させ、downloadFilesメソッドの中で生成しているDownloadProcessのインスタンスのdelegateプロパティにselfを渡しています。
そして、downloadFilesメソッドを呼び出してfilesプロパティに保持しているDownloadableFile型のdownloadメソッドを呼び出しています。処理状況に応じて各デリケートメソッドが呼び出されるので、それぞれに応じて処理します。
ここでは通知された内容をprint()で出力し、ダウンロード済みサイズをプロパティに保持しています。
エクステンションによるプロトコルへの適合
エクステンションを使うことで、既存の型をプロトコルへ適合させることができます。これはソースが無くライブラリとして提供されている型であっても可能です。
上のDownloadManagerクラスを、エクステンションを使って、同じく上で出てきたProgressingプロトコルに適合させてみます。
extension DownloadManager: Progressing {
// ダウンロード対象の総サイズ
var total: Int {
var amount = 0
for file in self.files {
amount += file.amount
}
return amount
}
// ダウンロード済みサイズ
var completed: Int {
get {
return self.downloadedSize
}
set {
self.downloadedSize = newValue
}
}
}
エクステンションでプロトコルに適合させる場合は、型名の後に、:「コロン」をつけてプロトコル名を記述します。
そして、プロトコルに適合させるためにメソッドを記述します。ここでは、Progressingプロトコルの要件である2つのメソッドを実装しています。
totalプロバティではダウンロードするファイルの総サイズを返す様にし、completedプロパティではデリゲートによる通知で集計したダウンロード済みサイズ(self.downloadedSizeの値)を返しています。
エクステンションで保持型プロパティを追加することはできません。既存の型をエクステンションでブロトコルに適合させられるかどうかは、その型で公開されているプロパティやメソッドに依存することになります。
既存の型が既にプロトコルへの適合条件であるプロバティーやメソッドを実装済みである場合、空のエクステンションを宣言するだけで、プロトコルへ適合させることができます。
/* タスク構造体 */
struct Task {
let name: String // タスク名
let amount: Int // タスク量
var completed: Int // 完了済み料
mutating func done(amount: Int = 1) {
completed += amount
}
}
extension Task: Progressing {}
var progressing: Progressing = Task(name: "大掃除", amount: 100, completed: 0)
プロトコルの継承
クラスの継承と同様、プロトコルも継承することができます。クラスの継承と異なり、プロトコルは複数のプロトコルを継承することができます。その場合、その複数のプロトコルが要求するプロパティやメソッドを全て引き継ぐことになります。
プロトコルを継承する場合は次の様にプロトコル名の後に、:「コロン」を書きその後に、継承するプロトコル名書きます。複数のプロトコルを継承する場合は、それらのプロトコルを,(カンマ)で区切って記述します。
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
:
}
プロトコルの継承の例をみてみます。次の例では、上で出てきたTextSerializableプロトコルを継承して、HTMLTranslatableというプロトコルを作成しています。
/* HTMLテキストへ変換可能なことを示すプロトコル */
protocol HTMLTranslatable: TextSerializable {
var asHTMLText: String { get }
}
/* パーソン構造体 */
struct Person: HTMLTranslatable {
var name: String
var age: Int
func serialize() -> String {
:
}
mutating func desirialize(text: String) {
:
}
var asHTMLText: String {
return "<span class=\"person-name-title\">名前:</span><span class=\"person-name\">\(name)</span>" +
"<span class=\"person-age-title\">年齢:</span><span class=\"person-age\">\(age)歳</span>"
}
}
let person: HTMLTranslatable = Person(name: "松井イチロー", age: 41)
print(person.asHTMLText)
/* 実行結果
<span class="person-name-title">名前:</span><span class="person-name">松井イチロー</span><span class="person-age-title">年齢:</span><span class="person-age">41歳</span>
*/
プロトコルの合成
複数のプロトコルに適合する型を示す場合は、次の様な形式で表現できます。
protocol<SomeProtocol, AnotherProtocol>
次の例では、HTMLTranslatableとJSONSerializableという2つのプロトコルと、これらに適合する型を引数にとる関数を示しています。
/* HTMLテキストへ変換可能なことを示すプロトコル */
protocol HTMLTranslatable: TextSerializable {
var asHTMLText: String { get }
}
/* JSON形式に相互変換可能なことを示すプロトコル */
protocol JSONSerializable {
func toJSON() -> String // JSON形式に変換
mutating func fromJSON() // JSON形式から復元
}
/* HTMLTranslatableとJSONSerializableに適合する型を引数にとる関数 */
func printAsHTMLAndJSON(printTarget: <HTMLTranslatable, JSONSerializable>) {
print("HTML形式:\(printTarget.asHTMLText)\nJSON形式:\(printTarget.toJSON())")
}
Person構造体をこの2つのプロトコルに適合させて、printAsHTMLAndJSON関数を呼び出してみます。
/* パーソン構造体 */
truct Person: HTMLTranslatable, JSONSerializable {
var name: String
var age: Int
func serialize() -> String {
:
}
mutating func desirialize(text: String) {
:
}
var asHTMLText: String {
return "<span class=\"person-name-title\">名前:</span><span class=\"person-name\">\(name)</span>" +
"<span class=\"person-age-title\">年齢:</span><span class=\"person-age\">\(age)歳</span>"
}
func toJSON() -> String {
return "{\"name\": \"\(name)\", \"age\": \(age)}"
}
mutating func fromJSON() {
:
}
}
let person = Person(name: "松井イチロー", age: 41)
printAsHTMLAndJSON(person)
/* 実行結果
HTML形式:<span class="person-name-title">名前:</span><span class="person-name">松井イチロー</span><span class="person-age-title">年齢:</span><span class="person-age">41歳</span>
JSON形式:{"name": "松井イチロー", "age": 41}
*/
プロトコル適合の確認
あるインスタンスがプロトコルに適合しているかどうか確認したり、より具体的なプロトコルへ変換するには、型キャストで使用するis演算子やas演算子を使う事ができます。
- あるインスタンスがis演算子で指定したプロトコルに適合していればtrue, 適合していなければfalseが返されます。
- as?演算子を使って、あるプロトコルにダウンキャストするとそのプロトコルのオプショナル型が返されます。失敗した場合は値がnilになります。
- as演算子を使ってダウンキャストに失敗すると、実行時エラーが発生します。
プロトコルで型キャストを使用するには、次の様にプロトコルに@objcをつけて宣言する必要があります。
// 進捗表示項目プロトコル
@objc protocol Progressing {
var total: Int { get } // 総量
var completed: Int { get set } // 完了した量
}
@objcは、Objective-Cとの互換性を示すための特殊なマークですが、@objcをつけて宣言したプロトコルはクラスでしか使えなくなります。構造体や列挙型をこのプロトコルに適合させることはできません。
以下に例を示します。
// 進捗表示項目プロトコル
@objc protocol Progressing {
var total: Int { get } // 総量
var completed: Int { get set } // 完了した量
}
// 音楽クラス
class Music: Progressing {
let title: String // タイトル
let total: Int // 総サイズ
var completed: Int = 0 // 再生済みサイズ
init(title: String, total: Int) {
self.title = title
self.total = total
}
}
let music: AnyObject = Music(title: "黒猫のタンゴ", total: 23846)
if music is Progressing {
print((music as Progressing).total) // 23846
}
if let total = (music as? Progressing)?.total {
print(total) // 23846
}
プロトコルの任意要求
プロトコルに適合する側で必ずしも実装する必要のないプロパティやメソッドを指定することができます。次のようにプロパティやメソッドの宣言の前に、optionalをつけて宣言します。
/* 翻訳プロトコル */
@objc protocol Translator {
optional var defaultLanguage: String { get set } // デフォルト言語
optional func toEnglish(text: String) -> String // 英語へ翻訳
optional func toFrench(text: String) -> String // フランス語へ翻訳
optional func toGerman(text: String) -> String // ドイツ語へ翻訳
:
}
optionalのつけられたプロパティやメソッドが実装されているかどうかは、オプショナルの連鎖で使用する構文を使って確認できます。
class MyTranslator: Translator {
var defaultLanguage: String
func toEnglish(text: String) -> String {
:
if text == "こんにちは" { return "Hello" }
:
}
init(defaultLanguage: String) {
self.defaultLanguage = defaultLanguage
}
}
var translator: Translator?
translator = MyTranslator(defaultLanguage: "Japanese")
if let lang = translator?.defaultLanguage? {
if lang == "Japanese" {
if let translated = translator?.toEnglish?("こんにちは") {
print(translated) // Hello
}
}
}
translatorプロトコルのdefaultLanguageはString型ですが、プロトコルの定義でoptionalが指定されているため、実装されているかどうかをオプショナルの連鎖による構文で確認でき、返される値はStringのオプショナル型となります。
同じく、toEnglishメソッドにもoptionalが指定されているため、オプショナルの連鎖を使って確認ができます。メソッドの戻り値はString型ですが、ここではStringのオプショナル型が返されます。オプショナルバインディングを使ってアンラップされた値を取り出すことができます。(値がnilでない場合)