1 مقدمه‌ای در مورد توابع ناشناخته

1.1 مقدمه‌ای نظری درباره توابع ناشناخته

توابع ناشناخته توابعی هستند که بدون نام اعلام شده‌ای می‌باشند. آنها می‌توانند به صورت مستقیم تعریف و استفاده شوند در جاهایی که نوع تابع مورد نیاز است. اینگونه توابع اغلب برای پیاده‌سازی محلی محافظت شده یا در وضعیت‌هایی با عمر کوتاه مورد استفاده قرار می‌گیرند. در مقایسه با توابع دارای نام، توابع ناشناخته به نام نیاز ندارند که به این معناست که می‌توانند در داخل یک متغیر تعریف شوند یا به صورت مستقیم در یک عبارت استفاده شوند.

1.2 تعریف و استفاده از توابع ناشناخته

در زبان Go، دستور نوشتار اساسی برای تعریف یک تابع ناشناخته به صورت زیر است:

func(arguments) {
    // محتوای تابع
}

استفاده از توابع ناشناخته می‌تواند به دو حالت تقسیم شود: اختصاص به یک متغیر یا اجرای مستقیم.

  • اختصاص به یک متغیر:
sum := func(a int, b int) int {
    return a + b
}

result := sum(3, 4)
fmt.Println(result) // خروجی: 7

در این مثال، تابع ناشناخته به متغیر sum اختصاص داده شده است، و سپس ما sum را همانند یک تابع معمولی فراخوانی می‌کنیم.

  • اجرای مستقیم (همچنین به عنوان تابع ناشناخته خوداجرا شونده):
func(a int, b int) {
    fmt.Println(a + b)
}(3, 4) // خروجی: 7

در این مثال، تابع ناشناخته فوراً پس از تعریف اجرا می‌شود، بدون اینکه نیاز به اختصاص به هر متغیری باشد.

1.3 نمونه‌های کاربردی از توابع ناشناخته

توابع ناشناخته به طور گسترده‌ای در زبان Go استفاده می‌شوند، و در زیر چندین سناریوی متداول آورده شده است:

  • به عنوان تابع فراخوانی پسخ: توابع ناشناخته اغلب برای پیاده‌سازی منطق فراخوانی پسخ استفاده می‌شوند. به عنوان مثال، هنگامی که یک تابع تابع دیگری را به عنوان پارامتر می‌گیرد، می‌توانید یک تابع ناشناخته را به عنوان پارامتر پاس دهید.
func traverse(numbers []int, callback func(int)) {
    for _, num := range numbers {
        callback(num)
    }
}

traverse([]int{1, 2, 3}, func(n int) {
    fmt.Println(n * n)
})

در این مثال، تابع ناشناخته به عنوان پارامتر پاس داده می‌شود تا traverse، و هر عدد پس از مربع‌کردن چاپ می‌شود.

  • برای انجام وظایف به صورت فوری: گاهی اوقات، نیاز داریم که یک تابع تنها یک بار اجرا شود و نقطه اجرا نزدیک است. توابع ناشناخته می‌توانند به صورت فوری فراخوانی شوند تا این نیاز را برآورده کنند و از تکرار کد بکاهی کنند.
func main() {
    // ...کد‌های دیگر...

    // بلوک کدی که نیاز به اجرای فوری دارد
    func() {
        // کد اجرای وظیفه
        fmt.Println("تابع ناشناخته فوری اجرا شد.")
    }()
}

در اینجا، تابع ناشناخته فوراً پس از اعلان اجرا می‌شود تا وظیفه کوچکی را بدون نیاز به تعریف یک تابع جدید به سرعت اجرا کند.

  • ختم‌ها: توابع ناشناخته به طور متداول برای ایجاد ختم‌ها استفاده می‌شوند چون می‌توانند متغیرهای بیرونی را ضبط کنند.
func sequenceGenerator() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

در این مثال، sequenceGenerator یک تابع ناشناخته برمی‌گرداند که متغیر i را در خود ضبط می‌کند، و هر فراخوانی مقدار i را یک واحد افزایش می‌دهد.

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

2 درک ژرف‌تری از ختم‌ها

2.1 مفهوم ختم‌ها

ختم یک مقدار تابع است که به متغیرهای بیرون از محتوای تابع خود مراجعه می‌کند. این تابع می‌تواند به این متغیرها دسترسی داشته باشد و آنها را بسته شود، به این معنی که نه‌تنها می‌تواند از این متغیرها استفاده کند، بلکه می‌تواند مقادیر مرجع را نیز تغییر دهد. ختم‌ها اغلب با توابع ناشناخته مرتبط هستند، زیرا توابع ناشناخته نام خود را ندارند و اغلب به صورت مستقیم در جایی که نیاز است تعریف می‌شوند، محیطی برای ختم‌ها را ایجاد می‌کنند.

مفهوم ختم به طور مستقیم از محیط اجرا و دامنه جدا نمی‌شود. در زبان Go، هر فراخوانی تابع چارچوب پشته خود را دارد که متغیرهای محلی تابع را ذخیره می‌کند. با این حال، حتی پس از اینکه تابع بیرونی برگشته باشد، ختم هنوز می‌تواند به متغیرهای بیرونی تابع مرجع دسترسی داشته باشد.

func outer() func() int {
    count := 0
    return func() int {
        count += 1
        return count
    }
}

func main() {
    closure := outer()
    println(closure()) // خروجی: 1
    println(closure()) // خروجی: 2
}

در این مثال، outer یک ختم برمی‌گرداند که به متغیر count مراجعه می‌کند. حتی بعد از پایان اجرای تابع outer، ختم هنوز می‌تواند count را تغییر دهد.

2.2 رابطه با توابع ناشناس

توابع ناشناس و بسته‌ها (closures) به طور نزدیکی مرتبط هستند. در زبان Go، یک تابع ناشناس یک تابع بدون نام است که می‌تواند تعریف شده و بلافاصله هنگام نیاز استفاده شود. این نوع تابع برای پیاده‌سازی رفتار بسته (closures) مناسب است.

بسته‌ها معمولاً در داخل توابع ناشناس پیاده‌سازی می‌شوند که می‌توانند متغیرهای محیطی خود را ضبط کنند. وقتی یک تابع ناشناس به متغیرها از محدوده بیرونی مراجعه می‌کند، تابع ناشناس همراه با متغیرهای مرجع، یک بسته را تشکیل می‌دهند.

func main() {
    adder := func(sum int) func(int) int {
        return func(x int) int {
            sum += x
            return sum
        }
    }

    sumFunc := adder()
    println(sumFunc(2))  // نتیجه: 2
    println(sumFunc(3))  // نتیجه: 5
    println(sumFunc(4))  // نتیجه: 9
}

در اینجا، تابع adder یک تابع ناشناس را برمی‌گرداند که با اشاره به متغیر sum یک بسته را تشکیل می‌دهد.

2.3 ویژگی‌های بسته‌ها

ویژگی مشخص‌ترین بسته‌ها، توانایی یادآوری محیطی است که در آن ایجاد شده‌اند. آن‌ها می‌توانند به متغیرهایی که خارج از تابع خود تعریف شده‌اند دسترسی داشته باشند. طبیعت بسته‌ها به آن‌ها امکان می‌دهد که وضعیت را (از طریق ارجاع به متغیرهای خارجی) محصور کنند که اساس پیاده‌سازی ویژگی‌های قدرتمندی در برنامه‌نویسی مانند دکوراتورها، محافظت وضعیت، و ارزیابی تنبل را فراهم می‌کند.

علاوه بر محافظت از وضعیت، بسته‌ها دارای ویژگی‌های زیر است:

  • تمدید عمر متغیرها: عمر متغیرهای خارجی که به بسته‌ها ارجاع داده می‌شود، در طول کل دوره وجود بسته تمتد می‌یابد.
  • محافظت از متغیرهای خصوصی: سایر روش‌ها نمی‌توانند به طور مستقیم به متغیرهای داخلی بسته‌ها دسترسی داشته باشند که امکان محافظت از متغیرهای خصوصی را فراهم می‌کند.

2.4 گلوگاه‌های متداول و ملاحظات

در استفاده از بسته‌ها، چند گلوگاه و جزئیات مشترک وجود دارد که باید آن‌ها را در نظر گرفت:

  • مشکل در بایند کردن متغیر‌های حلقه: استفاده مستقیم از متغیر حلقه برای ایجاد بسته‌ها در داخل حلقه ممکن است مشکلاتی ایجاد کند زیرا آدرس متغیر حلقه با هر دوره تغییر نمی‌کند.
for i := 0; i < 3; i++ {
    defer func() {
        println(i)
    }()
}
// خروجی ممکن است به جای 0، 1، 2، 3، 3، 3 از 3، 3، 3 باشد

برای جلوگیری از این گلوگاه، متغیر حلقه باید به عنوان یک پارامتر به بسته منتقل شود:

for i := 0; i < 3; i++ {
    defer func(i int) {
        println(i)
    }(i)
}
// خروجی صحیح: 0، 1، 2
  • نشتی حافظه بسته‌ها: اگر یک بسته دارای ارجاع به یک متغیر محلی بزرگ باشد و این بسته برای مدت زمان طولانی نگه داشته شود، ممکن است متغیر محلی بازیابی نشود که می‌تواند منجر به نشتی حافظه شود.

  • مسائل همزمانی با بسته‌ها: اگر یک بسته به صورت همزمان اجرا شود و به یک متغیر خاص ارجاع داشته باشد، باید اطمینان حاصل شود که این ارجاع به صورت همزمانی ایمن است. معمولاً ابزارهای همگام‌سازی مانند قفل‌های موتکس به این منظور نیاز دارند.

درک این گلوگاه‌ها و ملاحظات می‌تواند به توسعه‌دهندگان کمک کند تا بسته‌ها را به صورت ایمن‌تر و موثرتر استفاده کنند.