【ソースコード有】クロージャを理解しよう!JavaScriptとSwiftでの使用例も紹介
今回は、JavaScriptとSwiftによる具体的な例を取り上げながら、クロージャについて説明していきます。使い方を覚えれば、さまざまな場面で応用できるようになるでしょう。
今回は、JavaScriptとSwiftによる具体的な例を取り上げながら、クロージャについて説明していきます。使い方を覚えれば、さまざまな場面で応用できるようになるでしょう。
知識・情報
2022/05/23 UP
- プログラミング
- 開発
- 技術
「クロージャ(closure)」は、多数のプログラミング言語に採用されている便利な機能です。JavaScriptやSwiftのように、クロージャを全面的に取り入れた文法をもつものもあります。しかし、その意味や特徴を理解することについては、難しさを感じる人も少なくありません。
そこで今回は、JavaScriptとSwiftによる具体的な例を取り上げながら、クロージャについて説明していきます。使い方を覚えれば、さまざまな場面で応用できるようになるでしょう。
クロージャとは
クロージャは、しばしば「関数閉包」と訳されます。これをわかりやすい名前だと感じる人は、あまり多くないのではないでしょうか。そこで、名前よりも性質からクロージャを理解していくことにしましょう。
クロージャは、「第一級関数」に「レキシカルスコープ」をもたせる技術です。ここで出てくる2つの用語を理解すれば、クロージャの性質もわかります。
第一級関数(First-class Function)とは
「第一級関数」とは、「第一級オブジェクト」として扱える関数のことです。また、生成や代入といった、プログラミング言語で行なえる主要な操作の対象となるオブジェクトのことを「第一級オブジェクト」といいます。
次のJavaScriptによる例を見てみましょう。
サンプルコード:
function myFunction() {
console.log("関数が実行されました。");
};
const f = myFunction; // 関数を代入
f(); // 代入された関数を実行
実行結果:
関数が実行されました。
関数myFunction()を、変数fに代入している様子がわかるでしょうか。このように、「第一級関数」には整数などの値と同じような取り扱いができるという特徴があります。
レキシカルスコープとは
「レキシカルスコープ」とは、ソースコードを解析する時点におけるスコープのことです。プログラムの実行前に決定することから、「静的スコープ」とも呼ばれます。
以下は、レキシカルスコープの仕組みを確認できるJavaScriptの例です。
サンプルコード:
function makeFunction() {
let x = 123; // myFunction() から参照できる
function myFunction() {
x += 1;
console.log("xの値は%dです。", x);
};
return myFunction; // 関数を戻り値にする
}
{
let x = 456; // myFunction() から参照できない
const f = makeFunction();
f();
}
実行結果:
xの値は124です。
関数がネストしている様子がわかるでしょうか。内側の関数myFunction()からは、外側の関数makeFunction()のスコープ内にある変数xを参照できます。このように、レキシカルスコープとは関数が定義されている場所で有効な定数や変数を指すものだと考えればよいでしょう。
なお、JavaScriptにおける関数は第一級オブジェクトでもあるため、引数や戻り値に指定することも可能です。上の例では、戻り値として受け取った関数を呼び出しています。しかし、呼び出し元にある変数xは、関数の定義場所からはスコープ外となるため使われていません。
クロージャの意味と性質
2つの用語について理解できたところで、「クロージャは第一級関数にレキシカルスコープをもたせたもの」だということの意味を整理しましょう。クロージャには、以下の性質があるといえます。
・生成や代入が可能な関数である
・引数や戻り値として受け渡しておき、あとから呼び出しても良い
・あとから呼び出した場合でも、定義された時点で有効な定数や変数にアクセスできる
ここまでJavaScriptによる例を見てきましたが、実はJavaScriptやSwiftの関数は、すべてクロージャです。つまり、関数を書いた経験がある人は、すでにクロージャも使っています。クロージャとしての性質を理解すれば、関数をこれまで以上に使いこなせるようになるでしょう。
クロージャのメリットと注意点
クロージャを使うと便利なケースと、注意が必要なポイントについて説明します。
「カプセル化」を実現できる
クロージャは、「カプセル化」のための手軽な手段となります。
カプセル化とは、必要な情報のみを外部に公開するテクニックのことです。内部的な変数を関数で包み隠す手法だと理解すればよいでしょう。オブジェクト指向をサポートするプログラミング言語では、クラスや構造体とともに頻繁に用いられます。
一方、クロージャは関数とスコープがひとまとめになったものです。クロージャのスコープは呼び出し元のスコープとは異なるため、関数が定義された場所にある変数は呼び出し側からは見えません。この性質を利用すれば、クラスや構造体を定義しなくても、関数のみでカプセル化を実現することが可能です。
「コールバック関数」として使える
クロージャは、「コールバック関数」を必要とする場面でも活躍します。
コールバック関数とは、所定のタイミングで呼び出すように指定された関数のことです。ある関数に引数として渡す、別の関数のことだと理解すればよいでしょう。プログラム内で発生する各種イベントと、それらに対応する処理を紐付けたいときなどに用いられます。
ユーザーによるクリックや、タイマーなどのイベントに対応する処理は、コールバック関数で指定するのが一般的です。また、JavaScriptやSwiftでは、関数とクロージャの間に違いはありません。そのため、引数としてコールバック関数を指定する必要がある場面では、いつでもクロージャを用いて良いのです。
メモリリークを起こすケースがある
JavaScriptとSwiftのメモリ管理の仕組みは同じではありませんが、「プログラム中から参照されているオブジェクトは解放されない」という原則は共通です。この点を理解せずにクロージャを使うと、メモリリークを引き起こす恐れがあります。
クロージャのスコープ内にある変数が、そのクロージャを間接的に参照しているケースを考えてみましょう。通常であれば、スコープ内の変数はクロージャ自体が破棄されるタイミングで解放されます。しかし、このケースでは異なるオブジェクトが互いを参照しているため、メモリを解放する順番を決められません。結果としてクロージャがいつまでも解放されず、メモリを必要以上に消費してしまいます。
このような参照の循環は本質的にメモリ管理に関する問題であり、クロージャ以外でも起こることです。とはいえ、余計な問題を避けるためにも、クロージャのスコープ内でどの変数が有効になっているのかを意識することが大切だといえます。
JavaScriptのクロージャ
ここからは、クロージャの具体例を紹介していきます。まずは、JavaScriptからチェックしましょう。
カプセル化の例
以下は、所定の回数だけカウントを繰り返すJavaScriptのプログラムです。カウントの残り回数を格納するための変数が、関数によってカプセル化されています。
サンプルコード:
function makeCounter(initialValue) {
let count = initialValue; // この変数を隠す
function countDown() {
count -= 1; // ここでは変数にアクセスできる
return count;
}
return countDown; // 関数のみ公開する
}
let f = makeCounter(4);
while (f() > 0) {
console.log("カウント中……");
}
console.log("完了しました。");
実行結果:
カウント中……
カウント中……
カウント中……
完了しました。
外側の関数makeCounter()が引数として回数を受け取り、自身のスコープ内の変数countに格納している様子がわかるでしょうか。内側の関数countDown()では、countの値を1つ減らしたうえで、その結果を返却しています。クロージャとして動作する関数は、countDown()です。
呼び出し元では、回数を指定してクロージャを取得しています。これにより所定の回数だけカウントする動作が可能になりますが、内部にある変数countにアクセスすることはできません。
コールバックの例
次は、JavaScriptのコールバック関数としてクロージャを使う例を見てみましょう。以下は、タイマーを用いて所定の秒数だけ待つプログラムです。
サンプルコード:
function waitTimer(seconds) { // 引数はスコープ内にある
function timerCallback() {
console.log("%d秒経過しました。", seconds);
}
setTimeout(timerCallback, seconds * 1000); // コールバック関数を登録
}
waitTimer(1);
waitTimer(2);
waitTimer(3);
console.log("タイマーをセットしました。");
実行結果:
タイマーをセットしました。
1秒経過しました。
2秒経過しました。
3秒経過しました。
外側の関数waitTimer()で秒数を引数として受け取り、内側の関数timerCallback()でその値を表示している様子がわかるでしょうか。引数は外側の関数のスコープ内にあるため、クロージャの性質により内側の関数からも参照可能です。また、タイマーイベントに対応するコールバック関数として、内側の関数が指定されています。
呼び出し元では、異なる秒数を指定して3つのタイマーをセットしていることもわかるでしょう。これにより、3つのクロージャがそれぞれのタイミングで処理を実行します。
Swiftのクロージャ
ここからは、Swiftによるクロージャの具体例を紹介していきます。
カプセル化の例
以下は、所定の回数だけカウントするプログラムをSwiftで記述した例です。JavaScriptの例と同様、内部的な変数が関数によって隠されています。
サンプルコード:
func makeCounter(initialValue: Int) -> (()->Int) {
var count = initialValue; // この変数を隠す
func countDown() -> Int {
count -= 1; // ここでは変数にアクセスできる
return count;
}
return countDown; // 関数のみ公開する
}
let f = makeCounter(initialValue: 4);
while (f() > 0) {
print("カウント中……");
}
print("完了しました。");
実行結果:
カウント中……
カウント中……
カウント中……
完了しました。
細かい文法は異なるものの、JavaScriptの例と同様のプログラムになっていることがわかるでしょう。SwiftはJavaScriptとはまったく異なるプログラミング言語ですが、クロージャの仕組みは似ているといえます。
コールバックの例
次は、タイマーを用いて所定の秒数だけ待つプログラムを、Swiftで記述した例です。コールバック関数としてクロージャを使っています。
サンプルコード:
import Foundation
func waitTimer(seconds: TimeInterval) { // 引数はスコープ内にある
func timerCallback(_: Timer) {
print("\(seconds)秒経過しました。");
}
_ = Timer.scheduledTimer(withTimeInterval: seconds, repeats: false, block: timerCallback); // コールバック関数を登録
}
waitTimer(seconds: 1);
waitTimer(seconds: 2);
waitTimer(seconds: 3);
print("タイマーをセットしました。");
RunLoop.current.run(until: Date(timeIntervalSinceNow: 4)); // タイマーを動かす
実行結果:
タイマーをセットしました。
1.0秒経過しました。
2.0秒経過しました。
3.0秒経過しました。
この例の冒頭では、タイマーを使用するためにFoundationをインポートしています。また、最後の行にあるRunLoopの記述は、タイマーの動作に必要なイベントループを回すためのものです。
JavaScriptの例と比べると以上の点では異なるものの、これらはタイマー機能の違いに過ぎません。クロージャの使い方については、本質的に同じだということがわかるでしょう。
クロージャの意味や性質を理解すると関数がもっと便利になる
クロージャは、定義された時点のスコープをもち、引数や戻り値による受け渡しが可能な関数です。JavaScriptやSwiftでは、すべての関数がクロージャとしての性質を備えています。クロージャの使い方を覚えておくと、カプセル化の手段やコールバック関数として利用するのに便利です。クロージャの意味や性質を理解して、関数をこれまで以上に使いこなせるようにしましょう。