LoginSignup
4
3

More than 3 years have passed since last update.

【Swift Closure】読み方・書き方をキホンから学ぶ

Last updated at Posted at 2018-12-31

この投稿について

この投稿は投稿者の覚書を主な目的としているので、読みづらい部分があるかもしれません...。

誰のため?

  • Swift のクロージャ(Closure) を基本から理解したい人
  • 関数や制御構文について理解できている

実行環境

  • Swift4.2
  • Xcode10 (Playground)
  • macOS Mojave

関数を閉じ込める

あらかじめ定義した関数は、別の関数に引数として渡せました。
関数は、変数オブジェクトや定数オブジェクトにまるごと格納することもできます。
通常の関数定義はこのような記述でした。

func sayHelloIn(loud isLoud: Bool) {
    var message = "hello"
    if isLoud {
        message = message.uppercased() + "!!"
    }
    print(message)
}

sayHelloIn(loud: true)

関数をオブジェクトに代入

let sayHello = sayHelloIn
sayHello(false)

代入して使うなら、関数名を定義する必要なかった
使える名前が減ってしまう
名前を考える時間がもったいなかった
関数名を使わずに、オブジェクトに代入する方法がある

通常の関数定義と比較しながら、関数をオブジェクトに格納してみましょう。
定数オブジェクト hello に、関数を格納します。
まずは、{} でコードブロックを作成します。

let hello = {}

コードブロック内に、seyHelloIn() 関数のデータ型を記述しましょう。
引数 isLoudBool 型です。
この関数に返り値はありません。
Swift では、返り値がないことを Void と表現します。
引数がない場合は、単に () の中が空欄になります。

let hello =  { (isLoud: Bool) -> Void }

関数のデータ型を記述できました。
データ型の直後には、in キーワードを記述します。
関数で行いたい処理は in キーワード後に記述します。

let hello = { (isLoud: Bool) -> Void in
    var message = "hello"
    if isLoud { message = massege.uppercased() + "!"
    print(message)
 }

in キーワードの手前がデータ型で、in キーワード以降が処理内容ですね。
これらが、{} コードブロックでまとめられています。
コードブロックごと定数オブジェクトに代入します。

これで、関数をオブジェクトに格納できました。
格納されたオブジェクトは、どんなデータ型になっているでしょうか。
クイックリファレンスで確認しましょう。
Declaration には、(Bool) -> Void という表示されています。
この「カッコで括られた引数の型、矢印、返り値の型」が、関数のデータ型になります。
引数は、カンマで区切って列挙することもできます。
「コードブロック内に、関数のデータ型 in 実装」という構造になっています。
実行してみましょう。

hello(true)
hello(false)

オブジェクトに格納された関数を実行するには、オブジェクト名の後に () をつけます。
引数がある場合は、() に指定できます。

関数に引数がない場合は、コードブロックの後に () をつけて実行結果を直接、オブジェクトに格納することもできます。

let bigHello = { ( ) -> String in
    let message = "Hey!".uppercased()
    return message
}()

定数オブジェクト bigHello をコンソールに出力してみます。
関数の実行結果である "HEY!" が表示されていますね。

print(bigHello)

このように、{} を使って処理をまとめる記述方法を クロージャ(Closure) と言います。

友達リストを絞り込む

引数として関数を渡すケースをもう少し詳しく見てみましょう。
frends は友達のリスト

 let myFriends = ["Anny", "Dan", "Billy", "Adam","Rick", "Anthony","Erick", "Alex"]

filteredNames(friends:condition:) 関数は...
友達リストから、「特定の条件」に該当する名前の配列を返す。
filteredNames(friends:condition:) 関数の第2引数に注目します。
この2つ目の引数が「特定の条件」になっている(データ型は (String)->Bool

/*                                         ↓ 引数になってる「特定の条件」          */
func filterNames(_ friends: [String], _ condition: (String) -> Bool) -> [String] {
    var filteredNames = [String]()
    for name in friends {
        if condition(name) { filteredNames.append(name) }
    }
    return filteredNames
}

第2引数には、(String) -> Bool な関数ならなんでも渡すことができます。
startingWith() 関数という「特定の条件」を調べる関数を渡すことにします。
この関数は、名前の一文字目が "A" と一致するかを調べます。

func startingWithA(_ name: String) -> Bool {
    return name.hasPrefix("A")
}

特定の条件をいちいち関数に定義して実装するのは、面倒臭いですね。
実は、filterNames() 関数の呼び出しで引数を指定するときに直接、条件を記述できます。
この関数を、クロージャで記述してみると...。

let conditionClosure = { (name: String) -> Bool in
    return name.hasPrefix("A")
}

filterNames() 関数の 2 つ目の引数は、(String) ->Bool 型でした。
引数として渡す関数をクロージャにできたら、コードブロック {} ごと引数に記述します。

{} の中に、関数のデータ型を明示します。
引数は String 型のオブジェクト name
返り値として Bool 型を指定します。

{ (name: String) -> Bool }

in キーワードの後に実装を記述します。
実装には、接頭文字が "A" かどうかの結果を返すようにします。

 { (name: String) -> Bool in return name.hasPrefix("A") }

「関数はクロージャにすれば、関数の引数に記述できる」ということがわかりますね。
実行してみましょう。
結果を格納する result オブジェクトを宣言しておきます。
コンソールに result を出力すると、正しく機能していることがわかりますね。

let result = filterNames(myFriends, {(name: String) -> Bool in return name.hasPrefix("A") })
print("名前がAではじまる友達", result)

ショートハンド

直接、条件を記述できるようになったのはいいことですが、今度は関数の呼び出しコードが長ったらしくなった
そんな場合は、クロージャの省略記法(ショートハンド)
省略できる箇所を一つずつ見ていきましょう。
まず、引数のデータ型です。
filgerName(friends:condition) 関数の定義によって、2つめの引数は (String)->Bool 型と推論されるので :String が省略可能です。

/*                                           : String を省略した */
/*                                              ↓              */
let result_1 = filterNames(myFriends, {(name        ) -> Bool in return name.hasPrefix("A") })

また、引数が一つの場合は、それを括る () も省略可能です。

/*                                        (            ) を省略した    */
/*                                        ↓            ↓             */
let result_2 = filterNames(myFriends, { name          -> Bool in return name.hasPrefix("A") })

さらに、retrun キーワード以降が条件式になっているので、返り値が Bool 型であることが決定しています。
filterNames(friends:condition) 関数の定義によって推論されているのではないことに注意しましょう。
-> Bool も省略します。

/*                                                     -> Bool を省略した */
/*                                                         ↓             */
let result_3 = filterNames(myFriends, { name                  in return name.hasPrefix("A") })

in キーワード以降が 1 行だけなら、return も省略できます。

/*                                                                  return を省略した
                                                                    ↓    */
let result_4 = filterNames(myFriends, { name                  in        name.hasPrefix("A") })
let result_1 = filterNames(myFriends, {(name        ) -> Bool in return name.hasPrefix("A") })
let result_2 = filterNames(myFriends, { name          -> Bool in return name.hasPrefix("A") })
let result_3 = filterNames(myFriends, { name                  in return name.hasPrefix("A") })
let result_4 = filterNames(myFriends, { name                  in        name.hasPrefix("A") })

ここまでシンプルに記述できました。
パッと見で、{ name in name.hasPrefix("A") } は、第2引数であると理解できるでしょうか?

let result_5  = filterNames(myFriends, { name in name.hasPrefix("A") })

ずいぶんスッキリみえるようになりましたが、() の括りがちょっとわかりづらいですね。
filterNames(friends:condition) 関数を第1引数 friends で閉じて、第2引数である関数は、コードブロックとして分離できます。

/*                                                         ここにあった ) が...
                             こっちに移動して                           ↑
                                   ↓                                 ↑   */
let result_6 = filterNames(myFriends) { name in name.hasPrefix("A") }
/*                                  ↑
                                    , がなくなった                    */

これなら filterNames(friends:condition:) 関数の引数は、friends オブジェクトとコードブロックの 2 つだけということがハッキリと分かりますね。

もう少しだけ、省略できます。
コードブロックに注目しましょう。
引数として name を受けて、その name の接頭文字をチェックしていますが nameが 2 つ記述されて、冗長な印象があります。
クロージャの引数名は明示しなくても $ 記号で代替できます。
name は、最初の引数なので、$0 となります。
もし、クロージャに 2 つ目の引数があれば、$1 となります。

/* in キーワードも不要 */
let result_7 = filterNames(myFriends) { $0.hasPrefix("B") }    
print("名前がBではじまる友達", result_7)

ここまで省略できれば、「特定の条件」を変更したくなっても対応はカンタンです。
例えば、5 文字以上の名前を集めたいとします。
$0. 以降を変更すればOKです。

let result_greater5 = filterNames(myFriends) { $0.count >= 5 }
print("5文字以上の名前:", result_greater5)

Swift プログラミングでは、多くの場面でクロージャを駆使されています。
Swift には欠かせない機能なので、必ずマスターしましょう。

4
3
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
4
3