آموزش کامل lazy در کاتلین


اگه میخوای بدونی چطور lazy در کاتلین میتونه بهت کمک کنه تا کدهایی هوشمندانهتر بنویسی، حتماً این مقاله رو بخون و ترفندهای جالبش رو یاد بگیر!
اگر برنامه نویس کاتلین هستی یا تازه شروعش کردی امکان نداره که به کلمه lazy تا حالا برخورد نکرده باشی واسه همین تصمیم گرفتم توی این مقاله باهم دیگه زیر و بم lazy در کاتلین رو دربیاریم و اصلا ببینیم فلسفه به وجود اومدنش چیه و چطور میتونیم ازش توی کدهامون استفاده کنیم. برای این که این مفهوم بهتر واستون جا بیفته اول باید ببینیم که بهینه سازی در کاتلین چه اهمیتی داره پس در ادامه با من همراه باشید.
اهمیت بهینهسازی در کاتلین
همه میدونیم که بهینهسازی کدها چقدر میتونه توی کارمون مهم باشه. بهینهسازی یعنی کدی بنویسیم که نه تنها درست کار میکنه بلکه سریع و کارامد هم هست. مثلا وقتی یه برنامه خیلی طول میکشه تا اجرا بشه، ممکنه یوزر ازش دلزده بشه و حتی ازش دیگه استفاده نکنه. اینجا هست که بهینه سازی به دادمون میرسه تا بتونیم برنامهای بنویسیم که هم سریع باشه و هم منابع کمتری مصرف کنه.
معرفی مختصر ویژگی lazy در کاتلین
خب حالا که فهمیدیم بهینهسازی چقدر مهمه، بیایید با کلمه lazy آشنا بشیم. lazy یک ویژگی خیلی جالب توی کاتلین هست که به ما اجازه میده تا مقادیر رو فقط وقتی که بهشون نیاز داریم محاسبه کنیم اینطوریه که باعث میشه منابع سیستم هدر نره و برنامه سریعتر اجرا بشه.
lazy در کاتلین چیست
همونطور که توضیح دادم lazy در کاتلین به این معنی هست که یک مقدار رو فقط زمانی که لازم شد، محاسبه کنیم. برای مثال فرض کنید که یک لیست خیلی بزرگ داریم و نمیخوایم از اول همش رو بارگذاری کنیم بلکه فقط وقتی که کاربر درخواستش کرد اون موقع میایم بارگذاریش میکنیم این دقیقا کاری هست که lazy برای ما انجام میده.
حالا بیاید یکم دقیقتر به نحوه کارکرد lazy نگاه کنیم. طبق حرفهایی که تا الان زدیم، برای این که بخوایم یک شیئ رو تعریف کنیم ولی الان ازش استفاده نکینم، باید اون رو از نوع lazy تعریف کنیم. این شیء تا وقتی که مقدار نیاز بشه، دست نخورده باقی میمونه و فقط وقتی که اولین بار بهش دسترسی پیدا کنیم، مقدار محاسبه میشه. برای فهمیدن بهتر داستان کد زیر رو ببینید:
val myValue: String by lazy {
println("Computing the value...")
"Hello, World!"
}
توضیح کد:
- تعریف متغیر
val myValue:- با استفاده از val یه متغیر غیرقابل تغییر به نام my value تعریف میکنیم. این یعنی وقتی مقدار myValue تعیین شد، دیگه نمیتونیم تغییرش بدیم.
- نوع داده
: String:- myValue از نوع String هست. اینجا مشخص میکنیم که مقدار این متغیر باید یه رشته باشه.
- استفاده از
by lazy:- کلیدواژه by برای تعریف یک نماینده (delegate) استفاده میشه و lazy هم یکی از این نمایندههاست. این یعنی مقدار myValue به وسیله نماینده lazy محاسبه و مدیریت میشه. یکم جلو تر در مورد delegate هم توضیح میدم.
- بلاک کد
{ ... }برای lazy:- این بلاک کد شامل دستوراتی هست که زمانی که اولین بار به myValue دسترسی پیدا کنیم، اجرا میشه. هر چی توی این بلاک بنویسیم، همون موقع اجرا میشه.
تفاوت بین lazy در کاتلین و سایر روشهای بهینهسازی
خب حالا بیایید ببینیم lazy چه فرقی با بقیه روشهای بهینهسازی داره. روشهای مختلفی برای بهینهسازی وجود دارن، مثل استفاده از الگوریتمهای بهتر، کشینگ (caching)، و حتی بازنویسی کد به شکلی که کارآمدتر باشه. اما چیزی که lazy رو خاص میکنه، اینه که خیلی راحت و خودکار کار میکنه. یعنی نیازی نیست ما کلی تغییر توی کدهامون بدیم یا الگوریتمهای پیچیده پیاده کنیم؛ کافیه فقط از این ویژگی استفاده کنیم.
اگه میخواید بیشتر با کشینگ آشنا بشید به مثال زیر توجه کنید:
فرض کنید توی اپلیکیشنتون یه لیست بزرگ از محصولات دارید که کاربر بارها و بارها میبینه. اگه هر دفعه بخوایم این لیست رو از سرور بگیریم، خیلی زمان میبره و کاربر خسته میشه. ولی با کشینگ میتونیم این لیست رو یه بار بگیریم و بعدش از کش بخونیم. ساده و موثر!
val cache = mutableMapOf<String, String>()
fun getProduct(id: String): String {
return cache.getOrPut(id) {
// Fetch from server
fetchProductFromServer(id)
}
}
fun fetchProductFromServer(id: String): String {
// Simulate network request
println("Fetching product from server...")
return "Product details for $id"
}
ولی تفاوتی که این دو روش دارن توی زمان محاسبه و بارگذاری هست . درواقع lazy در کاتلین، زمان محاسبه رو به تعویق مینداره تا اولین بار بهش نیاز بشه اما کشینگ دادهها یا نتایج رو بعد از اولین محاسبه ذخیره میکنه تا دفعات بعدی سریعتر دسترسی پیدا کنیم.
delegation چیست
اول از همه، بگم که delegation به معنی واگذاری کارها به یه شیء دیگهست. یعنی چی؟ یعنی یه کلاس میتونه بعضی از وظایف یا رفتارهاشو به یه کلاس یا شیء دیگه بسپاره. این کار باعث میشه که کدها تمیزتر و قابل مدیریتتر بشن و از تکرار کدها جلوگیری بشه.
حالا ما توی کاتلین دو نوع delegation اصلی داریم با نامهای Class Delegation و Property Delegation که هرکدوم کاربردهای خاص خودشون رو دارند.
از اونجایی که صحبت در مورد دلیگیشنها خیلی طولانی میشه من اینجا فقط یک شرح مختصری میدم تا از موضوع اصلی مطلبون منحرف نشیم و حوصلتون سر نره.
Class Delegation
توی کلاس دلیگیشن، یه کلاس میتونه بعضی از وظایف یا متدهاشو به یه کلاس دیگه واگذار کنه. فرض کنید که یه اینترفیس داریم و چند تا کلاس که این اینترفیس رو پیادهسازی میکنن. حالا یه کلاس جدید داریم که میخوایم از یکی از اون کلاسهای پیادهسازیشده استفاده کنه، بدون اینکه دوباره همه متدها رو پیادهسازی کنه. اینجا از کلاس دلیگیشن استفاده میکنیم.
مثال:
interface SoundBehavior {
fun makeSound()
}
class DogSound : SoundBehavior {
override fun makeSound() {
println("Woof! Woof!")
}
}
class CatSound : SoundBehavior {
override fun makeSound() {
println("Meow! Meow!")
}
}
class Animal(private val soundBehavior: SoundBehavior) : SoundBehavior by soundBehavior
fun main() {
val dog = Animal(DogSound())
dog.makeSound() // Woof! Woof!
val cat = Animal(CatSound())
cat.makeSound() // Meow! Meow!
}
اینجا، کلاس Animal از کلاس SoundBehavior دلیگیت شده و بدون اینکه نیاز باشه متد makeSound رو دوباره پیادهسازی کنه، از پیادهسازی کلاسهای DogSound و CatSound استفاده میکنه.
Property Delegation
پراپرتی دلیگیشن که lazy هم از همین دسته هست یعنی واگذاری مدیریت یه property به یه شیء دیگه. مثلاً میتونیم از lazy به عنوان یه دلیگیت برای مدیریت مقداردهی یه property استفاده کنیم. این باعث میشه که مقدار property فقط وقتی محاسبه بشه که اولین بار بهش دسترسی پیدا کنیم.
توی این کد یک مثال دیگه از lazy واستون زدم :
val expensiveValue: Int by lazy {
println("Computing the value...")
42 // یه محاسبهی سنگین
}
fun main() {
println("قبل از دسترسی به expensiveValue")
println(expensiveValue) // اینجا مقدار محاسبه میشه
println("بعد از دسترسی به expensiveValue")
println(expensiveValue) // اینجا دیگه محاسبه نمیشه، مقدار قبلی استفاده میشه
}
توضیح:
- expensiveValue فقط وقتی مقداردهی میشه که برای اولین بار بهش دسترسی پیدا میکنیم.
- پیام “Computing the value…” فقط یک بار چاپ میشه، چون مقدارexpensiveValue فقط یک بار محاسبه میشه.
تفاوت lazy با delegation در کاتلین
طبق صحبتی که در بالا شد میشه گفت درواقع lazy یک نوع خاص از دلیگیشنهای کاتلین هست. برای اطلاعات بیشتر میتونید برید در مورد دلیگیشنهای دیگهی کاتلین هم بخونید واقعا جالب هستن و میتونن بهتون دانش عمیقی توی بهینهسازی بدن. من در اینجا فقط به اسم هاشون اشاره میکنم:
- Delegates.observable
- Delegates.vetoable
- NotNull
- Map-based Delegation
موارد استفاده از lazy در پروژههای واقعی
حالا بیاید یه نگاهی بندازیم به چند تا مثال واقعی از استفادهی lazy در کاتلین.
مثالمون رو از یک کتابخانه ی بسیار پرکاربرد و قدرتمند برای ارتباط با وب سرویسها میزنیم. کتابخانهی Retrofit.
اگر در مورد این کتابخونه میخواید بیشتر بدونید، توی دوره آموزش جامع برنامه نویسی اندروید به شکل جامع و مفصلی این کتابخونه توضیح داده شده و ازش در اکثر پروژهها استفاده شده.
خب بریم سر مثالمون
به کد زیر توجه کنید:
val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl("https://api.example.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val apiService: ApiService by lazy {
retrofit.create(ApiService::class.java)
}
توضیح کد:
- retrofit یه instance از کلاس Retrofit هست که با lazy مقداردهی میشه. یعنی فقط وقتی که اولین بار بهش دسترسی پیدا کنیم، ساخته میشه.
- apiService هم یه instance از ApiService هست که با retrofit ساخته میشه و اون هم به صورت lazy مقداردهی میشه.
وقتی اپلیکیشن رو اجرا میکنیم، ساختن instanceهای سنگین مثل Retrifit میتونه زمانبر باشه. با استفاده ازlazy ، این کار رو به تعویق میندازیم تا وقتی که واقعاً لازم بشه.
حالا اگه از lazy استفاده نکنیم چی میشه؟
بیایید کدش رو بنویسیم ببینیم اگر از lazy استفاده نکنیم، کدمون چه شکلی میشه و چه مشکلاتی ممکنه به وجود بیاد:
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService: ApiService = retrofit.create(ApiService::class.java)
اتفاقی که اینجا افتاده اینه که در بلافاصله وقتی برنامه اجرا میشه باید بیاد instance های retrofit و apiService رو میسازه حتی اگر کاربر نخواد از این سرویس استفاده کنه.
مثال بعدیمون رو از کاربرد lazy در دیتابیس میزنم. حتما تا حالا اسم کتابخونه Room رو شنیدید و شاید باهاش کار هم کرده باشید. این کتابخونه برای ارتباط و ساختن دیتابیس استفاده میشه توی دوره آموزش جامع برنامه نویسی اندروید به این کتابخونه هم کاملا پرداخته شده و مثالهای مختلف و پر کاربردی ازش زده شده پس اگه میخواید بیشتر در موردش بدونید میتونید به این دوره مراجعه کنید.
مثالمون رو با کد شروع میکنیم و بعد توضیحش میدیم:
val database: AppDatabase by lazy {
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"my-database"
).build()
}
val userDao: UserDao by lazy {
database.userDao()
}
توضیحات کد:
اولا بگم که شرح کامل نحوه کارکرد روم از حوصله این مقاله خارجه و این که اون Dao چیه و غیره رو باید در یک مطلب دیگه توضیح داد یا همونطور که گفتم میتونید به دوره هم مراجعه کنید ولی مابقی داستان:
-
- این کد میگه که
databaseفقط زمانی که ((اولین بار)) بهش نیاز داشته باشیم، مقداردهی میشه. Room.databaseBuilderبرای ساختن یک instance از دیتابیس Room استفاده میشه.context.applicationContextبه عنوان پارامتر اول برای اطمینان از این که از context صحیح استفاده میشه.AppDatabase::class.javaنوع دیتابیس رو مشخص میکنه."my-database"نام دیتابیس هست..build()دیتابیس رو میسازه.
- این کد میگه که
نکات و ترفندهای حرفهای
استفاده از lazy در کاتلین خیلی مفیده و میتونه به بهینهسازی کدهاتون کمک کنه. اما یه سری نکات و ترفندهای جالب هم در مورد استفاده از lazy وجود داره که شاید توی هر منبعی پیدا نکنید. این نکات میتونه به شما کمک کنه تا از lazy به بهترین شکل استفاده کنید. بیایید با هم نگاهی به این نکات بندازیم.
-
استفاده از
lazy در کاتلینبرای جلوگیری از مقداردهی تکراریاگه یه property دارید که محاسبهاش سنگینه و ممکنه بارها و بارها مقداردهی بشه، استفاده از
lazyمیتونه خیلی مفید باشه. این کار نه تنها زمان محاسبه رو کاهش میده، بلکه مصرف منابع رو هم بهینه میکنه.val config: Configuration by lazy { loadConfigurationFromDisk() } fun loadConfigurationFromDisk(): Configuration { // بارگذاری تنظیمات از دیسک که ممکنه زمانبر باشه println("Loading configuration...") return Configuration() } class Configuration { // تنظیمات برنامه } -
استفاده از
lazyدر فرگمنتهاتوی برنامههای اندرویدی، فرگمنتها میتونن سنگین باشن. میتونید از
lazyبرای به تأخیر انداختن مقداردهی ویوها و منابع سنگین توی فرگمنتها استفاده کنید.
مثال:class MyFragment : Fragment() { private val viewBinding: FragmentMyBinding by lazy { FragmentMyBinding.inflate(layoutInflater) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { return viewBinding.root } } -
استفاده از
lazyبرای عملیات طولانیمدتاگر یه عملیاتی دارید که طولانیمدت و زمانبر هست، مثل بارگذاری دادهها از شبکه، میتونید از
lazyاستفاده کنید تا این عملیات فقط وقتی انجام بشه که واقعاً به دادهها نیاز دارید. -
استفاده از
lazyبا پارامترهای Thread Safetyکاتلین به شما اجازه میده تا مشخص کنید که
lazyچطور باید از نظر Thread Safety رفتار کنه. این کار با استفاده از پارامترLazyThreadSafetyModeانجام میشه.val threadSafeValue: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { println("Computing thread-safe value...") "Thread Safe" } val nonThreadSafeValue: String by lazy(LazyThreadSafetyMode.NONE) { println("Computing non-thread-safe value...") "Not Thread Safe" }این مورد نیاز به توضیحات مفصل تری داره که در ادامه مقاله بهش پرداختیم.
-
. استفاده از lazy در کاتلین برای کپسولهسازی
lazyمیتونه به کپسولهسازی کمک کنه، چون میتونید مقداردهی و محاسبهی یک property رو مخفی کنید و فقط وقتی که واقعاً به اون property نیاز دارید، محاسبهاش رو انجام بدید.مثلا کد زیر رو ببینید:
class UserManager { val currentUser: User by lazy { loadUserFromDatabase() } private fun loadUserFromDatabase(): User { // بارگذاری کاربر از دیتابیس println("Loading user from database...") return User("John", "Doe") } } data class User(val firstName: String, val lastName: String)currentUserفقط وقتی که اولین بار بهش دسترسی پیدا میکنید، مقداردهی میشه.- تابع
loadUserFromDatabaseکه جزییات بارگذاری کاربر رو انجام میده، private هست و از دید بقیهی کدها مخفی شده. این یعنی جزییات پیادهسازی کپسوله شده و فقط وقتی که بهcurrentUserنیاز دارید، بارگذاری انجام میشه.
اجتناب از اشتباهات رایج در استفاده از lazy
در این قسمت از مقاله سعی کردم چند تا از اشتباهات رایج در زمان استفاده از lazy در کاتلین رو بهتون معرفی کنم تا دیگه مقالمون تکمیل بشه. (البته هیچ مقالهای نمیتونه جای تجربه رو بگیره پس بعد از خوندن این مقاله آستیناتونو بالا بزنید و دست به کد بشید)
دسترسی همزمان از چندین نخ بدون ایمنی نخ (Thread Safety)
یکی از اشتباهات رایج، دسترسی به lazy از چندین نخ بدون ایمنی نخ مناسب هست.وقتی چندین ترد به صورت همزمان به یک lazy property دسترسی پیدا میکنن، اگر از حالت ایمنی نخ استفاده نشده باشه، ممکنه هر نخ مقدار lazy رو به صورت مستقل و همزمان محاسبه کنه.
اگر از حالتLazyThreadSafetyMode.NONE استفاده کنید و چندین نخ به صورت همزمان به lazy دسترسی پیدا کنن، ممکنه اوضاع قاراشمیش بشه.
مثال زیر رو ببینید:
val nonThreadSafeValue: String by lazy(LazyThreadSafetyMode.NONE) {
println("Computing non-thread-safe value...")
"Not Thread Safe"
}
fun main() {
// اجرای این کد به صورت همزمان از چندین نخ
for (i in 1..10) {
Thread {
println(nonThreadSafeValue)
}.start()
}
}
nonThreadSafeValueیک lazy property هست که با حالتLazyThreadSafetyMode.NONEتعریف شده.LazyThreadSafetyMode.NONEبه این معنیه که هیچ ایمنی تردی اعمال نمیشه، بنابراین اگر چندین نخ همزمان به این property دسترسی پیدا کنن، هر نخ میتونه مقدار رو به صورت مستقل و همزمان محاسبه کنه.- در
mainیک حلقه for داریم که 10 نخ ایجاد میکنه و هر نخ سعی میکنهnonThreadSafeValueرو چاپ کنه.
مشکلات احتمالی:
- محاسبه همزمان:
- ممکنه چندین نخ همزمان به
nonThreadSafeValueدسترسی پیدا کنن و همزمان محاسبه رو شروع کنن. این میتونه منجر به محاسبات تکراری بشه که منابع سیستم رو بیهوده مصرف میکنه.
- ممکنه چندین نخ همزمان به
- نتایج نادرست:
- به دلیل عدم ایمنی نخ، ممکنه مقدار
nonThreadSafeValueبه درستی مقداردهی نشه و نتایج نادرستی برگردونده بشه.
- به دلیل عدم ایمنی نخ، ممکنه مقدار
حالا بیایید ببینیم چطور میتونیم از حالت پیشفرض LazyThreadSafetyMode.SYNCHRONIZED استفاده کنیم تا از مشکلات بالا جلوگیری کنیم کد زیر نحوه صحیحش رو بهتون نشون میده:
val threadSafeValue: String by lazy {
println("Computing thread-safe value...")
"Thread Safe"
}
fun main() {
// اجرای این کد به صورت همزمان از چندین نخ
for (i in 1..10) {
Thread {
println(threadSafeValue)
}.start()
}
}
توضیح کد:
threadSafeValueیک پراپرتیِlazyهست که با حالت پیشفرضLazyThreadSafetyMode.SYNCHRONIZEDتعریف شده. و مابقی کد مثل توضیحات بالا هست.
نادیده گرفتن نیاز به مقداردهی اولیه
در بعضی مواقع پیش میاد که برنامهنویسها یادشون میره که یک پراپرتیِ لیزی را مقدار دهی کنن و این باعث بروز مشکلاتی در عملکرد برنامه میشه. مثلا اگر یک lazy property داریم که باید حتما قبل از استفاده مقدار دهی بشه و فراموش کنیم این کار رو انجام بدیم، ممکنه با خطاهای ناخواسته روبرو بشیم.
راه حلش اینه که اطمینان حاصل کنیم که در زمان مناسب مقدار دهی میشه و از دسترسی نادرست جلوگیری کنیم.
مثلا کد زیر رو ببینید:
class MyClass {
val myValue: String by lazy {
println("Computing myValue...")
"Hello, World!"
}
fun printValue() {
println(myValue)
}
}
در این کد تا زمانی که متد print value صدا زده نشده، مقدار دهی ما انجام نمیشه.
استفاده نادرست از lazy در کاتلین برای عملیات ساده و سبک
استفاده از لِیزی در کاتلین برای عملیاتهایی که سبک و سریع هستن، نه تنها به بهینهسازی کمک نمیکنه بلکه میتونه باعث پیچیدگی غیرضروری بشه. lazy بیشتر برای محاسبات سنگین و زمانبر مفیده.
ایجاد حلقههای وابسته به هم
اگر دو یا چند lazy property به صورت متقابل به هم وابسته باشن، ممکنه باعث ایجاد حلقههای وابستگی بشن که به خطاهای ناخواسته منجر میشه پس باید حواستون باشه که وابستگیها به درستی مدیریت بشن و هیچ حلقهای درست نشه. کد زیر مثال یک حلقه رو بهتون نشون میده:
val firstValue: String by lazy {
secondValue
}
val secondValue: String by lazy {
firstValue
}








