رضا پویا
رضا پویا
خواندن ۹ دقیقه·۲ سال پیش

مفهوم Covariance و Contravariance در سی شارپ


مفهوم covariance و contra-variance درون سی شارپ، مفاهیمی هستند که اجازه می دهند ما بین انواع مختلف تبدیل رفرنس ضمنی (implicit reference conversion) انجام دهیم. این انواع شامل آرایه‎‌، انواعی delegate، و نوع آرگومان های جنریک هستند.

این مفاهیم باعث می شوند که بتوانیم sub-type را به type و برعکس، تبدیل کنیم. کد زیر را در نظر بگیرید.

تعریف اولیه کلاس
تعریف اولیه کلاس

قبل از اینکه وارد اصل موضوع شویم، بیائید مفاهیم پایه شی گرایی را مرور کنیم.

  • ارث بری : Apple و Orange هر دو از Fruit ارث بری می کنند.
  • زیرنوع sub-typing: اصل sub-typing اجازه می دهد رفرنس Apple یا Orange را به رفرنس Fruit را اختصاص داد. این کار دو پیامد در پی دارد :
- پارامتر ورودی: اگر متدی پارامتر ورودی از نوع Fruit داشته باشد، جایی که این متد را فراخوانی می کند، می تواند یک نمونه از Fruit یا Apple یا Orange را به عنوان آرگومان ورودی به آن ارسال کند.
- خروجی متد: اگر متدی خروجی از نوع Fruit داشته باشد، می تواند یک شی از نوع Fruit یا Apple یا Orange را به عنوان خروجی تحویل دهد. اگر این متد Virtual باشد و درون کلاس فرزند Override شده باشد، متد Override شده می تواند یک خروجی از نوع رفرنس Fruitبدهد. از سی شارپ 9 به بعد، متدهای override شده می توانند به صورت امن، رفرنسی از نوع Apple یا Orange را برگرداند. به این ویژگی سی شارپ، covariant return type می گویند.

بنابراین کدها ی زیر معتبر هستند:

حالت اولیه
حالت اولیه


و نحوه استفاده از این متدها:

03
03

همانطور که می بینید، مفهوم های covariance و contra-variance تبدیل رفرنس ضمنی را برای تبدیل انواع پارامتر، delegate و array را با استفاده از روابط sub-typing انجام می دهد. در این جا باید به مفهوم ضمنی (implicit) بودن تبدیل، که اساس covariance و contra-variance است توجه کنید.

نکته :

  • مفهوم covariance برای خروجی متد (out) است
  • مفهوم contra-variance برای ورودی متد (in) است. می توانید n درون contra-variance را معادل in در نظر بگیرید.

تا پیش از .Net Framework 4 نمی توانستیم از مفاهیم covariance و contra-variance درون انواع جنریک استفاده کنیم ولی از نسخه ی 4 به بعد ( از سال 2010 به بعد ) می توانیم با استفاده از کلمات کلیدی Out و in از این مفاهیم درون انواع جنریک نیز استفاده کنیم.

مفهوم covariance درون جنریک‌ها (استفاده از کلمه کلیدی outدرون تعریف جنریک)

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

علاوه بر این استفاده از کلمه کلیدی out در تعریف پارامتر جنریکش به این معناست که از نوع جنریک T فقط به عنوان خروجی متد درون نوع جنریک می توان استفاده کرد.

04
04

در اینجا متد Get() معتبر است ولی متد Cut(T item) خطا دارد و متن خطا این است.

Invalid variance: The type parameter 'T' must be contravariantly valid on 'IDish<T >.Cut (T)'. 'T' is covariant.

بنابراین امکان اینکه یک پارامتر covariant را به عنوان ورودی متد نوع جنریک تعریف کنیم، وجود ندارد.

حالا کلاسی را تعریف می کنیم که این اینترفیس را پیاده سازی می کند:

05
05

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

06
06

می بینید که می توانیم به راحتی متغیر orangeDish را به متد Process که ورودی از نوعی IDish<Fruit> می گیرد، بفرستیم. دلیل این موضوع این است که ما در تعریف اینترفیس IDish از کلمه کلیدی out استفاده کردیم و این کلمه کلیدی مفهوم covariance را برای نوع جنریک IDish فعال کرده است؛ در غیر این صورت ما فقط می توانستیم نوعی از نوع IDish<Fruit> را به متد مورد نظر ارسال کنیم.

نکته : فقط interface ها و delegate ها می توانند به عنوان variant تعریف کرد ( یعنی از کلمات کلیدی in و out فقط در جنریک های از نوع interface و delegate می توان استفاده کرد). بنابراین کد زیر نامعتبر است.

07
07

و متن خطا این است :

Invalid variance modifier. Only interface and delegate type parameters can be specified as variant.

مفهوم contra-variance درون جنریک ها (استفاده از کلمه کلیدی in درون تعریف جنریک)

می توانیم رابطه ی contra-variance را با استفاده از کلمه کلیدی in درون Generic ها تعریف کنیم. به کد زیر نگاه کنید:

08
08

در اینجا متد Add () معتبر است چون ورودی از نوع پارامتر T دارد که درون تعریف اینترفیس با استفاده از کلمه کلیدی in به عنوان Contravariance تعریف شده است. ولی متد Return نا معتبر است چون خروجی آن از نوع T هست ولی ما با استفاده از کلمه کلیدی in ، نوع T را فقط به ورودی متدها محدود کرده ایم. متن خطایی که کامپایلر برای این مشکل تولید می کند به شرح زیر است:

Invalid variance: The type parameter 'T' must be covariantly valid on 'IBasket<T>.Return(T)'. 'T' is contravariant.

بنابراین امکان اینکه یک پارامتر contra-variant را به عنوان خروجی متد نوع جنریک تعریف کنیم، وجود ندارد.

حالا کلاسی را تعریف می کنیم که این اینترفیس را پیاده سازی می کند:

09
09

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

در اینجا می بینید که می توانیم متغیر basket که از نوع IBasket<Fruit> است را به درون متد Process(IBasket<Orange> basket) ارسال کنیم. دلیل این موضوع این است که ما در تعریف اینترفیس IBasket از کلمه کلیدی in استفاده کردیم و این کلمه کلیدی مفهوم covariance را برای نوع جنریک IBasket فعال کرده است؛ در غیر این صورت ما فقط می توانستیم نوعی از نوع IBasket<Orange> را به متد مورد نظر ارسال کنیم.

بسیاری از کلاس های پایه سی شارپ از این مفهوم استفاده می کنند، برای مثال کلاس پایه IEnumerable<T> از این مفهوم استفاده می کند. نوع پارامتر T از IEnumerable<out T> به دلیل استفاده از کلمه کلیدی out یک covarianceاست. این به این معناست که می توانیم رفرنس IEnumerable<Derived> را به رفرنس یک IEnumerable<Base> اختصاص دهیم.

ما در اینجا می توانیم IEnumerable<Orange> را به IEnumerable<Fruit> اختصاص دهیم که معتبر است. دلیل معتبر بودن این تخصیص این است که به وسیله IEnumerable<T> ، المان های نوع T همیشه به بیرون می روند (out) و هرگز به درون آن تخصیص داده نمی شوند (in). در اینجا ما فقط نتیجه را بر می گردانیم (Out) و چیزی را به عنوان پارامتر ورودی در نظر نمی گیریم. بنابراین برای این مثال هیچ ریسکی برای اختصاص دادن رفرنس Orange به Apple وجود ندارد و اینگونه است که covariance پشتیبانی می شود.

در مورد Contra-variance مثال دیگر از انواع پایه سی شارپ که از این مفهوم پشتیبانی می کند، اینترفیس IComparer<T>است که تعریف اصلی آن در واقع به این صورت IComparer<in T> است. بیائید یک پیاده سازی از این اینترفیس بسازیم.

حالا یک مثال از استفاده از این کلاس را می نویسم:

می بینید که به راحتی می توانیم رفرنس fruitComparer را به orangeComparer نسبت دهیم و دلیل این کار استفاده از کلمه کلیدی in در تعریف IComparer<in T> است که آن را تبدیل به یک Contravariant کرده است که به ما اجازه می دهد نوع والد را به متدهایی که ورودی از نوع فرزند می گیرند، ارسال کنیم.

به صورت کلی این سه نکته را در نظر داشته باشید:

  • یک پارامتر جنریک نوع covariant که با کلمه کلیدی out مشخص شده را می توان به عنوان خروجی متد در نظر گرفت.
  • یک پارامتر جنریک نوع contra-variant که با کلمه کلیدی in مشخص شده را می توان به عنوان ورودی متد در نظر گرفت.
  • انواع مختلف covariant و contra-variant را می توان همزمان تعریف کرد و آنها مستقل از یک دیگر عمل خواهند کرد. یک اینترفیس جنریک یا یک delegate جنریک می تواند همزمان هر دوی اینها را داشته باشد.

بر اساس نکته سوم، کد زیر معتبر است:

مفهوم Covariance و Contra-variance درون delegate ها

مفهوم covariance و contra-variance درون سی شارپ انعطاف لازم را برای تطبیق نوع Delegate با امضای متدها ارائه می دهد.

  • مفهوم Covariance اجازه می دهد که یک نوع خروجی یک متد از sub-type هایی باشد که درون delegate تعریف شده است.
  • مفهوم Contra-variance اجازه می دهد که ورودی نوع پارامتر یک متد از نوع پایه تعریف شده درون delegate باشد.

در اینجا مثالی از covariance را درون delegate می بینید:

باید توجه داشته باشید که دو delegate جنریک درونی سی شارپ ، یعنی <..>Func و <..>Action اینگونه تعریف شده اند.

  • تابع ()<Func<T , TResult تا ()< Func<T1 ,…, T16, TResult خروجی از نوع covariance و ورودی های از نوع contra-variance دارند. دلیل این امر به استفاده از کلمات کلیدی in و outدرون تعریف آن ها بر می گردد.
  • به همین صورت ()<Action<T تا ()< Action<T1,..,T16 ورودی هایی از نوع contra-variance دارند.

به دلیل رفتار contra-variance ما می توانیم یک رفرنس نمونه ای از ()< Action<Fruit را به رفرنسی از ()<Action<Orange اختصاص دهیم. این تخصیص رفرنس عمل می کند چون نوع پارامتر می تواند از هر نوعی از Fruit باشد، یعنی در این مثال پارامتر ورودی می تواند از نوع پایه Fruit و تمام sub-type های آن باشد، به همین دلیل در این مثال، انوع Fruit، Orange و Apple معتبر هستند.

مفهوم covariance درون آرایه

سی شارپ از covariance درون آرایه ها پشتیبانی می کند. بنابراین درون سی شارپ، آرایه ای از Orange در واقع می تواند آرایه ای از نوع Fruit باشد. ولی مشکل اینجاست که این نوع تبدیل رفرنسی به صورت ذاتی type-safe نیست و می تواند در هنگام اجرا باعث ایجاد Exception شود.

البته می توانیم با استفاده از IEnumerable<T> تکه کد بالا را type-safe کنیم.

دلیل type-safe بودن کد بالا این است که IEnumerable<out T> به صورت فقط-خواندنی (read-only) تعریف شده است و هیچ پارامتر ورودی که با کلمه کلیدی in مشخص شده باشد، ندارد. بنابراین ریسکی برای آپدیت هیچ کدام از المان ها وجود ندارد.

مفهوم contra-variance درون آرایه

سی شارپ از contra-variance درون آرایه ها پشتیبانی نمی کند. بنابراین کد زیر کامپایل نمی شود و خطا می دهد.

می توانید به صورت صریح تبدیل(cast) انجام دهید ولی باز هم در هنگام اجرا، با خطای InvalidCastException مواجهه می شوید.

بنابراین نمی توان به صورت عملی از contra-variance درون آرایه سی شارپ استفاده کرد. علاوه بر این سعی کنید تا جای ممکن از covariance درون آرایه ها استفاده نکنید زیرا ممکن است استفاده از IEnumerable<T> را فراموش کنید و در زمان اجرا با خطاهای عجیب روبه رو شوید.

نتیجه گیری نهایی:

در اینجا ما با مفاهیم covariance و contra-variance آشنا شدیم و دیدیم که این مفاهیم چگونه در generic ، delegate و array ها درون سی شارپ پیاده سازی می شوند. استفاده از این ها درون آرایه ها باید ممنوع شود چون باعث خطاهای مهلک زمان اجرا می شود. با این حال پیاده سازی این مفاهیم درون Generic و delegate به صورت تمیز و type-safe انجام شده، بنابراین می توانید با خیال راحت از آنها استفاده کنید. با این مفاهیم دیگر نیازی به استفاده از متدهای درونی مثل Cast<T>() نداریم. احتمالا تا حالا در کدهایتان بدون اینکه متوجه شده باشید از این مفاهیم استفاده کرده اید.

منابع:

https://blog.ndepend.com/covariance-and-contravariance-in-csharp-explained/

https://programming.tosinso.com/fa/articles/43506

در اینجا کدها را می بینید:

https://github.com/RezaPouya/MasteringCsharp/tree/main/CovarianceAndContravariance

سی شارپ
من یک توسعه دهنده .Net هستم ، تلاش دارم کارهای درست رو درست انجام بدهم.
شاید از این پست‌ها خوشتان بیاید