Callback — چیست، توابع بازگشتی و چگونه کار می‌کنند

نویسنده: IT Sectr منتشر شده: 2026-03-17 زمان مطالعه: 11 دقیقه

Callback — تابعی است که به عنوان آرگومان به تابع دیگری ارسال می‌شود و پس از تکمیل عملیات ناهمزمان اجرا می‌گردد. در توسعه موبایل از callback برای پردازش نتایج درخواست‌های شبکه، کار با پایگاه داده و انیمیشن‌ها استفاده می‌شود. طبق Apple Documentation (2025)، closures در Swift شکل اصلی callback هستند و در URLSession، GCD و Combine استفاده می‌شوند. در Android، callback از طریق interfaceها، lambdaهای Kotlin و ListenableFuture پیاده‌سازی می‌شود.

نکات اصلی

  • Callback — تابع بازگشتی که به عنوان آرگومان برای اجرای ناهمزمان ارسال می‌شود.
  • Swift از closures با کلمه کلیدی @escaping برای callback استفاده می‌کند.
  • Kotlin از عبارات lambda و توابع مرتبه بالا برای callback استفاده می‌کند.
  • Retain cycle — نشت حافظه هنگام گرفتن self در callback در iOS.
  • Callback Hell — مشکل callbackهای تو در تو که با async/await و coroutine حل می‌شود.

Callback چیست؟

Callback (تابع بازگشتی) — کد قابل اجرایی است که به تابع دیگری ارسال می‌شود و پس از تکمیل یک عمل خاص فراخوانی می‌گردد. در توسعه موبایل، callback مکانیزم اساسی برنامه‌نویسی ناهمزمان است و امکان واکنش به تکمیل درخواست‌های شبکه، تایمرها، انیمیشن‌ها و عملیات ورودی/خروجی را بدون مسدود کردن نخ اصلی فراهم می‌کند. Swift و Kotlin ساختارهای نحوی داخلی برای ایجاد callback ارائه می‌دهند — به ترتیب closures و lambdaها.

اصل کار Callback

تابع مرتبه بالا تابع دیگری را به عنوان پارامتر می‌پذیرد و پس از اجرای منطق اصلی خود آن را فراخوانی می‌کند. جریان کنترل از طریق callback به caller بازمی‌گردد، از این رو نام آن. در iOS، callback در UIKit (انیمیشن‌های UIView.animate)، Foundation (URLSession.dataTask) و Combine (sink) استفاده می‌شود. در Android، callback در View.OnClickListener، Retrofit Callback و Room DAO استفاده می‌شود. APIهای مدرن به طور فزاینده‌ای callback را با async/await یا coroutine جایگزین می‌کنند، اما درک callback برای کار با کدهای قدیمی و APIهای سطح پایین ضروری است.

Callback همزمان و ناهمزمان

Callback می‌تواند همزمان (بلافاصله در داخل تابع فراخوانی شود) و ناهمزمان (بعداً از نخ یا صف دیگر فراخوانی شود) باشد. callbackهای همزمان برای مرتب‌سازی (comparatorها) و پیمایش مجموعه‌ها استفاده می‌شوند. callbackهای ناهمزمان برای درخواست‌های شبکه، خواندن فایل‌ها و کار با سنسورها استفاده می‌شوند. تفاوت برای درک threading حیاتی است: callback همزمان در همان نخ اجرا می‌شود، ناهمزمان — در نخی که توسط dispatcher تعیین می‌شود (DispatchQueue در iOS، Dispatchers در Kotlin).

Callback در iOS و Android چگونه کار می‌کند؟

مکانیزم callback در هر دو پلتفرم بر اساس یک اصل است: تابع به عنوان یک شی درجه یک ارسال می‌شود و تا زمان اجرا ذخیره می‌گردد. با این حال، پیاده‌سازی‌ها به دلیل پارادایم‌های زبانی متفاوت، تفاوت دارند. در iOS، callback یک closure است که متغیرها را از زمینه اطراف می‌گیرد. در Android، callback اغلب از طریق کلاس‌های ناشناس یا عبارات lambda Kotlin که به FunctionalInterface کامپایل می‌شوند، پیاده‌سازی می‌گردد.

چرخه حیات Callback در iOS

هنگام فراخوانی یک تابع ناهمزمان، closure همراه با متغیرهای گرفته شده در heap ذخیره می‌شود. وقتی عملیات کامل می‌شود، سیستم GCD یا OperationQueue callback را در صف مربوطه (main queue یا background queue) قرار می‌دهد. پس از اجرا، در صورت عدم وجود ارجاع قوی، callback از حافظه حذف می‌شود. Capture list ([weak self]) از نگهداری شی پس از deallocation جلوگیری می‌کند. بدون capture list، retain cycle رخ می‌دهد که در آن شی و callback به یکدیگر ارجاع می‌دهند.

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)
    }
}

چرخه حیات Callback در Android

در Android، callback از طریق interface یا lambda ارسال می‌شود. هنگام اجرای عملیات ناهمزمان از طریق ExecutorService یا coroutine، callback تا تکمیل کار پس‌زمینه در حافظه ذخیره می‌شود. lambdaهای Kotlin به کلاس‌های ناشناسی کامپایل می‌شوند که متغیرهای خارجی را می‌گیرند. عدم وجود ارجاع ضعیف در JVM نیاز به مدیریت دستی دارد: صفر کردن callback در onDestroy() یا لغو coroutine از طریق 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) }
            }
        }
    }
}

// استفاده با lambda
repository.loadData(object : Callback<List<User>> {
    override fun onSuccess(data: List<User>) { showUsers(data) }
    override fun onError(error: Throwable) { showError(error.message) }
})

نحو Callback در Swift و Kotlin

نحو callback توسط قابلیت‌های زبانی کار با توابع به عنوان اشیاء درجه یک تعریف می‌شود. در Swift، closures نحو مختصری با نام‌های خودکار آرگومان ($0, $1) دارند. در Kotlin، lambdaها نیز از it برای آرگومان تکی پشتیبانی می‌کنند. تفاوت‌ها در پردازش گرفتن متغیرها (capture list در Swift در مقابل ارجاعات تغییرپذیر در Kotlin) و تایپ‌بندی (Result<Success, Failure> در مقابل Result<T>) ظاهر می‌شود.

Callback در Swift: closures

Closure در Swift یک بلوک کد خودکفا است که می‌تواند به تابع دیگری ارسال و استفاده شود. Closures می‌توانند سراسری (نام‌دار)، تو در تو و expression-level باشند. @escaping closureای را مشخص می‌کند که پس از بازگشت از تابع اجرا خواهد شد — این یک الزام اجباری برای callbackهای ناهمزمان است. بدون @escaping، closure فقط می‌تواند در داخل بدنه تابع اجرا شود. Trailing closure syntax اجازه می‌دهد 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)
    }
}

Callback در Kotlin: lambdaها و توابع مرتبه بالا

Kotlin از توابع مرتبه بالا که توابع دیگر را به عنوان پارامتر می‌پذیرند، پشتیبانی می‌کند. Callback در Kotlin از طریق پارامتری از نوع (T) -> Unit یا (T) -> R برای مقدار بازگشتی ارسال می‌شود. توابع suspend در coroutineهای Kotlin جایگزین callback با کد ترتیبی می‌شوند، اما callback در APIهای سازگار با Java و Android SDK (View.setOnClickListener, TextWatcher) باقی می‌ماند. lambdaهای Kotlin به طور خودکار متغیرهای val را می‌گیرند، متغیرهای var نیاز به wrapperهای تغییرپذیر دارند.

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

// مثال با lambda
processWithCallback(
    input = "Hello",
    transform = { it.length },
    onResult = { length ->
        textView.text = "Length: $length"
    }
)

Retain cycles و نشت حافظه در Callback

Retain cycle — وضعیتی است که در آن دو شی ارجاعات قوی به یکدیگر نگه می‌دارند و از آزادسازی حافظه جلوگیری می‌کنند. در Swift، retain cycle زمانی رخ می‌دهد که viewController یک closure را می‌گیرد و closure self را می‌گیرد. در Kotlin/Java، نشت زمانی رخ می‌دهد که Activity یک کلاس داخلی یا lambda را به یک عملیات طولانی پس‌زمینه ارسال می‌کند. طبق WWDC Session 10216 (2024)، مدیریت نادرست closures سومین علت شایع نشت حافظه در برنامه‌های iOS است.

Retain cycles در Swift

Swift از Automatic Reference Counting (ARC) استفاده می‌کند که شی را هنگام صفر شدن شمارنده ارجاع آزاد می‌کند. Capture list [weak self] یا [unowned self] در closure از retain cycle جلوگیری می‌کند. weak self یک ارجاع اختیاری ایجاد می‌کند که هنگام deallocation شی nil می‌شود. unowned self فرض می‌کند که شی بیشتر از closure عمر می‌کند — نقض این فرض باعث crash می‌شود. استفاده از 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، نشت callback زمانی رخ می‌دهد که Activity یا Fragment یک listener را به یک کامپوننت singleton (مثلاً EventBus یا Service) ارسال می‌کند. WeakReference به garbage collector اجازه می‌دهد Activity را حتی با وجود ارجاع ضعیف آزاد کند. کامپوننت‌های Lifecycle-aware (LiveData, Flow) مشکل را به طور خودکار حل می‌کنند. lambdaهای Kotlin که زمینه Activity را می‌گیرند نیز می‌توانند باعث نشت شوند: lambda به طور ضمنی ارجاعی به 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 (همچنین به عنوان هرم نابودی شناخته می‌شود) — وضعیتی است که در آن تعداد زیادی callback تو در تو یک ساختار کد عمیقاً تودرتو ایجاد می‌کنند که خواندن و اشکال‌زدایی آن دشوار است. هر مرحله بعدی نیاز به انتظار برای تکمیل مرحله قبلی دارد که منجر به تودرتو شدن 5-10 سطح می‌شود. این مشکل برای عملیات ناهمزمان ترتیبی مشخص است: بارگذاری داده → تجزیه → ذخیره در پایگاه داده → به‌روزرسانی UI.

راه‌حل‌ها در Swift: async/await

Swift 5.5 توابع ناهمزمان (async/await) را معرفی کرد که امکان نوشتن کد ناهمزمان به صورت ترتیبی را فراهم می‌کنند. AsyncSequence و AsyncStream جایگزین تکرارهای مبتنی بر callback می‌شوند. فریمورک Combine عملگرهای flatMap، merge، combineLatest را برای ترکیب جریان‌های ناهمزمان بدون تودرتو ارائه می‌دهد. با این حال، callback برای کار با APIهای Objective-C و کتابخانه‌های شخص ثالث بدون پشتیبانی async ضروری باقی می‌ماند.

swift
// callbackهای تو در تو — 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: coroutineها و Flow

Coroutineهای Kotlin جایگزین callback با توابع suspend با اجرای ترتیبی می‌شوند. Flow جریان‌های cold با عملگرهای map، flatMapConcat، combine ارائه می‌دهد. CoroutineScope امکان لغو تمام coroutineهای راه‌اندازی شده را هنگام نابودی کامپوننت فراهم می‌کند. Room، Retrofit و سایر کتابخانه‌های Jetpack دارای پشتیبانی داخلی از توابع suspend هستند و نیاز به callback را برای عملیات استاندارد از بین می‌برند.

kotlin
// callbackهای ترتیبی — Callback Hell
api.login(credentials) { user ->
    api.fetchProfile(user.id) { profile ->
        api.download(profile.avatarUrl) { bytes ->
            file.save(bytes) { result ->
                textView.text = result.toString()
            }
        }
    }
}

// Coroutineها — راه‌حل
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: کدام را انتخاب کنیم؟

Callback و Delegate — دو رویکرد برای اعلان ناهمزمان هستند و انتخاب بین آنها به الزامات معماری بستگی دارد. Callback برای عملیات یکباره با یک نتیجه مناسب است. Delegate برای رویدادهای متعدد با امضاهای مختلف متد طراحی شده است. Apple توصیه می‌کند برای پروتکل‌های پیچیده با چندین متد از delegate و برای closures ساده با یک نتیجه از callback استفاده کنید. در Android، callback به دلیل پشتیبانی از lambda در بیشتر موارد جایگزین delegate می‌شود.

چه زمانی Callback را انتخاب کنیم

Callback برای عملیات با یک نتیجه واحد بهینه است: درخواست شبکه، خواندن فایل، انیمیشن با بلوک completion. مزایا: نحو فشرده، عدم وجود پروتکل جداگانه، گرفتن مستقیم زمینه. معایب: پیچیدگی در نتایج متعدد (پیشرفت، توقف، لغو)، عدم امکان ارسال چندگانه (اگر callback ممکن است بیش از یک بار فراخوانی شود — از publisher استفاده کنید).

چه زمانی Delegate را انتخاب کنیم

Delegate برای پروتکل‌هایی با چندین متد اجباری و اختیاری مناسب است: UITableViewDelegate، CLLocationManagerDelegate، اتصالات Bluetooth. مزایا: تایپ‌بندی واضح هر متد، مستندات از طریق پروتکل، پشتیبانی از متدهای اختیاری از طریق @objc optional. معایب: کد boilerplate، ارجاع ضعیف اجباری به delegate (weak var delegate)، پیچیدگی در گرفتن زمینه.

سوالات متداول

تفاوت بین callback و تابع مرتبه بالا چیست؟

Callback یک مورد خاص از تابع مرتبه بالا است. تابع مرتبه بالا تابع دیگری را به عنوان آرگومان می‌پذیرد یا آن را برمی‌گرداند. Callback تابعی است که به طور خاص برای اجرای ناهمزمان پس از تکمیل عملیات ارسال می‌شود. همه callbackها از طریق توابع مرتبه بالا پیاده‌سازی می‌شوند، اما هر تابع مرتبه بالا callback نیست.

آیا callback می‌تواند چندین بار فراخوانی شود؟

طبق قرارداد، callback باید دقیقاً یک بار فراخوانی شود — یا success یا failure. فراخوانی چندگانه یک callback خطای طراحی محسوب می‌شود. برای رویدادهای متعدد (پیشرفت، جریان داده) از Observable، Publisher یا Flow استفاده کنید — آنها از انتشار چندگانه مقدار پشتیبانی می‌کنند. برخی APIها این قانون را نقض می‌کنند که منجر به باگ‌های سخت‌یاب می‌شود.

Trailing closure در Swift چیست؟

Trailing closure — شکر نحوی Swift است که اجازه می‌دهد closure پس از پرانتزهای گرد فراخوانی تابع ارسال شود. اگر تابع closure را به عنوان آخرین آرگومان بپذیرد، می‌توان آن را خارج از پرانتز قرار داد: fetchData { result in ... }. برای چندین closure، trailing closure فقط برای آخرین مورد اعمال می‌شود، بقیه در داخل پرانتز نام‌گذاری می‌شوند. این کار خوانایی APIهای مبتنی بر callback را بهبود می‌بخشد.

چگونه از نشت حافظه در callback در Android جلوگیری کنیم؟

برای listenerهای طولانی‌عمر از WeakReference استفاده کنید، coroutine‌ها را از طریق Job.cancel() در onDestroy() لغو کنید، برای لغو خودکار از lifecycleScope استفاده کنید. ViewModel + LiveData/Flow مشکل را در سطح معماری حل می‌کنند. از ارسال زمینه Activity به callbackهای ایستا خودداری کنید — از Application context استفاده کنید. lambdaهای Kotlin به طور ضمنی this را می‌گیرند، با memory profiler بررسی کنید.

آیا async/await به طور کامل جایگزین callback خواهد شد؟

Async/await جایگزین callback برای کد ناهمزمان ترتیبی می‌شود، اما نه برای معماری event-driven. Callback در APIهای سیستمی (View.OnClickListener، delegateهای URLSession)، فراخوانی‌های با پیشرفت و کتابخانه‌های شخص ثالث باقی می‌ماند. جایگزینی کامل به دلیل سازگاری معکوس غیرممکن است. استراتژی مدرن استفاده از async/await با wrapperهای callback است (continuation در Swift، suspendCancellableCoroutine در Kotlin).

خلاصه

  • Callback — تابع بازگشتی که به عنوان آرگومان برای اجرای ناهمزمان پس از تکمیل عملیات ارسال می‌شود.
  • Swift callback را از طریق closures با @escaping، capture list [weak self] و trailing closure syntax پیاده‌سازی می‌کند.
  • Kotlin از lambdaها، توابع مرتبه بالا و توابع suspend coroutine برای ناهمزمانی استفاده می‌کند.
  • Retain cycle در iOS با capture list جلوگیری می‌شود؛ در Android — با WeakReference و کامپوننت‌های Lifecycle-aware.
  • Callback Hell از طریق async/await در Swift و coroutine با Flow در Kotlin حل می‌شود.
  • Delegate برای پروتکل‌های با چندین روش نسبت به callback ارجحیت دارد؛ callback — برای عملیات یکباره.
  • برای عملیات ساده ناهمزمان از callback، برای زنجیره‌های ترتیبی از async/await، برای رویدادهای متعدد از delegate استفاده کنید.

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید