برنامه نویسی 408 بازدید

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

fun myStringResUsage(@StringRes string: Int){ }
// Error: expected resource of type String
myStringResUsage(1)

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

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

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

inline class DoggoId(val id: Long)
data class Doggo(val id: DoggoId, … )
// usage
val goodDoggo = Doggo(DoggoId(doggoId), …)
fun pet(id: DoggoId) { … }

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

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

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

  • از اینترفیس‌ها ارث‌بری کنند.
  • مشخصه و تابع داشته باشند.
interface Id
inline class DoggoId(val id: Long) : Id {
    
    val stringId
    get() = id.toString()
    fun isValid()= id > 0L
}

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

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

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

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

پس‌زمینه

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

interface Id
inline class DoggoId(val id: Long): Id

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

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

/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
   // $FF: synthetic method
   private DoggoId(long id) {
      this.id = id;
   }

   public static long constructor_impl/* $FF was: constructor-impl*/(long id) {
      return id;
   }
}

DoggoId دو سازنده دارد:

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

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

val myDoggoId = DoggoId(1L)
// decompiled
static final long myDoggoId = DoggoId.constructor-impl(1L);

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

DoggoId u = new DoggoId(1L);
// Error: DoggoId() in DoggoId cannot be applied to (long)

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

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

/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
   private final long id;

   public final long getId() {
      return this.id;
   }
  
   // $FF: synthetic method
   @NotNull
   public static final DoggoId box_impl/* $FF was: box-impl*/(long v) {
      return new DoggoId(v);
   }
}

Id به دو روش بیان می‌شود:

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

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

fun walkDog(doggoId: DoggoId) {}
// decompiled Java code
public final void walkDog_Mu_n4VY(long doggoId) { }

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

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

fun pet(doggoId: DoggoId?) {}
// decompiled Java code
public static final void pet_5ZN6hPs/* $FF was: pet-5ZN6hPs*/(@Nullable InlineDoggoId doggo) {}

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

کالکشن‌ها

val doggos = listOf(myDoggoId)
// decompiled Java code
doggos = CollectionsKt.listOf(DoggoId.box-impl(myDoggoId));

امضای CollectionsKt.listOf به صورت زیر است:

fun <T> listOf(element: T): List<T>

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

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

fun handleId(id: Id) {}
fun myInterfaceUsage() {
    handleId(myDoggoId)
}
// decompiled Java code
public static final void myInterfaceUsage() {
    handleId(DoggoId.box-impl(myDoggoId));
}

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

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

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

/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
public final class DoggoId implements Id {
   public static boolean equals_impl/* $FF was: equals-impl*/(long var0, @Nullable Object var2) {
      if (var2 instanceof DoggoId) {
         long var3 = ((DoggoId)var2).unbox-impl();
         if (var0 == var3) {
            return true;
         }
      }

      return false;
   }

   public static final boolean equals_impl0/* $FF was: equals-impl0*/(long p1, long p2) {
      return p1 == p2;
   }

   public boolean equals(Object var1) {
      return equals-impl(this.id, var1);
   }
}
  • رویکرد doggo1.equals(doggo2)

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

equals_impl(long, Object)

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

DoggoId.equals-impl(doggo1, DoggoId.box-impl(doggo2))
  • رویکرد doggo1 == doggo2

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

oggoId.equals-impl0(doggo1, doggo2)

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

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

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

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

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

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

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

fun pet(doggoId: Long) {}
fun pet(doggoId: DoggoId) {}
// decompiled Java code
public static final void pet(long id) { }
public final void pet_Mu_n4VY(long doggoId) { }

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

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

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

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

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

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

void myJavaMethod(DoggoId doggoId){
    long id = doggoId.getId();
}

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

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

// Kotlin declaration
val doggo1 = DoggoId(1L)
// Java usage
long myDoggoId = GoodDoggosKt.getU1();

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

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

fun pet(doggoId: DoggoId) {}
// Java
void petInJava(doggoId: DoggoId){
    pet(doggoId) 
    // compile error: pet(long) cannot be applied to pet(DoggoId)
}

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

fun pet(doggoId: DoggoId) {}
// Java
void petInJava(doggoId: DoggoId){
    pet(doggoId.getId)
}

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

fun pet(doggoId: Long) {}
fun pet(doggoId: DoggoId) {}
// Java
TestInlineKt.pet(1L);
Error: Ambiguous method call. Both pet(long) and pet(long) match

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

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

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

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

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

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

==

بر اساس رای 0 نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.

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

نظر شما چیست؟

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