Swiftはすばらしい言語で便利な機能がとてもたくさんあります。一方でSwiftにはあまり知られてない細かい機能が数多くあります。それを知っていれば開発にかかる時間や労力を減らすことができます。try! Swiftにて話されたこの講演では、Swiftのあまり見慣れない機能を多数、初心者の方にもわかりやすく解説いたします。講演後は、日々の開発中にこのような構文に出会ったとしても、一目で解読できるようになっていることでしょう!
イントロダクション (0:00)
この記事ではHipster Swiftと題して、Swiftの見慣れない機能について解説します。下記は今回お話する機能の概要です。
@noscape
@autoclosure
- Lazy変数
- ラベル付きループ
- 型名の省略
@noescape
(1:56)
NSNotificationCenter.defaultCenter().addObserverForName("PokemonAttackListener",
object: nil,
queue: queue,
usingBlock: { notification in
//Determine if the attack is super effective!
})
@noescape
は私にとっては見るたびに非常に奇妙なものに見えていました。説明する前に覚えておいてほしいことは、まずSwiftではクロージャは第1級市民ということです。ですので、変数に格納することができます。普通のクラスのインスタンスと同じようにメモリに格納されます。このため、ほとんどの人は関数に渡したクロージャはどこかに格納されて、また他のところで使われるということを忘れがちで、後で使おうとしたときに使えないということが起こります。
NSNotificationCenterでブロックベースのAPIを例として取り上げます。usingBlock
引数に渡したこのクロージャは、通知の名前をキーにしてディクショナリに格納され、それの配列として保持されます。つまり、このブロックを解放する責任はNSNotificationCenterではなく私たちにあります。addObserver
メソッドが呼ばれると、クロージャは別の配列に格納されて保持されるので、addObserverForName
メソッドの制御からエスケープしたということになります。要するに、スコープを外れてもそれだけではクロージャは呼び出されません。
一方、渡したクロージャがすぐに呼び出されることを保証するために@noescape
を使います。
func trainForTheFightAgainstFrieza(@noescape preparations: () -> ()) {
// preparationsというクロージャはこの関数内で「必ず」呼ばなければなりません。
preparations()
}
@noescape
はSwiftの属性の1つで、関数のクロージャのパラメータに指定することができます。trainForTheFightAgainstFrieza
メソッドの呼び出し元に、渡されたクロージャはこの関数を抜ける前に呼ばれることを示しています。
逃げようとしても逃げられない (4:42)
func trainForTheFightAgainstFrieza(@noescape preparations: () -> ()) {
escapedClosure = preparations
tryToEscapeClosure(preparations)
tryToEscapeClosure() {
preparations()
}
}
func tryToEscapeClosure(closure: () -> ()) {
escapedClosure = closure
}
preparations
クロージャをtrainForTheFightAgainstFrieza
関数の制御フローから脱出させようと最善を尽くしましたが、コンパイラをそれを許しませんでした。non-escapingなクロージャをescapeさせようとするとコンパイルエラーが起こります。もう一つの@noescape
に関するすばらしいことは、コンパイラが実行効率を最適化する余地を与えるということです。さらに重要なことは non-escapingなクロージャの中ではself
をキャプチャする必要がない、ということです。
@autoclosure
(5:48)
func assert(condition: @autoclosure () -> Bool, message: String) {
if !condition() {
fatalError(message)
}
}
let zFighters = ["Goku", "Vegeta", "Gohan", "Trunks"]
assert(zFighters.contains("Krillin"), message: "Looks like Krillin isn't a Z-Fighter. That sucks.")
上記の関数にはとてもシンプルなテストが書かれています。生まれつきのZ戦士(サイヤ人)の配列にクリリンが含まれるのかどうかをテストしています。アサーション関数にクロージャを渡さなければならないところに、単にBool値を渡しています。これは少しおかしく感じます。アサーション関数はパラメータとしてクロージャを取るように書かれているからです。しかし、クロージャを渡していないにもかかわらず、実際にはこの関数の呼び出しは成功します。@autoclosure
を指定すると、自動的に引数をクロージャを使ってラップしてくれるのです。そして 生成されたクロージャはnon-escapingなクロージャです。
func assert(condition: @noescape () -> Bool, message: String) {
if !condition() {
fatalError(message)
}
}
let zFighters = ["Goku", "Vegeta", "Gohan", "Trunks"]
assert({
return zFighters.contains("Krillin")
}, message: "Looks like Krillin isn't a Z-Fighter. That sucks." )
@noescape
が戻ってきました。オートクロージャは自動的に@noescape
属性を付加するためです。つまり、上記のコードは先ほどのコードとまったく同一の意味を持ちます。@autoclosure
をどう使えばいいのかわかったと思います。
インラインLazy変数 (8:08)
lazy var kamehameha: KiAttack = {
// かめはめ波の気をためるのはとても時間がかかります。
// この変数をlazyに指定しているのは時間がかかるためです。
// 毎回毎回、気がたまるのを待ってはいられません。
// もっと早く世界を救わなければなりません。
return self.chargeAndReleaseAKamehameha()
}
lazyな変数の欠点は、変数に見えないことです。大きな関数に見えてしまいます。しかし、もっと良い方法があります。それがインラインLazy変数というものです。このように書きます。
lazy var kamehameha: KiAttack = self.chargeAndReleaseAKamehameha()
私はLazy変数ではいつもunowned selfを使っていました。たいていはそれでうまくいきます。
インラインLazy変数は@autoclosure
に似ています。=
の右辺を自動的にクロージャにラップしてくれるように見えます。しかし、注意しなければならないことはselfを強参照してキャプチャするとこうことです。なので循環参照について気にされるかもしれません。 しかし循環参照が作られることはないのです。
循環参照が起こらないということを示すために、1つの例をあげます。下記を見てください。
class Goku: ZFighter {
lazy var kamehameha: KiAttack = self.chargeAndReleaseAKamehameha()
func chargeAndReleaseAKamehameha() -> KiAttack {
return KiAttack()
}
deinit {
print("deinit is called")
}
}
var goku: Goku? = Goku()
goku?.kamehameha.attackEnemy()
goku = nil
上記のコードではgoku
変数にnil
を代入した後にdeinit
が呼ばれます。つまり循環参照はないということです。つまり、ほとんどの場合で安全に使用することができます。ただやはり循環参照を作らないようには気をつけて使ってください。ほとんどの場合は問題ありません。
ラベル付きループ (11:20)
func winnerOfBattleBetween(enemy: Enemy, andHero hero: Hero) -> Fighter? {
var winner: Fighter?
for enemyAttack in enemy.listOfAttacks {
var heroWon = false
for heroAttack in hero.listOfAttacks {
if heroAttack.power > enemyAttack.power && completedEpisodes.count > 5 {
heroWon = true
winner = hero
break
}
}
if heroWon {
break
}
}
return winner
}
print(winnerOfBattleBetween(majinBuu, andHero: Goku)) //prints Goku
上記はとても長い関数ですね。この関数は魔人ブウと孫悟空の戦いでどちらが勝ったのかを調べています。2重ループを抜けるためだけのBoolフラグを判定している読みにくいロジックがあることに気がつきましたか?
Swiftのラベル付きループを使うと、このようなコードはもっと読みやすくすることができます。
func winnerOfBattleBetween(enemy: Enemy, andHero hero: Hero) -> Fighter? {
var winner: Fighter?
enemyFightLoop: for enemyAttack in enemy.listOfAttacks {
heroFightLoop: for heroAttack in hero.listOfAttacks {
if heroAttack.power > enemyAttack.power && completedEpisodes.count > 5 {
winner = hero
break enemyFightLoop
}
}
}
return winner
}
print(winnerOfBattleBetween(majinBuu, andHero: Goku)) //prints Goku
もう余計なBool値のフラグはなくなりました。2つの各ループに名前をつけました。外側のループを見るとenemyFightLoop
ラベルの後にコロンとスペースを書き、普通のループ構文であるfor
が続きます。内側のループの名前は、heroFightLoop
としました。ここではbreak
文でラベルを使いましたが、break
文だけでしか使えないわけではありません。continue
文でも同じように使うことができます。
型名の省略 (14:16)
func changeSaiyanHairColorTo(color: UIColor) {
saiyan.hairColor = color
}
ドラゴンボールZでは髪の色は非常に重要です。サイヤ人が超サイヤ人になると、髪の色は金色になります。通常時は黒です。トランクスは地球人とのハーフなので唯一その法則に当てはまらず、通常時は紫色の髪の色をしています。つまり3つの髪の色が登場します。
上記の関数を使うコードはおそらくこんな感じになるでしょう。
if saiyan.isSuperSaiyan {
changeSaiyanHairColorTo(UIColor.yellowColor())
} else {
if saiyan.name == "Trunks" {
changeSaiyanHairColorTo(UIColor.purpleColor())
} else {
changeSaiyanHairColorTo(UIColor.blackColor())
}
}
もし超サイヤ人だったら、髪の色を金色にします。それ以外の場合で、トランクスでもなかった場合は、髪の色を黒にします。とても良いコードです。ちゃんと動きますし、Swiftらしいですね。
私は、少しだけ異議を唱えたいと思います。このコードはまだSwiftらしいとは言えません。Swiftらしくするには 型名の省略 を使います。型名を省略すると、Static変数や関数において、みなさんが大好きなEnumのような簡単な省略記法を使うことができます。
if saiyan.isSuperSaiyan {
changeSaiyanHairColorTo(.yellowColor())
} else {
if saiyan.name == "Trunks" {
changeSaiyanHairColorTo(.purpleColor())
} else {
changeSaiyanHairColorTo(.blackColor())
}
}
これを使うことができるかどうかについて非常に重要なポイントがあります。Static変数、または関数はクラスか値型に定義されている必要があり、自分自身のインスタンスを返さなければなりません。では型名の省略が可能な関数をどうやって作ればいいのか見ていきましょう。
struct PowerLevel {
let value: Int
static func determinePowerLevel(_ fighter: ZFighter) -> PowerLevel
}
func powerLevelIsOver9000(powerLevel: PowerLevel) -> Bool {
return powerLevel.value > 9000
}
// 型名を省略できる!
if powerLevelIsOver9000(.determinePowerLevel(goku)) {
print("It's over 9000!")
}
determinePowerLevel
というStatic関数はPowerLevel
に定義されています。そして、この関数はPowerLevel
のインスタンスを返します。さらにpowerLevelIsOver
というヘルパー関数を定義します。これは、PowerLevel
のインスタンスを引数として受け取ります。つまり、Static関数determinePowerLevel
はPowerLevel
のメンバ関数であり、そしてPowerLevel
インスタンスを返すので、上記の条件に当てはまるので、型名の省略を使うことができます。PowerLevel
を書く必要はまったくありません。
通常なら、一番下のif
分のところは、PowerLevel.determinePowerLevel
とかく必要があります。しかし、この関数は先ほどの条件、Static関数は対象のクラスのメンバであり、自分自身のインスタンスを返すというルールをすべて満たしているため、Enumのように利用することができます。
というのも、Enumの仕組みは実際にはそのようになっているからです。EnumはStatic変数として、それぞれの値を保持しています。そして、それは自分自身のインスタンスとして返されます。これが、ドットを用いたシンタックスシュガーがEnumで使える理由です。Static関数で使用した時と何も違いはないということなのです。
Q&A
Q: @autoclosure
とインラインlazy変数で何か違いはありますか?
Hector: はい、違いがあります。インラインlazy変数はオートクロージャのように振る舞いますが、オートクロージャではありません。インラインlazy変数はselfをキャプチャして強参照します。ということでその質問に対する答えは、違いがある、ということになります。
Q: @autoclosure
を使っている場合でも、何をしているのか明確にするために、オートクロージャに任せずちゃんとしたクロージャを自分で書く方が適切な場合というのもありますか?
Hector: まったくその通りです。
Q: @autoclosure
を使わない方が良いとき、というのはありますか?
Hector: 状況によります。それを使うことで読みにくくなることもあります。事実として、アップルはあまりコード上のいろいろなところで自由に使わないようにと言っています。それを使うことでコードが読みやすくなるというときにだけそれを使うようにしてください。私は@noescape
は使える時はいつでも使いますが、@autoclosure
はあまり日々の仕事で使う局面に会ったことはありません。
About the content
2017年3月のtry! Swift Tokyoの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。