کاتلین از ابتدا : آموزش جنریک‌ها به صورت عمیق ( قسمت 1 )

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

البته ابن قسمت یکم سنگین و نیاز به توجه بیشتر هست.

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

List<String> inst;

برای Map هم همینطور بود و به طور کلی میتونستیم به صورت زیر استفاده کنیم :

Map <K,V>

توی کاتلین، می تونیم به صورت غیرصریح - Implicit - نوع یک generic رو تعریف کنیم، که خود کامپایلر خودش میتونه نوع رو برامون infer کنه - تشخیص بده - ، برای مثال قسمت پایین، کامپایلر خودش String بودن متغییر رو تشخیص میده :

val authors = listOf("sajjad", "yousef")

  • البته یک نکته‌ی مهمی در مورد تفاوت بین جنریک‌های کاتلین و جاوا وجود داره، که توی دکیومنتیشن کاتلین بهش اشاره شده ولی دلیلش رو نگفته، البته قبلش بهتره دو مفهوم رو بهتون معرفی کنم : type parameters و type arguments.

شما توی جنریک‌ها میتونید type arguments رو تعریف کنید، حالا وقتی که یک instance از اونا میسازید، اسمش میشه type parameters. برای مثال List توی حالت عادی type paraneter هست ولی وقتی ازش یک instance می‌سازیم، میشه type argument.

جنریک‌ها و propertyها

خب، حالا بریم سراغ ادامه‌ی تفاوت بین کاتلین و جاوا در جنریک ها. توی کاتلین باید نوع داده‌ی type argumentها به صورت صریح یا غیرصریح، حتما باید مشخص بشه. در حالی که توی جاوا اینطوری نیست و میتونیم یک type argument رو بدون مشخص کردن نوعش تعریف کنیم. برای اینکه بهتر منظورمو درک کنید، مثلا توی جاوا میتونیم یک List رو تعریف کنیم بدون مشخص کردن نوعش، مثلا میتونیم بگیم نوعش T هست، در حالی که توی کاتلین همچین کاری امکان پذیر نیست، و نمیشه که یک List رو بدون مشخص کردن نوعش تعریف کرد. عامل این تفاوت توی کاتلین و جاوا اینه که جنریک‌ها از جاوای 1.5 به بعد به وجود اومدن، و در حقیقت میخواستن که یه جورایی بین جاوا قبل 1.5 و بعد از اون سازگاری وجود داشته باشه، در حالی که توی کاتلین مفهوم جنریک‌ها از اول تعریف شده. خب بگذریم :).

توی کاتلین برای جنریک‌ها میشه توابعی رو با استفاده از extension functions و higher-order functions نوشت، مثالش رو میتونید پایین ببینید :

https://gist.github.com/sajjadyousefnia/152787e741ebb5a10967500213b5fc79

پایین هم میتونید مثالی از استفاده از جنریک‌ها توی higher-order functions رو ببینید :

https://gist.github.com/sajjadyousefnia/da2da59dfd078f71f331388ee426ac00

نکته‌ : نمی‌تونید یک property به حالتی غیر extensionای بنویسید! خب برای توضیح بیشتر باید بگم که نمیشه چند تا مقدار با نوع مختلف رو توی یک property یک کلاس ذخیره کرد، بنابراین، به همین خاطر نمیشه یک property رو به صورت غیر extensionای تعریف کرد. اگه بخواید انجامش بدید، با خطای کامپایلر مواجه خواهید شد :


https://gist.github.com/sajjadyousefnia/6ef230ded2b67837c0ed8a4d4a2203fd

خب حالا میرسیم به بحث کلاسهای جنریک.

تعریف کلاسهای جنریک

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

https://gist.github.com/sajjadyousefnia/ffd196223d0f386c8e4654bb6bcd9db4

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

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

مفهوم کلی این قضیه خیلی تازه نیست توی جاوا هم وجود داشته، مثلا فرض کنید یک لیست ( <List<T ) داشته باشید که بخواید عناصرش رو با هم جمع کنید، بایستی حتما روی این عناصر این محدودیت رو اعمال کنید که حتما عدد باشه و برای مثال رشته نباشن. این محدودیت رو میشه با : که معادل همون extends توی جاوا میشه اعمال کرد.

https://gist.github.com/sajjadyousefnia/92aa7efaec30f03aae458dc9e6db5c70

جنریک‌ها هنگام Runtime : ه Type Parameterهای erased و reified

به طور معمول توی JVM ، جنریک‌ها با استفاده از type erasure پیاده‌سازی میشن، به این معنی که وقتی یک instance از یک کلاس جنریک میسازید، نمی‌تونیم موقع runtime بفهمیم که از چه نوع type argumentای برای ساخت این instance استفاده شده. برای مثال وقتی یک insatnce از <List<String میسازید و چند تا متغییر رشته‌ای بهش اضافه می‌کنید، صرفا میتونید این رو متوجه بشید که یک لیست داریم، و هیچ ایده‌ی نداریم که چه نوع عناصری داخلش قرار دارن.

مثلا دو لیست پایینی رو در نظر بگیرید :

https://gist.github.com/sajjadyousefnia/fd540fc27b6a3839b97ceee778306196
موقع runtime نمیتونید قضاوت کنید که هر کدوم از لیست‌ها از چه نوعی هستند.
موقع runtime نمیتونید قضاوت کنید که هر کدوم از لیست‌ها از چه نوعی هستند.

تعریف توابعی که دارای type parameterهای reifiedشده هستند

به مثال پایینی دقت کنید‌‌ :

https://gist.github.com/sajjadyousefnia/b0c8e9de30a642d058a54422c0254c16

مثال بالایی با خطا مواجه میشه، چون همونطور که گفتم در صورتی که یک instance از یک کلاس جنریک داشته باشید، نمی‌تونید متوجه این بشید که از چه نوع type argumentای موقع ساخت instance استفاده شده.

این قضیه‌ای که گفتم به طور کلی همه‌جا صدق میکنه ولی یه جورایی میشه این محدودیت رو دور زد. اون هم با استفاده از inline functions ( که قبلا توی قسمت جداگانه‌ای توضیحش دادم ). با استفاده ازinline funtions میشه type parameterها رو اصطلاحا reified کرد.

خب اول یه یادآوری از inline functions انجام بدیم و بعدش بحث رو ادامه بدیم :

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

اگه اون تابع قبلی ( isA ) رو که بالاتر گفتیم - که با خطا هم مواجه میشد - ، رو به صورت inline تعریف کنیم و قبل از پارامترش کلیدواژه‌ی refied رو اضافه کنیم، دیگه با خطا مواجه نمیشیم و در نتیجه میتونیم چک کنیم که آیا یک instance از نوع T هست یا نه؟

https://gist.github.com/sajjadyousefnia/5af4890e2df74e31f67fa72f95a15dd0

خب، حالا یه نگاهی به چند نوع مثال دیگه که از این قضیه استفاده میکنن بندازیم :

یکی از ساده‌ترین مثالهایی که برای کاربرد پارامتر reified شده میشه گفت، استفاده از اون توی متد filterIsInstance هست، روش استفاده از این متد به این صورته که،‌ این تابع یک کالکشن رو انتخاب میکنه و برای ما متغییرهایی که از جنس کلاس مورد نظر ما هستن رو بیرون میکشه، روش استفادش رو میتونید پایین ببینید :

https://gist.github.com/sajjadyousefnia/2cefd48dc9655bca902f0bfc2756358b

همینطور باید اینو بگم که، type argument، موقع ران‌تایم مشخص میشه و اون instanceای که از جنس filterIsInstance هست میتونه برای ما چک کنه که کدوم از عناصر لیست با type argument ما هم‌جنس هستن.

شکل ساده‌شده‌ی پیاده‌سازی filterIsInstance

https://gist.github.com/sajjadyousefnia/6d671a0e3185275399fb389e1d21a204

خب، حالا این عمل reification که در موردش صحبت کردیم، چرا فقط با استفاده از inline functions کار میکنه؟

همونطور که قبلا گفتیم، وقتی inline می کنیم، کامپایلر میاد و بایت‌کدهایی که قراره توی مبدا پیاده‌سازی بشن رو میبره توی مقصد. بنابراین، کامپایلر میتونه بایت‌کدی رو تولید کنه، که این بایت‌کد یک رفرنسی به type argument داره - یعنی همون نوع داده‌ی مورد نظر - . اون کدی که توی متد <filterIsInstance<String تولید میشه رو میشه یه جورایی معادل با کد زیر فرض کرد :

https://gist.github.com/sajjadyousefnia/e52510f736b334c87d69378d1565becd

همونطور که مشاهده می‌کنین، این کدی که ما داریم، به این دلیل که به جای اینکه به یک type argument ارجاع داشته باشه، به یک کلاس خاصی رفرنس یا ارجاع داده شده، بنابراین type-argument erasure در زمان runtime روی اون تاثیری نخواهد داشت.

البته نکته‌ای که وجود داره اینه که باید دقت کنید که تابع inline شده‌ای که دارای type parameterهای reified هست، رو نمیشه از جاوا فراخوانی کرد. توابع inline عادی گرچه از جاوا قابل دسترس هستن، ولی inline نمیشن.

البته نکته‌ای که وجود داره اینه که باید دقت کنید که تابع inline شده‌ای که دارای type parameterهای reified هست، رو نمیشه از جاوا فراخوانی کرد. توابع inline عادی گرچه از جاوا قابل دسترس هستن، ولی inline نمیشن.

اگه جایی مبهم بود یا بد توضیح داده بودم بود بفرمایید تا همین پایین خدمتتون توضیح بدم.