آموزش کامل 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 }