آموزش Swift یکی از مهمترین گامها برای افرادی است که قصد دارند برنامهنویسی iOS و macOS را به صورت حرفهای دنبال کنند. در این مقاله قصد داریم به صورت جامع و کامل به مبحث توابع و Closureها در Swift بپردازیم. این دو مفهوم از کلیدیترین مفاهیم زبان Swift هستند که برای ساختاردهی کد، بازاستفاده از منطق برنامه و افزایش خوانایی آن، نقش بسیار مهمی دارند.
توابع و Closureها در Swift
در زبان Swift، توابع و Closureها در Swift نقش بسیار حیاتی در سازماندهی و مدیریت کد ایفا میکنند. با توابع میتوانیم بخشهای مستقل از کد را که یک کار مشخص انجام میدهند، تعریف کرده و هر بار که نیاز داریم، آنها را فراخوانی کنیم. Closureها نیز همان توابع بدون نام (Anonymous Functions) هستند که در عین سادگی، انعطاف بسیار بالایی برای کدنویسی ارائه میدهند.
در ادامه، مباحث مربوط به توابع و Closureها در Swift را به صورت گامبهگام بررسی میکنیم.
توابع (Functions)
توابع در Swift به شما اجازه میدهند منطق خاصی را در یک بلوک کد کپسوله کرده و در هر جایی از کد که نیاز داشتید، آن را اجرا کنید. در این بخش، ابتدا نگاهی دقیق به نحوه تعریف توابع و سپس به جزئیات پارامترها و بازگشت مقادیر گوناگون میاندازیم.
چرا از توابع استفاده میکنیم؟
توابع به ما کمک میکنند تا منطق برنامه را ماژولار کرده و هر بخش از کد که یک وظیفه مشخص دارد، در قالب یک تابع جداگانه پیادهسازی شود. این رویکرد باعث میشود:
خوانایی کد افزایش یابد: وقتی منطق هر کار مشخص در یک تابع جداگانه نوشته شود، پیدا کردن آن منطق یا رفع اشکال آن سادهتر خواهد بود.
بازاستفاده از کد: میتوان تابع را یک بار نوشت و در جاهای مختلف برنامه فراخوانی کرد.
نگهداری راحتتر: در صورت تغییر در منطق یک بخش، تنها کافی است همان تابع را ویرایش کنید.
ساختار کلی تعریف یک تابع در Swift
یک تابع در Swift میتواند شامل چندین پارامتر ورودی، مقدار بازگشتی و حتی چندین مقدار بازگشتی (تاپل) باشد. در سادهترین حالت، ساختار یک تابع بدون پارامتر و بدون مقدار بازگشتی به صورت زیر است:
func functionName() {
// Function Body
}
func: کلیدواژهای که برای تعریف تابع در Swift استفاده میشود.
functionName: نام تابع که میتواند حاوی حروف، اعداد و برخی کاراکترهای خاص باشد (بهتر است مطابق اصول نامگذاری عمل کنیم تا کد خواناتر شود).
(): درون این پرانتز، پارامترهای تابع تعریف میشوند. اگر پارامتر نداشته باشیم، آن را خالی میگذاریم.
{ }: بدنه تابع که منطق و دستورالعملهای تابع در آن قرار میگیرد.
مثال:
func greet() {
print("Hello, Swift!")
}
در این تابع، نام آن greet بوده و هیچ پارامتری دریافت نمیکند. با اجرای greet()، تنها عبارت “Hello, Swift!” در کنسول چاپ خواهد شد.
فراخوانی تابع
همانطور که در مثال بالا دیدید، برای فراخوانی یک تابع، کافی است نام آن را به همراه پرانتزهای خالی یا دارای آرگومانها (بسته به نوع تابع) بنویسید:
greet()
اگر تابع شما پارامتر داشته باشد، در زمان فراخوانی باید مقادیر مناسب را به ترتیب تعریف در تابع ارسال کنید.
پارامترهای ورودی و خروجی (in-out parameters)
در زبان Swift، تمام انواع مقداری (Value Types) نظیر Struct و Enum به صورت پیشفرض بر اساس کپی (Pass by Value) منتقل میشوند. به این معنا که وقتی متغیری به یک تابع ارسال میشود، نسخهای از مقدار آن در اختیار تابع قرار میگیرد و هر تغییری که درون تابع روی این پارامتر صورت میگیرد، تنها روی نسخه کپی اعمال میشود و متغیر اصلی در بیرون تابع دستنخورده باقی میماند. حال اگر بخواهیم واقعاً روی متغیر اصلی تغییر ایجاد کنیم و این تغییر پس از خروج از تابع نیز باقی بماند، باید از مکانیزم پارامترهای ورودی و خروجی یا همان inout استفاده کنیم. در این حالت، تابع به نوعی یک ارجاع (Reference) به متغیر بیرونی را در اختیار میگیرد و هر تغییری که در تابع اتفاق میافتد، مستقیماً روی همان متغیر اصلی منعکس میشود. این سازوکار یکی از امکانات مهم در توابع و Closureها در Swift محسوب میشود.
نحوه تعریف پارامترهای inout
برای تعریف یک پارامتر inout کافی است قبل از نوع پارامتر، کلیدواژه inout را قرار دهید. مثال زیر را در نظر بگیرید که قرار است مقدار یک عدد را درون تابع افزایش دهد و این تغییر در متغیر اصلی بیرون تابع نیز اعمال شود:
func incrementValue(_ number: inout Int) {
number += 1
}
کلمه کلیدی inout قبل از نوع Int آورده شده است.
در بدنه تابع، مقدار number یک واحد افزایش مییابد.
نحوه فراخوانی تابع با پارامتر inout
برای فراخوانی تابعی که پارامتر inout دارد، باید از علامت & (Ampersand) قبل از نام متغیر استفاده کنیم:
var myNumber = 10 incrementValue(&myNumber) print(myNumber) // نتیجه: 11
در اینجا، هنگام فراخوانی تابع incrementValue(_:)، متغیر myNumber به صورت &myNumber ارسال میشود.
مقدار myNumber در داخل تابع افزایش یافته و این تغییر بر روی متغیر اصلی اعمال میشود.
پشت صحنه inout
وقتی از inout استفاده میکنید، Swift در زمان کامپایل و اجرای برنامه، این پارامتر را طوری مدیریت میکند که گویی یک ارجاع مستقیم به حافظه متغیر بیرون تابع است. در واقع مکانیزم زنجیرهای که در پسزمینه رخ میدهد به شکل زیر است:
کپی مقدار اولیه متغیر در ابتدای فراخوانی تابع.
اجازه تغییر مستقیم روی این کپی که در واقع همان ارجاع به متغیر اصلیست.
برگرداندن مقدار نهایی به متغیر بیرون تابع پس از اتمام تابع.
البته Swift هوشمندانه این مراحل را مدیریت میکند و از برخی بهینهسازیها استفاده میکند تا کارایی حفظ شود. اما از دید یک برنامهنویس کافیست بدانید که با inout، متغیر بیرونی واقعاً تحت تأثیر قرار میگیرد.
نکات مهم در استفاده از inout
استفاده محدود: اگرچه inout قابلیت مفیدی است، اما در عمل، بهتر است تنها در مواقعی به کار رود که واقعاً نیاز دارید مستقیم روی متغیر بیرون تابع اثر بگذارید. استفاده زیاد از پارامترهای inout میتواند کد را دشوارتر کرده و از رویکرد “تابع خالص” (Pure Function) که عاری از عوارض جانبی (Side Effects) است، فاصله بگیرد.
توجه به Reference Types: در Swift، انواع مرجع مانند کلاسها (Class) به صورت پیشفرض ارجاع شده و متغیرها اشارهگری به یک آبجکت مشترک هستند. از همین رو، اگر تابعی را روی یک آبجکت کلاس اعمال کنید و خواهان تغییرات باشید، معمولاً نیازی به inout ندارید. چرا که خودِ کلاسها از نوع مرجع هستند و هر تغییری روی آبجکت، برای تمام ارجاعات منعکس میشود. در مقابل، انواع مقداری (Struct و Enum) به صورت کپی منتقل میشوند؛ بنابراین، برای تغییر مستقیم روی آنها در تابع، به inout نیاز خواهید داشت.
جایگزین بازگشت مقدار: در بسیاری موارد ممکن است به جای استفاده از inout، به سادگی مقدار جدید را از تابع بازگردانید:
func incrementedValue(of number: Int) -> Int {
return number + 1
}
var myNumber = 10
myNumber = incrementedValue(of: myNumber)
print(myNumber) // 11
اگر نیاز به خوانایی بالا و حفظ رویکرد تابع خالص دارید، گاهی بازگرداندن مقدار جدید انتخاب بهتری نسبت به استفاده از inout است.
یکپارچگی داده: زمانی که از inout استفاده میکنید، باید مطمئن شوید که این رویکرد با منطق کلی برنامه تداخل ندارد. زیرا با هر بار فراخوانی تابع، مستقیماً ساختار بیرونی شما تغییر خواهد کرد و ممکن است در ساختار برنامهریزی و هماهنگی دادهها اختلال ایجاد شود (بهویژه در پروژههای چندنخی یا Async).
مقایسه با متدهای Mutating: در ساختارها (Struct) میتوان از متدهای نشانهگذاریشده با mutating استفاده کرد که اجازه تغییر مستقیم مقادیر ذخیرهشده درون استراکچر را میدهد. مکانیسم mutating در Structها تا حدی شبیه inout عمل میکند؛ با این تفاوت که متدهای mutating در خود Struct تعریف میشوند و در صورت نیاز، با فراخوانی آنها، وضعیت استراکچر تغییر میکند. در مقابل، inout روشی است که در سطح تابعی بیرون از استراکچر قابل اعمال است. پارامترهای ورودی و خروجی (inout) ابزاری قدرتمند در توابع و Closureها در Swift هستند که امکان تغییر مستقیم متغیر بیرون تابع را فراهم میکنند. این قابلیت بهویژه وقتی کاربرد دارد که میخواهید یک عملیات تغییر وضعیت (State) در دادههای خود داشته باشید بدون اینکه بخواهید نسخه کپیای از آن داده بسازید یا از یک نوع مرجع (Class) استفاده کنید. با این حال، باید در استفاده از inout جانب احتیاط را رعایت کرد و فقط در مواقع ضروری سراغ آن رفت تا کد از نظر مفهومی ساده و قابل پیشبینی باقی بماند.
پارامترهای پیشفرض (Default Parameters)
گاهی اوقات میخواهید تابعی بنویسید که برای برخی پارامترهایش مقدار ثابتی را در نظر بگیرد تا اگر هنگام فراخوانی، آن پارامتر را ندادید، همان مقدار پیشفرض استفاده شود. این کار را در Swift با تعیین مقدار پیشفرض برای پارامتر انجام میدهیم. در ادامه، جزئیات بیشتری درباره نحوه استفاده، مزایا و نکات مربوط به آن ارائه میشود.
نحوه تعریف پارامتر پیشفرض
برای هر پارامتر که میخواهید مقدار پیشفرض داشته باشد، پس از نام و نوع آن، یک علامت = قرار داده و سپس مقدار پیشفرض را مشخص میکنید:
func greet(name: String = "Swift Programmer") {
print("Hello, \(name)!")
}
اینجا، پارامتر name در صورت عدم ارسال مقدار توسط فراخوانیکننده، مقدار “Swift Programmer” را دریافت میکند.
میتوانید یک یا چند پارامتر پیشفرض داشته باشید. همه پارامترهای تابع میتوانند پیشفرض داشته باشند یا فقط برخی از آنها.
فراخوانی تابع با پارامتر پیشفرض
دو حالت کلی برای فراخوانی تابعی با پارامتر پیشفرض وجود دارد:
بدون ارسال آرگومان: مقدار پیشفرض استفاده میشود.
با ارسال آرگومان: مقدار ارسالی جایگزین مقدار پیشفرض خواهد شد.
// استفاده از مقدار پیشفرض greet() // چاپ: Hello, Swift Programmer! // ارسال مقدار اختصاصی greet(name: "Ali") // چاپ: Hello, Ali!
ترکیب چند پارامتر پیشفرض
ممکن است تابع شما چندین پارامتر داشته باشد که برای بعضی از آنها مقدار پیشفرض تعیین کرده باشید:
func createAccount(username: String, email: String = "no-email", age: Int = 18) {
print("Username: \(username), Email: \(email), Age: \(age)")
}
در این صورت، هنگام فراخوانی میتوانید هیچیک از پارامترهای پیشفرض را تعیین نکنید، برخی را مقداردهی کنید یا همه را مقداردهی کنید:
createAccount(username: "Mehdi") // Username: Mehdi, Email: no-email, Age: 18 createAccount(username: "Sara", email: "sara@example.com") // Username: Sara, Email: sara@example.com, Age: 18 createAccount(username: "Ali", email: "ali@example.com", age: 25) // Username: Ali, Email: ali@example.com, Age: 25
نکات مهم
افزایش خوانایی و انعطاف: پارامترهای پیشفرض کمک میکنند تعداد توابع لازم برای پاسخگویی به نیازهای مختلف کاهش یابد و در عین حال کد شما ساختار منسجمتری داشته باشد.
ترتیب پارامترها: اگر یک تابع چند پارامتر دارد که برخی از آنها پیشفرض هستند، بهتر است پارامترهای پیشفرض را در انتهای لیست قرار دهید تا فراخوانی تابع آسانتر شود.
همخوانی با Function Overloading: وقتی تابعی را اضافه میکنید که پارامترهای پیشفرض دارد، ممکن است در کنار توابع دیگری با نام مشابه تعریف شود. Swift در زمان کامپایل بررسی میکند که کدام امضای تابع (Signature) با فراخوانی فعلی سازگار است. این مکانیزم میتواند گاهی پیچیده شود؛ پس بهتر است در تعریف توابع و پارامترهای پیشفرض دقت داشته باشید که تداخلی رخ ندهد.
پارامترهای نامگذاری شده (Argument Labels)
Argument Label یا همان برچسب آرگومان، یک ویژگی منحصربهفرد در Swift است که به کد شما خوانایی و وضوح بیشتری میدهد. در زبان Swift، هر پارامتر میتواند دو نام داشته باشد:
External Parameter Name یا همان برچسبی که هنگام فراخوانی مورد استفاده قرار میگیرد.
Internal Parameter Name یا نام متغیری که در بدنه تابع از آن استفاده میکنید.
نحوه تعریف پارامتر با برچسب
برای تعریف یک برچسب آرگومان، در اعلان تابع، قبل از نام پارامتر داخلی، یک نام قرار میدهید. به عنوان مثال:
func greet(to name: String, from sender: String) {
print("Hello, \(name)! This is a greeting from \(sender).")
}
اینجا، to و from برچسبهایی هستند که در فراخوانی تابع استفاده خواهند شد (External Names).
اما در داخل تابع، از name و sender برای دسترسی به مقادیر پارامترها استفاده میشود (Internal Names).
فراخوانی تابع با برچسب آرگومان
در زمان فراخوانی تابع فوق، باید از برچسبهایی که تعیین کردید استفاده کنید:
greet(to: "Ali", from: "Sara") // چاپ: Hello, Ali! This is a greeting from Sara.
اگر برچسب to یا from را در فراخوانی ننویسید یا آنها را اشتباه بنویسید، کامپایلر خطا خواهد داد.
حذف برچسب آرگومان (Underscore)
ممکن است نخواهید هنگام فراخوانی تابع از یک برچسب خاص استفاده شود. در این صورت، میتوانید از علامت _ (Underscore) به جای برچسب بیرونی استفاده کنید. برای مثال:
func greet(_ name: String) {
print("Hello, \(name)!")
}
greet("Ali")
// برچسبی برای پارامتر وجود ندارد و مستقیماً مقدار "Ali" ارسال میشود.
در این حالت، Swift هیچ برچسبی را در زمان فراخوانی نمایش نمیدهد و مستقیماً مقدار آرگومان را میگیرد. این قابلیت گاهی برای توابع داخلی یا توابعی که در محدوده کوچکی از پروژه استفاده میشوند مفید است.
مزایای استفاده از برچسب آرگومان
خوانایی بالاتر کد: وقتی تعداد پارامترهای یک تابع زیاد باشد، وجود برچسبهای واضح کمک میکند تا راحتتر متوجه شوید کدام مقدار به کدام پارامتر مربوط است.
مستند کردن خودکار: برچسبهای مناسب، بخش زیادی از نیاز به نوشتن مستندات اضافی برای توابع را کاهش میدهند؛ چرا که هنگام فراخوانی، خود کد بیانگر هدف پارامترهاست.
جلوگیری از اشتباه: برچسبها مانع از اشتباه فرستادن پارامترها به ترتیب غلط میشوند. برای مثال، وقتی تابعی دارید که دو پارامتر از نوع یکسان دارد (مثل چندین رشته یا چندین عدد)، برچسبها به وضوح نشان میدهند هر مقدار چه معنایی دارد.
انواع بازگشتی چندگانه (Tuples)
در Swift، تاپلها (Tuples) یک ساختار قدرتمند برای گروهبندی چندین مقدار مختلف در قالب یک واحد هستند. با استفاده از تاپلها میتوانید به صورت مستقیم از یک تابع چند مقدار مجزا بازگردانید. این ویژگی به شما امکان میدهد بدون نیاز به ایجاد یک نوع دادهٔ سفارشی (مثلاً یک Struct یا Class)، مقادیر متعددی را که از نظر مفهومی به هم مرتبط هستند، در یک بسته واحد قرار دهید.
چرا از تاپل استفاده میکنیم؟
بازگرداندن چندین مقدار: اگر تابعی دارید که باید بیش از یک مقدار را بازگرداند، به جای برگرداندن یک مقدار مرکب یا تعریف ساختار جدید، میتوانید بهسادگی یک تاپل برگردانید.
سادهسازی کد: برای مثال، در بسیاری زبانهای دیگر، برای ارسال یا بازگرداندن چند مقدار، مجبور میشوید یک کلاس یا Struct موقت بسازید. اما در Swift این کار با تاپلها ساده و سریع است.
کاربردی در متدهای کوچک و سریع: تاپلها برای توابعی که عملیات ساده انجام میدهند (مثل پردازش ساده ورودی و خروجی) بسیار مفید هستند.
نحوه تعریف تاپل در خروجی توابع
هنگام تعریف تابع، بعد از علامت ->، مشخص میکنید که نوع خروجی از جنس تاپل است. اگر برای هر عنصر تاپل برچسب (Label) تعیین کنید، دسترسی به آن عنصر در زمان برگشت و استفاده از مقدار بسیار راحت خواهد بود.
func getUserInfo() -> (name: String, age: Int) {
let userName = "Mehdi"
let userAge = 27
return (userName, userAge)
}
در این مثال، getUserInfo یک تاپل را برمیگرداند که دو مقدار name و age دارد.
برچسبهای name و age باعث میشوند هنگام دریافت خروجی، بتوانیم مستقیماً با همین نامها به اعضای تاپل دسترسی داشته باشیم.
فراخوانی و استفاده از مقادیر تاپل
let userInfo = getUserInfo() print(userInfo.name) // "Mehdi" print(userInfo.age) // 27
با استفاده از نقطه (.) و نام برچسب، به سادگی مقادیر را جداگانه دریافت میکنید.
همچنین میتوانید کل تاپل را در یک متغیر ذخیره کنید و در صورت نیاز کل آن را به صورت یک واحد پاس دهید.
تعریف تاپلهای بدون برچسب
گاهی اوقات نیاز ندارید برای هر عنصر تاپل یک برچسب تعریف کنید. در این صورت، میتوانید تنها انواع داده را در خروجی مشخص کنید و در زمان بازگشت، مقدارها را در قالب (مقدار1, مقدار2, …) برگردانید:
func getCoordinates() -> (Double, Double) {
let x = 10.5
let y = 5.0
return (x, y)
}
let coordinates = getCoordinates()
print(coordinates.0) // 10.5
print(coordinates.1) // 5.0
در این حالت، برای دسترسی به هر عنصر از تاپل باید از اندیس (مثلاً 0، 1 و غیره) استفاده کنید.
این روش برای مواقعی مفید است که مقدارهای بازگشتی زیاد و یا برچسب زدن آنها چندان ضروری نباشد.
استخراج مقادیر تاپل در متغیرهای جداگانه
Swift امکان Destructuring یا همان استخراج مقادیر تاپل در چند متغیر مجزا را نیز فراهم میکند. اگر تابع شما تاپل برمیگرداند، میتوانید این مقادیر را مستقیماً در چند متغیر جای دهید:
let (userName, userAge) = getUserInfo() print(userName) // Mehdi print(userAge) // 27
در این حالت، لازم نیست حتماً از userInfo.name و userInfo.age استفاده کنید.
میتوانید از این ویژگی برای کاهش تعداد خطوط کد و افزایش خوانایی در برخی سناریوها بهره ببرید.
مزایا و محدودیتها
مزایا
سرعت توسعه: برای بازگرداندن چند مقدار از یک تابع، سریع و بدون دردسر عمل میکند.
سادگی استفاده: تعریف و دسترسی به مقادیر تاپل بسیار ساده و شهودی است.
پرکاربرد در کدهای کوچک: در پروژههای کوچک یا توابعی که هدفشان صرفاً بازگرداندن چند مقدار کوتاه است، استفاده از تاپل میتواند خوانایی را بالاتر ببرد.
محدودیتها
عدم مقیاسپذیری بالا: اگر تابعی دارید که باید ۱۰ مقدار مختلف برگرداند، احتمالاً تاپل انتخاب مناسبی نیست و بهتر است از یک ساختار (Struct) یا کلاس (Class) با مفهوم مشخص استفاده کنید.
نبود قابلیتهای OOP: تاپلها فاقد متد، ویژگی ارثبری و دیگر خصوصیات شیءگرایی هستند. در نتیجه، برای مدلسازی یک موجودیت پیچیده مناسب نیستند. کاهش خوانایی در تعداد بالای عناصر: اگر تاپلها شامل تعداد زیادی مقدار یا انواع داده نامرتبط باشند، ممکن است کد را شلوغ کنند و فهمیدن نقش هر مقدار دشوار شود.
نکتههای تکمیلی
تاپل به عنوان واسط موقت: اگر نیاز دارید دو یا چند مقدار را با هم پاس بدهید یا برگردانید، اما نمیخواهید یک Struct جدید بسازید، تاپل گزینهای عالی است.
ترکیب با Switch: در Swift، میتوانید از تاپلها در دستورات switch برای الگوهای پیچیده استفاده کنید. مثلاً:
let point = (x: 10, y: -5)
switch point {
case (0, 0):
print("Origin")
case (let x, let y) where x > 0 && y >= 0:
print("Top-right quadrant")
default:
print("Elsewhere")
}
تاپل در پارامترها: میتوانید یک تاپل را به عنوان پارامتر یک تابع تعریف کنید؛ به این شکل، تابع شما با یک واحد ورودی چندین مقدار دریافت میکند.
Closureها
Closures یا توابع بدون نام، بلوکهای کدی هستند که میتوانید آنها را مشابه توابع معمولی تعریف و استفاده کنید. تفاوت اصلی Closure با توابع، در نحوه تعریف و استفاده آنهاست. Closureها انعطاف بالایی در پاس دادن به عنوان آرگومان یک تابع یا ذخیره در متغیر دارند و در نتیجه بسیاری از الگوهای طراحی را سادهتر میکنند.
تعریف Closure
یک Closure ساده در Swift به شکل زیر تعریف میشود:
let sayHello = { (name: String) -> String in
return "Hello, \(name)!"
}
قسمتهای مختلف یک Closure
محل قرارگیری پارامترها: (name: String) -> String
در پرانتز نخست، پارامترهای ورودیClosure مشخص میشوند؛ در اینجا پارامتر name از نوع String است.
بعد از ->، نوع مقدار خروجی Closure تعیین میشود (در این مثال، String).
کلیدواژه in: پس از تعریف پارامترها و نوع خروجی، از in استفاده میکنیم تا به کامپایلر Swift بگوییم که بخش اعلان تمام شده و بدنه اصلی Closure آغاز میشود.
بدنه Closure: کدی است که پس از کلمه in میآید و کار اصلی Closure را انجام میدهد. در مثال بالا، بدنه Closure مقدار “Hello, \(name)!” را بازمیگرداند.
نکات مهم در تعریف Closure
نوعدهی صریح و ضمنی: اگر نوع ورودی و خروجی Closure از متن کد قابل استنباط باشد، میتوان آن را سادهتر نوشت. برای مثال، اگر یک تابع انتظار Closureای دریافت کند که نوع ورودی و خروجیاش مشخص است، درون Closure دیگر مجبور به اعلام صریح (name: String) -> String نخواهید بود.
Return کوتاه: اگر تنها یک خط کد وجود داشته باشد، میتوانید از Swift 5.1 به بعد، از بازگشت ضمنی (Implicit Return) استفاده کرده و return را حذف کنید.
پارامترهای ناشناس: در Closureها میتوانید از پارامترهای ناشناس ($0, $1, …) استفاده کنید که اغلب در توابعی مانند map، filter و غیره مشاهده میشود.
استفاده از Closure
پس از تعریف یک Closure در یک متغیر یا ثابت، میتوانید درست مانند یک تابع معمولی آن را صدا بزنید:
let sayHello = { (name: String) -> String in
return "Hello, \(name)!"
}
let greeting = sayHello("Ali")
print(greeting) // Hello, Ali!
در این مثال، sayHello رفتاری مانند یک تابع دارد و میتوانید پارامتر name را در پرانتز وارد کرده و نتیجه را ذخیره یا چاپ کنید.
با این تفاوت که Closure نام مستقل یا کلیدواژه func ندارد و در عوض بهصورت یک مقدار (Value) در متغیر قرار گرفته است.
نحو ساده شده (Trailing Closure)
یکی از قابلیتهای جذاب Swift برای کار با Closureها، سینتکس Trailing Closure است. وقتی آخرین پارامتر یک تابع، یک Closure باشد، میتوانید آن Closure را از پرانتز توابع جدا کرده و بهصورت یک بلوک کد در ادامه فراخوانی بیاورید. این رویکرد باعث خوانایی بیشتر و کاهش پرانتزهای تو در تو میشود.
به مثال زیر دقت کنید:
func performOperationOnArray(numbers: [Int], operation: (Int) -> Int) -> [Int] {
var result: [Int] = []
for number in numbers {
result.append(operation(number))
}
return result
}
let inputArray = [1, 2, 3, 4]
let outputArray = performOperationOnArray(numbers: inputArray) { number in
return number * 2
}
print(outputArray) // [2, 4, 6, 8]
نحوه کار Trailing Closure
تعریف تابع performOperationOnArray(numbers:operation:):
دو پارامتر دارد؛ پارامتر نخست numbers از نوع [Int] و پارامتر دوم operation که خودش یک Closure بهشکل (Int) -> Int است.
توجه کنید که operation آخرین پارامتر تابع است.
فراخوانی تابع به شکل Trailing Closure:
performOperationOnArray(numbers: inputArray) { number in
return number * 2
}
اگر از سینتکس معمولی استفاده میکردیم، باید تابع را اینطور فراخوانی میکردیم:
performOperationOnArray(numbers: inputArray, operation: { number in
return number * 2
})
با Trailing Closure، دیگر نیازی به قرار دادن Closure در پرانتز نیست و این موضوع کد را تمیزتر و خواناتر میکند.
مزایای Trailing Closure
خوانایی بالاتر: وقتی Closure طولانی است، قرار دادن آن در انتهای تابع (خارج از پرانتز فراخوانی) به وضوح کد و درک آسانتر آن کمک میکند.
کد کمتر: از آنجا که شما دیگر نیاز ندارید کلمه کلیدی مثل operation: را در فراخوانی بیاورید، کد تمیزتری خواهید داشت.
رایج در توابع استاندارد Swift: بسیاری از توابع کتابخانه استاندارد مانند map, filter, reduce نیز از این الگو تبعیت میکنند و با استفاده از سینتکس Trailing Closure، نوشتن کدهای تابعی (Functional) را در Swift بسیار لذتبخش میکند.
چند مثال بیشتر از Trailing Closure
مثال با تابع map:
let numbers = [1, 2, 3, 4]
let doubledNumbers = numbers.map { number in
return number * 2
}
print(doubledNumbers) // [2, 4, 6, 8]
در این مثال، map آخرین پارامترش یک Closure است (تابع تبدیل از نوع (Element) -> T). به همین دلیل میتوانیم از Trailing Closure استفاده کنیم.
مثال با چند پارامتر: اگر تابع بیش از یک پارامتر داشته باشد و Closure آخرین پارامتر باشد، سایر پارامترها همچنان باید در پرانتز بیایند:
func doOperation(a: Int, b: Int, operation: (Int, Int) -> Int) -> Int {
return operation(a, b)
}
let sum = doOperation(a: 5, b: 10) { x, y in
return x + y
}
print(sum) // 15
همانطور که میبینید، پارامترهای a و b همچنان در پرانتز هستند، ولی Closure به صورت Trailing در انتها قرار دارد.
نکات تکمیلی
استفاده از $0, $1, …: در Closureهایی که بلافاصله در محل خود تابع تعریف میشوند (مثلاً در map, filter, …)، میتوانید از پارامترهای پیشفرض $0, $1, … استفاده کنید. این کار در صورت کوتاهبودن بدنه Closure به تمیزتر شدن کد منجر میشود:
let doubled = numbers.map { $0 * 2 }
این شکل بسیار موجزتر از map { number in number * 2 } است و وقتی کد طولانی شود، باید خوانایی و سادگی را متعادل کرد.
Trailing Closure در چند پارامتر: اگر تابع شما دو پارامتر Closure داشته باشد، تنها Closure آخر میتواند به صورت Trailing Closure بیاید. برای Closure اول، یا باید از پرانتز معمولی استفاده کنید، یا از توابع جنریک متنوعی استفاده کنید که این ساختار را مدیریت میکنند.
تطابق با Function Overloading: گاهی میتوانید نسخههای مختلف یک تابع را بنویسید که یکیClosureای ندارد و دیگریClosureای بهعنوان آخرین پارامتر دارد. Swift با توجه به نوع فراخوانی تشخیص میدهد کدام را فراخوانی کند.
مقداردهی به متغیرهای بیرونی (Capturing Values)
وقتی در مورد مقداردهی به متغیرهای بیرونی (Capturing Values) صحبت میکنیم، به قابلیتی منحصربهفرد در Swift اشاره داریم که در آن، یک Closure میتواند به متغیرها و ثوابت محیط بیرونی (Scope) خود دسترسی پیدا کند، آنها را در حافظه نگه دارد و در فراخوانیهای بعدی Closure همچنان از آنها استفاده کند. این رفتار باعث میشود Closure به نوعی یک وضعیت داخلی (State) داشته باشد که در طول زمان تغییر میکند و با خروج از تابع ایجادکننده از بین نمیرود. در ادامه، به جزئیات این موضوع و تفاوت آن با توابع معمولی خواهیم پرداخت.
مفهوم Capturing Values در Closure
بهطور ساده، Closure میتواند متغیرهای محیط بیرونی که در همان محدوده (Scope) تعریف شدهاند را به دام بیندازد (Capture) و حتی پس از پایان اجرای تابع اصلی، آن متغیرها همچنان در حافظه باقی بمانند. در مثال زیر:
func makeCounter() -> () -> Int {
var count = 0
let counterClosure: () -> Int = {
count += 1
return count
}
return counterClosure
}
let counter1 = makeCounter()
print(counter1()) // 1
print(counter1()) // 2
print(counter1()) // 3
متغیر count درون تابع makeCounter تعریف شده است.
با این حال، وقتی تابع خاتمه مییابد، Closure برگردانده شده همچنان به count دسترسی دارد و آن را نگه میدارد.
هر بار که counter1() صدا زده شود، مقدار count یک واحد افزایش مییابد.
این ویژگی به Closure امکان میدهد تا وضعیت درونی خود را در طول فراخوانیهای مختلف حفظ کند.
چرا این اتفاق میافتد؟
در زبانهای برنامهنویسی سنتی (مثلاً C)، اگر متغیری درون یک تابع تعریف شده باشد، با اتمام تابع از بین میرود. اما Swift برای Closure سازوکار خاصی را پیادهسازی میکند:
زمانی که Swift متوجه میشود یک Closure به متغیری بیرونی نیاز دارد، آن متغیر را در محیطی ذخیره (Capture) میکند که عمرش به طول عمر Closure گره میخورد.
تا زمانی که Closure زنده است و جایی در کد استفاده میشود، مقادیر Capture شده نیز از بین نمیروند.
این منجر به ایجاد محیط Closure (Closure Environment) میشود که در آن متغیرهای لازم، همراه Closure حفظ میشوند.
کاربردهای عملی Capturing Values
مدیریت وضعیت (State): برای ایجاد شمارنده (Counter) یا هر آبجکتی که باید در طول زمان تغییر کند و وضعیتش حفظ شود، میتوان از این روش استفاده کرد. مثال makeCounter() نمونهای از این کاربرد است.
پردازشهای تودرتو: گاه لازم است Closure درون تابعی تعریف شود که اطلاعات و دادههایی را آماده میکند و Closure از همان دادهها استفاده کرده و نتیجه را برمیگرداند. بدین ترتیب نیازی نیست آن دادهها در سطح سراسری قرار بگیرند.
Callbacks: زمانی که Closure بهعنوان completion handler یا callback مورد استفاده قرار میگیرد و لازم است دادههایی از محیط بیرونی را دستکاری کند، Capturing Values بسیار مفید خواهد بود.
تفاوت با توابع معمولی
نامگذاری: Closureها بر خلاف توابع معمولی، نیازی به نام ندارند و میتوانند بهعنوان یک مقدار (Value) در متغیرها یا ثوابت ذخیره شوند. اما یک تابع معمولی همیشه با func و یک نام مشخص تعریف میشود.
نگه داشتن وضعیت: در توابع معمولی، اگر از متغیری بیرونی استفاده شود، بعد از خروج از تابع مقدار آن متغیر (در حالت Value Type) دیگر از نظر آن تابع دسترسپذیر نیست؛ مگر اینکه صراحتاً از ساختارهای دیگری مثل متغیرهای Global یا کلاسها (که از نوع Reference هستند) استفاده کنیم. Closureها بدون نیاز به تعریف صریح متغیرهای سراسری یا Reference Type، میتوانند متغیر بیرونی را جذب کنند.
استفاده بهعنوان آرگومان: Closureها را میتوان بهراحتی در زمان اجرای کد ساخت و به توابع دیگر فرستاد. این کار با توابع معمولی هم ممکن است (با استفاده از ارجاع به تابع)، اما انعطاف Closureها معمولاً بیشتر است، بهویژه زمانی که نیاز به تعریف منطق در همان محل فراخوانی داریم.
نکات مهم و ملاحظات
الف) Strong Reference Cycles
وقتی یک Closure متغیری را Capture میکند که از نوع کلاس است (Reference Type)، احتمال به وجود آمدن چرخههای مرجع (Reference Cycles) وجود دارد. برای مثال، اگر Closure به self (که یک آبجکت کلاس است) اشاره کند و آن آبجکت هم Closure را در یکی از پراپرتیهایش نگه دارد، هر دو یکدیگر را نگه میدارند و از بین نمیروند. برای اجتناب از این مشکل میتوان از [weak self] یا [unowned self] در لیست Capture استفاده کرد:
class MyClass {
var value = 0
lazy var closure: () -> Void = { [weak self] in
self?.value += 1
}
}
با این کار، Closure یک ارجاع ضعیف (Weak Reference) به self دارد و از ماندگار شدن غیرضروری آبجکت در حافظه جلوگیری میشود.
ب) نوعهای مقداری در Capture
اگر متغیری از نوع Struct یا Enum باشد، در زمان Capture، کپی میشود (مگر آنکه به صورت inout یا با روشهای دیگر رفتار شود). بنابراین، تغییر آن در Closure روی نسخه اصلی تأثیری ندارد، مگر اینکه از پارامترهای inout استفاده کنید یا از Structهایی که در متدهایشان mutating وجود دارد. در مثال Counter بالا، از یک متغیر در محدوده (Scope) تابع استفاده شده بود؛ Swift به شکل هوشمندانه آن را درون محیط Closure قرار داده و اجازه تغییر داده است (این سازوکار در سطح کامپایل زبان مدیریت میشود).
ج) نامگذاری متغیرها
وقتی متغیرهای بیرونی را Capture میکنید، بهتر است برای پیشگیری از سردرگمی، نامهای گویا انتخاب کرده و با کامنت یا توضیح مشخص کنید که این Closure از چه متغیرهایی در محیط بیرونی استفاده میکند. در پروژههای بزرگ، این کار در خوانایی کد بسیار مفید است.
مثالهای بیشتر از Capturing Values
مثال ۱: Closure تودرتو در یک تابع محاسباتی
func calculateScores(baseScore: Int) -> (Int) -> Int {
var totalScore = baseScore
let closure: (Int) -> Int = { bonus in
totalScore += bonus
return totalScore
}
return closure
}
let scoreCalculator = calculateScores(baseScore: 100)
print(scoreCalculator(10)) // 110
print(scoreCalculator(5)) // 115
totalScore در محدوده تابع calculateScores تعریف شده، اما Closure آن را Capture میکند.
هر بار که Closure اجرا میشود، مقدار جدیدی به totalScore اضافه شده و مقدار نهایی بازگردانده میشود.
مثال ۲: نگهداری وضعیت با چند Closure
func createAccountManager(initialBalance: Int) -> (deposit: (Int) -> Int, withdraw: (Int) -> Int) {
var balance = initialBalance
let depositClosure = { (amount: Int) -> Int in
balance += amount
return balance
}
let withdrawClosure = { (amount: Int) -> Int in
balance -= amount
return balance
}
return (depositClosure, withdrawClosure)
}
let account = createAccountManager(initialBalance: 1000)
print(account.deposit(500)) // 1500
print(account.withdraw(200)) // 1300
در این مثال، دو Closure مختلف (depositClosure و withdrawClosure) هر دو به متغیر balance در محیط بیرونی دسترسی دارند.
پس از ایجاد از طریق createAccountManager, با هر بار فراخوانی deposit یا withdraw, مقدار balance تغییر میکند ولی در همان محدوده Closureها باقی میماند.
توضیحات تکمیلی
در بخش توضیحات تکمیلی، معمولاً به نکات کلیدی و کاربردی میپردازیم که ممکن است در نگاه اول چندان مورد توجه قرار نگیرند اما در عمل تأثیر زیادی بر کیفیت کدنویسی، بهینهسازی و طراحی معماری کد در پروژههای Swift دارند. در اینجا سه محور اصلی را مورد بررسی قرار میدهیم: بهینهسازی، اکسپرسشدن کد و مقایسه نامگذاری. هرکدام از این موارد به نحوی با توابع و Closureها در Swift در ارتباط هستند و شناخت عمیقتر آنها میتواند شما را در انتخاب درست بین استفاده از Closure یا تابع یاری کند.
۱. بهینهسازی در Closureها
Swift یک زبان کامپایلری است و به خاطر استفاده از LLVM (Low Level Virtual Machine)، از بهینهسازیهای زیادی در زمان کامپایل و لینک نهایی کد بهره میبرد. این بهویژه در مورد Closureها نیز صدق میکند و باعث میشود که عملکرد آنها در قیاس با زبانهایی که Closure یا Callback را در زمان اجرا تفسیر (Interpret) میکنند، بهینهتر باشد.
جایگزین موقت برای توابع حجیم: اگر یک عملیات کوتاه و پرتکرار دارید که قرار است صرفاً در جای مشخصی استفاده شود، Closure انتخاب مناسبی است و Swift با انجام Inline کردن و سایر بهینهسازیها میتواند کد نهایی را سریعتر کند.
کاهش فراخوانی توابع: یکی از هزینههای رایج در زمان اجرا، Overhead فراخوانی تابع (Function Call) است. با استفاده از برخی الگوهای Closure، کامپایلر گاهی میتواند این فراخوانیها را کاهش داده و عملکرد را بهتر کند.
بدون سربار مفسر: در زبانهایی که Closureها بهصورت زبانهای تفسیری (Interpreted) یا Bytecode پیادهسازی میشوند، سربار زمان اجرا (Runtime Overhead) بیشتری وجود دارد. اما در Swift، Closureها مستقیم به کد ماشین کامپایل میشوند و این باعث بهبود سرعت میشود.
نکته: هرچند Closureها در Swift بهینه هستند، اما انتخاب بین تابع و Closure نباید تنها بر اساس سرعت انجام شود. ساختار کد خوانا و مفهومی نیز بسیار مهم است.
۲. اکسپرسشدن کد (Expressiveness)
یکی از دلایلی که Closureها در زبان Swift محبوبیت زیادی دارند، اکسپرسشدن (Expressiveness) کد است. مفهوم اصلی این است که چقدر میتوانید منطق خود را به نحوی خلاصه، روان و قابل فهم بیان کنید؟ وقتی از Closure استفاده میکنید، میتوانید عملیات مورد نظر را دقیقاً در جایی که نیاز دارید تعریف کنید. این رویکرد در الگوهای برنامهنویسی تابعی (Functional Programming) بسیار مرسوم است.
کوتاهی و خوانایی:
Closureها به شما اجازه میدهند یک تکه منطق را مستقیماً به یک تابع دیگر (مانند map, filter, reduce) پاس دهید، بدون اینکه نیاز باشد چندین خط کد برای معرفی و نامگذاری یک تابع بنویسید.
let numbers = [1, 2, 3, 4]
let doubled = numbers.map { $0 * 2 }
این مثال نشان میدهد که چگونه میتوان عملیات ضرب در دو را مستقیماً در محل فراخوانی تعریف کرد و کد را بسیار موجز و گویا ساخت.
حذف ساختارهای اضافه:
در برخی موارد، نیازی به تعریف تابع جداگانه ندارید؛ چرا که آن منطق یک بار مصرف است یا بسیار ساده است. Closureها مانع از شلوغشدن فایلهای کد با توابع متعدد میشوند.
شناخت سریع هدف:
زمانی که در کدتان از Closure استفاده میکنید، بلافاصله روشن است که چه عملیاتی قرار است انجام شود (بهخصوص اگر Closure کوتاه باشد). این امر میتواند خوانایی را بهبود ببخشد.
۳. مقایسه نامگذاری: کی تابع تعریف کنیم و کی Closure؟
الف) موارد مناسب برای تعریف تابع معمولی
استفاده مکرر: اگر عملیاتی دارید که قرار است چندین بار در بخشهای مختلف برنامه تکرار شود و نامگذاری آن به خوانایی کد کمک کند، بهتر است یک تابع معمولی بسازید.
منطق پیچیده: اگر بدنه عملیات بزرگ و چندخطی است یا نیازمند مستند کردن ورودی و خروجی (پارامترها، نوع بازگشتی) است، تابع معمولی میتواند شفافیت بیشتری ایجاد کند.
نیاز به خودمستندی: وقتی قرار است افراد مختلف یا اعضای تیم از آن منطق استفاده کنند، تعریف تابعی با نام و مستندات کامل، فهم آن را سادهتر میکند.
ب) موارد مناسب برای استفاده از Closure
یک بار مصرف: اگر فقط در یک نقطه از کد نیاز دارید منطق سادهای را تعریف کنید (خصوصاً زمانی که پارامتر یک تابع دیگر است)، Closure ایدئال است.
کوتاهی بدنه: Closureها زمانی خوانایی بیشتری دارند که بدنه آنها مختصر باشد و بتوانید بهسرعت معنایش را متوجه شوید (مثال ساده map { $0 * 2 }).
کپسولهکردن وضعیت: زمانی که میخواهید از مزیتهای Capturing Values استفاده کنید و حالت (State) درونی داشته باشید، Closureها راهحل قدرتمندی ارائه میدهند.
یک روش تشخیص: اگر نامگذاری خاصی برای عملیات موردنظر در ذهنتان ندارید (چون شاید آن عملیات خیلی ساده باشد یا فقط یک بار استفاده شود)، احتمالاً Closure انتخاب بهتری است. اما اگر یک نام مشخص برای این عملیات مفید به نظر میرسد (مثلاً calculateDiscount, fetchUserInfo)، احتمالاً تعریف یک تابع معمولی میتواند به کد نظم ببخشد.
نتیجهگیری
در این مقاله به شکل جامع به مفاهیم توابع و Closureها در Swift پرداختیم و دیدیم چگونه میتوان از انواع پارامترها، مقدارهای بازگشتی چندگانه، و تکنیک Capturing Values برای ساخت کدهای تمیز و بهینه بهره برد. با درک درست این مفاهیم، میتوانید ساختار برنامههایتان را بهبود دهید، از تکرار کد جلوگیری کنید و بهطور موثر از مزیتهای قدرتمند زبان Swift در توسعه اپلیکیشنها استفاده کنید. در نهایت، انتخاب آگاهانه بین تابع یا Closure بر اساس نیاز پروژه و میزان خوانایی کد، نقش بسزایی در افزایش کیفیت نرمافزار و تجربه توسعهدهنده خواهد داشت.
