حمیدرضا علیاری
حمیدرضا علیاری
خواندن ۱ دقیقه·۳ سال پیش

مقایسه عملی Covariance و Contravariance در سی‌شارپ

احتمالا زمانی که میخواهید یک اینترفیس از نوع Generic ایجاد کنید با پرسشی از طرف ریشارپر مواجه شدید که میگه پارامتر T میتونه به صورت Covariance یا Contravariance تعریف بشه.
اگه تایید بزنید، به پارامتر T پیشوند in یا out اضافه میکنه.

توی این مقاله میخوایم بررسی کنیم که تایپ‌های Covariant و Contravariant چیا هستن و به چه دردی میخورن و اینکه in و out به چه کاری میان.

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

public class Person { public Person(string name) { Name = name; } public string Name { get;} } public class Student : Person { public Student(string name) : base(name) { } // Student specific fields... } public class Teacher : Person { public Teacher(string name) : base(name) { } // Teacher specific fields... }


بدیهیه که کلاس Person به عنوان base قرار گرفته که فیلد Name داره و کلاس‌های Student و Teacher از این کلاس ارث‌بری میکنن.


نکات:

با توجه به اینکه کلاس‌های Student و Teacher از کلاس Person ارث‌بری میکنن، هر جایی که انتظار کلاس Person رو داریم، میتونیم کلاس‌های Teacher و Student رو جایگزینش کنیم.
بطور مثال اگه یه متد ورودی‌ای از جنس Person داشته باشه، همواره میتونیم به جاش instance کلاس‌های Student و Teacher رو پاس بدیم.
همچنین اگه یک متد طوری تعریف شده که کلاس Person به عنوان return type باشه، میتونیم کلاس‌های Student و Teacher رو هم return کنیم.
این موارد برای کسایی که OOP کار میکنن کاملا بدیهیه و ابهامی نداره.

اما سوالی که در اینجا مطرح میشه اینه که اگه یه متدی به عنوان پارامتر ورودیش، Person نه، بلکه یک Generic Type بصورت <G<Person بگیره چطور؟ ایا این به معنی اینه که میتونیم <G<Teacher یا <G<Student رو هم بهش پاس بدیم؟

این دقیقا همین موردیه که میخوایم توی این مقاله بررسی کنیم. در انتهای این مقاله میتونیم پاسخ درستی به این سوال بدیم.



بررسی Covariance در آرایه‌ها

به متد زیر توجه کنید:

public void PrintNames(Person[] people) { foreach (var person in people) { Console.WriteLine(person.Name); } }

این متد آرایه‌ای از Person رو توی ورودی میگیره و با دستور foreach اسم تک تک افراد رو توی console نمایش میده.
اما اگه به جای آرایه‌ای از Person، ما آرایه‌ای از Student یا Teacher رو براش ارسال کنیم چطور؟ مثل زیر:

Student[] students = { new Student(&quotJohn&quot), new Student(&quotPeter&quot) }; PrintNames(students);

این کد به درستی compile و اجرا میشه بدون هیچ مشکلی، چون هر دو کلاس Teacher و Student شامل فیلد Name هستند که اجازه پرینت کردن رو به این متد میده. این باعث میشه ما بتونیم کارای بیشتری با این متد بکنیم در مقایسه با اینکه فقط بتونیم از یک تایپ خاص مثل Person به عنوان ورودیش استفاده کنیم.

برای درک بهتر یه مثال دیگه رو بررسی کنیم:

public void Update(Person[] people) { people[0] = new Teacher(&quotPaul&quot); }

حالا اگه این متد رو به صورت زیر فراخوانی کنیم چه اتفاقی میافته؟

Student[] students = { new Student(&quotJohn&quot), new Student(&quotPeter&quot) }; Update(students); Student firstStudent = students[0];

این کد compile میشه ولی توی runtime بهمون ارور میده. یکم ساده‌ترش میکنیم:


Student[] students = { new Student(&quotJohn&quot), new Student(&quotPeter&quot) }; Person[] people = students; //Line 6 people[0] = new Teacher(&quotPaul&quot); // Line 7 Student student = students[0]; // Line 8

این موارد رو بدقت دنبال کنید:
جنس اولیه آرایه از نوع []Student است که با توجه به مواردی که بحث شد، میتونیم []Student رو جایگزین []Person بکنیم. توی خط ششم ما رفرنس زدیم روی []Person. همینطور هر Teacher ای یک نوع Person است که توی خط هفتم این مورد جلوی compile رو نمیگیره و ارور نمیده. اما این دقیقا اونجاییه که ما ارور زیر رو دریافت میکنیم:

Attempted to access an element as a type incompatible with the array.


البته انتظار این ارور رو میکشیدیم. اگه کد کار میکرد،چه اتفاقی توی خط هشتم میافتاد؟ ما اینجا سعی کردیم کلاس Teacher رو به Student اختصاص بدیم. این دو تایپ با همدیگه compatible نیستند و بی معنیه.


اینکه ما میتونیم جایی که انتظار داریم []Person پاس داده بشه، به جاش []Student یا []Teacher پاس بدیم به این معنیه که این آرایه‌ها Covariant هستند.

قبل از سی‌شارپ نسخه ۴ تمام generic type ها از نوع invariant بودن پس ما فقط میتونستیم دقیقا همون تایپ مشخص رو باهاش کار کنیم. طراحان زبان تصمیم گرفتن همین پیاده‌سازی که توی آرایه‌ها داریم رو هم برای generic typeها اضافه کنن تا بتونیم راحت‌تر از اونها استفاده کنیم.


بررسی Covariance در Generic Type ها

ما مواردی رو بررسی کردیم که مشخص میکرد با استفاده از covariance میتونیم یکسری الگوریتم‌های مشترک رو بصورت یکجا استفاده کنیم. اما مثالی رو هم دیدیم که covariance در آرایه‌ها مارو با ارور مواجه کرد.
میخوایم اینجا یک نگاه دقیقتری به مثال بالا داشته باشیم. مجددا بصورت زیر مینویسیم:

public void PrintNames(Person[] people) { foreach (var person in people) { Console.WriteLine(person.Name); } } public void Update(Person[] people) { people[0] = new Teacher(&quotPaul&quot); }

همونطور که مشخصه، متد PrintNames المنت‌هایی از آرایه رو فقط read میکنه. اما از اونطرف، متد update، آرایه رو تغییر میده که دقیقا همین حرکت باعث بروز ارور و مشکل میشه. با آرایه‌های Covariant عملا هیچ تضمینی برای جنس پارامتر پاس داده شده نداریم و میتونه Person، Teacher یا Student باشه. پس ما نمیتونیم به راحتی عملیات update رو انجام بدیم.
توی مثال ما، اگه پارامتر پاس داده شده از جنس []Teacher یا []Person باشه بطور صحیح عملیات انجام میشه اما اگه از نوع []Student باشه به ارور میخوره.
این کد خیلی شکننده‌ست (fragile) و قانون سوم SOLID رو نقض میکنه. به همین دلیله که طراحان سی‌شارپ در ابتدا از پیاده‌سازی این موارد توی generic ها صرف نظر کردن.

اما خب یه جاهایی هم covariance به کارمون میاد. اونجا چطور؟
مثلا ما اگه اینترفیس generic مون فقط عملیات read برای پارامتر ورودیش انجام بده، میتونیم بگیم که ارورهای دریافتیمون تقریبا از بین میره. به همین دلیل توی سی‌شارپ ۴ تصمیم گرفته شد با علم به این موضوع، مباحث Covariance و Contravariance با استفاده از سینتکس out و in پیاده‌سازی بشه.
بحث Contravariance بعدا توی همین مقاله بررسی خواهد شد.

خب حالا بریم یه درک کلی در مورد اینکه یک اینترفیس generic بتونه پارامتری بصورت read-only داشته باشه رو بدست بیاریم.
تصور کنید ک پارامتر T به عنوان مقدار برگشتی متد زیر در نظر گرفته میشه:

public interface IMyReadOnlyCollection<T> { T GetElementAt(int index); }

این مثال خوبی برای حالتیه که بخواهیم یک تایپی رو Covariant کنیم. که به سادگی و با اضافه کردن عبارت out به عنوان پیشوند برای T میتونیم بهش برسیم:

public interface IMyReadOnlyCollection<out T>

در اینجا به پارامتر T گفته میشه که توی این اینترفیس به عنوان output در نظر گرفته میشه.
بعد از اعمال این تغییر، میتونیم به راحتی <IMyReadOnlyCollection<Personرو با <IMyReadOnlyCollection<Student یا <IMyReadOnlyCollection<Teacherجایگزین کنیم.

اگه دقت کرده باشید، قبلا هم با یک سری اینترفیس‌های generic از نوع read-only کار کردید. دیگه جای تعجبی نداره که بدونید IEnumerator هم یک نوع از اوناست:

public interface IEnumerator<out T> : IEnumerator, IDisposable { T Current { get; } }

که به عنوان covariant شناخته میشود.
همینطور IEnumerable که یک instance از نوع IEnumerator برمیگردونه هم Covariant است:

public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }

در ادامه به بررسی Contravariance میپردازیم.




بررسی Contravariance در Generic Type ها

توی بعضی از مقاله‌ها، Contravariance رو برعکس Covariance تعریف میکنن. توضیح و تعریف درستی بنظر میاد ولی خیلی گنگه.
اینجا میخوایم یکم شفاف‌تر بهش بپردازیم تا با مثال کاربردی، بتونیم بهتر درکش کنیم.
خب اینجا یک اینترفیس ساده داریم برای مقایسه دو object:

public interface IMyComparer<T> { int Compare(T x, T y); }

و یک پیاده سازی برای Person:

public class PersonComparer : IMyComparer<Person> { public int Compare(Person x, Person y) { return string.CompareOrdinal(x.Name, y.Name); } }

این متد فقط فیلد Name این دوتا object رو مقایسه میکنه و تا همینجا کافیه تا بتونیم ساده‌تر پیش بریم.
حالا این متد رو در نظر بگیرید:

public int Compare(IMyComparer<Student> comparer) { var s1 = new Student(&quotJohn&quot); var s2 = new Student(&quotPeter&quot); return comparer.Compare(s1, s2); }


این متد دوتا Student رو ایجاد میکنه و توسط comparer ای که از پارامترهاش میگیره که از جنس <IMyComparer<Student هستش، مقایسه رو انجام میده.

ما قبلا یک پیاده‌سازی داشتیم که دوتا شی Person رو با name شون مقایسه میکنه توی کلاس PersonComparer. اینجا چه اتفاقی میافته اگه بخوایم از همون comparer برای مقایسه دوتا Student استفاده کنیم؟
مثل اینجا:

var personComparer = new PersonComparer(); var comparisonResult = Compare(personComparer); Console.WriteLine(comparisonResult);

اومدیم به عنوان پارامتر ورودی به متد Compare که <IMyComparer<Student انتظار داشت، به جاش PersonComparer پاس دادیم.
این کد ارور زمان compile میده. قبل از سی‌شارپ ۴ هیچ کاری نمیتونستیم بکنیم. دلیل این ارور هم واضحه، چیزی که این متد انتظار داشته رو پاس ندادیم. اما منطقا اگه ما یک Comparer داریم که روی Person کار میکنه، باید بتونیم از اون برای مقایسه Student ها هم استفاده کنیم، چون Student یک نوع Person هست با ویژگی‌های اضافی‌تر. اگه ما میگیم که دو تا Person هم‌اسم، یکی هستند، باید بتونیم همین شرط رو هم توی Student داشته باشیم.
این جاییه که Contravariance وارد بازی میشه. مشابه همون روشی که توی Covariance استفاده میکردیم، میتونیم اینجا هم پیش بریم. نیاز داریم که به پارامتر اینترفیس generic مون اینو بگیم. اما اینبار به جای استفاده از out، از in به عنوان پیشوند پارامترمون استفاده میکنیم.
حالا اینترفیسمون به این شکل در میاد:

public interface IMyComparer<in T> { int Compare(T x, T y); }

حالا اینترفیس IMyComparer به عنوان Contravariance تعریف میشه. با این حرکت، میتونیم PersonComparer رو توی متد Compare که انتظار <IMyComparer<Student رو داشت، پاس بدیم.
ارور compiler رفع شده و ما به چیزی که میخواستیم رسیدیدم که تونستیم از متد مشترکی برای مقایسه instance ها بهره ببریم.


بررسی Invariance در Generic Type ها

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

همونطور که قبلا گفتیم، IEnumerable با توجه به پارامتر generic ای که بصورت read-only داره، به عنوان covariant شناخته میشد.

اینجا مثال IList رو میزنیم:

public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable { T this[int index] { get; set; } int IndexOf(T item); void Insert(int index, T item); void RemoveAt(int index); }

ما نمیتونیم با این اینترفیس به صورت Covariant یا Contravariant رفتار کنیم. چون با توجه به پارامتر T که داره هم در موقعیت output قرار گرفته و هم input. اگر تلاش کنیم که اینکارو کنیم، به همون اروری که قبلا توی آرایه‌ها بهش برخورده بودیم، رو به رو میشیم.

var students = new List<Student>(); IList<Person> people = students; people.Add(new Teacher(&quotPeter&quot)); Student student = students[0];


این کد ارور compile میده و به هیچ نحوی قابل رفع نیست مگر اینکه دقیقا همون تایپی که انتظار داره رو بهش پاس بدیم.



خلاصه

توی این مقاله به بررسی مفاهیم Covariance در آرایه‌ها پرداختیم و در ادامه از ویژگی هایی که زبان در اختیارمون داده تا این مفاهیم رو توی Generic Type ها هم استفاده کنیم رو مشاهده کردیم.

امیدوارم این مقاله براتون مفید بوده باشه.
خوشحال میشم اگه سوالی داشتید، توی کامنت‌ها در موردش صحبت کنیم.


نویسنده: حمیدرضا علیاری
ایمیل: hamidayr@gmail.com




شاید از این پست‌ها خوشتان بیاید