Callbackとは何か、コールバック関数とその仕組み

著者: IT Sectr 公開日: 2026-03-17 読了時間: 11 分

Callback(コールバック)とは、別の関数に引数として渡され、非同期操作の完了後に実行される関数です。モバイル開発では、ネットワークリクエストの結果処理、データベース操作、アニメーションにコールバックが使用されます。Apple Documentation(2025)によると、Swiftのクロージャはコールバックの主要な形式であり、URLSession、GCD、Combineで使用されています。Androidでは、コールバックはインターフェース、Kotlinラムダ、ListenableFutureを通じて実装されています。

重要ポイント

  • Callback — 非同期実行のために引数として渡されるコールバック関数。
  • Swiftはコールバックに@escapingキーワード付きのクロージャを使用。
  • Kotlinはコールバックにラムダ式と高階関数を適用。
  • Retain cycle — iOSでコールバック内でselfをキャプチャすると発生するメモリリーク。
  • Callback Hell — ネストされたコールバックの問題で、async/awaitとコルーチンで解決。

Callbackとは?

Callback(コールバック関数)とは、別の関数に渡され、特定のアクションの完了後に呼び出される実行可能コードです。モバイル開発において、コールバックは非同期プログラミングの基本的なメカニズムであり、メインスレッドをブロックせずにネットワークリクエスト、タイマー、アニメーション、I/O操作の完了に反応することを可能にします。SwiftとKotlinは、コールバックを作成するための組み込みの構文構造を提供しています — それぞれクロージャとラムダです。

Callbackの動作原理

高階関数は別の関数をパラメータとして受け入れ、自身のメインロジックを実行した後にそれを呼び出します。制御フローはコールバックを通じて呼び出し元に戻されるため、この名前が付けられています。iOSでは、UIKit(UIView.animateアニメーション)、Foundation(URLSession.dataTask)、Combine(sink)でコールバックが使用されます。Androidでは、View.OnClickListener、Retrofit Callback、Room DAOでコールバックが使用されます。最新のAPIはますますコールバックをasync/awaitやコルーチンに置き換えていますが、レガシーコードや低レベルAPIを扱うにはコールバックの理解が必要です。

同期コールバックと非同期コールバック

コールバックには同期的なもの(関数内で即座に呼び出される)と非同期的なもの(後で別のスレッドやキューから呼び出される)があります。同期コールバックはソート(コンパレータ)やコレクションの走査に使用されます。非同期コールバックはネットワークリクエスト、ファイル読み込み、センサー処理に使用されます。スレッド化を理解するにはこの違いが重要です:同期コールバックは同じスレッドで実行され、非同期コールバックはディスパッチャ(iOSではDispatchQueue、KotlinではDispatchers)によって決定されたスレッドで実行されます。

iOSとAndroidでのCallbackの仕組み

両プラットフォームのコールバックメカニズムは同じ原理に基づいています:関数が第一級オブジェクトとして渡され、実行時まで保存されます。ただし、言語パラダイムの違いにより実装は異なります。iOSでは、コールバックは周囲のコンテキストから変数をキャプチャするクロージャです。Androidでは、コールバックは無名クラスまたはKotlinラムダ式を通じて実装され、FunctionalInterfaceにコンパイルされます。

iOSでのCallbackのライフサイクル

非同期関数が呼び出されると、クロージャはキャプチャされた変数とともにヒープに保存されます。操作が完了すると、GCDまたはOperationQueueシステムがコールバックを適切なキュー(メインキューまたはバックグラウンドキュー)に配置します。実行後、強い参照がなくなるとコールバックはメモリから削除されます。キャプチャリスト([weak self])は、オブジェクトの解放後も保持されるのを防ぎます。キャプチャリストがないと、オブジェクトとコールバックが相互に参照するリテインサイクルが発生します。

swift
func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        completion(.success(data))
    }
    task.resume()
}

// [weak self]の使用
fetchData { [weak self] result in
    guard let self else { return }
    switch result {
    case .success(let data):
        self.updateUI(data)
    case .failure(let error):
        self.showError(error)
    }
}

AndroidでのCallbackのライフサイクル

Androidでは、コールバックはインターフェースまたはラムダを通じて渡されます。ExecutorServiceまたはコルーチンを介して非同期操作を実行する場合、バックグラウンド処理が完了するまでコールバックはメモリに保持されます。Kotlinラムダは外部変数をキャプチャする無名クラスにコンパイルされます。JVMに弱参照がないため、手動管理が必要です:onDestroy()でのコールバックの無効化、Job.cancel()によるコルーチンのキャンセル。ViewModelとLiveDataはアーキテクチャコンポーネントレベルでこの問題を解決します。

kotlin
interface Callback<T> {
    fun onSuccess(data: T)
    fun onError(error: Throwable)
}

class Repository {
    fun loadData(callback: Callback<List<User>>) {
        thread {
            try {
                val result = api.fetchUsers()
                runOnUiThread { callback.onSuccess(result) }
            } catch (e: Exception) {
                runOnUiThread { callback.onError(e) }
            }
        }
    }
}

// ラムダの使用
repository.loadData(object : Callback<List<User>> {
    override fun onSuccess(data: List<User>) { showUsers(data) }
    override fun onError(error: Throwable) { showError(error.message) }
})

SwiftとKotlinでのCallback構文

コールバック構文は、言語が関数を第一級オブジェクトとして扱う能力によって決まります。Swiftでは、クロージャは自動引数名($0、$1)を持つ簡潔な構文を持ちます。Kotlinでは、ラムダは単一引数に対してitをサポートします。違いは変数キャプチャの処理(Swiftのキャプチャリスト vs Kotlinの可変参照)と型付け(Result vs Result)に現れます。

SwiftでのCallback:クロージャ

Swiftクロージャは、別の関数に渡して使用できる自己完結型のコードブロックです。クロージャにはグローバル(名前付き)、ネスト、式レベルのものがあります。@escapingは関数の戻り後に実行されるクロージャをマークします — これは非同期コールバックの必須要件です。@escapingがない場合、クロージャは関数本体の内部でのみ実行できます。Trailing closure構文を使用すると、括弧の後にクロージャを渡せます:fetchData { result in ... }。

swift
typealias NetworkResult = (Result<[String: Any], Error>) -> Void

func performRequest(
    url: URL,
    then handler: @escaping NetworkResult
) {
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        handler(Result {
            guard let json = try JSONSerialization.jsonObject(with: data)
            else { throw NetworkError.invalidData }
            return json as! [String: Any]
        })
    }
    task.resume()
}

performRequest(url: url) { result in
    switch result {
    case .success(let json): process(json)
    case .failure(let error): log(error.localizedDescription)
    }
}

KotlinでのCallback:ラムダと高階関数

Kotlinは他の関数をパラメータとして受け入れる高階関数をサポートしています。CallbackはKotlinでは(T) -> Unit型のパラメータ、または戻り値の場合は(T) -> Rを通じて渡されます。Kotlinコルーチンのsuspend関数はコールバックを順次コードに置き換えますが、コールバックはJava互換APIやAndroid SDK(View.setOnClickListener、TextWatcher)に残っています。Kotlinラムダは自動的にval変数をキャプチャし、var変数にはミュータビリティラッパーが必要です。

kotlin
fun <T, R> processWithCallback(
    input: T,
    transform: (T) -> R,
    onResult: (R) -> Unit
) {
    thread {
        val result = transform(input)
        runOnUiThread { onResult(result) }
    }
}

// ラムダの例
processWithCallback(
    input = "Hello",
    transform = { it.length },
    onResult = { length ->
        textView.text = "Length: $length"
    }
)

Retain cyclesとCallbackのメモリリーク

Retain cycle(リテインサイクル)は、2つのオブジェクトが互いに強い参照を持ち、メモリマネージャがそれらを解放できない状態です。Swiftでは、viewControllerがクロージャをキャプチャし、クロージャがselfをキャプチャするとリテインサイクルが発生します。Kotlin/Javaでは、Activityが内部クラスやラムダを長時間のバックグラウンド操作に渡すとリークが発生します。WWDCセッション10216(2024)によると、不適切なクロージャ管理はiOSアプリケーションにおけるメモリリークの3番目に一般的な原因です。

SwiftでのRetain cycles

Swiftは自動参照カウント(ARC)を使用し、参照カウンタがゼロになるとオブジェクトを解放します。クロージャ内のキャプチャリスト[weak self]や[unowned self]はリテインサイクルを防ぎます。weak selfはオブジェクトの解放時にnilになるオプショナル参照を作成します。unowned selfはオブジェクトがクロージャより長生きすることを前提とします — この前提が破られるとクラッシュが発生します。安全なデフォルトとしてweak selfの使用が推奨されます。

swift
class DataController {
    var onDataUpdate: ((String) -> Void)?

    func setupCallback() {
        // Retain cycle!
        onDataUpdate = { text in
            self.process(text)
        }

        // [weak self]で修正済み
        onDataUpdate = { [weak self] text in
            guard let self else { return }
            self.process(text)
        }
    }

    func process(_ input: String) { }
}

Androidでのメモリリーク

Androidでは、ActivityやFragmentがシングルトンコンポーネント(EventBusやServiceなど)にリスナーを渡すと、コールバックによるメモリリークが発生します。WeakReferenceにより、ガベージコレクタは弱参照があってもActivityを解放できます。Lifecycle-awareコンポーネント(LiveData、Flow)は自動的に問題を解決します。ActivityコンテキストをキャプチャするKotlinラムダもリークを引き起こす可能性があります:ラムダは暗黙的にthisへの参照を保持します。

kotlin
class SafeCallbackManager {
    private val listeners = mutableListOf<WeakReference<(String) -> Unit>>()

    fun addListener(callback: (String) -> Unit) {
        listeners.add(WeakReference(callback))
    }

    fun notifyAll(data: String) {
        val iterator = listeners.iterator()
        while (iterator.hasNext()) {
            val ref = iterator.next().get()
            if (ref != null) ref(data)
            else iterator.remove()
        }
    }
}

// Fragmentでの使用
manager.addListener { result ->
    // WeakReferenceはFragmentを保持しない
    updateUI(result)
}

Callback Hellとその対策

Callback Hell(Pyramid of Doomとしても知られる)は、多数のネストされたコールバックが深くネストされたコード構造を作り出し、読み取りとデバッグが困難になる状態です。各後続ステップは前のステップの完了を待つ必要があり、5〜10レベルのネストになります。この問題は逐次的な非同期操作に特徴的です:データ読み込み → 解析 → DB保存 → UI更新。

Swiftでの解決策:async/await

Swift 5.5は非同期関数(async/await)を導入し、非同期コードを逐次的に記述できるようにしました。AsyncSequenceAsyncStreamはコールバックベースの反復を置き換えます。Combineフレームワークは、ネストなしで非同期ストリームを構成するためのflatMap、merge、combineLatestなどの演算子を提供します。ただし、Objective-C APIやasyncをサポートしないサードパーティライブラリを扱うには、コールバックが引き続き必要です。

swift
// ネストされたコールバック — Callback Hell
loginUser(credentials) { user in
    fetchProfile(user.id) { profile in
        downloadAvatar(profile.avatarUrl) { image in
            cacheImage(image) { success in
                updateUI(user, profile, image)
            }
        }
    }
}

// async/await — 解決策
func loadUserExperience() async throws {
    let user = try await loginUser(credentials)
    let profile = try await fetchProfile(user.id)
    let image = try await downloadAvatar(profile.avatarUrl)
    try await cacheImage(image)
    updateUI(user, profile, image)
}

Kotlinでの解決策:コルーチンとFlow

Kotlinコルーチンは逐次実行のためにコールバックをsuspend関数に置き換えます。Flowはmap、flatMapConcat、combineなどの演算子を持つコールドストリームを提供します。CoroutineScopeはコンポーネントが破棄されたときに実行中のすべてのコルーチンをキャンセルできます。Room、Retrofit、その他のJetpackライブラリはsuspend関数の組み込みサポートを持ち、標準操作でのコールバックの必要性を排除します。

kotlin
// 逐次コールバック — Callback Hell
api.login(credentials) { user ->
    api.fetchProfile(user.id) { profile ->
        api.download(profile.avatarUrl) { bytes ->
            file.save(bytes) { result ->
                textView.text = result.toString()
            }
        }
    }
}

// コルーチン — 解決策
suspend fun loadUserData() {
    val user = withContext(Dispatchers.IO) { api.login(credentials) }
    val profile = withContext(Dispatchers.IO) { api.fetchProfile(user.id) }
    val bytes = withContext(Dispatchers.IO) { api.download(profile.avatarUrl) }
    withContext(Dispatchers.IO) { file.save(bytes) }
    textView.text = "Done"
}

Callback vs Delegate:どちらを選ぶべきか

CallbackDelegateは非同期通知の2つのアプローチであり、選択はアーキテクチャ要件に依存します。Callbackは単一の結果を持つ1回限りの操作に適しています。Delegateは異なるメソッドシグネチャを持つ複数のイベント向けに設計されています。Appleは複数のメソッドを持つ複雑なプロトコルにはdelegateを、単一の結果を持つシンプルなクロージャにはcallbackを推奨しています。Androidでは、ラムダサポートによりほとんどの場合でcallbackがdelegateに取って代わります。

Callbackを選ぶべき時

Callbackは単一の結果を持つ操作に最適です:ネットワークリクエスト、ファイル読み込み、完了ブロック付きアニメーション。利点:コンパクトな構文、個別のプロトコル不要、直接的なコンテキストキャプチャ。欠点:複数の結果(進捗、一時停止、キャンセル)での複雑さ、複数回の送信が不可能(コールバックが複数回呼び出される可能性がある場合はpublisherを使用)。

Delegateを選ぶべき時

Delegateは複数の必須メソッドとオプションメソッドを持つプロトコルに適しています:UITableViewDelegate、CLLocationManagerDelegate、Bluetooth接続。利点:各メソッドの明確な型付け、プロトコルによる文書化、@objc optionalによるオプションメソッドのサポート。欠点:ボイラープレートコード、delegateへの弱参照が必須(weak var delegate)、コンテキストキャプチャの複雑さ。

よくある質問

Callbackと高階関数の違いは何ですか?

Callbackは高階関数の特殊なケースです。高階関数は別の関数を引数として受け入れるか、それを返します。Callbackは操作完了後の非同期実行のために特別に渡される関数です。すべてのコールバックは高階関数を通じて実装されますが、すべての高階関数がコールバックであるとは限りません。

Callbackは複数回呼び出せますか?

慣例により、コールバックは正確に1回だけ呼び出されるべきです — successまたはfailureのいずれかです。同じコールバックの複数回の呼び出しは設計エラーと見なされます。複数のイベント(進捗、データストリーム)には、Observable、Publisher、またはFlowを使用してください — これらは複数の値の emission をサポートしています。一部のAPIはこの規則に違反し、発見が困難なバグを引き起こします。

Swiftのtrailing closureとは何ですか?

Trailing closureはSwiftのシンタックスシュガーで、関数呼び出しの括弧の後にクロージャを渡すことを可能にします。関数が最後の引数としてクロージャを受け取る場合、括弧の外に配置できます:fetchData { result in ... }。複数のクロージャの場合、trailing closureは最後のものにのみ適用され、残りは括弧内で名前指定されます。これによりコールバックベースのAPIの可読性が向上します。

Androidでコールバックによるメモリリークを防ぐには?

長寿命のリスナーにはWeakReferenceを使用し、onDestroy()でJob.cancel()によりコルーチンをキャンセルし、自動キャンセルにはlifecycleScopeを使用してください。ViewModel + LiveData/Flowはアーキテクチャレベルで問題を解決します。静的コールバックにActivityコンテキストを渡さないでください — Application contextを使用してください。Kotlinラムダは暗黙的にthisをキャプチャするため、メモリプロファイラで確認してください。

Async/awaitはコールバックを完全に置き換えますか?

Async/awaitは逐次的な非同期コードのコールバックを置き換えますが、イベント駆動型アーキテクチャは置き換えません。コールバックはシステムAPI(View.OnClickListener、URLSession delegates)、進捗コールバック、サードパーティライブラリに残ります。後方互換性のため完全な置き換えは不可能です。現代的な戦略は、コールバックラッパー(Swiftのcontinuation、KotlinのsuspendCancellableCoroutine)とともにasync/awaitを使用することです。

まとめ

  • Callback — 操作完了後に非同期実行するために引数として渡されるコールバック関数。
  • Swiftは@escaping付きクロージャ、キャプチャリスト[weak self]、trailing closure構文でコールバックを実装。
  • Kotlinは非同期処理にラムダ、高階関数、コルーチンのsuspend関数を使用。
  • Retain cycleはiOSではキャプチャリストで防止;AndroidではWeakReferenceとlifecycle-awareコンポーネントで防止。
  • Callback HellはSwiftではasync/await、KotlinではFlow付きコルーチンで解決。
  • Delegateは複数メソッドのプロトコルに適し、callbackは1回限りの操作に適する。
  • 単純な非同期操作にはcallback、逐次チェーンにはasync/await、複数イベントにはdelegateを使用。

ターンキー方式のモバイルアプリケーションを開発します

IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。

プロジェクトについて相談

こちらもお読みください