Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
1
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

忘備録-Swiftのクロージャ

趣味でIOSアプリ開発をかじっていた自分が、改めてSwiftを勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。いつか書き直します。

参考文献

この記事は以下の書籍の情報を参考にして執筆しました。

クロージャ概要

簡単なクロージャ の例

let c1 = {
  print("Hello")
  print("hoge")
}
let c2 = {
  (a:Int, b:Int) -> Double in
  return Double(a) * Double (b)
}
print("c1-------")
c1()
print("c2-------")
print(c2(10,2))

出力

c1-------
Hello
hoge
c2-------
20.0

クロージャと関数の型

クロージャーは関数と同じように実行でき、同等のものとして扱うことができる。
クロージャーと関数の違いは後で書く。

let c1 = {
  (a:Int, b:Int) -> Double in
  return Double(a) * Double (b)
}

var c2: (Int, Int) -> Double = c1
print(c2(10,8))    // 80.0
print(c1)    // (Function)
func f1(a: Int, b:Int) -> Double{ return Double(a) * Double (b) }
print(f1)    // (Function)
c2 = f1
print( f1(a:2, b:6) )    // 12.0
print( c2(a:2, b:6) )    // erroe
print( c2(2, 6) )    // 12.0

オーバーロードされた関数の区別

使うときに引数ラベルを指定する
関数の引数と戻り値がわかる方法で記述する。

let c1 = {
  (a:Int, b:Int) -> Double in
  return Double(a) * Double (b)
}

var c2: (Int, Int) -> Double = c1

func f1(a: Int, b:Int) -> Double{ return Double(a) * Double (b) }
func f1(c: Int, d:Int) -> Double{ return Double(c) / Double (d) }    // 追加
func f1(f: Int, g:Int) -> String{ return "\(f) + \(g)" }    // 追加

c2 = f1(c:d:)    // 変更 引数ラベルを指定
print( c2(2, 6) )    // 0.3333333333333333

//以下追加
var c3: (Int, Int) -> String
c3 = f1    // f1の中でj上記の引数と型を持つのは一つしかないので引数ラベル不要
print(c3(1,3))    // 1 + 3

クロージャーの複雑な宣言

// オプショナル型を返すクロージャー
let c1: (Int, Int) -> Double?

// クロージャー自体がオプショナル型
let c2: ((Int, Int) -> Double)?


// クロージャーの配列
let c3 = [(Int, Int) -> Double]()
// クロージャをtaypaliasで宣言してコンパクトにする
// クロージャの処理用途を記載して意味のある名前にすると可読性が上がる
typealias MyClosure = (Int, Int) -> Double
let c5 = [MyClosure]()

メモ

オプショナルのクロージャーってどういう状態?

let c1 = {
  (a:Int, b:Int) -> Double in
  return Double(a) * Double (b)
}

var c2: (Int, Int) -> Double
//print(c2) //error 初期化を促される
c2 = c1
print(c2)    // (Function)

var c3: ((Int, Int) -> Double)?
print(c3)    // nil
c3 = c1
print(c3)    // Optional((Function))

変数のキャプチャ

クロージャが関数と違うのは使い方。
クロージャは常に同じ機能のインスタンスが作られるのではなく、プログラムの外側にある変数の値を取り込んでインスタンスの一部とし、インスタンスが呼び出されるときにはいつでも値を取り出して使える。これをキャプチャという。

新しくファイルを作って
クロージャを返す関数を作成する。

var globalCount = 0

func maker(_ a: Int, _ b: Int) -> (() -> Int){
  var localvar = a
  return { () -> Int in
    globalCount += 1    // globalCount+1するだけ
    localvar += b
    return localvar
  }
}

作った関数を使って2つのクロージャの挙動をみる。
クロージャm1とm2はそれぞれで値が増えていくので、それぞれのインスタンスで処理の外にある変数の実体を保持していることがわかる。

var m1 = maker(10, 2)
print(m1(),globalCount)    // 12 1
print(m1(),globalCount)    // 14 2
globalCount = 100
print(m1(),globalCount)    // 16 101

var m2 = maker(100,50)
print(m2(),globalCount)    // 150 102
print(m1(),globalCount)    // 18 103
print(m2(),globalCount)    // 200 104

変数キャプチャの共有

同一の変数をキャプチャした場合変数のキャプチャが共有される

var m1:(() -> ())! = nil
var m2:(() -> ())! = nil

func makerW(_ a: Int){
  var localvar = a
  m1 = { localvar += 1; print("m1: \(localvar)")}
  m2 = { localvar += 5; print("m2: \(localvar)")}
  m1()    // m1: 11
  m2()    // m2: 16
}

makerW(10)
m1()    // m1: 17
m2()    // m2: 22
m2()    // m2: 27
m1()    // m1: 28

参照型変数のキャプチャ

クロージャがクラスのインスタンスをキャプチャすると強い参照でインスタンスを保持する。

class MyInt {
  var value = 0
  init(_ v: Int) { value = v }
  deinit {print("\(value) : deinit")}
}

func makerZ(_ a:MyInt, _ s: String) -> () -> () {
  let localvar = a
  return {
    localvar.value += 1
    print("\(s) : \(localvar.value)")
  }
}

var obj = MyInt(10)
var m1:(() -> ())! = makerZ(obj, "m1")
m1()    // m1 : 11
var m2:(() -> ()) = makerZ(obj, "m2")
m2()    // m2 : 12

m1()    // m1 : 13
m1 = nil    // この時点でインスタンスは解放されない
m2()    // m2 : 14
// ここでインスタンスが解放される 14 : deinit

キャプチャリスト

クロージャの先頭に[]で囲んだ変数はキャッチリストと呼ばれ、ここに記述した変数はクロージャの生成時に値がコピーされ元の変数の値に変更があっても影響を受けない。
ただし、コピーできるのは値型のデータで、参照型のデータはキャプチャリストを使っても同じ値を参照するので個別に値を持てない。

var a, b, c: () -> ()
do{
  var count = 0
  var name = "Hoge"
  a = { print(count, name) }
  b = { [count] in print(count, name) }
  c = { [count, name] in print(count, name) }
  count = 3
  name = "Fuga"
}
a()    // 3 Fuga
b()    // 0 Fuga
c()    // 0 Hoge

キャプチャリストの中の識別子はクロージャ内で別の名前を指定することも可能なので、このように記述したほうがわかりやすい。

c = { [num = count, title = name] in print(num, count) }

引数リストの省略

var c: (Int, Int) -> String

// 今まで取り扱った記述
c = { (a:Int, b:Int) -> String in "\(a)\(b)" }
print(c(1,3))

// 引数と返り値の省略
c = { a, b in "\(a)\(b)" }
print(c(1,3))

// $数字という記述で何番目の仮引数か知ることができる。
c = { "\($0)\($1)" }
print(c(1,3))

配列の整列

配列の要素を並び替えるメソッドsorted()は引数としてクロージャを使う

let list = ["hoge", "fuga", "piyo", "foo"]
let slist = list.sorted(by: {
  (a:String, b:String) -> Bool in a<b
})
print(slist)    // ["foo", "fuga", "hoge", "piyo"]
// 書き換え
let slist = list.sorted(by: {
  $0 < $1
})
//  <は次のように書くこともできる
let slist = list.sorted(by: < )

クロージャを引数とする関数の定義

//渡された文字列の配列を条件に合致する配列とそうでない配列に分けて返す関数
func separate(_ list: [String], by filter:(String) -> Bool) -> ([String],[String]){
  var sel = [String]()
  var des = [String]()
  for s in list {
    if filter(s) {
      sel.append(s)

    } else {
      des.append(s)
    }
  }
  return (sel, des)
}

let list = ["Hoge", "fuga", "piyo", "Foo", "AAA", "aaa"]

let t = separate(list, by:{
  for c in $0 {
    if case "A" ... "Z" = c {return true}
  }
  return false
})
print(t)    // (["Hoge", "Foo", "AAA"], ["fuga", "piyo", "aaa"])

接尾クロージャ

クロージャ式が引数の最後で呼ばれる場合、特別な記述ができる。
上記で記述した呼び出し部分の書き換え

let t = separate(list){
  for c in $0 {
    if case "A" ... "Z" = c {return true}
  }
  return false
}
print(t)    // (["Hoge", "Foo", "AAA"], ["fuga", "piyo", "aaa"])

エラーを投げるクロージャ

let hoge = { (a:Double) throws -> Double in
  guard a > 0 else { throw XError.zero }
  return 1.0
}

// 使い方
if let d = try? hoge(-9.0){
  print(d)
} else {
  print("error")    // error
}

再通報する関数

エラーを投げるクロージャを引数とする関数が、そのクロージャを実行した結果エラーを伝播させる場合、再通報するという。
引数リストの後にrethrowsを置く
発生したエラーを外部に伝播させる必要がないのであればrethrowsは書かなくていい

func Fuga(_ hoge: (Double) throws -> Double) rethrows -> Double{
  //処理
  let d = try hoge(-9.0)
  return d
}


let hoge = { (a:Double) throws -> Double in
  guard a > 0 else { throw XError.zero }
  return 1.0
}
do{
  let d = try Fuga(hoge)
  print(d)
}catch{
  print("error")    // error
}

参照の循環

インスタンスのメンバのクロージャが他のメンバのキャプチャをしたときに参照の循環が起きる。

class Hoge {
  let str = "hoge"
  var fuga = { () -> String in " " }
  func hoge() {
    fuga = { () -> String in
      return self.str
    }
  }
  deinit {
    print("deinit")
  }
}

do {
  let piyo = Hoge()
  piyo.hoge()
}
// deinitが呼ばれない

キャプチャリストを使った解決

キャプチャリストは変数のコピーを作成するので参照の循環が起きなくなる。

func hoge() {
  fuga = { [str] () -> String in
    return str
  }
}

弱い参照を使った解決

弱い参照にするとunwrapする必要がある。

func hoge() {
  fuga = { [weak self] () -> String in
    guard let self = self else { return " " }
    return self.str
  }
}

関数の引数にクロージャを渡す

関数の引数にクロージャを渡したあと、関数内でクロージャを変数に保存することによって、関数呼び出しが終了した後でもクロージャが呼び出される可能性がある。
このような状態をクロージャの離脱、またはエスケープという。
クロージャが保存されない場合、関数終了時にクロージャへの参照が消滅する。
クロージャが保存されると、強い参照でキャプチャしている他のインスタンスが解放されない。

var theFunc:((Int) -> Int)! = nil
func setFunc(_ f:(Int) -> Int) { theFunc = f }

関数の引数のクロージャが離脱する場合、@escapingで修飾しないといけない

func setFunc(_ f:@escaping (Int) -> Int) { theFunc = f }

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
1
Help us understand the problem. What are the problem?