Swiftのプロトコル指向プログラミング入門

公開: 2022-03-11

プロトコルは、Swiftプログラミング言語の非常に強力な機能です。

プロトコルは、「特定のタスクまたは機能の一部に適合するメソッド、プロパティ、およびその他の要件の青写真」を定義するために使用されます。

Swiftはコンパイル時にプロトコルの適合性の問題をチェックし、開発者がプロ​​グラムを実行する前でもコードの致命的なバグを発見できるようにします。 プロトコルを使用すると、開発者は言語の表現力を損なうことなく、Swiftで柔軟で拡張可能なコードを記述できます。

Swiftは、他の多くのプログラミング言語を悩ませているインターフェースの最も一般的な癖や制限のいくつかに回避策を提供することにより、プロトコルを使用する便利さをさらに一歩進めます。

Swiftのプロトコル指向プログラミング入門

プロトコル指向プログラミングを使用して、Swiftで柔軟で拡張可能なコードを記述します。
つぶやき

Swiftの以前のバージョンでは、多くの最新のプログラミング言語に当てはまるように、クラス、構造、および列挙型のみを拡張することが可能でした。 ただし、Swiftのバージョン2以降、プロトコルも拡張できるようになりました。

この記事では、Swiftのプロトコルを使用して再利用可能で保守可能なコードを作成する方法と、プロトコル拡張機能を使用して大規模なプロトコル指向のコードベースへの変更を1か所に統合​​する方法について説明します。

プロトコル

プロトコルとは何ですか?

最も単純な形式では、プロトコルはいくつかのプロパティとメソッドを記述するインターフェイスです。 プロトコルに準拠するタイプは、プロトコルで定義されている特定のプロパティに適切な値を入力し、必要なメソッドを実装する必要があります。 例えば:

 protocol Queue { var count: Int { get } mutating func push(_ element: Int) mutating func pop() -> Int }

キュープロトコルは、整数項目を含むキューを記述します。 構文は非常に単純です。

プロトコルブロック内で、プロパティを記述するとき、プロパティがgettable { get }のみであるか、gettableとsettable { get set } getset}の両方であるかを指定する必要があります。 この場合、( Int型の)変数Countはgettableのみです。

プロトコルでプロパティがgettableおよびsettableである必要がある場合、その要件は、定数に格納されたプロパティまたは読み取り専用の計算されたプロパティでは満たすことができません。

プロトコルがプロパティを取得可能にすることだけを要求する場合、要件は任意の種類のプロパティで満たすことができ、これが独自のコードに役立つ場合は、プロパティも設定可能であることが有効です。

プロトコルで定義された関数の場合、関数がmutatingキーワードで内容を変更するかどうかを示すことが重要です。 それ以外は、関数のシグニチャで十分です。

プロトコルに準拠するには、タイプがすべてのインスタンスプロパティを提供し、プロトコルで説明されているすべてのメソッドを実装する必要があります。 たとえば、以下はQueueプロトコルに準拠するContainerです。 構造体は基本的に、プッシュされたIntをプライベート配列itemsに格納します。

 struct Container: Queue { private var items: [Int] = [] var count: Int { return items.count } mutating func push(_ element: Int) { items.append(element) } mutating func pop() -> Int { return items.removeFirst() } }

ただし、現在のキュープロトコルには大きな欠点があります。

Intを処理するコンテナのみが、このプロトコルに準拠できます。

「関連付けられたタイプ」機能を使用することで、この制限を取り除くことができます。 関連する型はジェネリックのように機能します。 実例を示すために、関連するタイプを利用するようにキュープロトコルを変更してみましょう。

 protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }

現在、キュープロトコルでは、あらゆるタイプのアイテムを保存できます。

Container構造の実装では、コンパイラはコンテキストから関連する型(つまり、メソッドの戻り型とパラメータ型)を決定します。 このアプローチにより、汎用アイテムタイプのContainer構造を作成できます。 例えば:

 class Container<Item>: Queue { private var items: [Item] = [] var count: Int { return items.count } func push(_ element: Item) { items.append(element) } func pop() -> Item { return items.removeFirst() } }

多くの場合、プロトコルを使用するとコードの記述が簡単になります。

たとえば、エラーを表すオブジェクトはすべて、 Error (またはLocalizedErrorれた説明を提供する場合はLocalizedError)プロトコルに準拠できます。

次に、同じエラー処理ロジックを、コード全体でこれらのエラーオブジェクトのいずれかに適用できます。 したがって、エラーを表すために特定のオブジェクト(Objective-CのNSErrorなど)を使用する必要はありませんErrorまたはLocalizedErrorプロトコルに準拠する任意のタイプを使用できます。

文字列型を拡張してLocalizedErrorプロトコルに準拠させ、文字列をエラーとしてスローすることもできます。

 extension String: LocalizedError { public var errorDescription: String? { Return NSLocalizedString(self, comment:””) } } throw “Unfortunately something went wrong” func handle(error: Error) { print(error.localizedDescription) }

プロトコル拡張

プロトコル拡張は、プロトコルの素晴らしさに基づいて構築されています。 彼らは私たちに次のことを可能にします:

  1. プロトコルメソッドのデフォルトの実装とプロトコルプロパティのデフォルト値を提供し、それによってそれらを「オプション」にします。 プロトコルに準拠するタイプは、独自の実装を提供することも、デフォルトの実装を使用することもできます。

  2. プロトコルに記載されていない追加のメソッドの実装を追加し、これらの追加のメソッドを使用してプロトコルに準拠するタイプを「装飾」します。 この機能により、各タイプを個別に変更することなく、プロトコルにすでに準拠している複数のタイプに特定のメソッドを追加できます。

デフォルトのメソッド実装

もう1つのプロトコルを作成しましょう:

 protocol ErrorHandler { func handle(error: Error) }

このプロトコルは、アプリケーションで発生するエラーの処理を担当するオブジェクトについて説明します。 例えば:

 struct Handler: ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }

ここでは、エラーのローカライズされた説明を出力します。 プロトコル拡張を使用すると、この実装をデフォルトにすることができます。

 extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } }

これを行うと、デフォルトの実装を提供することにより、 handleメソッドがオプションになります。

デフォルトの動作で既存のプロトコルを拡張する機能は非常に強力であり、既存のコードの互換性を損なうことを心配することなく、プロトコルを拡張および拡張できます。

条件付き拡張

そのため、 handleメソッドのデフォルトの実装を提供しましたが、コンソールへの印刷はエンドユーザーにとってそれほど役に立ちません。

エラーハンドラがビューコントローラである場合は、ローカライズされた説明を含むある種のアラートビューを表示することをお勧めします。 これを行うには、 ErrorHandlerプロトコルを拡張できますが、特定の場合(つまり、タイプがビューコントローラーの場合)にのみ適用されるように拡張を制限できます。

Swiftでは、 whereキーワードを使用して、このような条件をプロトコル拡張に追加できます。

 extension ErrorHandler where Self: UIViewController { func handle(error: Error) { let alert = UIAlertController(title: nil, message: error.localizedDescription, preferredStyle: .alert) let action = UIAlertAction(title: "OK", style: .cancel, handler: nil) alert.addAction(action) present(alert, animated: true, completion: nil) } }

上記のコードスニペットのSelf (大文字の「S」)は、タイプ(構造、クラス、または列挙型)を指します。 UIViewControllerから継承するタイプのプロトコルのみを拡張するように指定することで、 UIViewController固有のメソッド( present(viewControllerToPresnt: animated: completion)など)を使用できます。

現在、 ErrorHandlerプロトコルに準拠するビューコントローラには、ローカライズされた説明を含むアラートビューを表示するhandleメソッドの独自のデフォルト実装があります。

あいまいなメソッドの実装

2つのプロトコルがあり、どちらも同じシグネチャを持つメソッドを持っていると仮定しましょう。

 protocol P1 { func method() //some other methods } protocol P2 { func method() //some other methods }

どちらのプロトコルにも、このメソッドのデフォルトの実装を備えた拡張機能があります。

 extension P1 { func method() { print("Method P1") } } extension P2 { func method() { print("Method P2") } }

ここで、両方のプロトコルに準拠するタイプがあると仮定しましょう。

 struct S: P1, P2 { }

この場合、あいまいなメソッドの実装に問題があります。 タイプは、使用するメソッドの実装を明確に示していません。 その結果、コンパイルエラーが発生します。 これを修正するには、メソッドの実装を型に追加する必要があります。

 struct S: P1, P2 { func method() { print("Method S") } }

多くのオブジェクト指向プログラミング言語は、あいまいな拡張定義の解決を取り巻く制限に悩まされています。 Swiftは、コンパイラーが不足している場所をプログラマーが制御できるようにすることで、プロトコル拡張機能を通じてこれを非常にエレガントに処理します。

新しいメソッドの追加

Queueプロトコルをもう一度見てみましょう。

 protocol Queue { associatedtype ItemType var count: Int { get } func push(_ element: ItemType) func pop() -> ItemType }

Queueプロトコルに準拠する各タイプには、保存されているアイテムのcountを定義するcountinstanceプロパティがあります。 これにより、特に、そのようなタイプを比較して、どちらが大きいかを判断できます。 このメソッドは、プロトコル拡張を介して追加できます。

 extension Queue { func compare<Q>(queue: Q) -> ComparisonResult where Q: Queue { if count < queue.count { return .orderedDescending } if count > queue.count { return .orderedAscending } return .orderedSame } }

このメソッドは、キュー機能に関連していないため、 Queueプロトコル自体には記述されていません。

したがって、これはプロトコルメソッドのデフォルトの実装ではなく、 Queueプロトコルに準拠するすべてのタイプを「装飾」する新しいメソッドの実装です。 プロトコル拡張がなければ、このメソッドを各タイプに個別に追加する必要があります。

プロトコル拡張と基本クラス

プロトコル拡張は基本クラスの使用と非常に似ているように見えるかもしれませんが、プロトコル拡張を使用することにはいくつかの利点があります。 これらには以下が含まれますが、必ずしもこれらに限定されません。

  1. クラス、構造体、および列挙型は複数のプロトコルに準拠できるため、複数のプロトコルのデフォルトの実装を採用できます。 これは、概念的には他の言語の多重継承に似ています。

  2. プロトコルはクラス、構造、列挙型で採用できますが、基本クラスと継承はクラスでのみ使用できます。

Swift標準ライブラリ拡張機能

独自のプロトコルを拡張することに加えて、Swift標準ライブラリからプロトコルを拡張できます。 たとえば、キュ​​ーのコレクションの平均サイズを知りたい場合は、標準のCollectionプロトコルを拡張することで見つけることができます。

Swiftの標準ライブラリによって提供されるシーケンスデータ構造は、インデックス付きの添え字を介して要素をトラバースおよびアクセスでき、通常はCollectionプロトコルに準拠しています。 プロトコル拡張により、このようなすべての標準ライブラリデータ構造を拡張したり、それらのいくつかを選択的に拡張したりすることができます。

注:以前はSwift 2.xでCollectionTypeと呼ばれていたプロトコルは、Swift3ではCollectionに名前が変更されました。

 extension Collection where Iterator.Element: Queue { func avgSize() -> Int { let size = map { $0.count }.reduce(0, +) return Int(round(Double(size) / Double(count.toIntMax()))) } }

これで、キューのコレクション( ArraySetなど)の平均サイズを計算できます。 プロトコル拡張がなければ、このメソッドを各コレクションタイプに個別に追加する必要がありました。

Swift標準ライブラリでは、プロトコル拡張機能を使用して、たとえば、 mapfilterreduceなどのメソッドを実装します。

 extension Collection { public func map<T>(_ transform: (Self.Iterator.Element) throws -> T) rethrows -> [T] { } }

プロトコル拡張とポリモーフィズム

前に述べたように、プロトコル拡張により、一部のメソッドのデフォルトの実装を追加したり、新しいメソッドの実装を追加したりすることができます。 しかし、これら2つの機能の違いは何ですか? エラーハンドラに戻って調べてみましょう。

 protocol ErrorHandler { func handle(error: Error) } extension ErrorHandler { func handle(error: Error) { print(error.localizedDescription) } } struct Handler: ErrorHandler { func handle(error: Error) { fatalError("Unexpected error occurred") } } enum ApplicationError: Error { case other } let handler: Handler = Handler() handler.handle(error: ApplicationError.other)

その結果、致命的なエラーが発生します。

ここで、プロトコルからhandle(error: Error)メソッド宣言を削除します。

 protocol ErrorHandler { }

結果は同じです:致命的なエラー。

プロトコルメソッドのデフォルトの実装を追加することと、プロトコルに新しいメソッドの実装を追加することの間に違いがないことを意味しますか?

番号! 違いは存在します。変数handlerのタイプをHandlerからErrorHandlerに変更することでそれを確認できます。

 let handler: ErrorHandler = Handler()

これで、コンソールへの出力は次のようになります。操作を完了できませんでした。 (ApplicationErrorエラー0。)

しかし、handle(error:Error)メソッドの宣言をプロトコルに戻すと、結果は致命的なエラーに戻ります。

 protocol ErrorHandler { func handle(error: Error) }

それぞれの場合に起こることの順序を見てみましょう。

プロトコルにメソッド宣言が存在する場合:

プロトコルはhandle(error: Error)メソッドを宣言し、デフォルトの実装を提供します。 このメソッドは、 Handler実装でオーバーライドされます。 したがって、変数のタイプに関係なく、メソッドの正しい実装が実行時に呼び出されます。

プロトコルにメソッド宣言が存在しない場合:

メソッドはプロトコルで宣言されていないため、型はメソッドをオーバーライドできません。 そのため、呼び出されたメソッドの実装は変数のタイプに依存します。

変数がHandler型の場合、その型からのメソッド実装が呼び出されます。 変数のタイプがErrorHandlerの場合、プロトコル拡張からのメソッド実装が呼び出されます。

プロトコル指向のコード:安全でありながら表現力豊か

この記事では、Swiftのプロトコル拡張機能のいくつかを紹介しました。

インターフェイスを備えた他のプログラミング言語とは異なり、Swiftは不必要な制限でプロトコルを制限しません。 Swiftは、開発者が必要に応じてあいまいさを解決できるようにすることで、これらのプログラミング言語の一般的な癖を回避します。

Swiftプロトコルとプロトコル拡張機能を使用すると、作成するコードはほとんどの動的プログラミング言語と同じように表現力があり、コンパイル時にタイプセーフである可能性があります。 これにより、コードの再利用性と保守性を確保し、Swiftアプリのコードベースに自信を持って変更を加えることができます。

この記事がお役に立てば幸いです。フィードバックやさらなる洞察を歓迎します。