ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
توابع
هیچچیزی توی دستور زبان Swift بهاندازهی روشی که شما توابع رو تعریف و فراخوانی میکنین، خاص نیست. احتمالاً هیچچیزی هم اینقدر اهمیت نداره! همونطور که توی مباحث قبلی گفتم، تمام کد شما توی توابع نوشته میشه و اینجاست که تمام اتفاقات رخ میده. در ادامه، مرحله به مرحله کار با توابع رو در سویفت یاد میدیم.
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
پارامترها و مقدار بازگشتی توابع
یه تابع شبیه یه ماشینحساب حرفهای هست که محاسبات ریاضی پیچیدهی شما رو براتون انجام میدن. شما فقط ورودیها رو بهش میدین و اون هم یه چیزی بعنوان خروجی تحویلتون میده. اگه بخوایم فنیتر صحبت کنیم، یه تابع ورودیها رو تحتعنوان «پارامتر» (Parameter) دریافت میکنه و اگه خروجی خاصی رو بهمون برگردونه، به اون خروجی «نتیجه»ی اجرای تابع میگیم. یه تابع ممکنه چیزی رو برنگردونه و فقط وقتی صداش زدیم، براساس ورودیهایی که بهش دادیم (یا حتی بدون ورودی خاصی) یه کارهایی رو برامون انجام بده. برای مثال، این تابع ساده ولی معتبر رو ببینین که دو ورودی Int میگیره و اونها رو با هم جمع میزنه و نتیجهی جمع رو برمیگردونه:
func sum (_ x:Int, _ y:Int) -> Int {
let result = x + y
return result
}
دستور زبان در اینجا خیلی سختگیرانه است و بهخوبی طراحی شده و اگه اونو بهخوبی درک نکنین، نمیتونین از Swift استفاده کنین. پس اجازه بدین اینجا توقف کنیم و جزئیات کار رو بهدقت توضیح بدیم. من خط اول رو به چند قسمت میشکنم تا بتونم اونها رو جداگانه توضیح بدم:
func sum // 1
(_ x:Int, _ y:Int) // 2,3
-> Int // 4
{ // 5
let result = x + y // 6
return result // 7
} // 8
- تعریف تابع با کلمهی کلیدی func و «اسم» این تابع شروع میشه که در اینجا sum هست. این اسم، چیزیه که باید برای «صدازدن» تابع (یعنی اجرای کدی که داخل تابع هست) ازش استفاده کنین.
- بعد از اسم تابع، «فهرست پارامترها» نوشته میشه که حداقل شامل پرانتزها هست. اگه این تابع پارامتر (ورودی) داشته باشه، باید اونها رو داخل پرانتزها بنویسیم و با کاما از هم جدا کنیم. هر پارامتر یه ساختار دقیق داره: «اسم» پارامتر، یه کارکتر دونقطه و بعد «نوع» پارامتر
- تعریف این تابع خاص، یه کارکتر خطزیر (Unserscore) و یه فاصله هم قبلاز اسم هر پارامتر داره. «نمیخوام الان درموردش هیچ توضیحی بدم». فقط توی این مثال بهش احتیاج داشتیم. بنابراین بهم اعتماد کنین و بگذارین بعداً درموردش توضیح بدم.
- بعد از پرانتزها یه عملگر فلش -> هست که بعدش نوع مقداری که این تابع بعنوان نتیجه برمیگردونه نوشته میشه.
- حالا آکولاد باز رو مینویسیم و از اینجا، بدنهی تابع شروع میشه (کد واقعی داخلش).
- داخل آکولادها یعنی توی بدنهی تابع، متغیرهایی که بعنوان پارامتر تعریف شدن، زنده میشن و میتونیم از اونها با همون اسامی و نوعی که براشون مشخص شده، استفاده کنیم.
- اگه تابع قراره یه مقدار برگردونه، باید اینکار رو با کلمهی کلیدی return و بعدش هم مقدار موردنظر انجام بده و تعجبی هم نداره که بگیم نوع مقدار مربوطه باید با نوعی که قبلاً برای مقدار بازگشتی تابع تعریف کردیم (بعد از عملگر فلش) مطابقت داشته باشه.
- در انتها هم آکولاد بسته رو مینویسیم و بدنهی تابع و تعریفش خاتمه پیدا میکنه.
چند نکتهی دیگه هم وجود داره که درمورد پارامترها و نوع بازگشتی تابعمون باید بهتون بگم:
- پارامترها: تابع sum ما انتظار دو پارامتر داره: یه Int که بهش اسم x دادیم و یه Int دیگه که اسمش y هست. بدنهی تابع نمیتونه بدون اینکه یهجای دیگه اون رو فراخوانی کنه و مقادیر واقعی رو که از نوع پارامترهای تابع هستن، براش بفرسته. درحقیقت اگه بخوایم این تابع رو بدون ارسال مقداری برای هرکدوم از این دو پارامتر صدا بزنیم یا نوع مقادیر ارسالی با نوع x و y مطابقت نداشته باشه، اجرای برنامه باعث میشه کامپایلر ما رو با یه پیغام خطای خوشگل متوقف کنه! توی بدنهی تابع میتونیم از این دو متغیر استفاده کنیم و از مقادیر ارسالی با کمک اسامی متغیرهای x و y متناظر با هر مقدار استفاده کنیم و خیالمون هم راحت باشه که این مقادیر وجود دارن و حتماً Int هستن. این موضوع هم خیال برنامهنویس رو راحت میکنه و هم خیال کامپایلر رو. ضمناً اینو هم درنظر بگیرین که این اسامی x و y موقت هستن و کاملاً توی این تابع، محلی (داخلی) محسوب میشن. درواقع با هر x و y دیگه که توی یه تابع دیگه یا توی سطح بالاتر از این تابع تعریف شده باشه، فرق میکنن. این اسامی فقط برای این تعریف شدن که بتونیم به مقادیری که موقع فراخوانی تابع براش ارسال میشن، توی تابع دسترسی داشته باشیم. بنابراین، تعریف پارامترها درحقیقت یهجور تعریف متغیره: ما متغیرهای x و y رو برای استفاده داخل این تابع تعریف میکنیم.
- نوع بازگشتی: آخرین دستور در بدنهی تابع sum ما مقدار یه متغیر بهاسم result رو برمیگردونه. این متغیر با جمعزدن دو مقدار Int تعریف شده و درنتیجه خودش هم یه Int هست که همون نوعی محسوب میشه که انتظار داریم این تابع تولید کنه. اگه سعی میکردم یه String برگردونم (مثلاً با دستور return "hello") یا کلاً دستور return رو نمیگذاشتم، کامپایلر با یه خطا من رو متوقف میکرد. دقتکنید که کلمهی کلیدی return درحقیقت «دو» کار انجام میده. اول اینکه مقدار همراهش رو برمیگردونه و دوم اینکه اجرای تابع رو هم خاتمه میده و جلوی اجرای دستورات بعدی رو توی تابع میگیره. میتونیم توی یه تابع، بیشتر از یکبار از دستور return استفاده کنیم ولی کامپایلر درصورتیکه اینکار رو بهنحوی انجام داده باشیم که برخی از خطوط تابع تحت هیچ شرایطی اجرا نشن، بهمون هشدار میده. مثلاً بدون اینکه شرط خاصی چک کرده باشیم، دستور return رو بنویسیم و بعدش یه دستور دیگه بیاریم. در اینجا چون «حتماً» دستور return اجرا میشه، دستور بعدی هیچوقت اجرا نخواهد شد و کامپایلر این موضوع رو به ما درقالب یه هشدار (نَه خطا) یادآوری میکنه.
تعریف تابع که قبلاز آکولادها میاد، بطور خلاصه یه «قرارداد» دربارهی انواع مقادیری هست که بعنوان ورودی استفاده میشن و نوع چیزی که بعنوان خروجی تولید میشه. براساس این قرارداد، تابع «انتظار» تعداد مشخصی پارامتر داره که هرکدوم نوع مشخصی هم دارن و «محصول» خروجی اون هم نوع مشخصی داره. همهچیز باید با این قرارداد مطابقت داشته باشه. بدنهی تابع (داخل آکولادها) میتونه از پارامترها مثل متغیرهای محلی استفاده کنه و نوع مقدار بازگشتی هم باید با نوعی که توی تعریف تابع مشخص شده مطابقت داشته باشه.
همین قرارداد درمورد کدی که این تابع رو «صدا میزنه» هم وجود داره. در اینجا کدی هست که تابع sum ما رو صدا میزنه:
let z = sum(4, 5)
توجهتون رو به سمت راست علامت تساوی یعنی sum(4, 5) جلب میکنم. اینجا جایی هست که تابع رو صدا زدیم. ساختارش چطوریه؟ داریم از «اسم» تابع استفاده میکنه. بعد از اسم، پرانتزها نوشته شدن و داخلشون هم مقادیری که برای هرکدوم از پارامترهای تابع ارسال میشن رو نوشتیم که با کاما از هم جدا شدن. ازنظر فنی، این مقادیر آرگومان (Argument) نام دارن. در اینجا من از مقادیر سادهی Int استفاده کردم ولی کاملاً آزاد هستیم که از متغیرهای Int بجاشون استفاده کنیم. تنها چیزی که لازمه اینه که نوع آرگومانها با نوع پارامترهای تابع یکسان باشه:
let x = 4
let y = 5
let z = sum(y, x)
توی این کد، من عمداً از اسامی x و y استفاده کردم و باز هم عمداً اونها رو با ترتیب جابجا موقع فراخوانی نوشتم تا متوجه بشیم که این اسامی هیچ ربطی به اسامی x و y داخل فهرست پارامترهای تابع و بدنهی اون ندارن. این اسامی با هیچ روش جادویی وارد تابع نمیشن. فقط «مقدار» اونها مهمه و این مقدارشونه که آرگومان رو تشکیل میده نه اسمشون.
چی سر مقدار بازگشتی تابع میاد؟ این مقدار در جایی که تابع رو صدا زدیم، «جایگزین» فراخوانی تابع میشه. توی کد قبلی، مقداری که تابع برمیگردونه عدد 9 هست و درنتیجه خط آخر کدمون دقیقاً معادل این دستوره:
let z = 9
برنامهنویس و کامپایلر هر دو میدونن که این تابع چه نوع مقداری برمیگردونه؛ پس هردو میفهمن که کجا میشه این تابع رو صدا زد و کجا نمیشه. استفاده از این تابع برای مقداردهی اولیهی متغیر z مجازه چون خروجی این تابع هم مثل عدد 9، یه Int هست و درنتیجه z بعنوان یه متغیر Int تعریف میشه. اما نوشتن چنین کدی مجاز نیست:
let z = sum(4, 5) + "hello" // compile error
چون sum یه مقدار Int برمیگردونه و این دستور بهمعنای اضافهکردن یه Int به یه String هست و بطور پیشفرض، شما نمیتونین چنین کاری رو توی سویفت انجام بدین.
دقتکنید که میتونیم مقدار بازگشتی تولیدشده با فراخوانی یه تابع رو نادیده بگیریم:
sum(4, 5)
این کد در این شرایط خاص، یه مقدار احمقانه بنظر میاد چون ما از تابع sum خودمون خواستیم تمام دردسر اضافهکردن 4 و 5 رو برای ما انجام بده و بعد جوابی که برامون تولیدکرده رو ریختیم دور بدون اینکه ازش استفاده کنیم. کامپایلر این موضوع رو میفهمه و به ما یه هشدار میده که میگه نمیتونیم از خروجی این فراخوانی تابع استفاده کنیم. بااینحال هشدار بهمعنای خطا نیست و این کد کاملاً مجازه. درحقیقت شرایط زیادی پیش میاد که نادیدهگرفتن خروجی یک تابع، معنا داره و حتی میتونه مفید هم باشه. اون تابع ممکنه خیلی کارهای دیگه هم توی بدنهی خودش انجام بده و هدف ما از فراخوانیش، اجرای اون کدها باشه نه کار با مقداری که بعنوان نتیجه برمیگردونه (ازنظر فنی به اون کارها میگن اثرات جانبی یا Side Effects).
نقل قول:اگه عمداً نتیجهی بازگشتی فراخوانی یه تابع رو نادیده میگیرین، میتونین هشدار کامپایلر رو با قراردادن خروجی تابع توی یه Underscore یا خطزیر (یه متغیر بدوننام)، ساکت کنین. برای مثال:
_ = sum(4, 5)
همچنین اگه تابعی که فراخوانی میکنین رو خودتون تعریف کردین، میتونین قبلاز تعریف تابع از @discardableResult استفاده کنین تا به کامپایلر بگین که خروجی این تابع، اجازهی نادیده گرفتهشدن رو داره.
بنظرتون اینکه تابع sum رو میشه هرجایی که یه Int قابل استفاده است، فراخوانی کنین و پارامترهاش هم Int هستن، به این معنا نیست که میشه sum رو داخل یه فراخوانی sum صدا زد؟ البته که میشه! این کد کاملاً مجاز (و حتی معنادار) محسوب میشه:
let z = sum(4, sum(5, 6))
تنها بحثی که درمورد چنین کدی وجود داره اینه که ممکنه باعث بشه خودتون گیج بشین و اشکالزدایی برنامه رو در آینده کمی مشکلتر کنه؛ ولی ازنظر فنی مجاز و یهجورایی طبیعی و رایج محسوب میشه.
نوع بازگشتی و پارامترهای Void (خالی)
اجازه بدین به تعریفمون از تابع برگردیم. با توجه به پارامترها و نوع بازگشتی یه تابع، دو مورد خاص وجود داره که به ما اجازه میده تعریف یه تابع رو کمی خلاصهتر کنیم:
مشخصه که یه تابع میتونه هم مقدار بازگشتی نداشته باشه و هم پارامتر ورودی. این کدها هر سه روش تعریف چنین تابعی رو نشون میدن:
func greet1() -> Void { print("hello") }
func greet2() -> () { print("hello") }
func greet3() { print("hello") }
درست همونطور که نمیشه موقع تعریف یه تابع، پرانتزها رو حذف کرد، موقع فراخوانی اون هم باید حتماً پرانتزها رو بنویسین و اگه تابع مربوطه، پارامتر ورودی نداره، موقع فراخوانی هم پرانتزهاش خالی خواهد بود:
greet1()
به پرانتزها دقت کنید!
[quote]حتی یه تابع که مقداری رو برنمیگردونه هم ازنظر فنی یه مقدار «برمیگردونه» که در اصل یه مقدار از نوع Void هست که بصورت مقدار () تعریف شده. در مباحث بعدی درمورد اینکه واقعاً مقدار () بیانگر چهچیزی هست، کامل توضیح میدم. بنابراین چنین وجود چنین دستوری توی این توابع کاملاً مجازه:
return ()
اما چه بگیم و چه نگیم، بهرحال () چیزی هست که این توابع برمیگردونن و چون نوشتنش هیچ کاربرد و قابلیت خاصی برامون اضافه نمیکنه، کاملاً مجاز و رایج هست که این مقدار رو از جلوی return حذف کنیم.
امضای تابع
اگه یهلحظه اسامی پارامترهای تابع رو نادیده بگیریم، میتونیم بطور کامل یه تابع رو باکمک نوع ورودیها و خروجیش مشخص کنیم و برای اینکار از عبارتی شبیه این استفاده میشه:
(Int, Int) -> Int
درحقیقت این یه عبارت مجاز در Swift محسوب میشه که بهش «امضا»ی یه تابع میگن. در این مورد خاص، امضای تابع sum ما ذکر شده. قطعاً توابع دیگری هم وجود دارن که دو پارامتر Int میگیرن و یه Int برمیگردونن و دقیقاً نکتهی مهم هم همینجاست که این امضا، «تمام» توابعی که این تعداد پارامتر با این انواع مشخص و اون نوع مقدار بازگشتی رو دارن رو شامل میشه. امضای یه تابع در عمل نوع اون تابع رو مشخص میکنه: «نوع تابع». درک این حقیقت که توابع هم نوع دارن، در آینده اهمیت بسیار زیادی خواهد داشت. پس از الان سعی کنید متوجه این مفهوم بشین.
امضای یه تابع باید هم شامل فهرست پارامترها (بدون اسامی اونها) و هم نوع بازگشتی باشه، حتی اگه یکی یا هر دوی اینها خالی باشن. بنابراین امضای یه تابع که پارامتر ورودی نداره و خروجی خاصی هم برنمیگردونه، میتونه یکی از حالتهای چهارگانهی زیر باشه:
(Void) -> Void
(Void) -> ()
() -> Void
() -> ()
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
اسامی خارجی پارامترها
یه تابع میتونه اسامی پارامترهاش رو به خارج صادر کنه. در اینصورت اسامی خارجی باید توی فراخوانی یه تابع، بعنوان برچسب آرگومان اعلام بشن. دلایل مختلفی وجود داره که چرا این قابلیت مفیده:
- باعث میشه هدف هر آرگومان مشخص بشه: برچسب هر آرگومان میتونه یه سرنخ دربارهی اینکه چطور اون آرگومان توی رفتار تابع مشارکت داره به کسی که کد رو میخونه بده.
- باعث میشه یه تابع رو از یکی دیگه تشخیص بدیم: دو تابع با اسم یکسان (قبل از پرانتزها) و امضای یکسان که اسامی خارجی متفاوتی برای متغیرهاشون اعلام کردن، ازنظر Swift دو تابع جداگانه محسوب میشن.
- به سویفت کمک میکنه که با Objective-C و Cocoa بتونه کار کنه که در اونها، تقریباً همیشه پارمترهای متدها اسم خارجی دارن.
اسامی خارجی پارامترها توی Swift اینقدر استاندارد هستن که یه قانون وجود داره: بطور پیشفرض «تمام» اسامی پارامترها «بطور خودکار» با استفاده از اسم داخلیشون، به خارج هم صادر میشن. بنابراین اگه بخواین اسم یه پارامتر رو به خارج صادر کنید و ازطرفی بخواین این اسم، همون اسم داخلی باشه، کافیه «هیچ کاری انجام ندین» چون اینکار خودبخود انجام میشه.
اگه میخواین از رفتار اصلی پیروی نکنین، میتونین یکیاز این کارها رو توی تعریف تابعتون انجام بدین:
- اسم پارامتر خارجی رو تغییر بدین: اگه میخواین اسم خارجی یه پارامتر رو از اسم اصلیش (اسم داخلی) تغییر بدین، اول اسم خارجی و بعد یه فاصله و بعد اسم داخلی رو بنویسین.
- جلوی صدور اسامی خارجی به بیرون رو بگیرین: برای اینکه یه پارامتر، اسم خارجی نداشته باشه، کافیه قبل از اسم داخلیش یه خطزیر یا Underscore بگذارین. الان متوجه شدین که چرا توی تابع sum قبل از اسم x و y کارکتر Underscore گذاشته بودم؟ دلیلش اینه که نمیخواستم این پارامترها اسم خارجی داشته باشن تا موقع صدازدن مجبور نباشم برای مقداردهی پارامترها با آرگومانهای ارسالی، اسمشون رو هم بنویسم.
در اینجا تعریف یه تابع رو میبینید که یه رشته رو بهتعداد دفعاتی که مشخص شده، تکرار میکنه و نتیجه رو برمیگردونه:
func echoString(_ s:String, times:Int) -> String {
var result = ""
for _ in 1...times { result += s }
return result
}
پارامتر اول این تابع فقط اسم داخلی داره ولی پارامتر دومش اسم خارجی هم داره که با اسم داخلیش یکیه (times). این تابع رو باید اینطوری صدا بزنیم:
let s = echoString("Hello", times:3)
موقع فراخوانی، همونطور که میبینین، اسم خارجی بعنوان برچسب قبل از مقدار نوشته میشه و بین برچسب و مقدار، کارکتر دونقطه میگذاریم.
حالا اجازه بدین توی echoString از اسم times فقط بعنوان اسم خارجی استفاده کنیم و توی بدنهی تابع، اسم داخلی این پارامتر n باشه و ضمناً کلمهی String رو هم از اسم تابع حذف کنیم و بجاش اسم خارجی پارامتر اول رو string (دقت کنید با s کوچک) بگذاریم. با این شرایط، تعریف تابع اینطوری میشه:
func echo(string s:String, times n:Int) -> String {
var result = ""
for _ in 1...n { result += s }
return result
}
توی بدنهی این تابع، دیگه متغیری بهنام times وجود نداره و این کلمه فقط اسم خارجی پارامتره که توی فراخوانی ازش استفاده میشه. اسم داخلی متغیرمون n هست و توی کدمون باید از این اسم استفاده کنیم. نحوهی فراخوانی این تابع اینشکلیه:
let s = echo(string:"Hello", times:3)
نقل قول:وجود اسامی خارجی بهمعنای این نیست که فراخوانی تابع میتونه ترتیب پارامترها رو تغییر بده. برای مثال، تابع echo(string:time:) انتظار داره که پارامتر String و پارامتر Int «با همین ترتیب» براش ارسال بشن. ترتیب رو نمیشه توی فراخوانی تغییر داد، حتی اگه بنظر برسه وجود برچسب میتونه هرگونه ابهام درمورد اینکه کدوم آرگومان متعلق به کدوم پارامتره رو از بین ببره.
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
سربارگذاری یا Overload توابع
توی Swift سربارگذاری توابع مجاز (و رایج) محسوب میشه. معنای این حرف اینه که دو تابع با اسم کاملاً یکسان، «شامل» اسامی خارجی پارامترهاشون، میتونین همزمان در یک محدوده وجود داشته باشن، مشروط به اینکه امضاهای متفاوتی داشته باشن. برای مثال، این دو تابع میتونن همزمان وجود داشته باشن:
func say(_ what:String) { }
func say(_ what:Int) { }
علت اینکه سربارگذاری امکانپذیره، اینه که سویفت به نوع متغیرها حساسه. یه String با یه Int یکسان نیست. Swift میتونه اونها رو توی تعریف و موقع فراخوانی تابع از هم تفکیک کنه. بنابراین سویفت بدون هیچگونه ابهامی میفهمه که say("Hello") خیلی فرق میکنه با say(1) و درنتیجه وقتی هرکدوم رو نوشتیم، کدوم نسخه از تابع say باید فراخوانی بشه.
سربارگذاری توابع رو میشه براساس نوع بازگشتی اونها هم انجام داد. دو تابع با اسم و نوع و تعداد پارامتر یکسان میتونن نوع بازگشتی متفاوتی داشته باشن ولی در اینجا، باید در زمان اجرا هیچ ابهامی در فراخوانی وجود نداشته باشه، یعنی مشخص باشه که چه نوع بازگشتی مورد انتظاره. برای مثال، این دو تابع میتونن همزمان در یک محدوده وجود داشته باشن:
func say() -> String {
return "one"
}
func say() -> Int {
return 1
}
اما نمیتونین اینطوری صداشون بزنین:
let result = say() // compile error
این فراخوانی مجاز نیست چون ابهام داره و کامپایلر به شما این موضوع رو اعلام میکنه. فراخوانی تابع باید در جایی انجام بشه که دقیقاً مشخص باشه چه نوع مقدار بازگشتی از تابع انتظار دارین. برای مثال فرض کنین یه تابع دیگه داریم که سربارگذاری نشده و یه پارامتر String میگیره:
func giveMeAString(_ s:String) {
print(s)
}
در این حالت، فراخوانی giveMeAString(say()) مجاز هست چون فقط یه String میتونه در اینجا قرار بگیره و باید اون تابع say که خروجی String تولید میکنه رو صدا بزنیم. بطور مشابه این فراخوانی هم مجازه:
let result = say() + "two"
چون فقط یه String رو میشه به یا String دیگه اضافه کرد و درنتیجه مشخصه که کدوم say باید صدا زده بشه. یا این مثال:
var n = 0
n = say()
در اینجا هم چون مقدار اولیهی n یه Int بوده، همیشه فقط میتونیم یه Int داخلش بریزیم و درنتیجه الان اون نسخه از تابع say که یه مقدار Int برمیگردونه فراخوانی میشه و دیگه ابهامی وجود نداره.
قوانین سربارگذاری در Swift ممکنه شما رو متعجب کنه اگه از Objective-C بهسمت سویفت اومده باشین چون اونجا اصلاً سربارگذاری مجاز «نیست». اگه سعی کنین دو نسخهی سربارگذاریشده از یه متد رو توی Objective-C تعریف کنین، خطای زمان کامپایل Duplicate declaration میگیرین. ضمناً اگه سعی کنین دو متد سربارگذاریشده سویفت در جایی تعریف کنین که Objective-C میتونه اونها رو ببینه (مثلاً توی برنامهی خودتون بخشهایی رو با Objective-C نوشته باشین و از این کد شما هم داخل اون بخشها استفاده شده باشه)، اونوقت کامپایلر Swift به شما خطای زمان کامپایل میده چون اون سربارگذاری با Objective-C سازگاری نداره.
نقل قول:دو تابع با امضای یکسان و اسامی خارجی متفاوت برای پارامترها، مفهوم سربارگذاری رو ایجاد «نمیکنن». درواقع این دو تابع، واقعاً دو تابع متفاوت هستن و باید اسامی متفاوتی داشته باشن.
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
مقدار پیشفرض پارامترها
یه پارامتر میتونه یه مقدار پیشفرض داشته باشه. معنای این حرف اینه که فراخواننده میتونه کلاً پارامتر رو نادیده بگیره و براش آرگومانی نفرسته و اونوقت مقدار پیشفرضی که تعریف کردیم، مورد استفاده قرار میگیره. برای مشخصکردن یه مقدار پیشفرض توی تعریف تابع، بعد از نوع پارامتر باید عملگر انتساب یا = و بعد هم مقدار پیشفرض اون پارامتر رو بنویسیم:
class Student {
func say(_ s:String, times:Int = 1) {
for _ in 1...times {
print(s)
}
}
}
در عمل، الان دو تابع، یکی با امضای say(_:) و یکی دیگه با امضای say(_:times:) وجود داره. اگه فقط بخواین یه چیزی رو یکبار بگین، میتونین say رو با یک پارامتر بدون برچسب صدا بزنین و پارامتر times مقدار 1 که مشخصکردین رو خواهید داشت:
let st = Student()
st.say("Hello") // same as saying st.say("Hello", times:1)
اگه دوست دارین پیامتون تکرار بشه، میتونین پارامتر دوم رو همراه با تعداد موردنظرتون بنویسین:
let st = Student()
st.say("Hello", times:3)
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
پارامترهایی با تعداد متغیر یا Variadic Parameters
یه پارامتر میتونه Variadic باشه. معناش اینه که فراخوانندهی تابع میتونه هرتعداد آرگومان از این نوع خاص که دلش میخواد رو بفرسته و با کاما از هم جدا کنه و بدنهی تابع اونها رو بصورت یه آرایه دریافت میکنه. برای اینکه توی تعریف یه تابع مشخصکنیم که یه پارامتر Variadic هست، کافیه که بعد از تعریفش سهتا نقطه پشتسرهم بگذاریم. مثل این:
func sayStrings(_ arrayOfStrings:String ...) {
for s in arrayOfStrings { print(s) }
}
و اینطوری میتونیم صداش بزنیم:
sayStrings("hi", "hello", "no", "good morning")
خود تابع سراسری print در Swift پارامتر اولش رو بصورت Variadic تعریف کرده. بنابراین میتونین مقادیر مختلفی رو توی یه دستور نمایش بدین:
print("Ali", 3, true) // output: Ali 3 true
پارامترهای پیشفرض تابع print جزئیات بیشتری رو درمورد خروجی مشخص میکنن. پارامتر separator که مقدار پیشفرضش کارکتر Space هست، مشخصکنندهی جداکنندهی مقادیر (وقتی بیش از یک مقدار نمایش میدین) هست و پارامتر terminator که مقدار پیشفرضش کارکتر خط جدید هست هم مشخص میکنه در انتهای تمام مقادیر، چه کارکتری درج بشه. میتونین هرکدوم (یا هر دوتای) این پارامترها رو تغییر بدین:
print("Leila", "Fatemeh", "Reyhaneh, separator:", ", terminator:", ")
print("Mohammad")
// output is "Leila, Fatemeh, Reyhaneh, Mohammad" on one line
یه تابع فقط میتونه حداکثر یه پارامتر Variadic تعریف کنه (چون درغیر اینصورت ممکنه تشخیص اینکه فهرست مقادیر هرکدوم کجا تمام میشه، غیرممکنه بشه).
نقل قول:متأسفانه یه حفره توی زبان Swift وجود داره: هیچ راهی برای تبدیل آرایه به فهرستی از آرگومانهایی که با کاما از هم جدا شدن وجود نداره (در مقایسه با قابلیت Splatting در Ruby). اگه دادهای که دارین، آرایهای از یه نوع خاص باشه، نمیتونین از اون در جایی استفاده کنین که یه Variadic از اون نوع نیازه.
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
پارامترهای نادیده گرفته شده
پارامتری که اسم داخلیش یه Underscore باشه، نادیده گرفته میشه. فراخواننده باید همچنان براش آرگومان بفرسته ولی هیچ اسمی توی بدنهی تابع نمیتونه به اون آرگومان دسترسی پیدا کنه. برای مثال:
func say(_ s:String, times:Int, loudly _:Bool) {
}
هیچ پارامتر loudly راهی به درون تابع پیدا نمیکنه ولی فراخواننده باید همچنان پارامتر سوم رو ارائه کنه:
say("hi", times:3, loudly:true)
تعریف تابع لازم نیست حتماً یه اسم خارجی برای پارامتر نادیده گرفته شده اعلام کنه:
func say(_ s:String, times:Int, _:Bool) {
}
ولی فراخواننده باز هم باید مقدار بفرسته براش:
say("hi", times:3, true)
هدف این قابلیت چیه؟ مطمئناً برای جلب رضایت کامپایلر نیست چون اصلاً کامپایلر به اینکه یه پارامتر رو توی بدنهی تابع مورد استفاده قرار نداده باشین گیر نمیده. من خودم شخصاً از این قابلیت بعنوان نوعی یادداشت واسه خودم استفاده میکنم که داره میگه: «آره، میدونم که اینجا یه پارامتر وجود داره ولی فعلاً (عمداً) ازش استفادهای توی کدم ندارم».
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
پارامترهای قابل ویرایش
توی بدنهی یا تابع، یه پارامتر اساساٌ یه متغیر محلی محسوب میشه که بطور پیشفرض، انگار که با let تعریف شده باشه چون نمیتونین مقدارش رو عوض کنین:
func say(_ s:String, times:Int, loudly:Bool) {
loudly = true // compile error
}
اگه توی کدتون نیاز به تغییر مقدار یه پارامتر داشتین، کافیه که یه متغیر var بصورت محلی داخل تابعتون تعریف کنین و مقدار پارامتر رو بهش بدین. این متغیر، پارامتر رو مخفی میکنه چون اولویت دسترسی با متغیرهای محلی هست و درنتیجه میتونین مقدارش رو تغییر بدین:
func say(_ s:String, times:Int, loudly:Bool) {
var loudly = loudly
loudly = true // no problem
}
توی این کد، متغیر loudly یه متغیر محلیه و مقداردهی اون، مقدار بیرون از بدنهی تابع (پارامتر) رو تغییر نمیده. هرچند این امکان هم وجود داره که پارامتر رو بهشکلی تعریف کنیم که مقداردهی اون باعث بشه مقدار متغیر خارج از تابع (آرگومانی که براش ارسال شده) هم تغییر کنه! یکیاز کاربردهای رایج این موضوع، زمانی هست که میخواین بیش از یک مقدار خروجی از تابع بگیرین. برای مثال، من میخوام یه تابع پیشرفته بنویسم که تمام مواردی که یه کارکتر توی یه رشته پیدا میشه رو حذف کنه و تعداد کارکترهایی که حذفشدن رو برگردونه:
func removeCharacter(_ c:Character, from s:String) -> Int {
var s = s
var howMany = 0
while let i = s.characters.index(of:c) {
s.remove(at:i)
howMany += 1
}
return howMany
}
موقع فراخوانی این تابع باید اینطوری کار کنیم:
let s = "hello"
let result = removeCharacter("l", from:s) // 2
این خیلی خوبه ولی یه موضوع کوچک رو فراموش کردیم: رشتهی اصلی s همچنان مقدار "hello" داره! توی بدنهی تابع ما تمام تکرارهای کارکتر رو از «نسخهی محلی» پارامتر String حذف کردین ولی اینموضوع تأثیری روی رشتهی «اصلی» نداره.
اگه بخواین تابع ما مقدار اصلی آرگومانی که براش ارسال میشه رو تغییر بده، باید کارهای زیر رو انجام بدیم:
- توی نوع پارامتری که میخوایم تغییرش بدیم باید inout رو هم اضافه کنیم
- وقتی تابع رو صدا میزنیم، متغیری که مقدار موردنظر ما رو نگهداری میکنه و میخوایم توی تابع تغییر داده بشه، باید با var تعریف شده باشه نه let
- بجای ارسال متغیر بعنوان آرگومان، باید «آدرس» اون رو توی حافظه بفرستیم. اینکار با قراردادن پیشوند & قبلاز اسم متغیر انجام میشه
نسخهی جدید removeCharacter(_:from:) ما الان اینشکلی میشه:
func removeCharacter(_ c:Character, from s: inout String) -> Int {
var howMany = 0
while let i = s.characters.index(of:c) {
s.remove(at:i)
howMany += 1
}
return howMany
}
و فراخوانی ما هم به این شکل در میاد:
var s = "hello"
let result = removeCharacter("l", from:&s)
بعد از فراخوانی، متغیر result مقدار 2 داره و رشتهی s هم مقدار "heo" خواهد داشت. به کارکتر & قبلاز اسم s دقت کنید! من این نیاز و اجبار رو دوست دارم، چون ما رو وادار میکنه که به کامپایلر و خودمون، صراحتاً اعلام کنیم که میخوایم کاری انجام بدیم که میتونه خطرناک باشه: ما به این تابع اجازه میدیم که درقالب تأثیرهای جانبی خودش، مقداری رو خارج از خودش تغییر بده. اگه این اجبار وجود نداشت، شاید حواسمون نبود و تصادفاً تابعی رو صدا میزدیم که مقدار آرگومان رو تغییر میداد و ما هم از این موضوع اطلاع نداشتیم. این موضوع بخصوص توی پروژههای تیمی و کار با ماژولهایی که توسط دیگران توسعه داده شده، اهمیت پیدا میکنه.
نقل قول:وقتی یه تابع با پارامتر inout فراخوانی میشه، متغیری که آدرسش رو بعنوان آرگومان میفرستیم «همیشه» مقداردهی میشه، حتی اگه تابع هیچ تغییری توی اون پارامتر ایجاد نکنه.
ممکنه موقعی که دارین با Cocoa کار میکنین، تغییراتی در این الگو مشاهده کنین. APIهای Cocoa با C و Objective-C نوشته شدن. بنابراین اصطلاح inout رو که مخصوص Swift هست، مشاهده نخواهید کرد. ممکنه یکسری انواع عجیب و غریب مثل UnsafeMutablePointer مشاهده کنید. از دید شما بعنوان بیننده و فراخوانندهی اون تابع، مفهومش یکیه. شما باید یه متغیر var آماده کنین و آدرسش رو بفرستین.
برای مثال، مشکل یادگیری یه جزء RGBA از UIColor رو درنظر بگیرین. چهارتا از این اجزاء وجود دارن: مقادیر قرمز، سبز، آبی و کانال آلفا (شفافیت) رنگ. یه تابع که برای UIColor میفرستین، اجزاء اون رنگ رو برمیگردونه که درنتیجه نیاز به برگردوندن چهار مقدار همزمان داره و اینموضوع چیزیه که Objective-C نمیتونه انجام بده. بنابراین یه استراتژی متفاوت بکار رفته. متد getRed(_:green:blue:alpha:) از کلاس UIColor فقط یه مقدار Bool برمیگردونه که میگه آیا استخراج اون جزء با موفقیت انجام شد یا نه. درعوض بجای برگردوندن خود جزء واقعی، میگه شما بهم چهار متغیر CGFloat «بعنوان آرگومان» بدین و من اونها رو براتون بهنحوی «تغییر» میدم که نتیجهی این عملیات داخلشون قرار بگیره. تعریف متد getRed اینطوری میشه:
func getRed(_ red: UnsafeMutablePointer<CGFloat>,
green: UnsafeMutablePointer<CGFloat>,
blue: UnsafeMutablePointer<CGFloat>,
alpha: UnsafeMutablePointer<CGFloat>) -> Bool
خوب چطور باید این تابع رو صدا بزنیم؟ پارامترها هرکدومشون یه اشارهگر از نوع UnsafeMutablePointer به یه متغیر CGFloat هستن. شما باید چهار متغیر از نوع CGFloat بسازین و بعد مقدار هرکدوم رو برای این تابع بفرستین، با این دیدگاه که این مقدار وقتی که تابع getRed رو صدا بزنین، تغییر داده میشه. شما آدرسهای این متغیرها رو بعنوان آرگومان میفرستین و این متغیرها بعد از فراخوانی، حاوی مقادیر نتیجهی فراخوانی خواهند بود و میتونین مطمئن باشین که استخراج اجزاء بطور کامل انجام شده و شما لازم نیست حتی زحمت قراردادن نتایج رو توی متغیر خاصی هم لازم نیست انجام بدین! برای مثال:
let c = UIColor.purple
var r : CGFloat = 0
var g : CGFloat = 0
var b : CGFloat = 0
var a : CGFloat = 0
c.getRed(&r, green:&g, blue:&b, alpha:&a)
// now r, g, b, a are 0.5, 0.0, 0.5, 1.0
گاهیاوقات لازم میشه که Cocoa توابع «شما» رو که پارامتر UnsafeMutablePointer دارن، فراخوانی کنه و «شما» میخواین مقدارش رو توی بدنهی تابع تغییر بدین. برای اینکار، نمیتونین مستقیماً (شبیه کاری که با متغیر inout خودمون یعنی s توی تابع removeCharacter(_:from:) انجام دادیم) بهش مقدار بدین. شما دارین با Objective-C حرف میزنین نه سویفت و این یه پارامتر UnsafeMutablePointer هست نه یه inout. تکنیکی که در اینجا وجود داره، مقداردهی مقصد آدرس UnsafeMutablePointer هست که اسمش pointee تعیین شده. در اینجا (بدون هرگونه توضیح اضافه)، یه مثال رو مشاهده کنید:
func popoverPresentationController( _popoverPresentationController: UIPopoverPresentationController,
willRepositionPopoverTo rect: UnsafeMutablePointer<CGRect>,
in view: AutoreleasingUnsafeMutablePointer<UIView>) {
view.pointee = self.button2
view.pointee = self.button2.bounds
}
یک وضعیتهای رایج وجود داره که در اون، تابع شما میتونه یه پارامتر رو بدون معرفی بعنوان inout تغییر بده. اونهم وقتیه که پارامتر مربوطه، یه «نمونه» از یه کلاس باشه. این یکیاز قابلیتهای خاص کلاسها دربرابر دو نوع دیگهی شئ (enum و struct) هست. String یه کلاس نیست و یه ساختار محسوب میشه و بخاطر همین هم هست که ما باید از inout برای ویرایش یه پارامتر String استفاده کنیم. برای مثال، این کلاس و تابع رو درنظر بگیرین:
class Student {
var name = ""
}
func changeName(of st:Student, to newName:String) {
st.name = newName
}
حالا به نحوهی استفاده از این تابع دقت کنید. هیچ پارامتری بصورت inout داخلش تعریف نشده و درنتیجه ما خود شئ Student رو «مستقیماً» میفرستیم (نه آدرسش رو) :
let st = Student()
st.name = "Ali"
print(st.name) // "Ali"
changeName(of:st, to:"Fatemeh")
print(st.name) // "Fatemeh"
دیدین که ما تونستیم مقدار خصوصیت شئ Student خودمون یعنی st رو تغییر بدیم، حتی با این وجود که بعنوان یه پارامتر inout اونو نفرستاده بودیم و حتی با این وجود که در ابتدا با let تعریف شده بود نه با var. این موضوع میتونه یه استثنا درمورد قواعد پارامترهای قابل ویرایش دیده بشه، ولی اینطور نیست. این یه قابلیت نمونههای کلاس هست که میگه: اونها خودشون قابل ویرایش هستن. توی متد changeName ما واقعاً تلاشی برای ویرایش «خود پارامتر» انجام ندادیم. اگه میخواستیم چنینکاری انجام بدیم، باید اون رو به یه شئ جدید Student نسبت میدادیم؛ ولی این چیزی نبود که سعی کردیم انجام بدیم و اگه میخواستیم چنینکاری انجام بدیم، «باید» پارامتر Student رو بصورت inout تعریف میکردیم و st هم با var تعریف میشد و آدرسش رو برای تابع میفرستادیم.
نقل قول:ازنظر فنی، ما میگیم که کلاسها، «انواع ارجاعی» هستن درحالیکه سایر انواع اشیاء «مقداری» محسوب میشن. وقتی یه نمونه از یه struct رو بعنوان آرگومان برای یه تابع میفرستین، یه «کپی مجزا» از اون نمونه برای تابع ارسال میشه ولی وقتی که یه نمونه از یه کلاس رو بعنوان آرگومان ارسال میکنین، یه ارجاع به «همون» شئ فرستاده میشه. درمورد این موضوع توی مباحث آینده بیشتر توضیح میدم.
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
تابع در تابع
یه تابع میتونه هر جایی تعریف بشه و این موضوع شامل بدنهی یه تابع دیگه هم میشه. تابعی که داخل یه تابع دیگه تعریف میشه (که بهاسم تابع محلی هم شناخته میشه)، میتونه توسط کدی که داخل همون محدوده (Scope) محل تعریفش وجود داره (و بعد از تعریف خود تابع نوشته میشه) مورد دسترسی قرار بگیره ولی از بیرون کاملاً نامرئیه.
این قابلیت، یه معماری زیبا برای توابعی هست که تنها هدفشون، کمک به یه تابع دیگه است. اگه فقط تابع A نیاز به فراخوانی تابع B داره، میتونیم تابع B رو داخل تابع A قرار بدیم. در اینجا یکی از مثالهای کاربرد این ویژگی رو توی یکیاز برنامههای خودم مشاهده میکنید (همهچیز بجز ساختار مربوطه رو حذف کردم) :
func checkPair(_ p1:Piece, and p2:Piece) -> Path? {
// ...
func addPathIfValid(_ midpt1:Point, _midpt2:Point) {
// ...
}
for y in -1..._yct {
addPathIfValid((pt1.x, y), (pt2.x, y))
}
for x in -1..._xct {
addPathIfValid((x, pt1.y), (x, pt2.y))
}
// ...
}
کاری که توی حلقهی اول انجام میدم (for y) با کار حلقهی دوم (for x) یکسانه ولی فقط مقادیر اولیه فرق میکنن. میتونستم کارهایی که توی تابع محلی انجام دادم رو توی هر حلقه تکرار کنم ولی اینکار لازم نیست و یه تکرار گیجکننده ایجاد میکنه. چنین تکرارهایی قانونی که اغلب توصیه میشه توی کدنویسی یعنی DRY رو نقض میکنه (Don't Repeat Yourself). برای جلوگیری از چنین تکراری، ترجیح میدیم کدمون رو بازنویسی کنیم و بصورت یه متد در بیاریم که توسط هر دو حلقه فراخوانی میشه ولی اینکار باعث میشه قابلیت موردنظرمون از چیزی که نیاز داریم فراتر بره چون «فقط» توی همین دو حلقه داخل تابع checkPair بهش نیاز داریم. در اینجا یه تابع محلی میتونه بهخوبی ما رو به هدفمون برسونه.
گاهی اوقات حتی اگه دقیقاً «یکبار» از یه کد توی یه تابع استفاده کنیم، ممکنه باز هم ارزش داشته باشه که اینکار با کمک یه تابع محلی انجام بشه. یه مثال دیگه رو ببینین که بخش دیگری از همون تابع رو نشون میده:
func checkPair(_ p1:Piece, and p2:Piece) -> Path? {
// ...
if arr.count > 0 {
func distance(_ pt1:Point, _ pt2:Point) -> Double { // 1
// utility to calculate physical distance between two points
let deltaX = pt1.x - pt2.x
let deltaY = pt1.y - pt2.y
return sqrt(Double(deltaX * deltaX + detaY * deltaY))
}
for thisPath in arr {
var thisLength = 0.0
for i in thisPath.indices.dropLast() {
thisLength += distance(thisPath[i], thisPath[i + 1]) // 2
}
// ...
}
}
// ...
}
باز هم ساختار واضحه (حتی با این وجود که کدمون از یکسری قابلیتهای Swift استفاده میکنه که هنوز مطرحشون نکردم). در اعمال تابع checkPair لحظهای وجود داره که من آرایهای از مسیرها دارم (arr) و میخوام بدونم طول هر مسیر چقدره. هر مسیر خودش یه آرایه از نقاط هست و درنتیجه برای اینکه طولش رو محاسبه کنیم، نیاز به مجموع فاصلههای بین هر دو نقطه داریم. برای محاسبهی فاصلهی بین دو نقطه، من از فرمول جذر حاصل جمع مربع تفاضل طولها و مربع تفاضل عرضها استفاده کردم. میتونستم این کد رو مستقیماً داخل حلقه (for i) هم بنویسم. در عوض:
- فرمول رو توی یه تابع محلی قرار دادم بهاسم distance و بعد...
- داخل حلقهی for اون تابع رو صدا زدم
در اینجا هیچ صرفهجویی در تعداد خطوط ایجاد نشده که هیچ، طول برنامهام هم بیشتر شده! حتی اگه رک بخوایم حرف بزنیم، خط تکرار کد رو هم نداشتم چون برنامهی من الگوریتم محاسبهی فاصله رو بارها داره تکرار میکنه ولی فقط در یک محل از کد برنامهام اینکار انجام میشه (داخل حلقهی for). با این اوصاف، جداکردن کد توی یه ابزار عمومیتر برای محاسبهی فاصله، باعث میشه که کدم خواناتر و واضحتر بشه: در عمل دارم بطور کلی میگم که میخوام چیکار کنم (ببین! من الان میخوام فاصلهی بین دو نقطه رو محاسبه کنم!) و بعد هم اینکار رو انجام میدم. اسم تابع (distance) به کد من معنا میده و قابلیت درک و نگهداری اون رو نسبتبه وقتی که مستقیماً مراحل محاسبهی فاصله رو داخل حلقه بنویسم، بالاتر میبره.
نقل قول:توابع محلی درحقیقت متغیرهای محلی هستن که مقدارشون یه تابعه (این موضوع رو بعداً توی یکی دو پست بعد توضیح میدم). بنابراین یه تابع محلی نمیتونه هماسم یه متغیر توی همون محدوده باشه و دو تابع محلی هم نمیتونن اسم یکسان با هم توی یه محدوده داشته باشن.
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
توابع بازگشتی
یه تابع میتونه خودش رو صدا بزنه. به این قابلیت، «بازگشت» یا Recursion میگن و به چنین توابعی هم بازگشتی یا Recursive گفته میشه. توابع بازگشتی ممکنه بنظر ترسناک بیان، یه چیزی شبیه پریدن از روی یه صخرهی بلند. چون ممکنه باعث ایجاد یه حلقهی بینهایت بشن؛ ولی اگه تابعتون رو بهشکل صحیح و اصولی نوشته باشین، همیشه یه وضعیت متوقفکنندهی تکرار خواهید داشت که جلوی بینهایتشدن فراخوانیهای تودرتو رو میگیره. به این مثال دقت کنید:
func coundDownFrom(_ i:Int) {
print(i)
if (i > 0) { // stopper
countDownFrom(i - 1) // recurse!
}
}
اینکه چهموقع و چطور از این قابلیت استفاده کنید، بستگی به مهارت شما بعنوان برنامهنویس داره. برای مثال، تابعی که سری معروف فیبوناچی رو تا جملهی nام که شما درخواست کردین، نمایش میده رو میشه با کمک توابع بازگشتی به این صورت نوشت:
func fib(_ n:Int) -> Int {
if n <= 2 { // stopper
return 1
} else {
return fib(n - 1) + fib(n - 2) // recurse!
}
}
for i in 1...7 {
print(fib(i))
}
// output:
1
1
2
3
5
8
13
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
استفاده از تابع بعنوان یک مقدار
اگه تا حالا از یه زبان برنامهنویسی که در اون، توابع بعنوان مهمترین رکن هستن استفاده نکردین، شاید بهتر باشه بقیهی مباحث رو نشسته بخونین، چون میخوام درمورد چیزی حرف بزنم که ممکنه باعث بشه فشارتون بیفته! توی Swift یه تابع یک شهروند درجهیک محسوب میشه. معناش اینه که یه تابع میتونه هرجایی که یه مقدار قابل استفاده است، بکار بره. برای مثال، یه تابع میتونه توی یه متغیر ذخیره بشه، یه تابع میتونه بعنوان آرگومان برای یه تابع دیگه ارسال بشه یا حتی یه تابع میتونه بعنوان مقدار بازگشتی یه تابع برگردونده بشه!
زبان Swift نسبت به نوع متغیرها حساسه. میتونین فقط مقدارهایی رو به متغیرها نسبت بدین یا برای توابع بفرستین یا ازشون تحویل بگیرین که «نوع» متناسب با اون متغیر رو داشته باشن. برای اینکه یه متغیر بعنوان یه مقدار استفاده بشه، باید یه نوع «داشته باشه». و نکتهی جالب اینجاست که نوع داره! حدس میزنین نوعش چیه؟ «امضا»ی یه تابع، نوع اون رو مشخص میکنه.
شاید مهمترین هدف استفاده از یه تابع بعنوان یک مقدار این باشه که این تابع رو بتونیم بعداً بدون اینکه دقیقاً بدونیم «کدوم» تابع هست، فراخوانی کنیم. سادهترین (و احمقانهترین) مثالی که میشه مطرح کرد، فقط بخاطر اینکه دستور زبان اینکار رو متوجه بشین، اینه:
func doThis(_ f:() -> ()) {
f()
}
در اینجا یه تابع doThis دارین که یه پارامتر میگیره و چیزی بر نمیگردونه. پارامتر تابع یعنی f خودش یه تابعه و ما اینو از اینجا میفهمیم که نوع پارامتر، امضای یه تابع بصورت () -> () هست که معناش همونطور که میدونین اینه که یه تابع داریم بدون پارامتر ورودی و بدون مقدار بازگشتی. تابع doThis بعداً تابع f رو که بعنوان پارامتر دریافت میکنه، بصورت f() صدا میزنه.
حالا بعد از تعریف تابع doThis به این شکل، چطور باید صداش بزنیم؟ برای اینکار باید یه تابع رو بعنوان آرگومان براش بفرستین. در اینجا یک راه برای انجام اینکار رو میبینید:
func doThis(_ f:() -> ()) {
f()
}
func whatToDo() {
print("I did it")
}
doThis(whatToDo)
اول یه تابع «از نوع مناسب» تعریف کردیم (whatToDo) - یعنی تابعی که پارامتر ورودی نداره و چیزی هم برنمیگردونه - و بعد هم تابع doThis رو صدا زدیم و بعنوان آرگومان، «یه ارجاع به تابع» رو فرستادیم (یعنی فقط اسم تابع). دقت کنید که ما تابع whatToDo رو اینجا صدا نزدیم و فقط ارسالش کردیم (پرانتزهای جلوی تابع نوشته نشدن). خوب بدون شک این کد کار میکنه و عبارت I did it توی پنجرهی کنسول ظاهر میشه.
ولی اینکه میتونیم چنین کاری انجام بدیم، چه فایدهای داره؟ هدف ما فراخوانی whatToDo هست. چرا مستقیماً صداش نزنیم؟ چهچیزی توی اینکه میتونیم به یه تابع «دیگه» بگیم صداش بزنه، مفیده؟! توی مثالی که گفتم، هیچ چیز مفیدی در این مورد وجود ندار و فقط دستور زبان و ساختار اینکار رو توضیح دادم. اما در دنیای واقعی، یه کار خیلی باارزش وجود داره که میشه باهاش انجام داد. قراردادن فراخوانی تابع داخل یه تابع دیگه میتونه باعث کاهش تکرار و احتمال بروز اشتباه بشه. بعلاوه یه تابع دیگه ممکنه تابعی که بعنوان پارامتر میگیره رو بهشکل خاصی صدا بزنه. مثلاً ممکنه اون رو بعد از انجام یکسری کارهای دیگه صدا بزنه یا برحسب شرایط موجود تصمیم بگیره که پارامترهای خاصی رو براش بفرسته و ما فقط خود تابع رو برای فراخوانی براش میفرستیم.
یکیاز مواردی که خودم از این قابلیت توی کدم استفاده کردم رو با هم ببینیم. یکیاز چیزهایی که توی Cocoa رایجه، ترسیم یه تصویر مستقیماً داخل کد هست. اینکار چهار مرحله داره:
let size = CGSize(width:45, height:20)
UIGraphicsBeginImageContextWidthOptions(size, false, 0) // 1
let p = UIBezierPath(roundedRect: CGRect(x:0, y:0, width:size.width, height:size.height), corenerRadius:8)
p.stroke() // 2
let result = UIGraphicsGetImageFromCurrentImageContext()! // 3
UIGraphicsEndImageContext() // 4
این دستورات چهار مرحله دارن:
- بازکردن فضای زمینهی تصویر (Image Context)
- ترسیم در فضای تصویر
- استخراج تصویر
- بستن فضای تصویر
اینکار بهشکل وحشتناکی زشته! هدف اصلی این کد گرفتن result یعنی تصویر هست ولی این هدف توی تمام اون کدها دفن شده. در همین حال، تمام ساختارمون هم خشک و یکسانه: هربار توی هر برنامه بخوام اینکار رو انجام بدم، مرحلهی 1 و 3 و 4 دقیقاً یکسان هستن. بعلاوه من همیشه توی ترس مرگباری بخاطر فراموشکردن یه مرحله دارم زندگی میکنم! برای مثال اگه مرحلهی 4 رو فراموش کنم، دنیا منفجر میشه!
تنها چیزی که هربار فرق میکنه، مرحلهی 2 هست. بنابراین، این مرحله تنها بخشی هست که باید بنویسیم. تمام مشکل ما با نوشتن یه تابع کمکی برای انجام کارهای تکراری حل میشه:
func imageOfSize(_ w:Int, _ h:Int, draw:(_:_:)->()) -> UIImage {
let size = CGSize(width:w, height:h)
draw(w, h)
let result = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return result
}
تابع imageOfSize که نوشتیم، اینقدر سودمنده که من توی سطح بالای یه فایل و خارج از کلاسها تعریفش میکنم تا تمام فایلهام بتونن ببیننش. برای ایجاد یه تصویر مرحلهی دوم (ترسیم واقعی) رو بصورت یه تابع جداگانه در میارم و اون تابع رو بعنوان آرگومان برای تابع کمکی imageOfSize میفرستم:
func drawing(_ w:Int, _h:Int) {
let p = UIBezierPath(roundedRect: CGRect(x:0, y:0, width:w, height:h), cornerRadius: 8)
p.stroke()
}
let image = imageOfSize(45, 20, drawing)
الان «این» کد بهشکل زیبایی خوانا و واضحه و یه مسیر شفاف برای انجام ترسیمات روی یه تصویر در اختیارمون میگذاره. الان میتونیم هر تابعی بنویسیم و داخلش ترسیمات دلخواهمون رو انجام بدیم و به تابع imageOfSize بدیم تا با سایز دلخواهمون برامون اون رو بسازه و تحویل بده.
نقل قول:درنهایت Apple با نظر من درمورد ساختار وحشتناک UIGraphicsBeginImageContext و... موافقت کرد چون توی iOS 10 یه کلاس جدید به اسم UIGraphicsImageRenderer معرفی کرد که ساختاری شبیه imageOfSize من داره (البته اونجا بجای اینکه پهنا و ارتفاع رو جداگانه بگیره، مستقیماً یه CGSize بعنوان پارامتر دریافت میکنه. بهرحال من در ادامهی این قسمت از آموزش، به استفاده از imageOfSize خودم ادامه میدم چون جنبههای مهمی از توابع Swift رو نشون میده.
API فریمورک Cocoa پر از شرایطی هست که شما در اونها یه تابع رو میفرستین تا توسط مجری سویفت بهشکل خاصی یا در زمان دیگری اجرا بشه. یکیاز شرایط رایط در Cocoa (کاکائو) وقتی هست که شما «دو» تابع برای اجرا میفرستین. برای مثال وقتی که میخواین یه انیمیشن روی یکیاز آیتمهای رابط کاربریتون داشته باشین، در اغلب موارد یه تابع برای توصیف عملی که باید بصورت انیمیشن انجام بشه و یکی دیگه هم برای مشخصکردن اینکه بعد از اتمام انیمیشن، چهکاری باید انجام بشه ارسال میکنین:
func whatToAnimate() // self.myButton is a button in the interface
self.myButton.frame.origin.y += 20
}
func whatToDoLater(finished:Bool) {
print("finished: (finished)")
}
UIView.animate(withDuration:0.4, animations:whatToAnimate, completion:whatToDoLater)
معنای این کد اینه که موقعیت فریم این دکمه رو در رابط کاربری تغییر بده ولی برای انجام اینکار 0.4 ثانیه زمان صرف کن و بعد، وقتی که تمام شد، یه پیام توی کنسول بنویس که میگه انیمیشن انجام شد یا نه.
نقل قول:اسامی مستعار برای نوع میتونن انواع تابع رو شفافسازی کنن
برای اینکه مشخصههای توابع رو واضحتر کنیم، میتونین از قابلیت خاصی در Swift موسوم به typealias استفاده کنیم تا به یه نوع خاص تابع یه اسم بدیم. اسمی که میتونه توصیفی باشه و جایگزین ساختار پرانتزی که احتمالاً گیجکننده است، بشه. برای مثال، اگه بگیم:
typealias VoidFunction = () -> ()
میتونیم بعداً هروقت خواستیم بجای () -> () از VoidFunction استفاده کنیم تا توابعی با اون امضا رو مشخص کنیم. بنابراین چیزی که قبلاً اینطوری نوشته بودیم:
func doThis(_ f:() -> ()) {
f()
}
رو میتونیم اینطوری تعریف کنیم:
typealias VoidVoidFunction = () -> ()
func doThis(_ f:VoidVoidFunction) {
f()
}
مستندات Cocoa اغلبشون یه تابع رو با این روش تعریف کردن تا بعنوان یه «کنترلکننده» (Handler) ارسال بشه و بهش بعنوان «بلاک» (Block) اشاره میکنن چون توی Objective-C چنین ساختاری مورد نیازه. توی Swift اینها تابع هستن و درنتیجه بهش به دید تابع نگاه کنین و بعنوان یه تابع بفرستینش. ما اینجا Objective-C کار نمیکنیم و هدفم از توضیح این موضوع، این بود که متوجه باشین توی Objective-C چهخبره و اگه یهزمان نیاز به کارکردن توی کدتون درکنار Objective-C داشتین، تفاوتها رو بدونین.
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
توابع بینام
یکبار دیگه مثال قبل رو ببینید:
func whatToAnimate() { // self.myButton is a button in the interface
self.myButton.frame.origin.y += 20
}
func whatToDoLater(finished:Bool) {
print("finished: (finished)")
}
UIView.animate(withDuration:0.4, animations:whatToAnimate, completion:whatToDoLater)
این کد یکم زشته! من تابع whatToAnimate و whatToDoLater رو فقط بخاطر اینکه میخواستم توی خط آخر اونها رو ارسال کنم تعریف کردم و درحقیقت هیچ نیازی به «اسامی« whatToAnimate و whatToDoLater ندارم، بجز اینکه از اونها توی خط آخر برای ارجاع به توابع استفاده میکنیم و نه اسم این توابع و نه خودشون هیچجای دیگه بکار نرفتن. بنابراین، خیلی خوب میشه اگه بتونیم فقط بدنهی این توابع رو بدون تعریف اسم براشون، بفرستیم.
به این قابلیت، تابع «بینام» (Anonymous Function) میگن و توی Swift قانونی و کاملاً رایجه. برای ایجاد یه تابع بینام باید دو کار انجام بدین:
- بدنهی تابع رو بسازین (شامل آکولادهای باز و بسته، ولی بدون تعریف تابع).
- اگه لازم بود، فهرست پارامترها و نوع بازگشتی تابع رو توی خط اول بدنهی تابع (داخل آکولادهای باز و بسته) بنویسین و بعدش کلمهی کلیدی in رو ذکر کنید.
اجازهبدین با تبدیل تابع نامدار خودمون به تابع بینام، این موضوع رو تمرین کنیم. در اینجا یه تابع نامدار بهاسم whatToAnimate تعریف شده:
func whatToAnimate() {
self.myButton.frame.origin.y += 20
}
حالا تابع بینام معادل همون تابع رو ببینید. دقتکنید که من فهرست پارامترها و نوع بازگشتی رو به داخل بدنهی تابع منتقل کردم:
{
() -> () in
self.myButton.frame.origin.y += 20
}
حالا تابع whatToDoLater رو درنظر بگیرین:
func whatToDoLater(finished:Bool) {
print("finished: (finished)")
}
و حالا معادل بینام اون رو ببینید:
{
(finished:Bool) -> () in
print("finished: (finished)")
}
حالا که فهمیدیم چطور توابع بینام بسازیم، بگذارین ازشون استفاده کنیم. نقطهای که نیاز به توابع داریم، همونجایی هست که آرگومانها رو برای تابع animate(withDuration:animations:completion:) میفرستیم. میتونیم توابع بینام رو مستقیماً در همون نقطه تعریف کنیم و بفرستیم. درست مثل این کد:
UIView.animate(withDuration:0.4,
animations: {
() -> () in
self.myButton.frame.origin.y += 20
},
completion: {
(finished:Bool) -> () in
print("finished: (finished)")
}
)
میتونیم همینروش رو در جایی که تابع imageOfSize رو در پست قبلی صدا زدیم هم بکار ببریم. قبلاً تابع رو اینطوری صدا میزدیم:
func drawing(_ w:Int, _h:Int) {
let p = UIBezierPath(roundedRect: CGRect(x:0, y:0, width:w, height:h), cornerRadius:8)
p.stroke()
}
let image = imageOfSize(45, 20, drawing)
الآن میدونیم که نیاز نیست تابع drawing رو جداگانه تعریف کنیم. میتونیم imageOfSize رو با یه تابع بینام صدا بزنیم:
let image = imageOfSize(45, 20, {
(_ w:Int, _ h:Int) -> () in
let p = UIBezierPath(roundedRect: CGRect(x:0, y:0, width:w, height:h), cornerRadius:8)
p.stroke()
})
توابع بینام خیلی زیاد توی سویفت بکار رفته، بنابراین سعی کنید مطمئن بشین که این قابلیت رو خوب یاد گرفتین و چنین کدهایی رو میتونین خیلی خوب و راحت بخونین و بنویسین! توابع بینام درحقیقت «اینقدر» رایج و مهم هستن که یکسری میانبر برای نوشتنشون هم ارائه شده:
- حذف نوع بازگشتی: اگه مقدار بازگشتی تابع برای کامپایلر مشخص باشه، میتونین عملگر -> و مشخصکردن نوع مقدار بازگشتی رو نادیده بگیرین:
UIView.animate(withDuration:0.4, animations: {
() in
self.myButton.frame.origin.y += 20
}, completion: {
(finished:Bool) in
print("finished: (finished)")
})
- حذف خط in اگه پارامتری وجود نداشته باشه: اگه تابع بینام ما هیچ پارامتر ورودی نداشته باشه، و مقدار بازگشتی اون رو هم بتونیم طبق شرایط بند قبل نادیده بگیریم، میتونیم کلاً خط in رو ننویسیم:
UIView.animate(withDuration:0.4, animations: {
self.myButton.frame.origin.y += 20
}, completion: {
(finished:Bool) in
print("finished: (finished)")
})
- حذف نوع پارامترها: اگه تابع بینام پارامتر ورودی داشته باشه و نوعشون برای کامپایلر مشخص باشه، میتونیم نوعشون رو اعلام نکنیم:
UIView.animate(withDuration:0.4, animations: {
self.myButton.frame.origin.y += 20
}, completion: {
(finished) in
print("finished: (finished)")
})
- حذف پرانتزها: اگه نوع پارامترها حذف بشه، پرانتزهای دور پارامترها رو هم میشه حذف کرد:
UIView.animate(withDuration:0.4, animations: {
self.myButton.frame.origin.y += 20
}, completion: {
finished in
print("finished: (finished)")
})
- حذف خط in وقتی که پارامتر وجود داره: اگه نوع بازگشتی رو بتونیم حذف کنیم و نوع پارامترها برای کامپایلر مشخص باشه، میتونین خط in رو حذف کنین و مستقیماً به پارامترها در بدنهی تابع با کمک ترتیب اونها و با اسامی خاصی مثل $0 و $1 و... دسترسی پیدا کنین:
UIView.animate(withDuration:0.4, animations: {
self.myButton.frame.origin.y += 20
}, completion: {
print("finished: ($0)")
})
- حذف اسامی پارامترها: اگه بدنهی تابع بینام نیاز به استفاده از پارامتری نداشته باشه، میتونین بجای اسم اون پارامتر، یه کارکتر خطزیر (Underscore) توی فهرست پارامترها در خط in بنویسین. درحقیقت اگه تابع بینام نیاز به استفاده از «هیچ» پارامتری نداشته باشه، میتونین بجای کل پارامترها، فقط یک خطزیر بنویسین:
UIView.animate(withDuration:0.4, animations: {
self.myButton.frame.origin.y += 20
}, completion: {
_ in
print("finished!")
})
ولی دقتکنید که اگه تابع بینام پارامتر میگیره، «باید» اونها رو یهجوری بشناسین. میتونین خط in رو نادیده بگیرین و از پارامترها با کمک اسامی خاص $0 و... استفاده کنین یا میتونین خط in رو بنویسین و پارامترها رو با کارکتر خطزیر نادیده بگیرین ولی نمیتونین همزمان هم خط in رو حذف کنین و هم از پارامترها با کمک اسامی ترتیبی اونها استفاده نکنید! اگه چنینکاری انجام بدین، کدتون کامپایل نمیشه.
- حذف برچسب آرگومان تابع: اگه همونطور که در اغلب مواقع پیش میاد، تابع بینام شما آخرین آرگومانی باشه که برای یه تابع دیگه ارسال میشه، میتونین فراخوانی تابع اصلی (که میخواین تابع بینام رو براش بفرستین، مستقیماً با قراردادن پرانتز بستهی فراخوانی، قبل از آخرین آرگومانش ببندین و بعد فقط تابع بدوننام خودتون رو «بدون برچسب» بنویسید (به این قابلیت، «تابع دنبالهرو» میگن) :
UIView.animate(withDuration:0.4, animations: {
self.myButton.frame.origin.y += 20
}) {
_ in
print("finished!")
})
- حذف پرانتزهای تابع فراخواننده: اگه از دستور زبان تابع دنبالهرو استفاده میکنین، و اگه تابع شما هیچ پارامتری بجز تابع بینام موردنظر شما نداره، میتونین پرانتزهای خالی رو از فراخوانی حذف کنین. این «تنها» وضعیتی هست که میتونین پرانتزها رو از فرخوانی یه تابع حذف کنین! برای اینکه متوجه بشین، به این مثال دقت کنید:
func doThis(_ f:()->()) {
f()
}
doThis { // no parentheses!
print("Hello")
}
- حذف کلمهی کلیدی return از تابع: اگه بدنهی تابع بینام شما «دقیقاً یک خط کد» داشته باشه و اون خط هم شامل برگردوندن یه مقدار با کلمهی کلیدی return باشه، میتونین کلمهی کلیدی return رو حذف کنین. اگه بخوام یهجور دیگه بگم، درصورتی که یه تابع بینام فقط شامل یه دستور باشه، Swift فرض میکنه که این دستور، شامل مقداری هست که باید از تابع بینام برگردونده بشه. برای مثال:
func greeting() -> String {
return "Hello"
}
func performAndPrint(_ f:()->String) {
let s = f()
print(s)
}
performAndPrint {
greeting() // meaning: return greeting()
}
وقتی توابع بینام مینویسین، مرتباً با شرایطی مواجه میشین که تمام قوانین حذف که در بالا گفتیم، مجاز هستن. بعلاوه اغلب اوقات «چیدمان» کدها (نه خود کد) رو با قراردادن کل تابع بینام و فراخوانیش «در یک خط» خلاصه میکنین. بنابراین کد Swift میتونه شامل توابع بینامی باشه که بهشدت فشرده شدن.
یه مثال عملی بزنیم تا این موضوع بهتر جا بیفته. ما با یه آرایه از مقادیر Int کار رو شروع میکنیم و یه آرایهی جدید از حاصلضرب اون مقادیر در 2 با کمک فراخوانی متد نمونهای map(_:) میسازیم. متد map(_:) از یک آرایه، یه تابع میگیره که یه پارامتر همنوع با عناصر آرایهی اصلی میگیره و یه مقدار جدید برمیگردونه. در اینجا آرایهی ما از مقادیر Int تشکیل شده و ما هم باید برای متد map(_:) یه تابع بفرستیم که یه مقدار Int میگیره و یه Int هم برمیگردونه. میتونین این موضوع رو اینطوری بنویسیم:
let arr = [2, 4, 6, 8]
func doubleMe(i:Int) -> Int {
reutrn i * 2
}
let arr2 = arr.map(doubleMe) // [4, 8, 12, 16]
خوب این کد خیلی جذاب نیست. ما نیاز به doubleMe در جای دیگری نداریم و درنتیجه میتونیم با تابع بینام جایگزینش کنیم:
let arr = [2, 4, 6, 8]
let arr2 = arr.map({
(i:Int) -> Int in
return i * 2
})
خوب بهتر شد. اما الان میتونیم کدمون رو با قوانین حذفی که در بالا اشاره کردیم، فشرده و کوتاه کنیم. نوع پارامترها دقیقاً مشخصه، بنابراین لازم نیست اونها رو مشخص کنیم. نوع بازگشتی هم مشخصه. پس به اونم نیاز نیست. ازطرفی چون فقط یه پارامتر داریم و ازش استفاده هم میکنیم، نیاز نیست خط in رو بنویسیم چون میتونیم به پارامترمون بصورت $0 اشاره کنیم. ضمناً بدنهی تابع ما فقط شامل یک خط هست که اونهم مقدار نتیجه رو داره برمیگردونه و بنابراین میتونیم کلمهی کلیدی return رو هم ننویسیم. نکتهی بعدی اینه که متد map(_:) هیچ پارامتر دیگری هم نداره و درنتیجه میتونیم پرانتزها رو هم حذف کنیم و مستقیماً بعد از اسمش، از تابع دنبالهرو استفاده کنیم:
let arr = [2, 4, 6, 8]
let arr2 = arr.map {$0*2}
دیگه خوشگلتر و خلاصهتر از این نمیشه کدی نوشت واسه اینکار !!!
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
تعریف و فراخوانی همزمان
یکیاز الگوهایی که برخلاف انتظار، توی Swift خیلی رایجه، تعریف یه تابع بینام و فراخوانی اون بطور همزمان و در یک مرحله است:
{
// ... code goes here
}()
به پرانتزهای بعد از آکولاد بسته دقت کنید. آکولادها بدنهی یه تابع بینام رو معرفی میکنن و پرانتزها هم اون تابع بینام رو صدا میزنن.
چرا باید یهنفر چنین کاری انجام بده؟ اگه بخوایم یه کدی اجرا بشه، خوب اجراش میکنیم؛ چرا باید اونو توی یه سطح عمیقتر بعنوان بدنهی یه تابع قرار بدیم و بعد بلافاصله اونو صدا بزنیم؟
در یک مورد خاص، یه تابع بینام میتونه روش خوبی برای ازبینبردن اثر کد و کارآیی اون باشه: یه عمل خاص میتونه در محلی که موردنیاز هست تعریف بشه و در یک مرحله فراخوانی بشه. به این مثال رایج در Cocoa دقت کنید: ما یه NSMutableParagraphStyle میسازیم و بعد از اون بعنوان یه آرگومان توی فراخوانی متد addAttribute(_:value:range:) از کلاس NSMutableAttributedString به این شکل استفاده کنیم:
let para = NSMutableParagraphStyle()
para.headIndent = 10
para.firstLineHeadIndent = 10
// ... more configuration of para ...
content.addAttribute( // content is NSMutableAttributedString
NSParagraphStyleAttributeName,
value:para,
range:NSRange(location:0, length:1)
)
بنظر من این کد زشت و شلوغه. ما از para فقط برای ارسالش بعنوان آرگومان value استفاده میکنیم. بنابراین خیلی بهتره که اونو مستقیماً همونجا موقع فراخوانی تعریف کنیم. خوب این شبیه یه تابع بینام بنظر میرسه (بجز اینکه پارامتر value یه تابع نیست بلکه یه شئ NSMutableParagraphStyle هست. میتونیم این مشکل رو با یه تابع بینام که خروجی اون یه شئ NSMutableParagraphStyle هست و فراخوانی اون، حل کنیم:
content.addAttribute(
NSParagraphStyleAttributeName,
value: {
let para = NSMutableParagraphStyle()
para.headIndent = 10
para.firstLineHeadIndent = 10
// ... more configuration of para ...
return para
}(),
range:NSRange(location:0, length:1)
)
یکیدیگه از قابلیتهای این تکنیک، تعریف متغیرهای محلی که فقط در همون قسمت از کد کاربرد دارن و بعدش دیگه ازشون استفاده نمیکنیم و میخوایم از حافظه خارج بشن هست:
{
let name = "Ali" // we don't need this variable in the code after print
print("Hello (name)")
}() // name variable doesn't exist anymore
توی پستهای بعدی کاربردهای بیشتری از این قابلیت رو در آینده معرفی خواهم کرد.
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
آشنایی با مفهوم Clusore یا بَستار
توابع توی Swift در حکم «بستار» (بستهبندی) هستن. معنای این حرف اینه که میتونن ارجاع به متغیرهای خارجی که توی محدودهی بدنهی تابع وجود دارن رو «در اختیار بگیرن». منظورم از این حرف چیه؟ خوب اگه از پستهای اولیهی این تاپیک یادتون باشه، کدی که داخل آکولادها قرار میگیره، یه محدوده ایجاد میکنه و این کد میتونه متغیرها و توابع داخل محدودهی بیرونی خودش رو ببینه:
class Student {
var name = "Ali"
func say() {
print(name)
}
}
توی این کد، بدنهی تابع say به متغیر name دسترسی داره. این متغیر برای بدنهی تابع «خارجی» محسوب میشه، چون خارج از بدنهی تابع تعریف شده. این متغیر «داخل محدوده»ی بدنهی تابع هست چون کد داخل بدنهی تابع میتونه اونو ببینه و بهش بهسادگی با اسم name «مراجعه» میکنه.
تا اینجا خیلی خوبه؛ اما میدونیم که تابع say رو میتونیم بعنوان یه مقدار، ارسال کنیم. در عمل، این تابع میتونه از یک محیط به محیط دیگه سفر کنه! وقتی این اتفاق بیفته، چهبلایی سر ارجاع به متغیر name میفته؟ اجازهبدین ببینیم:
func doThis(_ f : (Void) -> Void) {
f()
}
let s = Student()
s.name = "Fatemeh"
let sayFunction = s.say
doThis(sayFunction) // Fatemeh
ما این کد رو اجرا میکنیم و کلمهی Fatemeh توی پنجرهی کنسول ظاهر میشه. شاید این نتیجه براتون تعجبآور نباشه ولی درموردش فکر کنید. ما مستقیماً s.say() رو صدا نزدیم. یه شئ نمونهی Student ساختیم و تابع say اون رو بعنوان یه مقدار برای تابع doThis فرستادیم. در اونجا این تابع فراخوانی میشه. حالا name یه فیلد نمونهای از یه Student خاص هست. داخل تابع doThis هیچ متغیر name خاصی وجود نداره. درحقیقت اصلاً هیچ شئ Student خاصی وجود نداره! بااینحال همچنان فراخوانی f() کار میکنه. تابع s.say وقتی ارسال میشه، همچنان میتونه متغیر name که خارج از خودش تعریف شده رو ببینه، حتی اگه توی محیطی فراخوانی بشه که شئ Student خاصی (و درنتیجه خاصیت name وابسته بهش) وجود نداشته باشه.
اما باز هم چیزهای بیشتری وجود داره. من خطی که مقدار s.name رو مقداردهی میکنیم، میارم بعد از نسبتدادن s.say توی متغیر sayFunction که تعریف کردیم:
func doThis(_ f : (Void) -> Void) {
f()
}
let s = Student()
let sayFunction = s.say
s.name = "Fatemeh"
doThis(sayFunction) // Fatemeh
متوجه شدین که این تغییر چیو اثبات میکنه؟ در زمانی که ما تابع s.say رو به متغیر sayFunction نسبت میدیم، خصوصیت s.name مقدار "Ali" داره. ما این متغیر رو بعد از این مرحله، با مقدار "Fatemeh" جایگزین میکنیم و بعد sayFunction رو برای doThis میفرستیم و وقتی فراخوانی میشه، کلمهی Fatemeh چاپ میشه. این ثابت میکنه که sayFunction ارجاعش به این شئ خاص Student رو «نگهداری و مدیریت» میکنه (همونی که متد say رو ازش گرفتیم). تابع say وقتی توی برنامه پاسکاری میشه بین بخشهای مختلف، محیطی که در اصل داخلش قرار داشته رو با خودش حمل میکنه (شامل شئ نمونهای که خودش بعنوان یه متد ازش تعریف شده). علت این موضوع اینه که داخل بدنهی تابع، از self استفاده شده (فراموش نکنید که منظور از name توی تابع، همون self.name هست). این محیط همچنان وقتی که ما تابع رو توی یه محیط دیگه صدا میزنیم، توی حافظه وجود داره و از بین نرفته. بنابراین وقتی میگم «در اختیار بگیره» (Capture)، منظورم اینه که وقتی یه تابع بعنوان مقدار برای یه محیط دیگه ارسال میشه، همراه خودش ارجاعهایی که به متغیرهای بیرونی محیط خودش داره رو هم حمل میکنه. این چیزیه که تابع رو به Closure تبدیل میکنه و مفهوم واقعی بستار هست.
ارسالها: 3,701
موضوعها: 140
تاریخ عضویت: اردیبهشت 1394
اعتبار:
134
تشکرها: 195
3447 بار تشکر شده در 2120 پست
نحوهی ارتقاء کد با کمک Closure
وقتی که درک کردین که توابع، بَستار هستن، میتونین از این واقعیت برای ارتقاء دستور زبان کدتون استفاده کنین. Closure (کلوژِر) میتونه کمک کنه که کدتون عمومیتر و درنتیجه کاربردیتر بشه. یهبار دیگه مثال قبلی من رو ببینید که تابعی هست که دستورالعملهای ترسیم رو میگیره و اونها رو برای تولید یک تصویر اجرا میکنه:
func imageOfSize(_ size:CGSize, _ whatToDraw:() -> ()) -> UIImage {
UIGraphicsBeginImageContextWithOptions(size, false, 0)
whatToDraw()
let result = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return result
}
ما میتونیم imageOfSize رو با توابع دنبالهرو صدا بزنیم:
let image = imageOfSize(CGSize(width:45, height:20)) {
let p = UIBezierPath(
roundedRect: CGRect(x:0, y:0, width:45, height:20),
cornerRadius: 8
)
p.stroke()
}
البته این کد حاوی یه تکرار آزاردهنده است: ایجاد یه تصویر با ابعاد مشخص و ساخت یه مستطیل لبهگرد با همون ابعاد. ما ابعاد رو داریم تکرار میکنیم. زوج اعداد 45,20 دوبار تکرار شده. اینکار درست نیست. اجازهبدین جلوی این تکرار رو با قراردادن سایز توی یه متغیر در خارج از تابع بگیریم:
let sz = CGSize(width:45, height:20)
let image = imageOfSize(sz) {
let p = UIBezierPath(
roundedRect: CGRect(origin:CGPoint.zero, size:sz),
cornerRadius: 8
)
p.stroke()
}
متغیر sz که خارج از تابع بینام ما تعریف شده توی یه سطح بالاتر، توی تابع قابل مشاهده است. بنابراین میتونیم بهش داخل تابع بینام دسترسی پیدا کنیم و اینکار رو هم انجام میدیم. توی خط چهارم، ما «الآن» تابع CGRect رو صدا نمیزنیم و مقدار sz رو هم براش نمیفرستیم. همهچیز داخل آکولادها فقط بدنهی تابعه. این بدنه تاوقتیکه imageOfSize صداش نزنه، «اجرا نمیشه». بنابراین، در اینجا با Closure سروکار داریم. بنابراین تابع بینام ما ارجاع به sz رو میگیره (Capture) و اونرو به زمان فراخوانی imageOfSize منتقل میکنه.
حالا بگذارین جلوتر بریم. تا اینجا، ما سایز مستطیل لبهگرد خودمون رو مستقیماً کدنویسی کردیم. تصور کنید که ساخت تصاویر مستطیلهای لبهگیر با ابعاد مختلف، کاری باشه که ما بهش احتیاج داشته باشیم. در این حالت خیلی خوبه که این کد رو طوری بستهبندی کنیم که بصورت یه تابع در بیاد که توی اون، ابعاد تصویر ثابت نیست و یه پارامتره و این تابع وقتی اجرا بشه، تصویر موردنظر رو برامون برمیگردونه:
func makeRoundedRectangle(_ sz:CGSize) -> UIImage {
let image = imageOfSize(sz) {
let p = UIBezierPath(
roundedRect: CGRect(origin:CGPoint.zero, size:sz),
cornerRadius: 8
)
p.stroke()
}
return image
}
توی عبارت CGRect(origin:CGPoint.zero, size:sz) ما به sz اشاره کردیم. این عبارت، بخشی از یه تابع بینام هست که به imageOfSize پاس داده میشه. عبارت sz داره به پارامتر sz که برای تابع بیرونی یعنی makeRoundedRectangle ارسال میشه اشاره میکنه. پارامتر تابع بیرونی یه متغیر خارجی برای تابع بینام محسوب میشه و داخل محدودهی دیدش قرار داره و این تابع هم یه کلوژِر هست و درنتیجه ارجاع به اون پارامتر رو Capture میکنه و برای imageOfSize میفرسته.
کد ما بهشکل زیبایی فشرده شده. برای صدازدن makeRoundedRectangle، یه پارامتر size میفرستیم و یه تصویر تحویل میگیریم. بنابراین میتونم فراخوانی، گرفتن تصویر خروجی و قراردادنش توی رابط کاربری خودم رو توی یه دستور انجام بدم:
self.myImageView.image = makeRoundedRectangle(CGSize(width:45, height:20))
|