30
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

SwiftAdvent Calendar 2021

Day 2

Swiftのプロトコルは何?

Last updated at Posted at 2021-12-01

Swiftのプロトコルって何でしょうか。これはどういう役割のために存在するものでしょうか?この問いに対する、ぼくの答えはこうです。

Swiftのプロトコルはジェネリックの型パラメータに制約を与えるためのもの。

以下、具体例を交えて説明します。

ジェネリック

犬という型があったとしましょう。その一部を抜粋したものが以下です。

/// 犬。
class Dog {
    /// お腹が空いているかどうか。
    var isHungry: Bool { ... }
    
    /// 餌を食べます。
    func feed() { ... }
    
    /// 尻尾を追いかけます。
    func chaseTail() { ... }

    ...
}

ここで、犬の面倒を見てくれる人の型を作ります。

/// 犬の面倒を見る人。
struct Dogsitter {
    /// ペットのお世話をします。
    /// - Parameter pet: 対象のペット
    func care(for pet: Dog) {
        // お腹を空かせていたら餌をやる
        if pet.isHungry {
            pet.feed()
        }
    }
}

次のようにして、このドッグシッターにペットのポチの面倒を見てもらうことができます(こんなお世話でいいのかどうかはおいといて)。

let pochi = Dog()
let dogshitter = Dogsitter()
dogshitter.care(for: pochi)

ところで、猫の型もあったとしましょう。

class Cat {
    /// お腹が空いているかどうか。
    var isHungry: Bool { ... }
    
    /// 餌を食べます。
    func feed() { ... }
    
    /// じっと見つめます。
    func stare() { ... }

    ...
}

猫の面倒を見てくれる人の型を作りたいのですが、やることは犬の時と同じでした。

struct Catsitter {
    func care(for pet: Cat) {
        if pet.isHungry {
            pet.feed()
        }
    }
}

こんなとき、ジェネリックを使えばいちいち同じ処理を書くのを避けることができます。次のような型を定義すれば良いのです。

struct Petsitter<Pet> {
    func care(for pet: Pet) {
        if pet.isHungry {
            pet.feed()
        }
    }
}

ところが、残念ながら上のコードはコンパイルエラーになります。なぜなら、ジェネリックの型パラメーターである Pet は型のプレースホルダーです。実際に使われるときには Pet は具体的な型に置き換わるわけですが、このままではどんな型でも取り得るため、置き換えられた具体的な型が isHungryfeed() を持っているかどうかがわからないからです。

struct Petsitter<Pet> {
    func care(for pet: Pet) {
        if pet.isHungry {   // Error: Value of type 'Pet' has no member 'isHungry'
            pet.feed()  // Error: Value of type 'Pet' has no member 'feed'
        }
    }
}

プロトコルで型に制約を設ける

ここでプロトコルの登場です。プロトコルで isHungryfeed() の存在を要求することができます。

/// このプロトコルに適合するものは、
/// お腹を空かせてるか確認できたり、餌を食べたりできます。
protocol Animal {
    var isHungry: Bool { get }
    func feed()
}

Petsitter の型パラメーターである Pet に、このプロトコルで制約を与えます。これで、Pet には Animal に適合した型しか入れられなくなりました。 isHungryfeed() を持っていることが保証できるので、コンパイルが通ります。

struct Petsitter<Pet: Animal> { // ←ここ
    func care(for pet: Pet) {
        if pet.isHungry {
            pet.feed()
        }
    }
}

また、犬や猫の型を下記のようにこのプロトコルに準拠させておきます。

class Dog: Animal { // ←ここ
    ...
}
class Cat: Animal { // ←ここ
    ...
}

こうして、ジェネリックを使って Petsitter ひとつ定義するだけで DogsitterCatsitter を個別に定義するのと同等のことができました。猫のタマの面倒を見てもらうには、次のようにします。

let tama = Cat()
let catshitter = Petsitter<Cat>()
catshitter.care(for: tama)

この使い方が Swift におけるプロトコルのメインの存在意義だと、ぼくは考えています。

型消去

ところで、この Petsitter は、型パラメーターに与えた具体的な型によって、それぞれ異なる型として振る舞います。当然ですが、猫のシッターに犬の面倒を見てもらうことはできません。

let pochi = Dog()
let catshitter = Petsitter<Cat>()
catshitter.care(for: pochi) // Error: Cannot convert value of type 'Dog' to expected argument type 'Cat'

そもそも PetsitterDogsitterCatsitter をまとめて定義しただけです。もともとの CatsitterDog を与えたらエラーになってくれないと困ります。だから、これは正しい振る舞いです。

では、犬でも猫でも、 Animal プロトコルに準拠するものならなんでもお世話できる人を作りたかった場合はどうしたらいいでしょうか。

その場合は、ジェネリックでシッターを作るのではなく、「 Animal プロトコルに準拠するものならなんでも」の部分をジェネリックを使って解決することになります。例えば、 Animal プロトコルに準拠したものをラップする、次のような型を作ります。ここでは init(_:) にジェネリックを使っています。なお、このような型は、 DogCat という具体的な型を消すものなので、型消去(type erasure)と呼ばれたりもします。

class AnyAnimal {
    private let _isHungry: () -> Bool
    private let _feed: () -> ()
    init<Wrapped: Animal>(_ wrapped: Wrapped) {
        _isHungry = { wrapped.isHungry }
        _feed = wrapped.feed
    }
    
    var isHungry: Bool { _isHungry() }
    func feed() { _feed() }
}

シッターの方は、この AnyAnimal のお世話をするように作ります。

struct Animalshitter {
    func care(for pet: AnyAnimal) {
        if pet.isHungry {
            pet.feed()
        }
    }
}

そうすれば、次のように、犬のポチも猫のタマも AnyAnimal に型消去することで、同じ Animalshitter のインスタンスに面倒を見てもらうことができるようになります。

let pochi = Dog()
let tama = Cat()
let animalshitter = Animalshitter()
animalshitter.care(for: AnyAnimal(pochi))
animalshitter.care(for: AnyAnimal(tama))

プロトコルを型として利用する

先程の AnyAnimal の例のように、プロトコルに準拠するものを型によらずまとめて扱いたいことがあります。実は Swift のプロトコルには、特別大サービスとして AnyAnimal のようなものを自動的にやってくれる仕組みがあります。それが、プロトコルを型として利用する方法です。

AnyAnimal という型消去を用意しなくても、次のように、プロトコルを直接使って型のように利用できます。このようにして利用する型を存在型(existential type)と呼んだりもします。

struct Animalshitter {
    func care(for pet: Animal) {
        if pet.isHungry {
            pet.feed()
        }
    }
}

使う側でも、型消去の必要はありません。

let pochi = Dog()
let tama = Cat()
let animalshitter = Animalshitter()
animalshitter.care(for: pochi)
animalshitter.care(for: tama)

ただし、これは特別大サービスなので、単純なプロトコルでしか利用できません。例えば、プロトコルが associatedtype を持っていたり Self を使っていたりすると、この使い方はできません。そういうときは、自分で型消去を用意する必要があります。

あくまでも、プロトコルはジェネリックの型パラメーターに制約を設けるためのもので、存在型の利用は特別大サービスだと考えるのがいいと思います。

余談:プロトコルに対する誤解(ハマりポイント)

特別大サービスである、プロトコルを存在型として使う方法は、その使い方が Java や C# のインタフェースによく似ています。実際、今回の例のようにプロトコルに適合するものがシンプルな参照型( class )だけなら、Java や C# のインタフェースと同じように使えます。しかし、値型( struct )を適合させた場合は、AnyAnimal を使ったときのような感じで、コンパイラが生成した具体的な型にラップされ、実行時のコストも発生します。

Java や C# の経験のある人が Swift を始めると、「プロトコルは Java や C# のインタフェースに相当するものだ」と捉えがちです。ぼく自身、最初はそのように考えていましたし、ぼくの周りでもそういう人を見かけました。そのような理解で、実際にコーディングを始めてしまうと、 associatedtypeSelf を使ったプロトコルを使いたくなったところでたいてい一旦ハマります。そして、Swift のプロトコルは難しいという苦手意識が残ります。

そもそもプロトコルはそういうものじゃなく、ジェネリックの型パラメータに制約を与えるためのものであって、型としての利用の方がおまけなのだ、と考えた方がシンプルに理解しやすいと思います。

実際のところ、Swift のコミュニティでも、プロトコルを型として利用する機能があまりにも単純に利用できてしまい、裏に存在型がいることを隠してしまっていることが問題視されています。Swift 6では存在型を使いたい場合は明示的に記述させるようにしよう、という提案も始まっているようです。もし、この提案がそのまま通れば、最後の Animalsitter の例は、次のように書くことになりそうです。

追記(2023/1/16):Swift 5.6から、プロトコル名の前に any を付けて明示的に記述する書き方ができるようになりました。将来のSwift 6ではanyを付けないとコンパイルエラーになることが予定されています。

struct Animalshitter {
    // ↓「Animal」は型ではなくてあくまでもプロトコルを指し、
    //  「any Animal」が存在型を示す(型として利用する)構文になる。
    func care(for pet: any Animal) {
        if pet.isHungry {
            pet.feed()
        }
    }
}

もっと知りたい

この記事では「プロトコルはジェネリックの型パラメータに制約を与えるためのもの(存在型はおまけ)」という説明を行いました。そう捉えた上で、その一歩先として読んでほしい、他の方の記事を紹介します。

  • Heart of Swift - 第 2 章 Protocol-oriented Programming

    • 従来の Object-oriented Programming(オブジェクト指向プログラミング)で扱うのがプロトコルの存在型。一方、Protocol-oriented Programming(プロトコル指向プログラミング)では、ジェネリックを積極的に使い、プロトコルでそこに制約を与えるという考え方が解説されています。この第2章は3ページに渡って書かれています。一度に理解するのは難しいかもしれませんが、丁寧に詳しく書かれているので何度も参照したいです。
  • Swiftのprotocolの存在型がそれ自身のprotocolに準拠しない理由

    • 記事そのものの目的とは違うかもしれませんが、存在型を使う際の振る舞いや注意点が書かれているのでぜひ見てほしいです。
  • Swift の Type Erasure の実装パターンの紹介

    • この記事では触れなかった、associatedtypeSelf を持つプロトコルの型消去を行う方法が書かれています。
30
15
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?