2020/1/27 Swift プロトコル(Protocols)

プロトコル(Protocols)は、ある特定の仕事や機能に必要なメソッド・プロパティなどの設計図にあたります。それらの要請を実際に実装するためにプロトコルがクラス・構造体・列挙型に取り入れられます。あるプロトコルが要求する事柄を満たした型を「そのプロトコルに準拠した型」と言います。

Contents

プロトコルの構文

クラス・構造体・列挙型と似た方法でプロトコルを定義できます。

protocol SomeProtocol {
    // プロトコルの定義
}

独自の型は、自身の定義時に、準拠するプロトコル名を型名の後ろにコロンで区切って記述することで、自身がそのプロトコルに準拠する型であることを示します。準拠する複数のプロトコルを列挙することができ、その時はカンマで区切ります。

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 構造体の定義
}

上記サンプルではSomeStructure構造体はFirstProtocol,AnotherProtocolの二つのプロトコルに準拠する構造体です。

クラスがスーパークラスを持つ場合、下記のようにスーパークラス名を先に記述し、カンマで区切り、その後に準拠するプロトコル名を記述します。

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // クラスの定義
}

必要なプロパティをプロトコルで要請する

プロトコルは自身に準拠する型に対して、特定の名前・型のインスタンスプロパティ・タイププロパティを用意することを要求することができます。プロトコルはプロパティがストアドプロパティかコンピューテドプロパティかは特定しません。プロパティの名前と型を特定します。それからプロトコルは各プロパティが「gettable」か、「gettableとsettable」かを特定します。

プロトコルがあるプロパティを「gettableとsettable」と指定した場合、そのプロパティは

  • 定数のストアドプロパティ
  • 読み取り専用コンピューテドプロパティ

であってはなりません。プロトコルがそのプロパティに対して「gettable」を要求している場合、全ての種類のプロパティがその要求を満たすことができます。

「gettable」はセッターを有していてもいなくても、とにかくゲッターを有していれば良いので、「gettable」と指定されたプロパティは

  • ゲッターのみ有するプロパティ
  • ゲッターとセッターを有するプロパティ

どちらでも問題ありません。


まとめ

(1)varで宣言されたストアドプロパティ

(2)letで宣言されたストアドプロパティ

(3)varで宣言されたコンピューテドプロパティ(ゲッターのみ)

(4)varで宣言されたコンピューテドプロパティ(ゲッター+セッター)

※コンピューテドプロパティは必ずvarで宣言しないといけない(下のページの読み取り専用コンピューテドプロパティをご覧ください)

2019/12/31 Swift プロパティ パート1

の四種類ある中で、

「gettable」→(1),(2),(3),(4)全てOK

「gettable & settable」→(1)(4)のみOK


プロトコルの中で、プロパティの要求はvarキーワードを用いて宣言されます。gettable & settableプロパティであることを型注釈の後ろに{ get set }と記述することで示します。gettableプロパティであることは{ get }と記述することで示します。

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

プロトコル内でタイププロパティの要請を定義する場合常にstaticキーワードを前置してください。このルールは

工事中🏗

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}

下のサンプルは一つのインスタンスプロパティの要請を定義したプロトコルです。

protocol FullyNamed {
    var fullName: String { get }
}

FullyNamedプロトコルは、自身に準拠する型に対して、fullNameプロパティを用意することを要求します。このプロトコル定義により、FullyNamed型(FullyNamedプロトコルに準拠する型)は、String型でgettableなfullNameプロパティを用意(宣言)する必要がある、ということを示しています。

下のサンプルはFullyNamedプロトコルに準拠したシンプルな構造体の定義の例です。

struct Person: FullyNamed {
    var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"

このサンプルではPerson構造体を定義しています。特定の名前を有する人間を表しています。Person構造体の定義の1行目でFullyNamedプロトコルに準拠していることを示しています。

それぞれのPersonクラスのインスタンスはString型のfullNameという名のストアドプロパティを有しています。これはFullyNamedプロトコルの要求と一致します。つまりPersonクラスはFullyNamedプロトコルに正しく準拠していることを意味しています。(Swiftはプロトコルの要求が満たされていない場合コンパイルエラーをリポートします。)


下のサンプルはFullyNamedプロトコルに準拠した少し複雑なクラス定義の例です。

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 != nil ? prefix! + " " : "") + name
    }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"

このクラスはfullNameプロパティの要請を、読み取り専用コンピューテドプロパティとして実装しています。それぞれのStarshipクラスのインスタンスは必須であるnameプロパティとオプショナルなprefixプロパティを格納しています。fullNameコンピューテドプロパティは、prefixに値がある場合それをnameプロパティの値の前に付け加えてstarshipのフルネームを返します。


必要なメソッドをプロトコルで要請する

プロトコルは、自身に準拠する型に対して、特定のインスタンスメソッド・タイプメソッドを実装することを要求することができます。これらのメソッドをプロトコル内で宣言する時は、通常のインスタンスメソッド・タイプメソッドと似た方法で記述できますが、丸かっこ()とメソッドのボディは書きません。通常のメソッドと同じルールで可変長引数を使用できます。しかしプロトコル内のメソッドの引数にデフォルト値を設定することはできません。

プロトコル内でタイプメソッドを宣言する時は常にstaticキーワードを前置します。

protocol SomeProtocol {
    static func someTypeMethod()
}

下のサンプルは一つのインスタンスメソッドの実装を要求するプロトコルの例です。

protocol RandomNumberGenerator {
    func random() -> Double
}

RandomNumberGeneratorプロトコルは、randomという名の、Double型の値を返すインスタンスメソッドの実装を、自身に準拠する型に要求します。プロトコル内では記述していませんが、この返り値は0.0から1.0の間の値(1.0は含まない)を想定しています。

RandomNumberGeneratorプロトコルは返されるランダムな数値の生成方法については何の仮定も用意していません。生成方法は問わず、ただランダムな値を返すrandomメソッドを実装することを要求しているだけです。

以下は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)
            .truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"

必要なmutatingメソッドをプロトコルで要請する

時にメソッドは、自身が属するインスタンスを変更する必要がある場合もあります。値型(構造体と列挙型)のインスタンスメソッドには、メソッドのfuncキーワードの前にmutatingキーワードを記述することで、そのメソッドが「自身の属するインスタンス、あるいはそのインスタンスのプロパティ」を変更することができることを示すことができます。

プロトコル定義の中で、準拠した型のインスタンスを変更するメソッド(つまりmutating メソッド)の実装を要請したい場合、プロトコル定義の中でmutatingキーワードを記述します。これにより、そのプロトコルに準拠する構造体・列挙型内でmutatingキーワードを実装できます。

 

 

 

 

sample1-1では、Togglableプロトコルを定義しています。このプロトコルは一つのインスタンスメソッドtoggle()の実装を要求します。toggle()メソッドは準拠した型の、特にプロパティを転換・反転させることを目的としたメソッドです。

toggle()メソッドは、Togglableプロトコルの定義の一部としてmutatingキーワードを付けて定義されています。それは、このメソッドが呼び出された時に、準拠した型インスタンスの状態を変化させるメソッドであることを表しています。

sample1-1

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()
print(lightSwitch)
print(type(of:lightSwitch))
// on
// OnOffSwitch

もしTogglableプロトコルに準拠した構造体・列挙型を実装する場合、準拠する型の中で、mutatingを付けたtoggle()メソッドを実装することで、Togglableプロトコルに準拠します。

sample1-1ではOnOffSwitch列挙型を定義しています。この列挙型は、二つのケースonとoffで示された二つの状態の間でトグル(反転)します。OnOffSwitch列挙型内のtoggleメソッドの実装はmutatingキーワードを付けられています。これによりTogglableプロトコルに準拠します。


必要なイニシャライザをプロトコルで要請する

プロトコルは、自身に準拠する型に対して、特定のイニシャライザを実装することを要求することができます。プロトコル定義内でイニシャライザを宣言する場合、通常のイニシャライザ定義と似た方法で宣言しますが、中かっことイニシャライザのボディは書きません。

protocol SomeProtocol {
    init(someParameter: Int)
}

プロトコルによりイニシャライザ実装を要求されたクラスでの実装

準拠する型内で、プロトコルに要求されたイニシャライザを定義する場合、指定イニシャライザ、またはコンビニエンスイニシャライザとして実装することができます。両方のケースで、イニシャライザ定義の前にrequired修飾子を前置する必要があります。

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // イニシャライザの実装
    }
}

required修飾子を付けることで、明示的な定義あるいは自動的な継承により、プロトコルに準拠した全てのサブクラスも、要求されたイニシャライザを実装することが確実になります。

NOTE

final修飾子のあるクラスには、イニシャライザ要求に際しrequired修飾子を付ける必要はありません。なぜならfinal修飾子のあるクラスはサブクラスを作ることができないからです。

サブクラス(SomeSubClass)がスーパークラス(SomeSuperClass)の指定イニシャライザをオーバーライドし、尚且つイニシャライザを要求するプロトコル(SomeProtocol)に準拠する場合、イニシャライザにはrequiredとoverrideの両方を前置する必要があります。

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // initializer implementation goes here
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance; "override" from SomeSuperClass
    required override init() {
        // initializer implementation goes here
    }
}

 

工事中🏗(失敗可能イニシャライザ)


型としてのプロトコル

プロトコルは実際には自身の中で機能を実装しません。にもかかわらずコードの中で、プロトコルを完全な一つの型として使用することができます。

他の一般的な型が使用できる多くの場所で、プロトコルを使用することができます。

  • 関数・メソッド・イニシャライザの引数の型、戻り値の型として
  • 定数・変数・プロパティの型として
  • 配列・ディクショナリ・その他のコンテナの要素の型として

NOTE

プロトコルは型なので、大文字で書き始めます。(FullyNamedやRandomNumberGeneratorのように)

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
    }
}

このサンプルでは、ボードゲームで使用するn面ダイスを表現するDiceクラスを定義しています。Diceインスタンスは、面の数を表現するInt型のプロパティsidesを持ちます。さらにダイスをふった結果を返すgeneratorプロパティを持ちます。

sample2-1

protocol RandomNumberGenerator {
    func random() -> Double
}

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)
            .truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}

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
    }
}

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("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

generatorプロパティはRandomNumberGenerator型として型注釈されています。ですので、generatorプロパティにセットできるのはRandomNumberGeneratorプロトコルに準拠した型のインスタンスです。RandomNumberGeneratorプロトコルに準拠した型以外の型のインスタンスをセットすることはできません。

工事中🏗

Diceクラスはイニシャライザも定義しています。このイニシャライザはgeneratorという引数があり、RandomNumberGenerator型と型注釈されています。Diceインスタンスを生成する時にこの引数にRandomNumberGeneratorプロトコルに準拠した型を渡します。

Diceクラスは一つのインスタンスメソッドrollを定義しています。rollは1からダイスの面数の中の一つの整数を返します。rollメソッドはgeneratorのrandomメソッドを呼び出し、0.0から1.0の間のランダムな数値を生成します。このランダムな数値を使用して、サイコロの適切な試行結果を生成します。generatorはRandomNumberGeneratorプロトコルに準拠していることがわかっていますから、random()メソッドの呼び出し成功が保証されます。

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("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)

デリゲーション(Delegation)は、「クラスや構造体が、特定のやるべき処理を他の型のインスタンスに任せること」を可能にするデザインパターンです。このデザインパターンは他のクラスに任せる処理をプロトコルとして定義(任せる処理をカプセル化)することで実装します。このプロトコルに準拠した型(delegate)は、任せられる(引き受ける)処理を実装していることが保証されます。デリゲーションを、特定のアクションが起こった時の応答処理に使うことができます。あるいは、

 

下のサンプルはサイコロを使用するボードゲームのための二つのプロトコルを定義しています。

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate: AnyObject {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

DiceGameプロトコルは「サイコロを使用するゲーム」を表現するあらゆる型が準拠するプロトコルです。

DiceGameDelegateプロトコルは、DiceGameプロトコル(が準拠するゲームクラス)のゲームの進行を追跡するクラスが準拠するプロトコルです。強参照循環を回避するため、delegates(delegateクラス)は弱参照で宣言されます。

工事中🏗


エクステンションを用いてプロトコルに準拠させる(Adding Protocol Conformance with an Extension)

エクステンションを利用すれば、既存の型のソースコードへアクセスすることなく、既存の型を新しいプロトコルへ準拠させる拡張を行うことができます。エクステンションは既存の型に対してプロパティ・メソッド・サブスクリプトを追加することができます。それゆえにプロトコルの要求する実装を追加することもできます。

例えば、下のTextRepresentableというプロトコルは、テキストとして表現する方法を持つ任意のタイプで実装できます。それはインスタンス自身の説明文かもしれないし、現在の状態の説明文かもしれません。

protocol TextRepresentable {
    var textualDescription: String { get }
}

上のDiceクラスはTextRepresentableプロトコルへ準拠することで、拡張できます。

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

このエクステンションにより、新しいプロトコルへ準拠するのは、Diceクラス定義の中で実装するのと全く同じ意味となります。プロトコル名(TextRepresentable)を型名(Dice)の後にコロン( : )で区切って記述します。そしてエクステンションの本体内で、プロトコルが要求する実装を記述します。

あらゆるDiceインスタンスはこれでTextRepresentableとして(TextRepresentableプロトコルに準拠した型として)扱うことができます。

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"

 

同様に、SnakesAndLaddersクラスも、TextRepresentableプロトコルに準拠することで拡張することができます。

extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "A game of Snakes and Ladders with \(finalSquare) squares"
    }
}
print(game.textualDescription)
// Prints "A game of Snakes and Ladders with 25 squares"

 

条件付きでプロトコルへ準拠する

工事中🏗

 


エクステンションを使用してプロトコルへの準拠を宣言する(Declaring Protocol Adoption with an Extension)

もし既存の型がすでにプロトコルの要求を全て満たしているが、そのプロトコルへの準拠はまだしていない場合、empty extension(空のエクステンション)によりプロトコルへ準拠させることができます。

protocol TextRepresentable {
    var textualDescription: String { get }
}


struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"

Hamster構造体のインスタンスは、TextRepresentable型が要求される場面でどこでも使うことができます。

NOTE

ある型の定義内に、あるプロトコルが要求する実装が全て存在したとしても、それだけで自動的にプロトコルへ準拠した型になることはありません。プロトコルへ準拠した型にするためには、必ず明示的にプロトコルへの準拠を宣言しなければなりません。

 

工事中🏗


クラス専用プロトコル(Class-Only Protocol)

プロトコルの継承リストの中にAnyObjectプロトコルを加えることで、クラスのみが準拠できるプロトコルを定義することができます。

(AnyObjectプロトコルを継承したプロトコルは、クラスのみが準拠できるプロトコルになる、ということ。)

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // class-only protocol definition goes here
}

上のサンプルでは、someClassOnlyProtocolプロトコルはクラスのみが準拠できるプロトコルです。構造体や列挙型の定義でSomeClassOnlyProtocolプロトコルに準拠しようとするとコンパイルエラーが発生します。

NOTE

プロトコルが要求する挙動が、値型ではなく、参照型の振る舞いを前提として挙動である場合に、class-only protocolとして宣言しましょう。


プロトコルコンポジション(Protocol Composition)

「複数のプロトコルへの準拠していること」を同時に型に要求することは便利なことです。プロトコルコンポジション(protocol composition)を使って複数のプロトコルを一つの要求に結合することができます。プロトコルコンポジションはまるで全ての要求を合成した一時的なローカルなプロトコルを定義するような振る舞いをします。しかしプロトコルコンポジションは新しいプロトコルを定義しているわけではありません。

プロトコルコンポジションは

SomeProtocol & AnotherProtocol

のように記述します。アンパサンド( & )で区切って、必要な数のプロトコルをリストすることができます。プロトコルコンポジションはプロトコルに加えて、一つのクラスを加えることもできます。クラスを加えた場合、そのクラスをスーパークラスとして持っていることを要求することになります。

下のサンプルはNamedとAgedという名の二つのプロトコルを一つのプロトコルコンポジションに結合して、関数の引数の型注釈として、要求を表現しています。

protocol Named {
    var name: String { get }
}
protocol Aged {
    var age: Int { get }
}
struct Person: Named, Aged {
    var name: String
    var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// Prints "Happy birthday, Malcolm, you're 21!"

このサンプルで、NamedプロトコルはgettableなString型のプロパティnameを要求しています。AgedプロトコルはgettableなInt型のプロパティageを要求します。Person構造体はNamed,Aged両方のプロトコルに準拠しています。

サンプルはwishHappyBirthday(to:)関数も定義しています。引数celebratorは

Named & Aged

と型注釈されています。これは「NamedプロトコルとAgedプロトコルの両方に準拠しているあらゆる型」という意味です。要求されている両方のプロトコルに準拠している限り、引数に渡されたインスタンスの型が具体的に何型かは問題にはなりません。

サンプルは次に新しいPersonインスタンス、birthdayPersonを生成し、そのインスタンスをwishHappyBirthday(to:)関数に引数として渡しています。Person構造体は両方のプロトコルに準拠していますから、この関数呼び出しは認められるものです。そしてwishHappyBirthday(to:)関数はお祝いのメッセージを表示します。

下のサンプルは前のサンプルのNamedプロトコルとLocationクラスを結合した例です。

protocol Named {
    var name: String { get }
}

class Location {
    var latitude: Double
    var longitude: Double
    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}
class City: Location, Named {
    var name: String
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
//Hello, Seattle!

beginConcert(in:)関数の引数の型注釈は

Location & Named

と指定されています。これは「Locationクラスのサブクラスであり、尚且つNamedプロトコルに準拠している、この二つの条件を満たすあらゆる型」という意味です。Cityクラスはこの条件を満たしています。

birthdayPersonをbeginConcert(in:)関数に引数として渡すことは許されません。なぜならPerson構造体はLocationクラスのサブクラスではないからです。同様に、もし「Namedプロトコルに準拠していない、Locationクラスのサブクラス」を定義したとして、そのクラスのインスタンスをbeginConcert(in:)関数に引数として渡すことも許されません。


プロトコルへの準拠をチェックする(Checking for Protocol Conformance)

プロトコルへの準拠のチェックやプロトコル型へのキャストは、型チェックや型キャストと同じ構文で記述することができます。

  • is演算子は、インスタンスがプロトコルへ準拠している場合trueを、準拠していない場合falseを返します。
  • as?バージョンのダウンキャスト演算子は、ダウンキャスト成功時はオプショナル型のキャスト後の型の値を返します。ダウンキャスト失敗時はnilを返します。
  • as!バージョンのダウンキャスト演算子は強制的にダウンキャストを試み、ダウンキャストが失敗時は実行時エラーが発生します。

以下のサンプルはHasAreaプロトコルを定義しています。HasAreaプロトコルは一つのgettableなDouble型のareaプロパティを要求します。

protocol HasArea {
    var area: Double { get }
}

そしてCircleとCountryの二つのクラスを定義します。二つともHasAreaプロトコルに準拠したクラスです。

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 }
}

 

Circleクラスではストアドプロパティradiusを基に面積を計算するコンピューテドプロパティとしてareaプロパティを定義(実装)しています。Countryクラスではストアドプロパティとしてareaプロパティを実装しています。どちらのクラスも適切にHasAreaプロトコルへ準拠できています。

次にAnimalという名の、HasAreaプロトコルに準拠していないクラスを定義します。

class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

Circle,Country,Animalクラスは共通のベースクラスを持ちません。しかし、三つともクラスですから、要素の型としてAnyObject型を指定された配列(つまり[AnyObject])を初期化するときに、三つのクラスのインスタンスを使用することができます。

let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

 

配列objectsはCircleインスタンス、Countryインスタンス、Animalインスタンスを要素として持つ配列リテラルにより初期化されています。

配列objectsは一つずつ要素を取り出して処理することができます。そして配列内のそれぞれの要素がHasAreaプロトコルに準拠しているかをチェックすることができます。

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

配列の要素がHasAreaプロトコルに準拠している場合はいつでも、as?演算子により返されるオプショナル型の値は、オプショナルバインディングによりアンラップされて定数objectWithAreaにセットされます。定数objectWithAreaはHasAreaプロトコルに準拠していることがわかっていますから、タイプセーフな方法でareaプロパティにアクセスすることができます。

キャストが行われてもそれぞれのオブジェクト自体は変わらないことに注意してください。AnyObject型→HasArea型への型キャストが行われてもCircleインスタンス、Countryインスタンス、Animalインスタンスであることに変わりはありません。しかし、定数objectWithAreaにセットされた時点で、HasArea型と型推定され、areaプロパティにのみアクセスできるわけです。

クラスを生成した時点で、全てのクラスはAnyObjectプロトコルに準拠している。

まず、「AnyObject型→各クラス」へダウンキャスト。これをダウンキャストと言うか、だが、多分言うのだと思われる。書籍では言っていたような気がする。多分このダウンキャストは100%成功する。はず。

次に「各クラス→HasArea」へキャスト。これをアップキャストと言うか?

このキャストはクラスがHasAreaに準拠しているクラスなら成功、そうでなければ失敗する。失敗するキャスト試みはそもそもアップキャストと言わないのだと思われる。準拠していないプロトコルにキャストしようとしてもできないのは自明。準拠しているプロトコルへのキャストは100%成功する。それをアップキャストと言うのだろう。

そしてこの二段階のキャストを試みて、成功した場合オプショナル型の値を返すのが、

object as? HasArea

なのだろう。上のサンプルはオプショナルバインディングでアンラップした値をobjectWithAreaにセットしている。


と言うか「AnyObject→HasAreaプロトコルに準拠したクラス」へのダウンキャスト、これが成功する場合と失敗する場合がある、という考え方の方が自然か。

成功する場合➡️クラスがHasAreaプロトコルに準拠している場合(CircleとCountry)、

失敗する場合➡️クラスがHasAreaプロトコルに準拠していない場合(Animal)

そしてダウンキャストに成功したクラスは100%HasArea型へのアップキャストは成功する。


というかアップキャストは無くて、

と言うか「AnyObject→HasAreaプロトコルに準拠したクラス」へのダウンキャスト

これが

object as? HasArea

なのだろう。


AnyObject→各クラス型、のダウンキャストもあるし

AnyObject→HasArea型、のキャストもある。両方のケースがある。


optional requirements for protocols

工事中🏗

以下のサンプルはCounterという名の整数をカウントするクラスを定義しています。このクラスは増分値を得るために、外部のデータソースを使用します。このデータソースはCounterDataSourceプロトコルで定義されています。このプロトコルは二つのオプショナル要求(optional requirements)を持っています。

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

CounterDataSourceプロトコルはincrement(forCount:)という名のオプショナルメソッドの要求を定義しています。それとfixedIncrementという名のオプショナルプロパティの要求を定義しています。この二つの要求はCounterインスタンスに対して適切な増分値を提供するための、二つの異なる方法を定義しています。

NOTE

厳密に言うと、CountrDataSourceプロトコルの二つの要求を実装せずに、CounterDataSourceプロトコルに準拠した独自のクラスを定義することはできます。二つの要求はどちらもオプショナル(無くても問題ない)だからです。

Counterクラスの定義を以下のサンプルに示します。オプショナル型のdataSourceプロパティを持っています。

class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        if let amount = dataSource?.increment?(forCount: count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

 

Counterクラスは変数プロパティcountに現在のカウント値を格納します。Counterクラスincrementメソッドも定義されています。このメソッドが呼ばれるたびにcountプロパティを増分します。

increment()メソッドはまずdataSourceプロパティのincrement(forCount:)メソッドの実装を見て、増分値を取得しようとします。increment()メソッドはオプショナルチェイニングを使ってincrement(forCount:)メソッドを呼び出そうとします。そして現在のcountプロパティの値を引数forCountに渡します。

二段階のオプショナルチェイニングが使用されていることに注目してください。まず一つ目に、dataSourceプロパティにnilがセットされている可能性がありますので、dataSourceの直後に?(クエスチョンマーク)を記述して、dataSourceがnilでない時にだけincrement(forCount:)メソッドが呼び出されることを示しています。二つ目に、dataSourceプロパティにnilでない値がセットされている場合でも、increment(forCount:)メソッドが実装されている保証はありません。なぜならincrement(forCount:)メソッドはオプショナル要求だからです。increment(forCount:)メソッドが実装されていない場合に備えて二つ目のオプショナルチェイニングが使用されています。increment(forCount:)メソッドが実装されている場合のみincrement(forCount:)が呼び出される、その場合nilでない、と言うことです。

increment(forCount:)メソッドの呼び出しは、上記の二つの理由により失敗する可能性がありますから、このメソッド呼び出しはオプショナル型のInt型の値(Int?)を返します。これは、CounterDataSourceの要求の実装に置いて返り値が非オプショナル型と定義されていても変わりません。

increment(forCount:)メソッド呼び出しで返されたオプショナルInt型の値はオプショナルバインディングによりアンラップされamount定数にセットされます。

dataSource?.increment?(forCount: count)

上記のメソッド呼び出しがnilでない値を返した場合、つまりdelegateとメソッドの両方が存在しメソッドが値を返した場合、amountにセットされたInt型の値がcountに加算され、増分が完了します。

もしincrement(forCount:)メソッド呼び出しでの増分値の取得が失敗した場合、–つまりdataSourceがnilだったか、あるいはdataSourceがincrement(forCount:)メソッドを実装していなかった場合、increment()メソッドは

 


プロトコルエクステンション(Protocol Extensions)

自身に準拠する型に、「メソッド・イニシャライザ・サブスクリプト・コンピューテドプロパティ」の実装を提供するためにプロトコルをエクステンション(拡張)することができます。これにより、個別の準拠する型の定義内・グローバル関数では無く、プロトコルそのものに振る舞いを定義することが可能となります。

例えば、RandomNumberGeneratorプロトコルにrandomBool()メソッドを提供するためにエクステンション(拡張)をすることができます。randomBool()メソッドはrandom()メソッドの結果を利用してランダムなBool値を返すメソッドです。

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

RandomNumberGeneratorプロトコルへエクステンションを行ったことで、これだけで、RandomNumberGeneratorプロトコルに準拠する全ての型が自動的にrandomBool()メソッドを使用できるようになりました。

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And here's a random Boolean: \(generator.randomBool())")
// Prints "And here's a random Boolean: true"

プロトコルエクステンションは準拠する型へ実装を追加できますが、プロトコルに定義されている要求を追加したり、他のプロトコルから継承することはできません。プロトコルの継承は常にプロトコルの定義自体で明示します。


プロトコルエクステンションでデフォルト実装を提供する(Providing Default Implementation)

プロトコルエクステンションを使って、プロトコルのメソッドや今ピューテドプロパティの要求に対して、デフォルト実装を提供することができます。準拠している型の定義内で、要求されているメソッドやコンピューテドプロパティの実装をしている場合、デフォルト実装ではなく「準拠している型の定義内の実装」が優先して使用されます。

例えば以下のサンプルは、TextRepresentableプロトコルを継承したPrettyTextRepresentableプロトコルがエクステンションにより、自身が要求するprettyTextualDescreprionプロパティのデフォルト実装を提供する例です。デフォルト実装はシンプルにtextualDescriptionプロパティにアクセスしてその結果を返すようにしています。

extension PrettyTextRepresentable  {
    var prettyTextualDescription: String {
        return textualDescription
    }
}

 


制約を加えたプロトコルエクステンション

プロトコルエクステンションを定義する際、エクステンションにより追加するメソッドやプロパティを利用する前に、そのプロトコルに準拠する型が満たすべき制約を記述することができます。

つまり制約を満たす状況でのみエクステンションによる実装を利用できる、ということ。

このような制約は、拡張するプロトコル名の後ろに記述します。generic where clauseを使って記述します。

例えば、要素(Element)がEquatableプロトコルに準拠しているコレクションにのみ適用されるエクステンションをCollectionプロトコルに行う例を以下に示します。要素に対してSwift標準ライブラリのEquatableプロトコル準拠の制約を課すことで、==と!=演算子による二つの要素の等値性のチェックを利用できるようになります。

extension Collection where Element: Equatable {
    func allEqual() -> Bool {
        for element in self {
            if element != self.first {
                return false
            }
        }
        return true
    }
}

 

allEqual()メソッドは、コレクション内の全ての要素が等しい値の場合のみtrueを返します。

Array<Element>型はCollectionプロトコルに準拠しており、要素の整数(Int型)はEquatalbleプロトコルに準拠しています。ですのでequalNumbersとdifferentNumbersはallEqual()メソッドを呼び出せます。

let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]

print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"

 

 

 

 

参考

https://docs.swift.org/swift-book/LanguageGuide/Protocols.html

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です