رتبه موضوع:
  • 0 رای - 0 میانگین
  • 1
  • 2
  • 3
  • 4
  • 5
آموزش برنامه‌نویسی Swift برای iOS 10 با Xcode 8
#31
توابعی که خروجی تابع دارند
اجازه بدین باز هم جلوتر بریم! بجای برگردوندن یه عکس، تابع ما میتونه یه «تابع» برگردونه که مستطیل‌های لبه‌گرد رو با «اندازه‌ی مشخص‌شده» تولید میکنه. اگه تابحال ندیدین که یه تابع، بعنوان نتیجه‌ی خروجی یه تابع دیگه برگردونده بشه، ممکنه الان نفستون بند بیاد. اما فراموش نکنید که درنهایت یه تابع میتونه بعنوان یه مقدار مورد استفاده قرار بگیره. ما تا الان یه تابع رو بعنوان پارامتر برای یه تابع دیگه موقع فراخوانی فرستادیم. حالا هم میتونیم یه تابع رو از یه تابع دیگه موقع فراخوانی تحویل بگیریم:
func makeRoundedRectangleMaker(_ sz:CGSize) -> () -> UIImage { // 1
    func f() -> UIImage { // 2
        let im = imageOfSize(sz) {
            let p = UIBezierPath(
                roundedRect: CGRect(origin:CGPoint.zero, size:sz),
                cornerRadius: 8
            )
            p.stroke()
        }
        return im
    }
    return f // 3
}

اجازه بدین کدمون رو با آرامش تحلیل کنیم:

  1. تعریف تابع، سخت‌ترین قسمتشه. نوع (امضای) تابع makeRoundedRectangleMaker چیه؟ درحقیقت امضای این تابع اینه:
    (CGSize) -> () -> UIImage
    این عبارت دو عملگر فلش داره. برای اینکه درکش کنین، بخاطر بسپارین که هر چیزی بعد از عملگر فلش، نوع مقدار بازگشتی رو مشخص میکنه. بنابراین، تابع makeRoundedRectangleMaker یه تابع هست که یه پارامتر CGSize میگیره و یه ()->UIImage برمیگردونه. خوب حالا این چیه؟ ما اینو میدونیم: یه تابع هست که هیچ پارامتری نمیگیره و یه UIImage برمیگردونه. بنابراین makeRoundedRectangleMaker تابعی هست که یه CGSize میگیره و یه تابع برمیگردونه که اون تابع هم وقتی فراخوانی بشه، پارامتری نمیگیره و یه UIImage تحویل میده.
  2. حالا توی بدنه‌ی تابع makeRoundedRectangleMaker هستیم و اولین قدم تا تعریف یه تابع هست (تابعی داخل یه تابع دیگه، یا یه تابع محلی). این تابع دقیقاً از همون نوعی هست که میخوایم برگردونیم (امضاش با خروجی تابع اصلی یکیه)، یعنی پارامتر ورودی نداره و خروجیش UIImage هست. در اینجا این تابع رو f نامگذاری کردیم. کارکرد این تابع ساده و آشناست: تابع imageOfSize رو صدا میزنه و یه تابع بی‌نام براش میفرسته که یه تصویر از مستطیل با لبه‌ی گرد میسازه (im) و بعد اون رو برمیگردونه.
  3. درنهایت ما از دستور return برای برگردوندن f استفاده میکنیم. بنابراین ما قرارداد امضای تابع رو رعایت کردیم: ما گفتیم که یه تابع بدون پارامتر ورودی و با خروجی UIImage برمیگردونیم و همین‌کار رو هم انجام دادیم.

اما ممکنه هنوز با دهان باز دارین به makeRoundedRectangleMaker نگاه میکنین و تعجب کردین از اینکه چطور باید صداش بزنین و وقتی صداش زدین، چی تحویل میگیرین؟ اجازه بدین امتحان کنیم:
let maker = makeRoundedRectangleMaker(CGSize(width:45, height:20))

خوب الان مقدار متغیر maker چیه؟ یه تابعه که پارامتر ورودی نداره و خروجیش UIImage حاوی مستطیل لبه‌گرد با ابعاد مشخص‌شده یعنی 45,20 هست. حرفمو باور نمیکنین؟ ثابت میکنم. کافیه تابع رو که الان توی متغیر maker ذخیره شده صدا بزنیم:
self.myImageView.image = maker()

حالا که متوجه شدین که چطور میشه یه تابع، یه تابع دیگه تولید کنه و بعنوان نتیجه برگردونه، یکبار دیگه به پیاده‌سازی makeRoundedRectangleMaker دقت‌کنید و اجازه‌بدین دوباره به‌شکل دیگری تحلیلش کنیم. یادتون باشه که من اون تابع رو برای اینکه فقط نشون بدیم که یه تابع میتونه یه تابع دیگه رو برگردونه ننوشتم. من این مثال رو برای نمایش کلوژرها نوشتم. اجازه بدین درمورد چگونگی دراختیارگرفتن محیط فکر کنیم:
func makeRoundedRectangleMaker(_ sz:CGSize) -> () -> UIImage {
    func f () -> UIImage {
        let im = imageOfSize(sz) { // *
            let p = UIBezierPath(
                roundedRect: CGRect(origin:CGPoint.zero, size:sz), // *
                cornerRadius: 8
            )
            p.stroke()
        }
        return im
    }
    return f
}

تابع f هیچ پارامتری نمیگیره ولی دوبار داخل بدنه‌ی تابع f (که با علامت ستاره مشخص کردم، از متغیر sz استفاده شده. بدنه‌ی تابع f میتونه sz رو ببینه که پارامتر ورودی تابع بیرونی محسوب میشه چون توی محدوده‌ی بیرونی تابع f قرار داره. تابع f ارجاع به sz رو زمانی که تابع makeRoundedRectangleMaker صدا زده میشه، «در اختیار میگیره» و این ارجاع رو وقتی که تابع f برگردونده میشه و به متغیر maker نسبت داده میشه، «نگه میداره»:
let maker = makeRoundedRectangleMaker(CGSize(width:45, height:20))

بخاطر همینه که maker الان تابعی هست که وقتی فراخوانی بشه، یه تصویر با ابعاد مشخص 45,20 میسازه حتی در زمانی که خودش رو «بدون هیچ پارامتری» صدا میزنیم:
self.myImageView.image = maker()

اگه جور دیگه به قضیه نگاه کنیم، تابع makeRoundedRectangleMaker یه کارخانه برای ساخت تمام خانواده‌ی توابع مشابه maker هست که هرکدوم تصویری با ابعاد خاص خودشون رو میسازن. این مثال، خیلی قشنگ قدرت بستارها (Closure) رو نشون میده.

قبل از اینکه تابع makeRoundedRectangleMaker رو رها کنیم، دوست دارم اون رو به‌شکل جذاب‌تری با قابلیت‌های Swift بازنویسی کنم. داخل f نیاز به تولید im و برگردوندنش نداریم! میتونیم نتیجه‌ی فراخوانی imageOfSize رو مستقیماً برگردونیم:
func makeRoundedRectangleMaker(_ sz:CGSize) -> () -> UIImage {
    func f () -> UIImage {
        return imageOfSize(sz) { // *
            let p = UIBezierPath(
                roundedRect: CGRect(origin:CGPoint.zero, size:sz), // *
                cornerRadius: 8
            )
            p.stroke()
        }
    }
    return f
}

اما در اینجا حتی نیاز به تعریف f و بعد، برگردوندنش هم نداریم و میتونیم این دو مرحله رو هم ترکیب کنیم:
func makeRoundedRectangleMaker(_ sz:CGSize) -> () -> UIImage {
    return {
        return imageOfSize(sz) { // *
            let p = UIBezierPath(
                roundedRect: CGRect(origin:CGPoint.zero, size:sz), // *
                cornerRadius: 8
            )
            p.stroke()
        }
    }
}

ولی الان تابع بی‌نام ما فقط شامل یه دستوره که اونم نتیجه‌ی فراخوانی imageOfSize رو برمیگردونه (تابع بی‌نام فراخوانی imageOfSize چند دستور داره ولی خود فراخوانی imageOfSize یه دستور در زبان سویفت محسوب میشه). بنابراین نیاز به دستور return هم نداریم:
func makeRoundedRectangleMaker(_ sz:CGSize) -> () -> UIImage {
    {
        imageOfSize(sz) { // *
            let p = UIBezierPath(
                roundedRect: CGRect(origin:CGPoint.zero, size:sz), // *
                cornerRadius: 8
            )
            p.stroke()
        }
    }
}
تشکر شده توسط:
#32
مقداردهی متغیر در اختیار گرفته‌شده توسط Closure
قدرتی که کلوژر ازطریق قابلیت در اختیار گرفتن محیطش بدست میاره، خیلی بیشتر از چیزیه که تا اینجا دیدین. اگه یه کلوژر، یه ارجاع به متغیری خارج از خودش رو بدست بیاره و اون متغیر هم قابل مقداردهی باشه، «کلوژر میتونه مقدار متغیر رو تغییر بده».

برای مثال، فرض کنید این تابع ساده رو تعریف کردیم. تمام کاری که انجام میده اینه که یه تابع دریافت کنه که یه پارامتر Int میگیره و اون تابع رو با مقدار 100 صدا میزنه:
func pass100 (_ f:(Int)->()) {
    f(100)
}

حالا به‌دقت به این کد نگاه کنین و سعی کنین حدس بزنین وقتی اجرا بشه چه اتفاقی میفته؟
var x = 0
print(x)
func setX(newX:Int) {
    x = newX
}
pass100(setX)
print(x)

واضحه که اولین print(x) عدد 0 رو چاپ میکنه. دومین print(x) باعث چاپ عدد 100 میشه! تابع pass100 توی کدمون مورد دسترسی قرار گرفته و مقدار متغیر x رو تغییر داده. علت این موضوع اینه که تابع setX که برای pass100 فرستادیم، حاوی یه ارجاع به x هست و نه‌تنها شامل این متغیر میشه بلکه اونو در اختیار میگیره و درنتیجه میتونه مقدارش رو تغییر بده. درحقیقت x داخل setX همون x بیرونیه. بنابراین pass100 قادر به تنظیم x هست، درست مثل زمانی که مستقیماً setX رو صدا بزنیم.
تشکر شده توسط:
#33
حفظ محیط در اختیار گرفته‌شده توسط Closure
وقتی که یه کلوژر محیطش رو در اختیار میگیره، این محیط رو «حفظ میکنه»، حتی اگه «هیچ کار دیگری انجام نده». این مثال رو ببینید تا مغزتون منفجر بشه (تابعی که یه تابع دیگه رو تغییر میده) :
func countAdder(_ f: @escaping ()->()) -> () -> () {
    var ct = 0
    return {
        ct = ct + 1
        print("count is \(ct)")
        f()
    }
}

تابع countAdder یه تابع بعنوان پارامتر میگیره و یه تابع بعنوان نتیجه برمیگردونه (درمورد خصوصیت @escaping توی مطلب بعدی توضیح میدم). تابعی که برمیگردونه، تابعی که بعنوان پارامتر گرفتیم رو صدا میزنه با یه تفاوت: یه متغیر رو افزایش میده و مقدار نتیجه رو هم برمیگردونه. بنابراین، سعی کنید الان حدس بزنین که وقتی این کد اجرا بشه، چه اتفاقی میفته:
func greet () {
    print("Hello")
}
let countedGreet = countAdder(greet)
countedGreet()
countedGreet()
countedGreet()

کاری که انجام دادیم اینه که یه تابع greet رو ساختیم که کلمه‌ی Hello رو چاپ میکنه و اونو به countAdder پاس دادیم. چیزی که از اون‌طرف countAdder خارج میشه، یه تابع جدیده که اسمش رو countedGreet گذاشتیم. بعد این تابع رو سه‌بار صدا زدیم. در اینجا خروجی که توی کنسول چاپ میشه رو میبینید:
count is 1
Hello
count is 2
Hello
count is 3
Hello

واضحه که countAdder قابلیت «گزارش تعداد دفعات فراخوانی» رو به تابعی که بهش پاس داده میشه، اضافه کرده. حالا از خودتون بپرسین: چطور میشه یه متغیر، این تعداد رو نگهداری کنه؟ داخل countAdder یه متغیر محلی به‌اسم ct داریم ولی این متغیر داخل تابع بی‌نامی که countAdder برمیگردونه تعریف نشده. این‌کار عمدی بوده! اگه داخل تابع بی‌نام تعریف میشد، هردفعه مقدار 0 داخلش قرار میگرفت و نمیتونستیم تعداد دفعات فراخوانی رو حساب کنیم. درعوض ct یکبار مقداردهی اولیه میشه و بعد توسط تابع بی‌نام Capture میشه. بنابراین، این متغیر بعنوان بخشی از محیط counterGreet «حفظ میشه». این متغیر خارج از countedGreet در یه دنیای مرموز حفظ‌کننده قرار گرفته، درنتیجه میتونه هربار تابع countedGreet فراخوانی میشه، افزایش پیدا کنه.
تشکر شده توسط:
#34
فرار کردن از Closure
اگه یه تابع که بعنوان پارامتر برای یه تابع دیگه ارسال شده، برای استفاده‌های بعدی حفظ بشه (بجای اینکه مستقیماً صداش بزنیم)، نوعش باید با علامت @escaping علامت‌گذاری بشه تا مشخص‌کنه که این کلوژر، محیطش رو در اختیار میگیره و حفظ میکنه.

برای اینکه بهتر متوجه بشین، بگذارین یه مثال بزنم. این تابع رو درنظر بگیرین. این کد مجازه چون تابعی که دریافت میکنه رو مستقیماً صدا میزنه:
func funcCaller(f:()->()) {
    f()
}

این‌کد هم قانونیه، حتی با این‌وجود که تابعی رو برمیگردونه که قراره بعداً اجرا بشه (چون خودش تابع رو «میسازه») :
func funcMaker() -> () -> () {
    return { print("Hello World!") }
}

ولی این کد غیرمجازه چون داره تابعی رو برای فراخوانی‌های بعدی برمیگردونه که بعنوان پارامتر ورودی «گرفته» و مستقیماً صداش نزده:
func funcPasser(f:()->()) -> () -> () { // compile error
    return f
}

راه‌حل اینه که نوع پارامتر ورودی f رو با علامت @escaping مشخص کنیم و کامپایلر هم به شما چنین‌کاری رو تذکر میده:
func funcPasser(f:@escaping ()->()) -> () -> () { // compile error
    return f
}

نتیجه‌ی دوم این ساختار متفاوت اینه که اگه یه تابع بی‌نام برای یه پارامتر @escaping ارسال بشه که به یه خصوصیت یا متد از self دسترسی داشته باشه، کامپایلر تأکید میکنه که حتماً باید self رو صراحتاً بنویسین. علت این موضوع اینه که چنین ساختاری باعث «در اختیار گرفتن» self میشه و کامپایلر هم میخواد که شما اطلاعتون از این حقیقت رو با «نوشتن» self اعلام کنید.
تشکر شده توسط:
#35
توابع دست‌کاری‌شده
یکبار دیگه برگردیم سراغ تابع makeRoundedRectangleMaker :
func makeRoundedRectangleMaker(_ sz:CGSize) -> () -> UIImage {
    return {
        imageOfSize(sz) {
            let p = UIBezierPath(
                roundedRect: CGRect(origin:CGPoint.zero, size:sz),
                cornerRadius: 8
            )
            p.stroke()
        }
    }
}

یه چیزی اینجا وجود داره که من دوستش ندارم: سایز مستطیل لبه‌گرد یه پارامتره (sz) ولی cornerRadius با عدد 8 مقداردهی مستقیم شده. من دوست دارم قابلیت مشخص‌کردن مقدار گردی گوشه‌ها رو هم داشته باشم. در اینجا دو روش برای انجام این‌کار وجود داره. یکی که احتمالاً حدس زدین، اینه که به تابع makeRoundedRectangleMaker یه پارامتر دیگه هم اضافه کنیم:
func makeRoundedRectangleMaker(_ sz:CGSize, _ r:CGFloat) -> () -> UIImage {
    return {
        imageOfSize(sz) {
            let p = UIBezierPath(
                roundedRect: CGRect(origin:CGPoint.zero, size:sz),
                cornerRadius: r
            )
            p.stroke()
        }
    }
}

و بعد اینطوری صداش بزنیم:
let maker = makeRoundedRectangleMaker(CGSize(width:45, height:20), 8)

ولی یه راه دیگه هم وجود داره. تابعی که ما از makeRoundedRectangleMaker تحویل میگیریم، هیچ پارامتری نداره. درعوض میتونیم یه پارامتر اضافه براش تعریف کنیم:
func makeRoundedRectangleMaker(_ sz:CGSize) -> (CGFloat) -> UIImage {
    return {
        r in
        imageOfSize(sz) {
            let p = UIBezierPath(
                roundedRect: CGRect(origin:CGPoint.zero, size:sz),
                cornerRadius: r
            )
            p.stroke()
        }
    }
}

حالا makeRoundedRectangleMaker یه تابع برمیگردونه که خودش یه پارامتر میگیره و این موضوع رو باید موقع فراخوانی درنظر داشته باشیم:
let maker = makeRoundedRectangleMaker(CGSize(width:45, height:20))
self.myImageView.image = maker(8)

اگه نیاز به نگهداری maker برای کار دیگه‌ای نداشته باشیم، میتونیم این کارها رو توی یه‌خط انجام بدیم و پرانتز دوم رو هم در ادامه‌ی فراخوانی makeRoundedRectangleMaker بنویسیم و تابع خروجی رو همونجا صدا بزنیم:
self.myImageView.image = makeRoundedRectangleMaker(CGSize(width:45, height:20))(8)

نقل قول:وقتی یه تابع، تابع دیگری رو برمیگردونه که یه پارامتر به این شکل میگیره، بهش اصطلاحاً Curried Function میگن (از اسم دانشمند Haskell Curry گرفته شده).
تشکر شده توسط:




کاربران در حال بازدید این موضوع: 1 مهمان