通常 Swift で protocol を定義した場合、その protocol は class のみならず struct や enum などにも適用できてしまいます。ほとんどの場合これは非常に便利ですが、時々逆に不便になることもあります。例えば Delegate を protocol で定義した際、値型の struct などにも使えるため、var delegate = Delegate? が書けても weak var delegate = Delegate? が書けません。値型に weak がそもそも意味ないからです。もちろん Delegate を struct にするという手もありますが、Delegate の性質上、参照型の方が嬉しいという場面が非常に多いです、例えば MVC アーキテクチャで UIViewController を Model の Delegate にしたいときとかまさにそれです。そういった場合、あらかじめこのプロトコルは class だけにしか使えないとわかっていても、通常の protocol の宣言の仕方ではそれがわからないため、weak が使えずメモリリークにつながりかねません。
例えば下記のようなソースコード見てみればわかると思います:
protocol ADelegate {
}
class A {
var delegate: ADelegate?
deinit {
print("A deinited")
}
}
class P {
let child: A
init(child: A) {
self.child = child
}
deinit {
print("P deinited")
}
}
extension P: ADelegate {
}
var a: A? = A()
var p: P? = P(child: a!)
a!.delegate = p
p = nil
a = nil
上記のコードを Playground で実行してみればわかると思いますが、A も P も deinit が呼び出されていません。そのつもりはなかったが結局循環参照が起こってしまうため a!.delegate が参照カウンターを保持しており結局 a も p もメモリが解放されません。これを解決するには a = nil の前に a!.delegate = nil を入れないといけません。でもこれだと面倒だし忘れがちだしあまりいい解決法とは言い難いですね。
というわけで、ここはこの delegate は参照型だと最初から分かっている場合、やはり weak を使いたいのですが、ここで登場してもらいたいのがこの Class-Only Protocol です。
実は Class-Only Protocol の宣言は非常に簡単で、定義した protocol に class の継承を付け加えるだけで OK です。つまりこう:
protocol ADelegate: class {
}
です。こうすることによって、ADelegate は class のみに適用される protocol とみなされ、当然 ADelegate の変数も weak が使えるようになります。
これで最初のコードをちょっと修正してみましょう:
protocol ADelegate: class {
}
class A {
weak var delegate: ADelegate?
deinit {
print("A deinited")
}
}
class P {
let child: A
init(child: A) {
self.child = child
}
deinit {
print("P deinited")
}
}
extension P: ADelegate {
}
var a: A? = A()
var p: P? = P(child: a!)
a!.delegate = p
p = nil
a = nil
これでもう一度 Playground で実行してみると、deinit がちゃんと両方の class に呼び出されたことが確認できます。めでたしめでたし。
ちなみにもう一つ嬉しいのは、参照型だとわかったから、protocol には mutating func の宣言が要らなくなります。すべて func のみで OK ですので、これで mutating func のメソッドが入ってる protocol を class で対応した時も変数は var 使わなければなりません、という問題が根本的に解決されます。例えば下記のようなものがあるとします:
protocol SomeProtocol {
var aVariable: Int { get set }
mutating func increase()
}
extension SomeProtocol {
mutating func increase() {
self.aVariable += 1
}
}
class SomeClass: SomeProtocol {
var aVariable: Int
init() {
self.aVariable = 0
}
}
let some = SomeClass()
some.increase()
ここで increase() は self.aVariable の値を変更しているのでどうしても mutating func で宣言しないといけませんが、そのせいで本来 let で宣言できた some が increase() を使いたい場合 var で宣言しないといけなくなるのでこのコードではエラーが出ます。これも SomeProtocol を Class-Only Protocol として宣言すれば解決されます:
protocol SomeProtocol: class {
var aVariable: Int { get set }
func increase()
}
extension SomeProtocol {
func increase() {
self.aVariable += 1
}
}
class SomeClass: SomeProtocol {
var aVariable: Int
init() {
self.aVariable = 0
}
}
let some = SomeClass()
some.increase()
ちなみに C# では Abstract Class(抽象クラス)という、いわば protocol と非常に似ているものがあって、要するに class のようにプロパティやメソッドなどの宣言があるがその定義や実現は一切せず、すべてはこの Abstract Class を継承した子クラスに任せており、当然ながら Abstract Class 自身のインスタンスも作ることができません。Swift では Abstract Class と言う概念がありませんが、Class-Only Protocol を使えばある意味 Abstract Class とほぼ同じようなものが作れるのではないでしょうか。
というわけで、もし class 専用の protocol を作りたい場合は、ぜひこの Class-Only Protocol の宣言を活用しましょう。
2018/11/06 追記
The Swift Forums によりますと、今後は : class が非推奨となり、代わりに : AnyObject の方が推奨となるそうです、なぜならば実際欲しい動きは「クラスに限定」よりも、「参照型に限定」ですので:
protocol SomeProtocol: AnyObject {
// ...
}