استفاده از توابع Scope یا Scope Functions در کاتلین (let، run، with، alsoو apply)، تمامی توسعه دهندگانی که از جاوا به کاتلین مهاجرت کرده اند قریب به اتفاق به این نتیجه رسیده اند که کاتلین برای توسعه بسیار راحت تر و سریع تر خواهد بود چرا که میزان کد کمتری به کار خواهد رفت و تمام توسعه دهندگان قطعا می دانند که کد کمتر برابر است با میزان خطای کمتر (Less Code Means Less Bugs).
یکی دیگر از امکانات کاتلین توابع Scope یا scope function هایی ست که در اختیار ما قرار می دهد.اگر مدتی است که با کاتلین کار می کنید قطعا به scope function های let , run , with , also , apply برخوردید و می دانید که چه عملکردی دارند.اما قطعا تفاوتی بین این scope ها هست و هرکدام را باید جای درست استفاده کرد که ممکن است تا به حال به این تفاوت توجه ای نکرده باشید، چرا که همه آنها در ظاهر یک عملکرد مشخص دارند اما شرایط خاصی که در اختیار ما قرار می دهند مهم و مطرح است که در این مقاله از تجاری اپ بطور کامل به آن میپردازیم.
به نقل از خود کاتلین:
By definition, Scoped functions are functions that execute a block of code within the context of an object.
بخواهیم به طور مختصر به معنی function scope بپردازیم می توانیم بگوییم: درواقع function scope ها یک محدوده یا بلاک را در اختیار ما قرار می دهند که بتوانیم دستورات خاصی را درون آن، با شرایط (context) خاص آن محدوده یا scope اجرا کنیم.به عبارت دیگر این scope ها یک بلاک با context موقت در اختیار توسعه دهنده قرار می دهد که بتواند دستورات خود را درون آن scope با context مشخص اجرا کند.در نهایت کد ما بسیار تمیز و مرتب خواهد بود چرا که تمام کدها ( بسته به نیاز ) بلاک بندی شده و هر بلاک کار خود را انجام خواهد داد.
در مجموع ما 5 نوع Scope function مختلف در کاتلین داریم: let، run، with، also و apply ،که در ادامه هرکدام را جداگانه با مثال مورد بررسی قرار خواهیم داد.اما پیش از آموزش مثال ها، فرض کنید ما یک مدل بصورت زیر داریم که درادامه برای تمامی مثال ها از این model استفاده می کنیم.
class Person() {
var name: String = "Abcd"
var contactNumber: String = "1234567890"
var address: String = "xyz"
fun displayInfo() = print("\n Name: $name\n " +
"Contact Number: $contactNumber\n " +
"Address: $address")
}
به مثال زیر دقت کنید:
private fun performLetOperation() {
val person = Person().let {
return@let "The name of the Person is: ${it.name}"
}
print(person)
}
output:
The name of the Person is: Abcd
همانگونه که از قطعه کد زیر مشخص است متد let به آبجکت person اختصاص داده شده و خروجی آن یک مقدار String است که البته خروجی می تواند هر مقدار از هرنوع دیگر هم باشد.به عبارت دیگر اپراتور let این موقعیت را فراهم می کند که یک سری عملیات برروی آبجکت جاری انجام شود و در نهایت یک خروجی با نوع مورد نظر return شود.
توجه کنید که هیچ الزامی به نوشتن دستور reutn@let قبل از مقدار بازگشتی نیست. این دستور تنها برای خوانایی بیشتر کد نوشته شده .درصورتی که در کاتلین آخرین خط کد که به هیچ موقعیت خاصی اختصاص نداده باشد مقدار بازگشتی تلقی می شود.
برای مثال، مثال فوق بدین صورت می تواند نوشته شود:
private fun performLetOperation() {
val person = Person().let {
"The name of the Person is: ${it.name}"
}
print(person)
}
output:
The name of the Person is: Abcd
مثال فوق دقیقا همان عملکرد را خواهد داشت چرا که اخرین خط که print(person) است به هیچ state خاصی اختصاص داده نشده پس به عنوان مقدار خروجی let شناخته می شود.نکته دیگر درباره let این است که می توانیم خروجی آن را حذف کنیم طوری که let هیچ خروجی نداشته باشد.در آن صورت let مانند یک تابع عمل می کند که هیچ خروجی ندارد یا عبارتی از نوع Unit است.
private fun performLetOperation() {
val person = Person().let {
it.name = "NewName"
}
print(person)
}
output:
kotlin.Unit
همانگونه که می بینید دستور print همچنان کار میکند اما خروجی آن نوع Koltin.Unit است بدین معنی که هیچ مقداری ندارد.
اپراتور let به Context خود با it اشاره می کند (به صورت پیش فرض).
private fun performLetOperation() {
val person = Person().let { it ->
it.name = "NewName"
}
print(person)
}
که البته می توانیم جهت خوانایی و سادگی بیشتر نام it را به هر مقدار دیگری تغییر دهیم.
private fun performLetOperation() {
val person = Person().let { it ->
it.name = "NewName"
}
print(person)
}
این ویژگی زمانی مفید خواهد بود که ما چندین بلاک let تو در تو داشته باشیم و بخواهیم از Context هرکدام استفاده کنیم، خب طبیعتا اگر تمامی آنها it باشند به مشکل برخواهیم خورد پس این تغییر نام میتواند کمک بزرگی برای توسعه دهنده باشد.
ویژگی دوم let برای تشخیص و چک کردن null هاست که باعث می شود این عمل راحت تر انجام شود.خب نیاز به گفتن نیست که nullSafe بودن کاتلین برگ بنده آن است که در این بحث let این قابلیت را بطور کامل پشتیبانی می کند و nullSafe بودن خود را به صورت زیر حفظ میکند.
var name: String? = "Abcd"
private fun performLetOperation() {
val name = Person().name?.let {
"The name of the Person is: $it"
}
print(name)
}
فرض کنید که مقدار name می تواند null باشد یا به عبارتی nullable است و ما می خواهیم مقدار name را هم تنها درصورتی که null نباشد print کنیم پس می توانیم کد تمیز و مختصر فوق را بنویسیم که با let.? تشخیص می دهد که آیا مقدار name از آبجکت Person نال است یا خیر و اگر نال نبود به داخل بلاک رفته و مقدار باز گشتی معتبری print می شود.اما let حتی می تواند در دستورات زنجیروار یا chain call هم به صورت زیر کاربرد موثری داشته باشد.
fun main() {
val numbers = mutableListOf("One","Two","Three","Four","Five")
val resultsList = numbers.map { it.length }.filter { it > 3 }
print(resultsList)
}
در مثال فوق هدف ما این بوده که آیتم هایی که طول آنها بیشتر از ۳ است را درون یک متغیر دیگر قرار دهیم و در نهایت آن را print کنیم .اما می توانیم مثال فوق را به طور خلاصه تری با let به صورت زیر بنویسیم.
fun main() {
val numbers = mutableListOf("One","Two","Three","Four","Five")
numbers.map { it.length }.filter { it > 3 }.let {
print(it)
}
}
عملگر run همانند عملگر let در بخش برگرداندن (return) داده عمل می کند یعنی به این صورت که هم می تواند داده ای برنگرداند و هم می تواند داده از هرنوعی return کند. به همین خاطر معمولا دستور run برای مقدار دهی یک آبجکت به کار می رود که بتواند در انتهای دستورات مقداردهی یک مقدار مشخص هم برگرداند ( return ).
private fun performRunOperation() {
Person().run {
name = "iman"
contactNumber = "09123456789"
return@run "The details of the Person is: ${displayInfo()}"
}
}
output:
Name: Asdf
Contact Number: 0987654321
Address: xyz
همانطور که می بینید run scope به یک آبجکت اختصاص داده شده و درون بلاک آن تمامی فیلد های آن آبجکت مقداردهی شد و در انتها فانکشن displayInfo() مربوط به کلاس Person فراخوانی شده.
خب طبق توضیحات دستور run بسیار شبیه به let بود اما دلیل نوشتن این پست نیز درک تفاوت های همین دستوراتی ست که به ظاهر یک کار مشخص را انجام میدهند.
همانطور که گفته شد هردو let و run یک مقدار را return می کنند که می تواند از هر نوعی باشد.اما تفاوت دستورات let و run در چیست ؟
دستور run به context آبجکت با عبارت this اشاره می کند در حالی که دستور let با عبارت it به context خود اشاره می کرد.
به همین خاطر است که در بلاک run ما از this.name استفاده نمی کنیم ( استفاده از دستور this اختیاری ست ) و تنها نوشتن خود name کفایت می کند چرا که بلاک run تشخیص میدهد اما در let شما باید بدین صورت به name اشاره کنید it.name .
اما دستور run بدلیل اینکه از this استفاده می کند این اجازه را از شما می گیرد که نام اشاره گر را از this به نام دیگری تغییر دهید اما در let شما می توانید نام it را به هر نام دیگری تغییر دهید که این مسئله در let های زنجیره ای یا تودرتو به شما کمک بزرگی در درک هر بلاک خواهند کرد.
Person().name?.let {some ->
"The name of the Person is: $some"
}
این نکته را هم درنظر داشته باشید که هردو let و run به طور کامل null checks هستند یعنی توانایی مدیریت کامل آبجکت های null را دارند.
val name = Person().name?.run {
"The name of the Person is: $this"
}
print(name)
دستور with بسیار شبیه به run است بلکه حتی از this به عنوان اشاره گر به context بلاک خود استفاده می کند.
private fun performWithOperation() {
val person = with(Person()) {
return@with "The name of the Person is: ${this.name}"
}
print(person)
}
Output:
The name of the Person is: Abcd
خب حال دستور run با این میزان شباهت به with چه تفاوت هایی با هم دارند؟
خب نقطه مهم و البته حیاتی در کدنویسی کاتلین بین این دو function scope اینجاست که دستور with قابلیت null check ندارد و این عمل باید بصورت دستی برای استفاده از هر فیلد در بلاک with پیاده شود.
همانطور که در تصویر میبینید ما یک خطا از سمت کاتلین داریم که احتمال null بودن مقدار name را می دهد و از ما می خواهد که با یکی از دستورات ? یا !! آن را کنترل کنیم.پس باید دستورات فوق را بدین صورت تغییر دهیم:
private fun performWithOperation() {
val person: Person? = null
with(person) {
this?.name = "asdf"
this?.contactNumber = "1234"
this?.address = "wasd"
this?.displayInfo()
}
}
برخلاف run که خود قابلیت null check داشت و کار را بسیار راحتتر میکرد( در بالا بصورت کامل توضیح دادیم ).
private fun performRunOperation() {
val person: Person? = null
person?.run {
name = "asdf"
contactNumber = "1234"
address = "wasd"
displayInfo()
}
}
دستور apply تنها در بحث اشاره گر context که از this استفاده می کند و نه it به run شبیه است .
private fun performApplyOperation() {
val person: Person? = null
person?.apply {
name = "iman"
contactNumber = "1234"
address = "tehran"
displayInfo()
}
}
استفاده های پرکاربرد از apply در برنامه نویسی اندروید، دستور apply مخصوصا در برنامه نویسی اندروید بسیار پرکاربر است.برای مثال زمان های زیادی پیش می آید که ما نیاز داریم یک نمونه از Intent یا Alert Dialog یا … را به همراه اعمال یک سری خصیصه ها (attribute) برگردانیم.برای مثال:
fun createIntent(intentData: String, intentAction: String): Intent {
val intent = Intent()
intent.action = intentAction
intent.data = Uri.parse(intentData)
return intent
}
مثال فوق به چشم بسیاری از توسعه دهندگان اندروید آشناست که یک تابع برای تولید یک Intent خاص پیاده شود.اما می توان تابع فوق را با استفاد از apply خیلی تمیزتر به صورت زیر پیاده کرد:
fun createIntent(intentData: String, intentAction: String) =
Intent().apply {
action = intentAction
data = Uri.parse(intentData)
}
همانطور که می بینید با استفاده از apply دیگر نیازی به ساختن یک نمونه از Intent و اعمال صفت ها به آن نیست پس در نهایت کد تمیزتر و بهینه تری خواهیم داشت.
ما گفتیم که دو دستور apply و run به هم شباهت دارند اما تفاوت اصلی این دو دستور در نوع بازگشتی آنهاست.دستور run می تواند مقدار بازگشتی با هر نوعی داشته باشد در صورتی که دستور apply به صورت Unit است و نمی تواند بازگشتی داشته باشد.
متد also شبیه به let است اما تنها در بحث اشاره گر به Context که با عبارت it مشخص میشود و نه this .
private fun performAlsoOperation() {
val name = Person().also { currentPerson ->
print("Current name is: ${currentPerson.name}\n")
currentPerson.name = "modifiedName"
}.run {
"Modified name is: $name\n"
}
print(name)
}
output:
Current name is: Abcd
Modified name is: modifiedName
در مثال فوق مزیت استفاده از اشاره گر با نام دلخواه را متوجه می شوید.همانطور که می بینید به جای it از currentPerson برای خوانایی بیشتر استفاده کردیم.یکی از مزایای also این است که می توان از آن به صورت زنجیروار استفاده کرد.اما تصور کنید که ما مثال فوق را بدون استفاده از also پیاده می کردیم:
var name = Person().name
print("Current name is: $name\n")
name = "modifiedName"
name = name.run {
"Modified name is: $name\n"
}
print(name)
همانطور که می بینید از تمیزی و خوانایی کد کاسته شده درحالی که خروجی همان است.اما با استفاده از also می توان کد تمیزتر و readable تری داشت و همچنین می توان زنجیره های scope ها را به وجود آورد و کد را به حداقل رساند.
خب همانطور که گفته شد دستور also به let شباهت بسیار دارد اما تفاوت این دو function scope در چیست ؟
دستور also مقدار برگشتی نمی تواند داشته باشد و از نوع Unit است درحالی که let می تواند مقدار برگشتی با هرنوعی داشته باشد.
امیدوارم این آموزش برای شما مفید بوده باشد…