توابع محدوده‌بندی (Scope Functions) در کاتلین – apply, with, let, also, run

توابع محدوده‌بندی (Scope Functions) در کاتلین – apply, with, let, also, run

برنامه‌نویسی عملکردی (functional) به خوبی توسط سینتکس و کتابخانه استاندارد کاتلین پشتیبانی می‌شود. در این پست به تشریح پنج تابع مرتبه بالا (higher-order) در زبان کاتلین، یعنی apply ،with ،let ،also و run، می‌پردازیم.هنگام یادگیری این پنج تابع باید دو چیز را به خاطر بسپارید: زمان و چگونگی استفاده از آن‌ها. به این دلیل که با توجه به ماهیت مشابه این پنج تابع، در ابتدا ممکن است وجود هر پنج تای آن‌ها غیر ضروری به نظر برسد.در این پست ما ابتدا مشترکات این پنج تابع را بررسی می‌کنیم. سپس با تفاوت‌های آن‌ها آشنا می‌شویم و در آخر موارد استفاده‌ی آن‌ها را یاد خواهیم گرفت.همه‌ی این پنج تابع اساسا یک کار انجام می‌دهند. آنها توابع محدوده‌بندی هستند که یک آرگومان گیرنده و یک قطعه کد می‌گیرند و بعد قطعه کد ارائه شده را بر روی گیرنده اجرا می‌کنند.بیایید با نحوه کار یکی از این توابع آشنا شویم. تابع with به صورت زیر تعریف شده است:inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}با استفاده از این تابع می‌توان کد را مختصرتر کرد. ابتدا بیایید مقداری کد معمولی بدون استفاده از توابع محدوده‌بندی ببینیم:class Person {
var name: String? = null
var age: Int? = null
}
val person: Person = getPerson()
print(person.name)
print(person.age)قطعه کدی که در ادامه می‌بینید معادل کد بالا است با این تفاوت که در این‌جا از تابع with جهت حذف ارجاع‌های مکرر به متغیر person استفاده کرده‌ایم:val person: Person = getPerson()
with(person) {
print(name)
print(age)
}بسیار عالی! پس چه نیازی به چهار تابع دیگر است؟اگرچه این توابع یک کار انجام می‌دهند، اما تفاوت هایی نیز در امضا و نحو پیاده‌سازی آن‌ها، وجود دارد. این تفاوت‌ها مشخص می‌کند که چگونه باید از این توابع استفاده کرد.بیایید تابع with را با امضا و پیاده‌سازی یک تابع دیگر مانند also، مقایسه کنیم:inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}
inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}تابع with و also در سه مورد با هم تفاوت دارند:در تابع with، آرگومان گیرنده با یک پارامتر صریح از نوع T مشخص شده است. این در حالی است که در تابع also ما گیرنده‌ی ضمنی T را داریم.آرگومان قطعه کد (block) در تابع with، به صورت یک تابع با گیرنده‌ی ضمنی T تعریف می‌شود. در صورتی که تابع also یک پارامتر صریح از نوع T دارد.تابع with هر چه را که اجرای آرگومان قطعه کد برمیگرداند، برمیگرداند. ولی تابع also همان آبجکتی را که به عنوان گیرنده به آن داده شده بود را برمی‌گرداند.به خاطر این سه تفاوت از تابع also باید به شکل دیگری استفاده کرد:val person: Person = getPerson().also {
print(it.name)
print(it.age)
}این قطعه کد، یک Person توسط تابع getPerson گرفته و آن را در متغیر person می‌ریزد. اما قبل از آن تابع also نام و سن person گرفته شده را چاپ می‌کند.در مورد سایر توابع (apply، let، run) چطور؟ تمامی آن‌ها در یکی از سه مورد بالا با یکدیگر تفاوت دارند.پارامتر گیرنده‌ی صریح یا گیرنده‌ی ضمنیآرگومان قطعه کد به صورت یک پارامتر صریح تعریف می‌شود یا به صورت یک گیرنده‌ی ضمنیبرگرداندن گیرنده یا برگرداندن آن چه آرگومان قطعه کد برمی‌گردانددر این‌جا تعریف هر پنج تابع را می‌بینید:inline fun <T, R> with(receiver: T, block: T.() -> R): R {
return receiver.block()
}
inline fun <T> T.also(block: (T) -> Unit): T {
block(this)
return this
}
inline fun <T> T.apply(block: T.() -> Unit): T {
block()
return this
}
inline fun <T, R> T.let(block: (T) -> R): R {
return block(this)
}
inline fun <T, R> T.run(block: T.() -> R): R {
return block()
}حالا ما تفاوت‌های این پنج تابع را می‌دانیم ولی همچنان نمی‌دانیم کی از کدام تابع استفاده کنیم. این توابع ماهیت مشابهی دارند و اغلب قابل تعویض هستند.تعدادی روش ایده‌آل (Best practice) در مستندات کاتلین موجود است. با یادگیری این روش‌ها، هم می‌توانید کد‌های بهتری بنویسید و هم کد‌های دیگر برنامه نویسان را بهتر متوجه می‌شوید.از apply زمانی استفاده کنید که نیازی به دسترسی به تابعی از گیرنده ندارید و همچنین می‌خواهید همان گیرنده را برگردانید. این اتفاق معمولا زمانی می‌افتد که می‌خواهید شی جدید بسازید. برای مثال قطعه کد زیر را ببینید:val peter = Person().apply {
// only access properties in apply block!
name = &quotPeter&quot
age = 18
}بدون استفاده از apply کد به این شکل در می‌آید:val clark = Person()
clark.name = &quotClark&quot
clark.age = 18اگر کد شما به پارامتر گیرنده خود دسترسی پیدا نمی کند ، یا اگر پارامتر گیرنده خود را تغییر ندهد از این تابع استفاده کنید. به عنوان مثال، استفاده از این تابع هنگام اجرای برخی از عوارض جانبی روی یک شی یا تایید اعتبار داده های آن قبل از اختصاص دادن آن به یک property بسیار مفید است:class Book(author: Person) {
val author = author.also {
requireNotNull(it.age)
print(it.name)
}
}قطعه کد بالا بدون استفاده از also:class Book(val author: Person) {
init {
requireNotNull(author.age)
print(author.name)
}
}از تابع let در یکی از مواقع زیر استفاده کنید:اجرای قطعه کدی تنها اگر مقدار داده شده null نباشدتبدیل یک شی nullable به یک شی nullable دیگرمحدود کردن محدوده‌ی یک متغیر محلیgetNullablePerson()?.let {
// only executed when not-null
promote(it)
}
val driversLicence: Licence? = getNullablePerson()?.let {
// convert nullable person to nullable driversLicence
licenceService.getDriversLicence(it)
}
val person: Person = getPerson()
getPersonDao().let { dao ->
// scope of dao variable is limited to this block
dao.insert(person)
}کد زیر معادل کد بالاست بدون استفاده از let:val person: Person? = getPromotablePerson()
if (person != null) {
promote(person)
}
val driver: Person? = getDriver()
val driversLicence: Licence? = if (driver == null) null else
licenceService.getDriversLicence(it)val person: Person = getPerson()
val personDao: PersonDao = getPersonDao()
personDao.insert(person)از این تابع زمانی استفاده کنید که گیرنده غیر null است و نیازی به نتیجه ندارید:val person: Person = getPerson()
with(person) {
print(name)
print(age)
}کد بالا بدون استفاده از with:val person: Person = getPerson()
print(person.name)
print(person.age)از این تابع زمانی استفاده کنید که می‌خواهید مقداری را حساب کنید یا اینکه محدوده‌ی چند متغیر محلی را محدود‌‌تر کنید. همچنین از این تابع زمانی که می‌خواهید پارامتر‌های صریح را به گیرنده‌ی ضمنی تبدیل کنید.val inserted: Boolean = run {
val person: Person = getPerson()
val personDao: PersonDao = getPersonDao()
personDao.insert(person)
}
fun printAge(person: Person) = person.run {
print(age)
}کد زیر برابر با کد بالاست بدون استفاده از تابع run:val person: Person = getPerson()
val personDao: PersonDao = getPersonDao()
val inserted: Boolean = personDao.insert(person)fun printAge(person: Person) = {
print(person.age)
}بخش‌های قبل نشان دادند که چگونه توابع محدوده‌بندی کد را خواناتر می‌کنند. بعضی مواقع ترکیب چند تابع محدوده‌بندی در یک قطعه کد وسوسه‌انگیز است.هنگامی که توابع محدوده تو در تو قرار می گیرند، کد به سرعت گیج کننده می شود. به عنوان یک قانون، سعی کنید از توابع محدوده‌بندی‌ای که آرگومان گیرنده‌شان را با گیرنده‌ی قطعه لامبدا متصل می‌کنند (apply، run، with) به شکل تو در تو استفاده نکنید. زمانی که از دیگر توابع (let، also) به شکل تو در تو استفاده می‌کنید، یک نام صریح برای پارامتر قطعه لامبدا مشخص کنید. یعنی از پارامتر ضمنی it استفاده نکنید.در کنار تو در تو استفاده کردن، توابع محدوده‌بندی می‌توانند به صورت زنجیره هم استفاده شوند. بر خلاف استفاده تو در تو در این حالت نه تنها از خوانایی کد کم نمی‌شود بلکه کد خواناتر هم می‌شود.در این‌جا مثالی از استفاده از این توابع به صورت زنجیره‌ای را می‌بینیم:private fun insert(user: User) = SqlBuilder().apply {
append(&quotINSERT INTO user (email, name, age) VALUES &quot)
append(&quot(?&quot, user.email)
append(&quot,?&quot, user.name)
append(&quot,?)&quot, user.age)
}.also {
print(&quotExecuting SQL update: $it.&quot)
}.run {
jdbc.update(this) > 0
}قطعه کد بالا یک تابع dao، برای درج یک user در دیتابیس را نشان می‌دهد. در تابع insert از قابلیت expression body کاتلین استفاده شده است. با این حال به خوبی دغدغه‌ها تفکیک شده‌اند: آماده‌سازی SQL، لاگ کردن SQL، و در نهایت اجرای SQL. در نهایت این تابع مقداری از نوع boolean که موفقیت یا عدم موفقیت عملیات درج را نشان می‌دهد را بر می‌گرداند.منبع

Author: admin

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *