4
4

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 3 years have passed since last update.

【Swift】プロトコルを構成する要素

Last updated at Posted at 2020-12-19

#プロトコルの構成要素

型と同じように、プロパティやメソッドが構成要素として挙げられますが、
定義方法は異なるのでそちらについて説明していきます。

私も結構間違えるので、似たような方の参考になればなと思います。

##プロパティ

プロトコルにはプロパティを定義することができます。
プロパティを定義することにより、準拠する型にプロパティの実装を要求できます。

###定義方法

プロトコルのプロパティの定義方法は、
プロパティ名、型、ゲッタとセッタの有無のみを定義します。

また、プロパティは常にvarキーワードで宣言し、{ }内にゲッタとセッタの有無に応じて
getキーワードとsetキーワードを追加します。

letキーワードを使用できない理由としては、プロトコルのプロパティには
ストアドプロパティやコンピューテッドプロパティの区別がないからです。


protocol プロトコル名 {
    var プロパティ名:  { get set }
}

###ゲッタの実装

準拠しているプロトコルにプロパティが存在した場合は、
そのプロパティを実装する必要があります。

そのプロパティがゲッタしかない場合は、
変数または定数のストアドプロパティを実装するか
ゲッタを持つコンピューテッドプロパティを実装する必要があります。


protocol SampleProtocol {
    var value: Int { get }
}

// 変数のストアドプロパティ
struct Sample1: SampleProtocol {
    var value: Int
}

// 定数のストアドプロパティ
struct Sample2: SampleProtocol {
    let value: Int
}

// ゲッタのみのコンピューテッドプロパティ
struct Sample3: SampleProtocol {
    var value: Int {
        return 1
    }
}

###セッタの実装

プロトコルで定義されているプロパティがゲッタとセッタを必要としている場合は、
変数のストアドプロパティを実装するか、
ゲッタとセッタの両方を持つコンピューテッドプロパティを実装する必要があります。

ゲッタの実装と比べて、定数のストアドプロパティでの実装がなくなった理由としては、
定数のストアドプロパティは変更不可なので要件を満たせないためです。


protocol SampleProtocol {
    var value: Int { get set }
}

// 変数のストアドプロパティ
struct Sample1: SampleProtocol {
    var value: Int
}

// ゲッタとセッタのコンピューテッドプロパティ
struct Sample3: SampleProtocol {
    var value: Int {
        get {
            return 1
        }
        set { }
    }
}

// 定数のストアドプロパティ
struct Sample2: SampleProtocol {
    let value: Int    // コンパイルエラー
}

エラー内容:Type 'Sample2' does not conform to protocol 'SampleProtocol'
和訳:タイプ「Sample2」はプロトコル「SampleProtocol」に準拠していません

##メソッド

プロトコルにはメソッドを定義することができます。
メソッドを定義することにより、準拠する型にメソッドの実装を要求できます。

###定義方法
プロトコルのメソッドの定義方法は、メソッド名・引数の型・戻り値の型のみを定義します。
また、プロパティと同じく、プロトコルに準拠する型でその要求を満たす実装を提供します。

プロトコルにメソッドを定義する場合は、{ }を記述する必要がありませんのでご注意を。
(こういうメソッドを定義してくださいね!くらいのニュアンスです。)


protocol プロトコル名 {
   func 関数名(引数) -> 戻り値の型
}

###メソッドの実装

メソッドが定義されているプロトコルに準拠するには
同じインターフェースを持つメソッドを実装します。


protocol SampleProtocol {
    func sampleMethod() -> Int
    static func sampleStaticMethod() -> Void
}

struct Sample: SampleProtocol {
    func sampleMethod() -> Int {
        return 1
    }
    
    static func sampleStaticMethod() {
        
    }
}

###mutatingキーワード

値型のメソッドで自身が持つプロパティの値を変更する場合は、
mutatingキーワードをつけていたと思います。

プロトコルに関しても変更し得るメソッドを定義する場合には、
mutatingキーワードを追加して定義します。

プロトコルでメソッドを定義したがmutatingキーワードがついていない場合は、
準拠している型で実装する際にmutatingキーワードをつけることができません。

逆に、プロトコルで定義されたメソッドにmutatingキーワードがついていたとしても
準拠している型で実装する際にmutatingキーワードをつける必要はありません。

また、参照型のメソッドではmutatingキーワードによって
インスタンスの変更の有無を区別する必要がないので、
クラスをプロトコルに準拠させる際にmutatingキーワードを追加する必要がありません。


protocol SampleProtocol {
    mutating func sampleMutatingMethod()
    func sampleMethod()
}

struct SampleStruct: SampleProtocol {
    var value: Int
    
    mutating func sampleMutatingMethod() {
        value = 1
    }
    
    func sampleMethod() {
        value = 1   // コンパイルエラー
    }
}

class SampleClass: SampleProtocol {
    var value = 0
    
    func sampleMutatingMethod() {
        value = 1
    }
    
    func sampleMethod() {
        value = 1
    }
}

##連想型

先ほどまでの説明では、プロトコルの定義時に型を全て決める必要がありました。

事前に型を指定することにより意図せぬ処理が行われにくのですが、
一つの型に依存してしまい利便性がなくなったりもします。

そこで使えるのが連想型で、
それを用いるとプロトコルの準拠時に型を指定することができます。

プロトコル側では連想型はプレースホルダ(仮の値)として働き、
準拠する型の方で実際の連想型の型を指定します。

連想型を用いれば、1つの型に依存しないより抽象的なプロトコルを定義できます。

###定義方法
プロトコルの連想型の名前はassociatedtypeキーワードを用いて定義します。
連想型は同じプロトコル内のプロパティやメソッドの引数や戻り値の型として使用できます。


protocol プロトコル名 {
   associatedtype 連想型名

   var プロパティ名: 連想型名
   func メソッド名(引数名: 連想型名)
   func メソッド名() -> 連想型名
}

連想型の実際の型は、プロトコルに準拠する型ごとに決めることができます。

型を決める方法としては全部で3つあります。
① 型エイリアスを使用し、typealias 連想型名 = 指定する型名と定義する
② 実装から連想型が自動的に決定する
③ 同名のネストによって指定する

次のサンプルコードでは、
SampleProtocolプロトコルで連想型Associativeを定義しています。
また、プロパティやメソッドで連想型を使用しています。

Sample1型では、型エイリアスを使用しAssociativeの型を指定しています。
定義方法は、typealias 連想型名 = 指定する型名です。

Sample1型でのAssociativeは全てInt型として扱われます。

Sample2型では、実装から型を指定し型エイリアスを省略しています。
プロトコルでAssociativeとして定義していた箇所にInt型を記述することで、
自動的にAssociativeの型がInt型に決まります。

Sample3型では、連想型と同名のネストした型を定義しています。
この型が連想型となります。


protocol SampleProtocol {
    associatedtype Associative
    
    var value: Associative { get }
    func printValue(value: Associative) -> Associative
}

// ① Associativeを定義する
struct Sample1: SampleProtocol {
    typealias Associative = Int

    var value: Associative
    func printValue(value: Associative) -> Associative {
        return value
    }
}

// ② 実装から自動的に決定
struct Sample2: SampleProtocol {
    
    var value: Int
    func printValue(value: Int) -> Int {
        return value
    }
}

// ③ 連想型と同名のネスト型を定義する
struct Sample3: SampleProtocol {
    struct Associative { }
    
    var value: Associative
    func printValue(value: Associative) -> Associative {
        return value
    }
}

個人的には記述も減りますし、ぱっと見でわかるので②がいいかなと思います。
実際に業務する環境でルールがあるかもしれないのでそれ次第かもしれませんね!

###型制約の追加

プロトコルの連想型が準拠すべきプロトコルやスーパークラスを指定し、
連想型に制約を設けることができます。

制約を追加するには、連想型の宣言の後に: プロトコル名(またはスーパークラス名)とします。
連想型が型の制約を満たすかどうかはコンパイラによってチェックされ、
満たさない場合はコンパイルエラーになります。


protocol プロトコル名 {
   associatedtype 連想型名: プロトコル名またはスーパークラス名
}

実際に記述すると次のサンプルコードのようになります。

プロトコル内のassociatedtype Associative: SuperSampleの部分で、
連想型であるAssociativeに制約を設けています。

その制約とは、SuperSample型、もしくはSuperSample型を継承した型になります。

class Sample: SuperSample {}
Sample型はSuperSample型を継承しているので、
SampleStruct1のtypealias Associative = Sampleが成り立ちます。

逆にSampleStruct2ではInt型を指定しているので制約に阻まれ
コンパイルエラーが発生します。


class SuperSample {}

protocol SampleProtocol {
    // SuperSample型もしくはそれを継承していなければならない
    associatedtype Associative: SuperSample
}

// SuperSample型を継承している
class Sample: SuperSample {}


struct SampleStruct1: SampleProtocol {
    // Sample型はSuperSample型を継承している
    typealias Associative = Sample
}

struct SampleSatuct2: SampleProtocol {
    // Int型はSuperSample型を継承していない
    typealias Associative = Int   // コンパイルエラー
}

エラー内容:Type 'SampleSatuct2' does not conform to protocol 'SampleProtocol'
和訳:タイプ「SampleSatuct2」はプロトコル「SampleProtocol」に準拠していません

###デフォルト値の指定

プロトコルの連想型には、宣言と同時にデフォルト値を指定することができます。

デフォルト値を指定した場合は、
プロトコルに準拠する型側での連想型の指定が任意になります。


protocol SampleProtocol {
    associatedtype Associative = Int
}

struct Sample1: SampleProtocol {
    // Sample1.Associative はデフォルト値のInt型になる
}

struct Sample2: SampleProtocol {
    // Sample2.Associative はString型になる
    typealias Associative = String
}

##プロトコルの継承

プロトコルは他のプロトコルを継承できます。

プロトコルの継承は、単純に継承元のプロトコルで定義されている
プロパティやメソッドなどをプロトコルに引き継ぐだけです。

クラスにおけるオーバーライドのような概念はありません。

継承方法は、型にプロトコルを準拠させる時のように,で区切り複数継承させます。


protocol プロトコル名1: プロトコル名2, プロトコル名3 {
   プロトコルの定義
}

先ほどのコードを元にサンプルコードを記述しました。

ProtocolCでは、ProtocolAとProtocolBを継承していますので、
id と title の2つを要求するプロトコルになります。

なので、Sample型を定義する際には、
id と title の2つを定義する必要があります。


protocol ProtocolA {
    var id: Int { get }
}
protocol ProtocolB {
    var title: String { get }
}

// id と title の2つを要求するプロトコルとなる
protocol ProtocolC: ProtocolA, ProtocolB {}

struct Sample: ProtocolC {
    var id: Int
    var title: String
}

以上がプロトコルを構成する要素になります。

結構ややこしいと思いますので、
実際に自分でコードを書いてみてより深く理解してみてください!

この記事の他にもプロトコルに関しての記事があるのでぜひご覧ください!

【Swift】プロトコルの概念と定義方法
【Swift】プロトコルエクステンションについて

最後までご覧いただきありがとうございました。

4
4
0

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?