Generic ها در Kotlin

در این نوشتار تلاش خواهیم کرد که Generic ها (برنامه نویسی همگانی) را در زبان کاتلین و با نگاهی به زبان جاوا بررسی کنیم. در ابتدا این پرسش را مطرح میشود که اساسا Generic چیست و از چه زمانی بوجود آمد؟ 

برنامه‌نویسی همگانی (Generic programming) نوعی روش برنامه نویسی رایانه است که در آن الگوریتم ها به صورت نوع داده ی «تعیین شونده در آینده» نوشته می شوند، و موقعی که به نوع خاصی نیاز باشد، آن نوع ها نمونه برداری می شوند، و به عنوان پارامتر ها ارائه می گردند.

پیشگام این رویکرد زبان ای دا (Ada) در ۱۹۸۳ بود که اجازه نوشتن انواع یا تابع‌های مشترک که فقط در مجموعه‌ای از انواع در زمان استفاده باهم تفاوت دارند، می‌داد؛ بنابراین از تکرار جلوگیری می‌کرد.

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

نکته ۱:  اعلان و استفاده از کلاس ها و توابع عمومی در کاتلین شبیه جاوا است.

هنگامی که یک نمونه از یک نوع ایجاد می شود،type parameter با انواع خاصی به نام type argument جایگزین می شوند.

نکته ۲ : برخلاف جاوا کاتلین از raw type ها پشتیبانی نمیکند!

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

در مثال زیر، کلاس List از نوع generic تعریف شده است و به همین خاطر بجای اینکه فقط با یک نوع داده از لیست کار کند می‌تواند با هر نوع داده‌ای کار کند.

Type parameter constraints

محدودیت های type parameter به شما امکان می دهد type هایی را که می توانند به عنوان type argument  برای یک کلاس یا تابع استفاده شوند محدود کنید. مثلا تابع Generic ی که فقط sub type های کلاس Number را بپذیرد می‌تواند انواع داده های Int ، Double و Number را بپذیرد.

استفاده از مقادیر type T بعنوان مقادیر کرانه بالایی(upper bound) آن

اعلان یک تابع با محدودیت type parameter

تعیین محدودیت های متعدد برای یک type parameter

شما قادر خواهید بود که محدودیت‌های متعددی را تعیین کنید.

غیر تهی کردن type parameter

به این نکته توجه داشته باشید، در صورتیکه type parameter شما از Any ارث بری کرده باشد، دیگر نمی‌تواند مقدار تهی بپذیرد.

تا کنون، ما اصول اولیه Generics را پوشش داده ایم – موضوعاتی که بیشتر شبیه جاوا هستند.
حال میخواهیم به بررسی رفتار Generic زمان اجرا بپردازیم.

پاک شدن در زمان اجرا (Erased at runtime)

یک نمونه از یک generic class اطلاعاتی در مورد type argument های استفاده شده برای ایجاد آن نمونه ندارد. Generics جاوا و کاتلین در زمان اجرا پاک می شوند.

به عنوان مثال: اگر یک <List<String ایجاد کنید و تعدادی رشته در آن قرار دهید، در زمان اجرا فقط می توانید ببینید که یک لیست است.

جنریک های جاوا و کاتلین در زمان اجرا پاک می شوند.

مزایای erasing generic type information

مقدار کلی حافظه مورد استفاده برنامه شما کمتر است، زیرا اطلاعات نوع کمتری باید در حافظه ذخیره شود.
چگونه بررسی کنیم که مقدار به جای یک مجموعه یا یک شی دیگر، یک لیست است؟
می توانید این کار را با استفاده از 🌠 star projection 🌠 انجام دهید.

as? casts

💡 آیا می توانید از انواع generic در as استفاده کنید؟
اگر کلاس دارای نوع پایه صحیح و type argument اشتباه باشد، Cast شکست نخواهد خورد، زیرا type argument در زمان اجرا زمانی که Cast انجام می‌شود، شناخته شده نیست.
به همین دلیل، کامپایلر یک اخطار “کست بررسی نشده” را در چنین cast هایی منتشر می کند که فقط یک هشدار است. بنابراین می توانید بعداً از مقدار به عنوان نوع لازم استفاده کنید.

استفاده از یک type check با یک type argument مشخص

اعلان توابع با reified type parameters

وقتی یک تابع generic را فراخوانی می‌کنید، در بدنه آن نمی‌توانید نوع آرگومان‌هایی که با آن فراخوانی شده است را تعیین کنید:

یک مورد وجود دارد که می توان از این محدودیت اجتناب کرد: توابع درون خطی(inline functions).

یادآوری

اگر تابعی را با کلمه کلیدی inline مشخص کنید، کامپایلر هر فراخوانی به تابع را با کد فعلی اجرای تابع جایگزین می کند.

با استفاده از کلمه کلیدی reified در توابع درون خطی می‌توانید نوع آرگومان‌های تابع را مشخص کنید.

کاری که نمی توانید انجام دهید!

چند reified type parameters

💡 یک inline method می تواند چندین پارامتر نوع reified داشته باشد و علاوه بر پارامترهای reified می تواند دارای type parameter های غیر reified نیز باشد.

اطمینان از عملکرد خوب …

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

جایگزینی با reified type parameters

ساده کردن عملکرد startActivity در اندروید

محدودیت در reified type parameters

در اینجا موارد استفاده از reified type parameters آورده شده است:
در تایپ چک و casts
is , !is , as , as?
برای استفاده از API های reflection کاتلین
برای دریافت java.lang.Class مربوطه (::class.java)
به عنوان type argument برای فراخوانی توابع دیگر

شما نمی توانید کارهای زیر را انجام دهید :
نمونه های جدیدی از کلاس مشخص شده به عنوان type parameter ایجاد کنید
فراخوانی متدها بر روی شی همراه از کلاس type parameter
هنگام فراخوانی یک تابع با reified type parameters از یک پارامتر نوع غیر reified به عنوان type argument استفاده کنید
پارامترهای نوع کلاس‌ها، ویژگی‌ها یا توابع غیرخطی را به‌عنوان reified علامت‌گذاری کنید.

variance: covariance, contravariance

مفهوم واریانس چگونگی ارتباط انواع type با type پایه یکسان و type arguments متفاوت را با یکدیگر توصیف می کند. به عنوان مثال <List<String و <List<Any .

Covariance : یک کلاس generic است که در آن رابطه ها بدین صورت می‌باشد که اگر A زیرگروه B باشد، <List<A هم زیرگروه <List<B می‌باشد .

به عنوان مثال، <<List<String Producer<Cat یک زیرگونه از <Producer<Animal است زیرا Cat یک زیرگروه از Animal است.

Covariance بعنوان Producer استفاده می‌شود و فقط می‌تواند در مقدار بازگشتی قرار گیرد.(شما هرگز نمی‌توانید از Covariance در مقدار ورودی استفاده کنید.) برای به کار بردن Covariance کفیست از کلمه کلیدی out استفاده نمایید.

هم‌چنین از T می‌توانید بعنوان مقدار بازگشتی برای تابع دیگر نیز استفاده کنید. برای مثال:

Contravariance : یک کلاس generic است که در آن رابطه ها به این صورت است که اگر A زیرگروه B باشد ،در اینصورت <List<B زیرگروه <List<A خواهد بود. در contravariance رابطه ها بصورت عکس می‌باشد.

Contravarianceها فقط در موقعیت in قرار میگیرند و فقط می‌توانند بعنوان ورودی مورد استفاده قرار گیرند(بعنوان consumer استفاده می‌شوند.) .

Consumer<A> یک زیرگروه از <Consumer<B اگر B زیرگروه A باشد.

برای مثال <Consumer<Animal یک زیرگروه از <Consumer<Cat است زیرا Cat یک زیرگروه از Animal می‌باشد.

💡 اگر نیاز به مقایسه روی اشیاء از type خاصی دارید، می توانید از مقایسه کننده ای استفاده کنید که آن نوع یا هر یک ازsuper type های آن را کنترل می کند. این به این معنی است که <Comparator<Any یک زیرنوع از <Comparator<String است، که در آن Any یک supertype از String است.

بطورکلی موقعیت in و out بصورت زیر می‌باشد.

یعنی in همیشه بعنوان ورودی(Consumer) و out بعنوان خروجی(Producer) قرار می‌گیرد.

به این نکته توجه داشته باشید که پارامترهای constructor در موقعیت in و out نمی‌توانند قرار بگیرند.

💡 در جاوا مفهوم covariance, contravariance را نداریم و در جاوا از wildcard استفاده می‌شود.

در جاوا، هر بار که از یک type با type parameter استفاده می‌کنید، می‌توانید تعیین کنید که آیا این پارامتر نوع می‌تواند با زیرگروه‌ها یا سوپرتایپ‌های آن جایگزین شود. به این واریانس use-site variance می‌گویند. اعلان‌های Use-site variance در Kotlin مستقیماً با Java bounded wildcards مطابقت دارد. <MutableList<out T یعنی همان <MutableList<? extends T در جاوا و به همین صورت <MutableList<in T یعنی همان <MutableList<? Super T در جاوا.

Upper Bounded Wildcards : List<? extends Number>

Lower Bounded Wildcards: Collectiontype <? super A>

💡 در مورد wildcards جاوا، <*>MyType در Kotlin با MyType جاوا مشابهت دارد.
💡 برای contravariant type parameters مانند است. در واقع، شما نمی توانید هیچ روشی را که دارای T در signature چنین star projection ای است فراخوانی کنید. اگر type parameters یک contravariant باشد، فقط به عنوان یک consumer عمل می کند، و همانطور که قبلا بحث کردیم، شما دقیقا نمی دانید چه چیزی می تواند consume کند. بنابراین، شما نمی توانید چیزی برای consume به آن بدهید.

در کاتلین مفهوم دیگری به نام Invariance وجود دارد که در جاوا بعنوان Unbounded Wildcard استفاده می‌شود که این‌ها در مواقعی استفاده می‌شوند که هیچ کدام از type ها sub type یا super type دیگری نیست.

خلاصه

  • generic ها در Kotlin تقریباً مشابه جاوا هستند: شما یک تابع یا کلاس generic را به همان روش اعلان می کنید.
  • مانند جاوا، type arguments برای generic types فقط در زمان کامپایل وجود دارند.
  • نمی توانید از types با type arguments همراه با عملگر is استفاده کنید، زیرا type arguments در زمان اجرا پاک می شوند.
  • type parameters توابع inline را می‌توان به‌عنوان reified پیاده سازی کرد، که به شما امکان می‌دهد از آنها در زمان اجرا برای انجام بررسی‌ها و به دست آوردن نمونه‌های java.lang.Class استفاده کنید.
  • اگر این پارامتر فقط در موقعیت‌ out استفاده شود، می‌توانید یک کلاس را به‌عنوان covariant روی type parameter اعلان کنید.
  • عکس آن برای موارد contravariant صادق است: اگر یک کلاس فقط در موقعیت in استفاده شود، می‌توانید یک کلاس را به عنوان contravariant بر روی type parameter اعلان کنید.
  • فهرست واسط فقط خواندنی در Kotlin به عنوان covariant اعلان شده است، به این معنی که<List<String زیرنوعی از <List<Any است.
  • interface تابع در type parameter اول خود به عنوان contravariant و در پارامتر دوم خود به عنوان covariant اعلان می شود، که باعث می شود (Animal)->Int یک زیرگروه از(Cat)->Number باشد.
  • زمانی که type arguments ناشناخته یا بی‌اهمیت هستند، می‌توان از star-projection استفاده کرد.