مهدیس یاوری
مهدیس یاوری
خواندن ۴ دقیقه·۱۰ ماه پیش

Type variance in Kotlin


Variance

زمانی که در برنامه نویسی از مفهوم وراثت استفاده میکنیم، گاهی نیاز است بدانیم کلاس فرزندی که از پدر ارث بری میکند، آیا هر نوعی از زیر کلاس قابل تعویض با هر نوع از کلاس پدر است؟!

برای مثال کلاس Circle از کلاس Shape ارث بری میکند، آیا می توان نتیجه گرفت <List<Circle می تواند با <List<Shape جایگزین شود؟

مفهوم Variance چگونگی ارتباط بین این اجزا را بیان میکند.


در اینجا می خواهیم با سه مدل از تایپ های Variance بیشتر آشنا شویم که پایین تر با مثال هریک را بیان خواهیم کرد.

Invariance-
Covariance-
Contravariance-



Invariance

فرض کنید می خواهیم با استفاده از وراثت یک ارتباطی بین کلاس هایمان در پروژه بوجود آوریم. برای مثال یک کلاس ساده Shape را در نظر بگیرید که کلاس Circle از آن ارث بری کرده است :

open class Shape {} class Circle : Shape() {}

در ادامه ما یک لیستی از Circle به اسم circleList می سازیم :

val circleList: List<Circle> = listOf(Circle())

حال با استفاده از ایجاد یک لیستی از کلاس پدر یعنی Shape, سعی می کنیم که circleList را به آن assign کنیم :

val shapeList: List<Shape> = circleList

این خط کدی که ایجاد کردیم بدون هیچ خطایی قابل استفاده است.

شاید اینگونه تصور کنید که اجازه استفاده از این نوع تعریف را به صورت مشابه درجاهای دیگر نیز داریم و چون کلاس Circle از Shape ارث بری کرده پس کامپایلر این امکان را می دهد تا هر نوعی از Circle را بتوانیم به عنوان جایگزین برای هرنوعی از Shape استفاده کنیم اما جواب "خیر" است.

با کمی دقت می توانیم متوجه بشویم که List در تایپ generic کلاس اصلی خود از کلمه ای به اسم "out" استفاده کرده که این امکان را برای پیاده سازی مثال بالا برای ما فراهم کرده است که در ادامه بیشتر با آن آشنا می شویم.

public interface List<out E> : kotlin.collections.Collection<E>

پس اگر ما در مثال بالا بجای List از MutableList استفاده می کردیم با خطا مواجه می شدیم چون MutableList این قاعده را پیاده سازی نکرده است.

نتیجه: به طور کلی این مفهوم (Invariance) به این اشاره دارد که هرنوعی از کلاس A و B را نمیتوان زیر نوع هریک از دیگری دید، علیرغم اینکه ارتباط بین A و B هرچه باشد.



Covariance

این مفهوم زمانی استفاده می شود که بخواهیم یک subtype را بجای super type اش استفاده کنیم .

با فرض همان مثال قبل و ارتباطی که بین کلاس ها ایجاد کردیم، میخواهیم این مفهوم را در Generic ها پیاده سازی کنیم .

تایپ Generic کلاس ShapeContainer را از نوع Shape در نظر میگیریم و در main، با استفاده از circleContainer یک instance می سازیم. در اینجا با کمک "out" مجوز استفاده از این subtype را به عنوان جایگزین با super type اش گرفتیم .

یادآوری: اگر تجربه کار با زبان جاوا را داشته باشید، در واقع این مفهوم مشابه <List<? extends E عمل میکند ولی در کاتلین به این شکل <List<out E استفاده می شود.

حال بیایید فرض کنید out را از تایپ generic حذف کنیم در این صورت ملاحظه میشود که با خطای Type mismatch error در زمان کامپایل مواجه میشویم.


نتیجه: با اینکه Circle ، زیرنوعی از Shape است اما کامپایلر نمیتواند بپذیرد که درواقع Circle یک Shape است.

راه حل: با استفاده از out در تایپ Generic کلاس مشکل حل میشود .



Contravariance

این مفهوم عکس Covariance عمل میکند و زمانی استفاده می شود که بخواهیم یک super type را بجای subtype اش استفاده کنیم.

در این مثال ما یک contravariant Interface Printer ، با استفاده از “keyword “in در نوع پارامترش تعریف کردیم. این اینترفیس یک سینگل متد print دارد که نوع پارامتر T را دریافت میکند.

در main ما یک implementation ای از printer Interface با استفاده از anonymous object میسازیم که متد print آن یک shape در ورودی میگیرد و پرینت میکند.

در مرحله بعد ما یک circle printer تعریف میکنیم و shape printer را به آن assign میکنیم.

نکته: این واگذاری فقط به دلیل استفاده از "keyword "in و پیاده سازی مفهوم contravariance ممکن بوده است.




به طور خلاصه: زمانی که کلاسی را به صورت covariant type تعریف میکنید، آن کلاس فقط میتواند producer آن تایپ باشد و در مقابل زمانی که از contravariant type استفاده میکنید، فقط میتواند consumer آن تایپ باشد.








برنامه نویسیkotlinandroidgeneric classcovariance vs contravariance
شاید از این پست‌ها خوشتان بیاید