در این مقاله به بررسی مفهوم و ماهیت کلاس inline در کاتلین می‌پردازیم. «امنیت نوع» (Type Safety) موجب جلوگیری از بروز خطا و همچنین نیاز به دیباگ بعدی می‌شود. در مورد انواع منابع اندرویدی از قبیل String ،Font یا Animation می‌توان از حاشیه‌نویسی اندرویدی (androidx.annotations) به صورت StringRes@، یا FontRes@ استفاده کرد و بدین ترتیب Lint ما را ملزم می‌سازد که پارامتری با نوع مناسب ارسال کنیم:

در کد فوق اگر id یک منبع اندرویدی نباشد، بلکه id یک شیء دامنه مانند Doggo یا Cat باشد، در این صورت تمییز بین دو شناسه Int به سادگی صورت نمی‌پذیرد. برای دستیابی به آن امنیت نوع که تضمین کند شناسه گربه و سگ یکسان نیستند، باید id را درون یک کلاس قرار دهیم. عیب این روش آن است که هزینه عملکردی ایجاد می‌کند، چون یک شیء جدید باید وهله‌سازی شود، اما ما عملاً فقط به انواع primitive نیاز داریم.

«کلاس‌های درون‌خطی» (Inline Classes) کاتلین امکان ایجاد این انواع پوششی را بدون عملکرد هزینه‌ای فراهم می‌سازند. این یک ویژگی آزمایشی است که در نسخه 1.3 کاتلین اضافه شده است. کلاس‌های «درون‌خطی» (inline) باید تنها دقیقاً یک مشخصه داشته باشند. وهله‌های کلاس‌های درون‌خطی در زمان کامپایل در صورت امکان با مشخصه مربوطه جایگزین می‌شوند و هزینه عملکردی یک کلاس پوششی معمول را حذف می‌کنند.

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

تنها نقش یک کلاس inline این است که پوششی برای یک نوع باشد تا کاتلین چندین محدودیت را الزام کند:

  • بیش از یک پارامتر نداشته باشد (محدودیتی روی نوع وجود ندارد).
  • فیلدهای backing نداشته باشد.
  • بلوک‌های init نداشته باشد.
  • کلاس‌های بسط یافته نداشته باشد.

با این حال کلاس‌های inline می‌توانند مشخصه‌های زیر را داشته باشند:

  • از اینترفیس‌ها ارث‌بری کنند.
  • مشخصه و تابع داشته باشند.
هشدار: ممکن است فکر کنید که «اسامی مستعار نوع» (Type Aliases) نیز مشابه کلاس درون‌خطی هستند، اما آن‌ها یک نام جایگزین برای انواع موجود ارائه می‌کنند، در حالی که کلاس‌های درون‌خطی نوع جدیدی خلق می‌کنند.

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

برای مثال زمانی از کلاس درون‌خطی استفاده می‌کنیم که انتظار می‌رود یک Object یا Any مثلاً در کالکشن‌ها، آرایه‌ها یا اشیای تهی‌پذیر دریافت شود. بسته به این که چه زمانی دو کلاس درون‌خطی از نظر برابری ساختاری بررسی می‌شوند، یک یا هیچ کدام از آن‌ها پوشش می‌یابند:

  • doggo1 == doggo2 – نه doggo1 و نه doggo2 پوشش نمی‌یابند.
  • doggo1.equals(doggo2)  – در این مورد doggo1 به عنوان یک نوع ابتدایی استفاده می‌شود، اما doggo2 پوشش می‌یابد.

پس‌زمینه

در این بخش به اتفاقاتی که در پس‌زمینه هنگام استفاده از کلاس‌های درون‌خطی رخ می‌دهد می‌پردازیم. کلاس درون‌خطی ساده زیر را در نظر بگیرید:

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

پس‌زمینه سازنده‌ها

DoggoId دو سازنده دارد:
  • یک سازنده سنتزی خصوصی به نام DoggoId(long id)
  • یک سازنده عمومی به نام constructor-impl

زمانی که یک وهله جدید از شیء را می‌سازیم، سازنده عمومی مورد استفاده قرار می‌گیرد:

اگر تلاش کنیم doggo را در جاوا بسازیم، با خطای زیر مواجه خواهیم شد:

سازنده پارامتری شده از نوع خصوصی است و سازنده دوم شامل یک علامت خط تیره (-) در نام خود است که کاراکتر غیر مجاری در جاوا محسوب می‌شد. این بدان معنی است که کلاس‌های درون‌خطی را نمی‌توان از جاوا وهله‌سازی کرد.

پس‌زمینه کاربرد پارامتر

Id به دو روش بیان می‌شود:
  • به صورت یک نوع ابتدایی از طریق یک getId
  • به صورت یک شیء از طریق متد box_impl که وهله جدیدی از DoggoId می‌سازد.

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

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

اشیای تهی‌پذیر

از آنجا که تنها شیءها می‌توانند تهی پذیر باشند، پیاده‌سازی پوشش یافته مورد استفاده قرار می‌گیرد.

کالکشن‌ها

امضای CollectionsKt.listOf به صورت زیر است:
از آنجا که این متد انتظار یک شیء را دارد، کامپایلر کاتلین پوششی برای نوع ابتدایی فراهم می‌سازد و مطمئن می‌شود که شیء مورد استفاده قرار گرفته است.

کلاس‌های مبنا

از آنجا که در این جا انتظار یک نوع super را داریم، پیاده‌سازی پوشش یافته مورد استفاده قرار می‌گیرد.

بررسی‌های برابری

کامپایلر کاتلین تلاش می‌کند تا در موارد امکان از پارامتر پوشش نیافته استفاده کند. به این منظور کلاس‌های درون‌خطی 3 پیاده‌سازی متفاوت برای برابری دارند که یکی equals را باطل می‌کند و دو متد دیگر نیز تولید شده‌ هستند:

  • رویکرد doggo1.equals(doggo2)

متد equals متد تولید شده زیر را فراخوانی می‌کند:

از آنجا که equals یک شیء می‌پذیرد، مقدار doggo2 پوشش خواهد یافت، اما doggo1 به صورت یک نوع ابتدایی مورد استفاده قرار می‌گیرد:

  • رویکرد doggo1 == doggo2

در این رویکرد از == استفاده می‌شود:

بنابراین در مورد ==، نوع ابتدایی برای هر دو مورد doggo1 و doggo2 استفاده می‌شود.

  • رویکرد doggo1 == 1L

اگر کاتلین بتواند تشخیص دهد که doggo1 عملاً یک long است، در این صورت انتظار می‌رود که این بررسی برابری پاسخ‌گو باشد. اما از آنجا که از کلاس‌های درون‌خطی برای حفظ امنیت نوع استفاده می‌کنیم، نخستین چیزی که کامپایلر انجام خواهد داد، بررسی این است که آیا نوع دو شیء که می‌خواهیم مقایسه کنیم یکسان هستند یا نه و از آنجا که چنین نیست، با خطای کامپایلر مواجه می‌شویم. عملگر == را نمی‌توان روی long و DoggoId استفاده کرد. در نهایت از نظر کامپایلر این وضعیت شبیه آن است که بگوییم cat1 == doggo1 است، که در واقع قطعاً چنین نیست.

  • رویکرد doggo1.equals(1L)

این بررسی برابری کامپایل می‌شود، زیرا کامپایلر کاتلین از پیاده‌سازی equals استفاده می‌رود که یک مقدار long و یک object انتظار دارد. اما از آنجا که نخستین کار این متد بررسی نوع Object است، این بررسی برابری false خواهد بود، چون Object یک DoggoId نیست.

باطل کردن تابع‌ها با انواع ابتدایی و پارامترهای کلاس درون‌خطی

کامپایلر کاتلین امکان تعریف تابع‌ها را با هر دو نوع ابتدایی و همچنین کلاس درون‌خطی «غیر تهی پذیر» (non-nullable) به عنوان پارامتر می‌دهد:

در کد دیکامپایل شده می‌توان دید که هر دو تابع از نوع ابتدایی استفاده کرده‌اند.

کامپایلر کاتلین برای فراهم ساختن امکان این کارکرد، نام تابع را تغییر می‌دهد و کلاس درون‌خطی را به عنوان پارامتر می‌گیرد.

استفاده از کلاس‌های درون‌خطی در جاوا

قبلاً دیدیم که امکان وهله‌سازی کلاس‌های درون‌خطی در جاوا وجود ندارد، اما آیا می‌توان از آن‌ها استفاده کرد؟

ارسال کلاس‌های درون‌خطی به تابع‌های جاوا

امکان ارسال کلاس‌های درون‌خطی به صورت پارامتر وجود دارد تا به عنوان اشیا مورد استفاده قرار گیرند و به این ترتیب می‌توان مشخصه‌هایی که درون آن‌ها قرار دارد را به دست آورد:

استفاده از وهله‌های کلاس‌های درون‌خطی در تابع‌های جاوا

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

فراخوانی تابع‌های کاتلین که دارای پارامترهایی به شکل کلاس خطی هستند

اگر یک تابع جاوا داشته‌ باشیم که یک پارامتر کلاس درون‌خطی می‌گیرد و بخواهیم یک تابع کاتلین را فراخوانی کنیم که یک کلاس درون‌خطی می‌پذیرد، با خطای کامپایل زیر مواجه می‌شویم:

از نظر جاوا DoggoId یک نوع جدید است، اما کامپایلر pet(long) را تولید می‌کند و pet(DoggoId) وجود ندارد، اما می‌توانیم نوع زیرین را ارسال کنیم:

اگر در همان کلاس تابعی را با کلاس درون‌خطی و نوع زیرین override کنیم و در زمان فراخوانی تابع از جاوا با خطایی مواجه می‌شویم، چون کامپایلر نمی‌تواند تشخیص دهد دقیقاً کدام تابع باید فراخوانی شود:

چه زمانی باید از کلاس‌های درون‌خطی استفاده کنیم؟

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

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

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

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

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

==

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

آیا این مطلب برای شما مفید بود؟

نظر شما چیست؟

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