118
84

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

Swift で Class-Only Protocol を定義する

Last updated at Posted at 2016-05-16

通常 Swift で protocol を定義した場合、その protocolclass のみならず structenum などにも適用できてしまいます。ほとんどの場合これは非常に便利ですが、時々逆に不便になることもあります。例えば Delegate を protocol で定義した際、値型の struct などにも使えるため、var delegate = Delegate? が書けても weak var delegate = Delegate? が書けません。値型に weak がそもそも意味ないからです。もちろん Delegate を struct にするという手もありますが、Delegate の性質上、参照型の方が嬉しいという場面が非常に多いです、例えば MVC アーキテクチャで UIViewController を Model の Delegate にしたいときとかまさにそれです。そういった場合、あらかじめこのプロトコルは class だけにしか使えないとわかっていても、通常の protocol の宣言の仕方ではそれがわからないため、weak が使えずメモリリークにつながりかねません。

例えば下記のようなソースコード見てみればわかると思います:

before
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 で実行してみればわかると思いますが、APdeinit が呼び出されていません。そのつもりはなかったが結局循環参照が起こってしまうため a!.delegate が参照カウンターを保持しており結局 ap もメモリが解放されません。これを解決するには a = nil の前に a!.delegate = nil を入れないといけません。でもこれだと面倒だし忘れがちだしあまりいい解決法とは言い難いですね。

というわけで、ここはこの delegate は参照型だと最初から分かっている場合、やはり weak を使いたいのですが、ここで登場してもらいたいのがこの Class-Only Protocol です。

実は Class-Only Protocol の宣言は非常に簡単で、定義した protocolclass の継承を付け加えるだけで OK です。つまりこう:

protocol ADelegate: class {
	
}

です。こうすることによって、ADelegateclass のみに適用される protocol とみなされ、当然 ADelegate の変数も weak が使えるようになります。

これで最初のコードをちょっと修正してみましょう:

after
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 のメソッドが入ってる protocolclass で対応した時も変数は var 使わなければなりません、という問題が根本的に解決されます。例えば下記のようなものがあるとします:

before
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 で宣言できた someincrease() を使いたい場合 var で宣言しないといけなくなるのでこのコードではエラーが出ます。これも SomeProtocol を Class-Only Protocol として宣言すれば解決されます:

after
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 の宣言を活用しましょう。

参考:https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html#//apple_ref/doc/uid/TP40014097-CH25-ID281


2018/11/06 追記

The Swift Forums によりますと、今後は : class が非推奨となり、代わりに : AnyObject の方が推奨となるそうです、なぜならば実際欲しい動きは「クラスに限定」よりも、「参照型に限定」ですので:

protocol SomeProtocol: AnyObject {
    // ...
}
118
84
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
118
84

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?