Realmは、SQLiteやCoreDataから置き換わるモバイルデータベースです。

Hipster Swift

Swiftはすばらしい言語で便利な機能がとてもたくさんあります。一方でSwiftにはあまり知られてない細かい機能が数多くあります。それを知っていれば開発にかかる時間や労力を減らすことができます。try! Swiftにて話されたこの講演では、Swiftのあまり見慣れない機能を多数、初心者の方にもわかりやすく解説いたします。講演後は、日々の開発中にこのような構文に出会ったとしても、一目で解読できるようになっていることでしょう!

Transcription below provided by Realm: a replacement for SQLite & Core Data with first-class support for Swift! Check out the Swift docs!

About the Speaker: Hector Matos

テキサス州というすばらしい場所でラマによって育てられ、ゼルダの伝説をしたり、Game of Thronesを見ながらテレビの前で叫んだりするカウチポテトとして成長しました。家では座ってのんびり過ごさず、KrakenDev.ioでブログを書いている一方で、Capital Oneのオフィスではデスクに座ってiOSやAndroidのモバイルアプリの開発をしています。


イントロダクション (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関数determinePowerLevelPowerLevelのメンバ関数であり、そして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はあまり日々の仕事で使う局面に会ったことはありません。