VASILYのiOSエンジニアにこらすです。最近、Swift Evolutionに私の2つ目の提案がマージされました。
今回は、Swiftで型にExtensionを作る特殊な方法について説明します。 今回紹介する方法を使ってExtensionを作ると、名前空間が切り分けられ、コードの読み書きがしやすくなります。
ブログを書くに当たって、この Extension 実装方法を研究しましたが、この手法の正確な名前がわからなかったため、この記事では「Targeted Extensions」と呼ぶことにします。
Extensionについて
通常、 Extensionを書くとき、String
なら下記のようになります。
extension String { var count: Int { return characters.count } } "hello".count // 5
Extensionを作ることで .characters.count
を .count
だけで書くことができます。しかし、便利ではありますが、コードを読む時 .count
プロパティは String
に元からあるものなのか、Extensionで定義されたオリジナルのプロパティなのかがわかりません。
さらに、将来のSwiftのアップデートで同じ名前のプロパティが追加されてExtensionのプロパティと衝突してしまう可能性もあります。
これらの問題を回避するためには衝突を避けるような別のExtension実装方法が必要になります。
衝突を避けるために"some string".my_length
のように『Extensionのメソッドにプレフィックスを付ける』という命名規則を採用することもできますが、言語の仕組みでこの命名規則を強制するような仕組みはありません。
"some string".ex_length
ではなく "some string".ex.length
と書けるなら、よりSwiftyな感じがしますが、どのようにすればよいのでしょうか?
それが今回のこの記事のテーマです!
最近では、 RxSwift, Kingfisher, ReactiveSwift などのOSSライブラリがこのExtensionの書き方を採用しています。
例を見よう: RxSwiftの場合
リアクティブプログラミングライブラリの RxSwift では rx
というキーワードを使って、オリジナルのクラスに属するものと、 RxSwiftに属するものを明確に区別できるようになります。
また、Xcodeでのコード補完もキレイ動作します。
実際のコード例です。
myButton.rx .controlEvent(.touchUpInside) .subscribe(onNext: { _ in // ボタンをタップした時、ログに流れる print("Hello There!") })
String
の場合は?
先ほどのString
のExtensionを下記のように書けると良いです。
"hello".ex.count // 5
ex
があることによって、名前空間が分けられます。そのため、他のサードパーティーライブラリがString
にcount
というプロパティを作ったとしてもプロパティ名が衝突することがありません。
さて、Targeted Extensionsはどのように動作しているのでしょうか?
ここから、このExtensionの動作について説明します。 5つのステップに分かれているので1つずつ説明しますが、それほど大きくないので、一旦全てのコードを貼っておきます。 下記のコードをXcodeのPlaygroundにコピー&ペーストすればそのまま動作します。
public protocol ExampleCompatible { associatedtype CompatibleType var ex: CompatibleType { get } } public final class Example<Base> { let base: Base public init(_ base: Base) { self.base = base } } public extension ExampleCompatible { public var ex: Example<Self> { return Example(self) } } extension String: ExampleCompatible { } extension Example where Base == String { public var count: Int { return base.characters.count } } "hello".ex.count // 5
ステップ1: ex
の定義
"hello".ex.count
を見ると、 String
が ex
というプロパティを持っているはず。
どこで定義されているか確認しましょう。
public protocol ExampleCompatible { associatedtype CompatibleType var ex: CompatibleType { get } }
上記のプロトコルを採用したらex
というプロパティを持ってないといけません。
次はExampleCompatible
を採用しましょう!
ステップ2: ExampleCompatible
extension String: ExampleCompatible { }
String
が ExampleCompatible
を採用していることがわかります。
しかし、ex
の実装がありません。
ステップ3: ex
の実装
public extension ExampleCompatible { public var ex: Example<Self> { return Example(self) } }
まだ説明していませんが、Example<Self>
型の変数を返しています。
Self
は ExampleCompatible
を採用している String
になり、 self
は "hello"
になるはずです。
色々な型で実現するために、ジェネリクスを使ってプロトコルを直接拡張します。そうすることでString
だけではなくNSString
やInt
でも同じ名前空間 を (ex
) 使って様々な型を拡張することができます。
実際に様々な型に対応した場合、下記のようなコードになります。
"Yeah".ex.count 1024.ex.foo ["a", "bc", "def"].ex.hoge
このコードを見ると、foo
やhoge
がcount
と同じ Example
の名前空間に宣言されたものと分かります。
ステップ4: Example
public final class Example<Base> { let base: Base public init(_ base: Base) { self.base = base } }
Example<Base>
は Base
型のプロパティを1つだけ持ったクラスです。
ここで Base
は、ステップ3で説明したように String
になります。
ステップ5: count
の実装
extension Example where Base == String { public var count: Int { return base.characters.count } }
Example<Base>
の Base
が String
の時だけ、動作するExtensionが宣言されています。
base
は String
なので、実際には "hello"
が入っているはずです。
ここでやっと "hello"
の文字数を返すことができます。
String 以外の実例
Targeted Extensions でどんな型でも拡張することができます。
例えばInt
に偶数か奇数というプロパティを追加するには、String
と同じように2つのステップで書けます。
まずInt
をExampleCompatible
に拡張します。
extension Int: ExampleCompatible { }
こうするとInt
はex
というプロパティを持つので、String
と同じようにExtensionを書けます。
extension Example where Base == Int { var isEven: Bool { return base % 2 == 0 } } 1.ex.isEven // false 2.ex.isEven // true
パフォーマンスについて
ex
がアクセスされるたびに、新しい Example
インスタンスが生成されるため、若干のパフォーマンスの差があります。
// 通常のExtension extension String { var length: Int { return characters.count } } // 名前空間を使ったExtension extension Example where Base == String { var length: Int { return base.characters.count } }
第5世代 iPod touch で検証した結果 (5回テストした平均値)
実行回数 | 通常のExtension (sec.) | Targeted Extensions (sec.) |
---|---|---|
100 | 0.02140 | 0.02239 |
1000 | 0.02475 | 0.02875 |
1000000 | 2.32037 | 4.12863 |
このパフォーマンステストの結果からわかるのは、Exampleインスタンスを 8848回生成して、やっと1フレーム(60FPSのとき16ms)の遅延が発生することになり、このパフォーマンスの遅延は無視できると言えます。
0.016 / ((4.12863 - 2.32037) / 1000000) ≒ 8848 回
こんな場面で導入しました
最近、私のライブラリでTargeted Extensionsを導入しました。良い実例になると思いますので、参考にしてみてください。
NSAttributedString
を型安全に書けるようにするライブラリです。(是非スターを付けてください)
let attributedText: NSAttributedString = "Steve".at.attributed { return $0.font(UIFont(name: "Chalkduster", size: 24.0)!) .foreground(color: .red) .underlineStyle(.styleSingle) }
まとめ
この Extension の書き方によって、名前空間ができるため、メソッド名の衝突を避けることができます。
また、コードを読むときも、名前空間があることで、拡張されたメソッドなのか、元からあるメソッドなのかがわかりやすくなります。 Targeted Extensionsは少し複雑ですが、上記のようなメリットがあります。 VASILYでもSwiftの言語仕様に追加されない限りはTargeted Extensionsを採用していく予定です。
P.S なお、今回紹介した Targeted Extensions について、正しい名前をご存知の方は教えていただけると嬉しいです。
VASILYではモダンなSwiftコードを書きたいエンジニアを募集しています。 少しでも興味がある方は以下のリンク先をご覧ください。
https://www.wantedly.com/projects/88978www.wantedly.com
- にこらす