楽水

人々の創造が自由に表現できる舞台づくり

Swift

Swiftのクロージャについてわかりやすく解説

投稿日:2020年8月24日 更新日:


Swiftのクロージャって何?
いまひとつわからない、という方むけに、今回は、Swiftのクロージャについて以下の観点で丁寧に解説します。

  • Swiftのクロージャとは何か
  • Swiftのクロージャ式
  • Swiftの後置クロージャ
  • Swiftのクロージャの特徴

参考本
[改訂新版]Swift実践入門 ── 直感的な文法と安全性を兼ね備えた言語 WEB+DB PRESS plus

なお、一般的にクロージャ(関数閉包)とは、
プログラミング言語における関数オブジェクトの一種で、引数以外の変数を、実行時の環境ではなく自身が定義された環境(静的スコープ)において解決することを特徴とする
と定義されています。
「Swiftのクロージャの特徴」の箇所で説明しますが、Swiftのクロージャも、この一般的なクロージャの持つ特徴を備えており特別なものではありません。

Swiftのクロージャとは何か

Swiftのクロージャとは、コードで使用するために渡すことができる独立した機能ブロックです。
つまり、クロージャは第一級オブジェクト(first-class object)ということです
第一級オブジェクトとは、あるプログラミング言語において、たとえば生成、代入、演算、(引数や戻り値としての)受け渡しといった、その言語における基本的な操作を制限なしに使用できる対象のことです。
さて、クロージャの形式には以下があります。

  • グローバル(通常の)関数は、名前があり、値をキャプチャ(閉包)しないクロージャです
  • ネストされた関数は、名前があり、囲っている関数にある値をキャプチャ(閉包)できるクロージャです
  • クロージャ式は、周りのコンテキストにある値をキャプチャ(閉包)できる、軽量シンタックスで記述された名前が無いクロージャです

なので、関数はクロージャの特殊なケースということです。
なお、関数については
Swiftの関数
を参照してください。
ネストされた関数については
Swiftの関数のネスト
を参照してください。

Swiftのクロージャ式

ここでは、クロージャの形式の一つ、クロージャ式について解説します。
クロージャ式は、名前も必要とせず関数のような構成をより短く記述できる、簡潔かつ明瞭なシンタックスでインラインに記述する手段です。
クロージャ式のシンタックスは、次のような一般形式になります。

{ (parameters) -> return type in
statements
}

クロージャ式シンタックスには、定数パラメータやinout パラメータを使用することができます。
デフォルト値を持たせることはできません。
可変長パラメータに名前を付けた場合には、可変長パラメータを使用することができます。
パラメータの型および戻り値の型として、タプルを使用することもできます。
Swift の標準ライブラリに、型がわかっている値の配列を、ソート用クロージャからの出力をもとにしてソートする sort(_:) メソッドがあるので、これを例にして見ていきましょう。
なお、関数のパラメータに関しては
関数のパラメータ
を参照してください。

let names = [“Chris”, “Alex”, “Ewa”, “Barry”, “Daniella”]
reversed = names.sort({ (s1: String, s2: String) -> Bool in
return s1 > s2
})

これを見ると、配列のsort(_:) メソッドに{ (s1: String, s2: String) -> Bool in return s1 > s2 }というクロージャ式を渡していることがわかります。
このクロージャ式ですが以下の3つの観点から簡略化することができます。

文脈から型を推論できることによる簡略化

ソート用クロージャは引数としてメソッドに渡されるため、Swift はパラメータの型と返す値の型を推論することができます。
sort(_:) メソッドは文字列の配列で呼び出されているため、その引数を (String, String) -> Bool 型(関数型)の関数にする必要があります。
そのため、(String, String) と Bool 型をクロージャ式の定義の一部として記述する必要はありません。
また、すべての型を推論することができるため、リターンアロー (->) とパラメータ名を囲む丸括弧も省略することができます
その結果、以下のように記述することができます。

reversed = names.sort( { s1, s2 in return s1 > s2 } )

ここで、関数型とありますが、これについては
Swiftの関数型
を参照してください。

式が 1 つのクロージャからリターンを省略することによる簡略化

この sort(_:) メソッドの引数の関数型では、クロージャによって Bool 値が返されるということが明らかです。
クロージャの本体が Bool 値を返す式 (s1 > s2) 1 つであるため、あいまいでなく、return キーワードを省略することができます
その結果、以下のように記述することができます。

reversed = names.sort( { s1, s2 in s1 > s2 } )

簡略引数名による簡略化

Swift は自動的に、$0、$1、$2 などの名前でクロージャの引数の値を参照できる簡略引数名をインラインクロージャに与えます。
クロージャ式で簡略引数名を使用する場合、定義からクロージャの引数リストを省略することができ、簡略引数名の数値と型は関数型から推論されます。
また、クロージャ式が本体だけで構成されるため、in キーワードも省略できます
その結果、以下のように記述することができます。

reversed = names.sort( { $0 > $1 } )

Swiftの後置クロージャ

関数の最後の引数として関数にクロージャ式を渡す必要があって、かつクロージャ式が長い場合には、後置クロージャとして記述すると効果的です。
後置クロージャは、サポートする関数呼び出しの丸括弧の外側(後ろ)に記述されるクロージャ式です。
例えば、以下のような関数の場合を考えてみましょう。

func someFunctionThatTakesAClosure(closure: () -> Void) {
// 関数本体
}
通常のクロージャ式を渡して関数を呼び出す場合、以下のようになります。
someFunctionThatTakesAClosure({
// クロージャの本体
})
後置クロージャ式を渡して関数を呼び出す場合、以下のようになります。
someFunctionThatTakesAClosure() {
// 後置クロージャの本体
}

これを見ると、関数呼び出しの丸括弧の外側(後ろ)に{}を設けてクロージャの本体を記述していることがわかります。
また、クロージャ式が関数やメソッドの唯一の引数で、そのクロージャ式を後置クロージャとする場合には、関数を呼び出すときに関数名やメソッド名の後に丸括弧 () を記述する必要はありません。
その結果、以下のように記述することができます。

someFunctionThatTakesAClosure{
// 後置クロージャの本体
}

後置クロージャの例として、Swift の Array 型には引数 1 つのクロージャ式を受け取る map(_:)メソッドの例を見てみましょう。
map(_:)メソッドでは、クロージャは配列内の項目ごとに一度呼び出され、その項目にマッピングした別の(型が異なることもある)値を返します。
マッピングの性質と戻り値の型は、指定するクロージャに委ねられています。

let digitNames = [
0: “Zero”, 1: “One”, 2: “Two”, 3: “Three”, 4: “Four”,
5: “Five”, 6: “Six”, 7: “Seven”, 8: “Eight”, 9: “Nine”
]
let numbers = [16, 58, 510]
let strings = numbers.map {
(number) -> String in
var number = number
var output = “”
while number > 0 {
output = digitNames[number % 10]! + output
number /= 10
}
return output
}
// strings は [String] 型と推論される
// その値は [“OneSix”, “FiveEight”, “FiveOneZero”]

クロージャ式は、呼び出されるたびに文字列 output を構築します。
剰余演算 (number % 10) を使用して number の最後の桁の数値を算出し、辞書 digitNames から適切な文字列を調べるためにこの数値を使用します。

Swiftのクロージャの特徴

クロージャは、定義されているコンテキストにある定数や変数をキャプチャすることができます。
つまり、クロージャは、定数や変数が定義されていた元のスコープにもはや存在しなくなっている場合でも、それらの定数や変数の値をクロージャの本体内から参照あるいは変更することができます
次はネストされた関数 incrementer を含む、関数 makeIncrementer の例です。
ネストされた関数 incrementer() は、自身にはパラメータが無いにもかかわらず、外側の関数内に定義されている定数や変数であるrunningTotal や、外側の関数の引数である amount の値をコンテキストからキャプチャします。
これらの値をキャプチャした後、呼び出されるたびに runningTotal を amount 増加させるクロージャとして、incrementer が makeIncrementer から返されます。

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}

次は makeIncrementer の実行例です。

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// 値 10 を返す
incrementByTen()
// 値 20 を返す
incrementByTen()
// 値 30 を返す

この例では、呼び出されるたびに変数runningTotalに10を加える関数incrementerを、定数incrementByTen が参照するよう設定しています。
以下は別の例です。

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 値 7 を返す

この例では、呼び出されるたびに変数 runningTotal に7を加える関数 incrementerを、定数 incrementBySeven が参照するよう設定しています。
これらのincrementBySevenとincrementByTenに設定されたクロージャは、キャプチャしている変数runningTotalを増加させることができます。
これは、関数を含めクロージャが参照型であるためです。
関数やクロージャを定数や変数に代入するとき、実際にはクロージャの参照を定数や変数に設定しています。

let incrementByTen = makeIncrementer(forIncrement: 10)
let incrementBySeven = makeIncrementer(forIncrement: 7)

において、 incrementByTen とincrementBySevenには、それぞれ別々のクロージャの参照が設定されています。
以上のように、クロージャは特に高階関数に対して活用されます
高階関数とは以下のうち少なくとも1つを満たす関数のことです。

  • 関数を引数に取る
  • 関数を返す

さて、ここで、クロージャの効用についてもう少し考えてみましょう。
クロージャの特徴は、定義されているコンテキストにある定数や変数をキャプチャすることができるということでした。
これは、定義されているコンテキストにある定数や変数をクロージャの中に閉じ込める(閉包する)ということで、これによって、外部から、その値にアクセスできなくなさせるということを意味しています。
例えば、次のようなクラスを考えて見ましょう。

class Incrementer{
var amount: Int
var runningTotal = 0
init(amount: Int) { self.amount = amount }
func increment() -> Int {
runningTotal += amount
return runningTotal
}
}

これを実行して、上のincrementByTenと同じような振る舞いをさせてみましょう。

let incrementByTen = Incrementer(amount:10)
print(incrementByTen.increment())
//10と出力
print(incrementByTen.increment())
//20と出力

このように、上記incrementByTenと同じ機能を持つインスタンスをつくることができました。
ここで、以下の操作をしてみましょう。

incrementByTen.amount = 5

そうすると以下のように、それまでと異なる挙動をするようになります。

print(incrementByTen.increment())
//25と出力
print(incrementByTen.increment())
//30と出力

これは、incrementByTen.amountを外部から編集できることで予期せぬ挙動を招いてしまった結果です。
しかし、上記クロージャのincrementByTenの場合、定数や変数をクロージャの中に閉包しているので、外部からアクセスできず、予期せぬ挙動を招くことはありません。
ある機能がコンピュータの(論理的な)状態を変化させ、それ以降で得られる結果に影響を与えることを「副作用(SideEffects)」といいますが、クロージャの場合、変数や定数をキャプチャ(閉包)することで副作用を防いでいるのです。
以上、今回は、Swiftのクロージャについて解説しました。

-Swift
-,

執筆者:

関連記事

Swiftのメモリ管理【weakやunownedをわかりやすく解説】

Swiftのプログラムでweakやonwnedというキーワードをみたことはありませんか? これはSwiftが採用しているARCというメモリ管理の仕組みに関係しています。 今回は以下の観点でSwiftの …

Swiftの関数型についてわかりやすく解説

Swift のすべての関数には型があり、関数のパラメータの型と戻り値の型で構成されます。 この型を Swift の他の型と同じように使うことができ、他の関数にパラメータとして関数を渡すことや、関数から …

Swiftのプロパティラッパーをわかりやすく解説

Swiftのプロパティには、プロパティラッパーという機能が用意されています。 今回は、Swiftのプロパティラッパーについて以下の観点で解説します。 プロパティラッパーとは何か プロパティラッパーの使 …

Swiftの継承についてわかりやすく解説

クラスは、別のクラスからメソッド、プロパティ、および他の特徴を継承することができます。 あるクラスが別のクラスから継承するとき、継承するクラスをサブクラス、継承されるクラスをスーパークラスと呼びます。 …

Swiftの関数【Swiftの関数の書き方をわかりやすく解説】

Swiftの関数は、特定のタスクを実行する独立したコードブロックです。 Swiftの関数は、パラメータ名の無いシンプルな C スタイルの関数から、パラメータごとにローカルと外部のパラメータ名がある複雑 …