LoginSignup
141
127

More than 5 years have passed since last update.

Hipster Swift(Swift通) - Swift の秘密を解き明かす

Last updated at Posted at 2016-03-06

※この記事はtry!Swift で講演したHectorの記事「HIPSTER SWIFT: DEMYSTIFYING THE MYSTERIOUS」を本人の許可を得て翻訳したものです。翻訳のフィードバックいただけるとうれしいです。

static1.squarespace.jpg

Swift のコードを読んでいて「なにこれ!どういう意味!?」と思ったのは私だけじゃないと思う。Swift にはほとんどの人が使っていない多くの機能がある。ある時そういう奇妙なコードに遭遇しググることになった。

この記事はとりわけ役に立つかというとそうではないかもしれないけど、誰かは気に入ってくれると思う。個人的にはこの記事を Swift を読んでいて見かける最先端なコードのリファレンスとして使っている。

この記事は長いですが全部読む必要はありません。章ごとに別の話をしているので興味のある部分だけ読んで貰えればと思う。

@noescape

この小さな歌のフレーズのようなキーワードはSwift のコードの中で蒸気を発しているようだった。このキーワードの意味が分かるまで、このキーワードをみるたび私の目はどんより曇っていました。

最初は自分に「これは Rob Gods や@rob_rix@cocoaphonyの使うツールだと言い聞かせていました(自分には関係ないと)」でも実際のところ @noescape とは何を意味しているんだろう?

意味が分かった時、めちゃくちゃ便利なことに気がつきました。@noescape は下の例のように関数(closure)の引数に割り当てることにできる arrtibute でした。

func prepareTheKrakenArmyForBattle(@noescape preparations: () -> Void) {
    // preparations は この関数の中で使用されなければならない
}

@noescape はコンパイラに「引数で渡した closure は関数の本体の外で使えない」と伝えています。
Swift では closure は 第一級オブジェクトなので closure を変数に格納して、どこでも・いつでも使用することができます。 closure に @noescape をつけることで、コンパイラに「引数で渡した closure はその関数内でのみ生存する」と制限を加える事ができます。

関数の本体は監獄のように動作し、closure はそれに(escapeではなく)囚われている、と考えると分かりやすいです。

これを説明する画像がこちらです。

static1.squarespace-1.jpg

@noescape の素晴らしい点は、@noescape 属性をつけることでコンパイラが実際に指定している最適化の設定よりさらに最適化してくれることです。さらに素晴らしいことに @noescape をつけた closure の中では self をキャプチャする必要がないことです。@noescape 付 closure は関数の本体の中だけでのみ生存するので、closure 内でキャプチャする記法について悩む必要もありません。これは下記のような素晴らしいコードを可能にします。

class Kraken {}
class KrakenGeneral {
    var soldiers = [Kraken]()

    func startTheWar() {
        prepareTheKrakenArmyForBattle(punishments: {
            soldiers.removeFirst() // ここで self を書く必要がありません
        })
    }

    func prepareTheKrakenArmyForBattle(@noescape punishments punishments: () -> Void) {
        if someoneReportedMisbehavior {
            punishments()
        }
    }
}

@autoclosure

@autoclosure も見たことない、また何をやっているのかよく分からない Swift の attribute です。@noescape ととてもよく似ています。 引数の closure に割り当てることができ、@autoclosure をつけると自動的に @noescape も割り当てられます。

@autoclosure は関数の「引数として渡される関数の実行を遅せる」という意味があります。具体的には、引数として渡す関数の呼び出しを closure の呼び出しに変換し、関数本体の中で後で使用する、ということをします。

コードを例に上げながら説明します。

krakenGeneral.punishSoldier(soldiers.removeFirst())

func punishSoldier(soldier: Kraken) {
    throwSoldierInTheBrig(soldier)
    sendSoldierHomeToMommy(soldier)
}

上記の例では krakenGeneral(General: 将軍) が punishSoldier() を呼び出し、soldiers.removeFirst() が呼びだされます。soldiers.removeFirst() が即座に実行され、soldiers の配列から先頭のオブジェクトが取り除かれ、punishSoldier の引数 soldier(soldier: 兵士) として渡されます。もし後で soldier を配列から取り除きたかったとしたら?もしあなたが慈悲深い将軍(兵士をクビにするのをためらう)だとしたら?

@autoclosure をつけましょう。

@autoclosure は渡された引数を closure として扱います。上の例では soldiers の配列から取り除くのを先送りにすることができます。@autoclosure をつけた場合の例がこちらです。

// removeFirst() はここでは呼び出されません。
punishSoldier(soldiers.removeFirst())

//@autoclosure は closure に適応可能です。 ここでは removeFirst() と同じ関数型(引数と戻り値)を定義します。
func punishSoldier(@autoclosure soldierPunishment: () -> Kraken) {
    // この例ではここで 兵士 を配列から取り除くかどうかを判定しています
    if someoneReportedMisbehavior {
        // ここで実際に兵士を配列から取り除く(removeFirst()を実行しています)
        sendSoldierHomeToMommy(soldierPunishment())
    } else {
        apologizeToSoldierAndFeedHimAHuman()
    }
}

これはコードとしては下記と意味は同じになります。

punishSoldier {
    soldiers.removeFirst()
}

func punishSoldier(@noescape soldierPunishment: () -> Kraken) {
    if someoneReportedMisbehavior {
        //This will remove the soldier and send that soldier home.
        sendSoldierHomeToMommy(soldierPunishment())
    } else {
        apologizeToSoldierAndFeedHimAHuman()
    }
}

この大きな違いは関数を呼び出す側の文法にあります。@autoclosure は可読性を落とすのであまり頻繁には使われません。ここで挙げた @autoclosure を使う例では、関数を呼び出す側は punishSoldier() に soldiers.removeFirst() を渡したとしても、実際に実行されたか(兵士が取り除かれたか)分かりません。@autoclosure は渡した関数を closure として扱うためです。実装を隠すことはほとんどのケースで良いことではありません。個人的には、@autoclosure を使うことでメリットがない限り使うことをおすすめしません。

最初に、@autoclosure は自動的に closure に変換し、@noescape と同じ扱いにするといいました。
もしあなたがどんな理由であれ closure を escape できるように(変数に確認できるように)したい場合は、@autoclosure に追加の attribute をつけることができます。

var storageForAClosure: (() -> Bool)?

func executeFunctionWithEscapableClosure(@autoclosure(escaping) houdiniClosure: () -> Bool) {
    storageForAClosure = houdiniClosure
}

executeFunctionWithEscapableClosure("kraken".hasPrefix("k"))
storageForAClosure?() //hasPrefix doesn't execute until this line of code

@autoclosure に escaping キーワードを加える事でコンパイラに「この closure は escape できる」と伝えることができます。

Inline Lazy Vars(Variables)

これはみなさんにとって新しいことではないかもしれませんが、Hipster Swift(Swift通) に含める価値があると感じています。lazy 変数(variable) はご存知だと思います。(そうでない方向けに)lazy 変数はオブジェクトのライフタイムで一度だけ初期化されるインスタンス変数で、アクセスする度に一度だけ初期化された変数が返されます。これは計算量の多い変数や、オブジェクトが作成されるまで値が決まらない変数を定義したい時に役に立ちます。

class KrakenFoodGenerator {
    lazy var lazyHuman: Human = {
        //We want a lazy human. Kraken's get annoyed 
        //when they have to chase their food.
        return self.generateWithOptions(.Lazy)
    }()
}

Swift では遅延初期化(lazy initialization)を定義するのはとても簡単ですが、コード量が増えてしまいイライラすることがあります。ですが inline lazy vars を用いて上記のコードを下記のように1行にまとめることができます。

class KrakenFoodGenerator {
    lazy var lazyHuman: Human = self.generateWithOptions(.Lazy)
}

でも待って、、これはちょっと気持ち悪い・・変数定義に self を使ってるよね!この self って何?循環参照持たせてない?結局のところ Lazy vars はただの closure だ。lazy キーワードを使ってどうしてここで self を使うことができるのだろう?

正確になぜ可能かを説明するのはできなかったが @autoclosure の章を書いた後とてもいい推測ができた。lazy 変数の = の右辺は自動的に closure として扱われているのだろう。self を使ってキャプチャをしても escape されるので循環参照が起こらないのだ。

これを試してみたコードがこちら。

static1.squarespace.png

deinit が呼ばれているのが分かる。クールだよね?これでコードを短くすることができそうだ。

()() a.k.a カリー化

もう一つ奇妙なものがあります。初めて見た時面食らいました。Objective-C に慣れていたので複数の括弧() で囲むというの少し難しかった。カリー化はこの記事に含める価値があります。個人的にはカリー化は使っていなかったのでobjc.io で書かれたこの素晴らしい記事この素晴らしい記事を読むまでは使おうともは思いませんでした。

※カリー化についての詳細は Qiita のこちらの記事も参照:食べられないほうのカリー化入門

カリー化は「一つの関数から次の関数へパラメータを渡す」関数を表現する良い方法です。これまでに連続した括弧()の書き方を見たことがなかったら、次のような書き方をするかもしれません。

func fourChainedFunctions(a: Int) -> (Int -> (Int -> (Int -> Int))) {
    return { b in
        return { c in
            return { d in
                return a + b + c + d
            }
        }
    }
}

fourChainedFunctions(1)(2)(3)(4)

これはすごく分かりにくい書き方で、関数を組み合わせて使いたいかもしれないですが、パラメータの名前がなく、return だらけのこの関数から逃げ出してくなってしまいます。関数をカリー化したものはもっと読みやすく書くことができます。

func fourChainedFunctions(a: Int)(b: Int)(c: Int)(d: Int) -> Int {
    return a + b + c + d
}

fourChainedFunctions(1)(b: 2)(c: 3)(d: 4)

私の意見としては、こちらの方がより整理されていると思います。見事なコードは健全なコードだと思います。

注意点: この記法は将来的に変更されます。個人的にはこれは大した問題ではないと思います。

可変長引数(奇妙な "..." のこと)

Swift には他にも知らない機能がたくさんあります。可変長引数もその一つです。このようなコードを見たことがあるかもしれません。

func printEverythingWithAnEmojiInBetween(objectsToPrint: Any...)

この3つの ドット"..." が引数が可変長であることを表しています。この"..."を書くと引数は列挙できるようになります。可変長引数を使うにはこのように使います。

func printEverythingWithAKrakenEmojiInBetween(objectsToPrint: Any...) {
    for object in objectsToPrint {
        print("\(object)🐙")
    }
}

//This will print "Hey🐙Look🐙At🐙Me🐙!🐙"
printEverythingWithAKrakenEmojiInBetween("Hey", "Look", "At", "Me", "!")

呼び出す時はカンマ区切りで引数を渡します。実のところ Swift の print 関数も可変長引数を取ります。

"dynamic" キーワード

dynamic キーワードは関数にも変数にも適用できる declaration-modifier(関数や変数を修飾するもの)です。参考:Swift Declarations

NewRelicや似たようなアプリ解析用ライブラリを使ったことがあると見たことがあるかもしれません。dynamic キーワードはランタイムに dynamic dispatch を使うように指示しています。これは暗黙的に @objc を関数または変数に付与しています。

重要な点として、dynamic キーワードを使うものは全て message を dispatch するのに Swift ではなく、Objective-C のランタイムを使用します。

私達は static dispatch が大好きです。ですが、アプリ解析についてはそうではありません。static dispatch の場合、解析用のコードを動的に生成して挿入するのが難しいのです。dynamic キーワードはそういった状況には便利ですが、 static dispatch とくらべて最適化を犠牲にします。dynamic キーワードは下記のように使用します。

class Kraken {
    dynamic var imADynamicallyDispatchedString: String

    dynamic func imADynamicallyDispatchedFunction() {
        //Hooray for dynamic dispatch!
    }
}

dynamic dispatch を使用することで得られる事として Objective-C ランタイムで使用できる魔法のようなもの(CoreData のような ランタイムの KVC/KVO に依存している機能)と相互運用性があります。 dynamic キーワードを使用することで Objective-C ランタイムの恩恵を受けることができます。もちろん用途次第ではありますが😎

基本的に特定の関数やプロパティを誰かがランタイムで変更する・差し替えることが分かっている場合、dynamic キーワードを使うのは良いアイデアです。もしそうでない場合(ランタイムで何か変更を加えられると分かっていない場合)、コンパイラの最適化処理によってクラッシュを引き起こすかもしれません。

Special Literals

FILE, LINE, COLUMN, & FUNCTION これらはとてもクールで、時間の節約になります。これらは Special literal expression と呼ばれます。これらは数値や文字列を表します。それぞれは Literal の意味は下記の表のようになっています。

Literal 戻り値の型 値の意味
__FILE__ String このliteralが配置されているファイル名
__FUNCTION__ String このliteralが配置されている関数名
__LINE__ Int このliteralが配置されている行数
__COLMUN__ Int このliteralが配置されている文字の位置

個人的に気に入っているのは__FUNCTION__ です。これはデバッグ目的にとても役に立ちます。例えば、何回自分に問いかけたことか。「一体どこからこの関数が呼ばれてるんだ?たぶんバグだ・・」

こんな時ももう心配しなくていい!

関数のデフォルト引数に __FUNCTION__を設定して関数名を取得すれば、呼び出し元を突き止めることができます。

func wreakHavocOnHumanity(krakenArmy: [Kraken], callingFunction: String = __FUNCTION__) {
    //ここにバグがある!この関数が呼ばれすぎている!
    //原因を突き止めるために呼び出し元を関数名を print 文で書き出す
    print(callingFunction, "called \(__FUNCTION__)")
}

func startTheWar() {
    wreakHavocOnHumanity(krakenArmy)
}

func startEatingDinner(platter: Human) {
    wreakHavocOnHumanity(krakenArmy)
}

startTheWar() //prints "startTheWar called wreakHavocOnHumanity"
startEatingDinner(human) //prints "startEatingDinner called wreakHavocOnHumanity"

ラベル付きループ(多分知らないと思う)

Swift は break と continue キーワードを取りますがさらにループにラベルを付けることができます。時々下記のようなループのロジックを見かけることがあるかもしれません。

for section in sections {
    var foundTheMagicalRow = false
    for row in rows {
        if row.isMagical {
            foundTheMagicalRow = true
            break
        }
    }
    if foundTheMagicalRow {
        //We found the magical row! 
        //No need to continue looping through our sections now.
        break
    }
}

ループに対してラベルをつけることで、コードはさらに短くすることができます。ラベルをつけることで break/continue するループを明確にすることができます。ループにラベルをつけたコードの例がこちらです。

sectionLoop: for section in sections {
    rowLoop: for row in rows {
        if row.isMagical {
            break sectionLoop
        }
    }
}

まとめ

ほとんどの項目が知ることができてよかった、という程度のものかもしれませんがちょっとしたコードの最適化のよさを知ることができたと思います。さらにこれらがクールなものになる前から知ってたと言うことができます。
これらを日々のコーディングの中で使用しないかもしれませんが、知らなかったことを学ぶことができるというのはいつでも良いことです。私は間違いなく楽しみながらこの記事を書きました、まぜなら私の調査がSwiftを明らかにしていったからです。もしあなたがSwift の Hipster(すすんでるもの・イケてること)を見つけたら、それが本当にクールになる前に誰かに知らせてください。コーディングオタクを増やそう!

static1.squarespace-1.png

#記事で書いたコードをまとめてgistに上げています。
https://gist.github.com/takecian/32bbb3022b93a4ddfb0a

141
127
1

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
141
127