Covariance و Contravariance در سی شارپ
اگر درک معنای کوواریانس (Covariance) و تضاد (Contravariance) در #NET C. برای شما بسیار سخت است، از آن خجالت نکشید، شما تنها نیستید. این اتفاق برای من و بسیاری از توسعه دهندگان دیگر افتاده است. من حتی توسعه دهندگان باتجربه ای را می شناسم که یا درباره آنها نمی دانند و یا از آنها استفاده می کنند اما هنوز نمی توانند آنها را به اندازه کافی درک کنند.
هر بار که با مقاله ای روبرو می شوم که در مورد Covariance و Contravariance صحبت می کند، متوجه می شوم که آنها بر برخی اصطلاحات فنی متمرکز شده اند به جای اینکه که نگران آن باشند که چرا لازم است آنها را در وهله اول مورد توجه قرار داریم و اگر وجود نداشتند چه چیزی را از دست می دادیم.
درک Invariance ، Covariance و Contravariance به شما در درک موضوعات دیگر و تصمیم گیری درست در طراحی کمک می کند.
تعریف مایکروسافت
اگر Microsoft’s documentation را برای Covariance و Contravariance در #NET C. بررسی کنید، این تعریف را خواهید یافت:
در سی شارپ، کوواریانس و تضاد، تبدیل مرجع ضمنی را برای انواع آرایه، انواع delegate و آرگومان های نوع generic فعال می کنند. کوواریانس سازگاری انتساب را حفظ می کند و تضاد یا Contravariance آن را معکوس می کند.
شما می توانید در اینترنت جستجو کنید و منابع زیادی در مورد این موضوع پیدا خواهید کرد. شما با تعاریف، تاریخچه، هنگام معرفی، نمونه کد و بسیاری موارد دیگر مواجه خواهید شد و این چیزی نیست که در این مقاله آن را پیدا کنید. من به شما قول می دهم که آنچه در اینجا خواهید دید متفاوت است.
آنها در واقع چه هستند؟
اساساً، کاری که مایکروسافت انجام داد این بود که آنها افزودنی کوچکی را به روشی که شما نگهدارنده مکان نوع قالب عمومی (generic template type place holder) خود را تعریف می کنید اضافه کردند، یعنی <T>.
کاری که عادت داشتید هنگام تعریف یک رابط عمومی انجام دهید، پیروی از الگوی {…} <public interface IMyInterface<T است. پس از معرفی کوواریانس و تضاد، اکنون می توانید الگوی {…} <public interface IMyInterface<out T یا {…} <public interface IMyInterface<in T را دنبال کنید.
آیا out و in اضافی را تشخیص می دهید؟
آیا آنها را در جای دیگری دیده اید؟
ممکن است دات نت معروف <public interface IEnumerable<out T باشد؟ یا دات نت معروف <public interface IComparable<in T؟
مایکروسافت مفهوم جدیدی را معرفی کرد طوری که کامپایلر -در زمان طراحی- مطمئن شود که انواع اشیایی که استفاده می کنید و اعضای عمومی که ارسال می کنید استثنائات و خطاهای زمان اجرا ناشی از نوع اشتباه (wrong type expectations) را ایجاد نمی کنند.
بیایید فرض کنیم که کامپایلر هیچ محدودیت زمان طراحی اعمال نمی کند و ببینیم چه اتفاقی می افتد.
اگر کامپایلر هیچ محدودیت زمان طراحی را اعمال نکند چه اتفاقی می افتد؟
برای اینکه بتوانیم روی یک مثال مناسب کار کنیم، بیایید موارد زیر را تعریف کنیم:
با نگاهی به کد بالا متوجه خواهید شد که:
1 - کلاس A دارای ()F1 تعریف شده است.
2 - کلاس B دارای ()F1 و ()F2 تعریف شده است.
3 - کلاس C دارای ()F1() ، F2 و ()F3 تعریف شده است.
رابط IReaderWriter دارای ()Read است که یک شی از نوع TEntity و Write(TEntity entity)که انتظار پارامتری از نوع TEntity را دارد را بر می گرداند.
سپس متد ()TestReadWriter را به صورت زیر تعریف می کنیم:
فراخوانی ()TestReadWriter هنگام ارسال نمونه ای از <IReaderWriter<B
این باید به خوبی کار کند زیرا ما هیچ قانونی را نقض نمی کنیم. ()TestReadWriter در حال حاضر منتظر پارامتری از نوع <IReaderWriter<B است.
فراخوانی ()TestReadWriter هنگام ارسال نمونه ای از <IReaderWriter<A
با در نظر گرفتن این فرض که کامپایلر هیچ محدودیت زمان طراحی را اعمال نمی کند، به این معنی است که:
1 - ()param.Read نمونه ای از کلاس A را برمی گرداند نه B
=> بنابراین، var b در واقع از نوع A خواهد بود، نه B
=> این منجر به شکست خط ()b.F2 می شود زیرا var b که در واقع از نوع A است، ()F2 تعریف نشده ندارد.
2 - خط ()param.Write در کد بالا انتظار دارد پارامتری از نوع A دریافت کند، نه B
=> بنابراین، فراخوانی ()param.Write در حالی که پارامتری از نوع B را ارسال می کند، هر دو به خوبی کار می کنند.
بنابراین، از آنجایی که در شماره 1 ما انتظار شکست و خطا در زمان اجرا را داریم، پس نمی توانیم ()TestReadWriter را با ارسال یک نمونه از <IReaderWriter<A فراخوانی کنیم.
فراخوانی ()TestReadWriter هنگام ارسال نمونه ای از <IReaderWriter<C
با در نظر گرفتن این فرض که کامپایلر هیچ محدودیت زمان طراحی را اعمال نمی کند، به این معنی است که:
1 - ()param.Read نمونه ای از کلاس C را برمی گرداند نه B
=> بنابراین، var b در واقع از نوع C خواهد بود، نه B
=> این باعث می شود که خط ()b.F2 به خوبی کار کند زیرا var b دارای ()F2 خواهد بود.
2 - خط ()param.Write در کد بالا انتظار دارد پارامتری از نوع C دریافت کند نه B
=> بنابراین، فراخوانی ()param.Write در حین ارسال یک پارامتر از نوع B با شکست مواجه می شود زیرا به سادگی نمی توانید C را با والد B آن جایگزین کنید.
بنابراین، از آنجایی که در شماره 2 ما انتظار شکست در زمان اجرا را داریم، پس نمی توانیم ()TestReadWriter را با ارسال یک نمونه از <IReaderWriter<C فراخوانی کنیم.
حال، بیایید آنچه را که تا این لحظه کشف کرده ایم، تجزیه و تحلیل کنیم:
فراخوانی TestReadWriter(IReaderWriter<B> param) هنگام ارسال یک نمونه به آن از <IReaderWriter<B همیشه خوب و بدون مشکل است.
اگر فراخوانی ()param.Read را نداشته باشیم، فراخوانی TestReadWriter(IReaderWriter<B> param) هنگام ارسال یک نمونه از <IReaderWriter<A خوب است.
اگر فراخوانی ()param.Write را نداشته باشیم، فراخوانی TestReadWriter (ReaderWriter<B>) هنگام ارسال نمونه ای از <IReaderWriter<C خوب است.
با این حال، از آنجایی که ما همیشه ترکیبی بین ()param.Read و ()param.Write داریم، همیشه باید TestReadWriter (IReaderWriter<B> param) را با ارسال یک نمونه از <IReaderWriter<B فراخوانی کنیم، نه چیز دیگری.
مگر اینکه…….
جایگزین
اگر مطمئن شویم که رابط <IReaderWriter<TEntity متد ()TEntity Read یا void Write(TEntity entity) را تعریف می کند، نه هر دوی آنها را به طور همزمان.
بنابراین، اگر ()TEntity Read را حذف کنیم، ما خواهیم توانست که TestReadWriter(IReaderWriter<B> param) را با ارسال نمونه ای از <IReaderWriter<A یا <IReaderWriter<B فراخوانی کنیم.
به طور مشابه، اگر void Write(TEntity entity) را حذف کنیم، ما خواهیم توانست TestReadWriter(IReaderWriter<B> param) را با ارسال یک نمونه از <IReaderWriter<B یا <IReaderWriter<C فراخوانی کنیم.
این برای ما بهتر است زیرا محدودیت کمتری دارد، درست است؟
ذکر برخی از حقایق
1 - در دنیای واقعی، کامپایلر -در زمان طراحی- هرگز اجازه فراخوانی TestReadWriter (IReaderWriter<B> param) را با ارسال یک نمونه به آن از <IReaderWriter<A نمی دهد. در این حالت شما یک خطای کامپایل دریافت خواهید کرد.
2 - همچنین، کامپایلر -در زمان طراحی- اجازه فراخوانی TestReadWriter (IReaderWriter<B> param) را با ارسال از یک نمونه از <IReaderWriter<C نمی دهد. شما یک خطای کامپایل دریافت خواهید کرد.
3 - از پوینت های شماره 1 و 2، این را Invariance می نامند.
4 - حتی اگر ()TEntity Read را از interface یا رابط <IReaderWriter<TEntity حذف کنید، کامپایلر -در زمان طراحی- به شما اجازه نمی دهد که TestReadWriter (IReaderWriter<B> param) را با ارسال یک نمونه از <IReaderWriter<A فراخوانی کنید. شما یک خطای کامپایل دریافت خواهید کرد. این به این دلیل است که کامپایلر - بطور ضمنی به خودی خود - به اعضای تعریف شده در interface نگاه نمی کند و ببیند که آیا همیشه در زمان اجرا کار می کند یا خیر. شما باید این کار را خودتان از طریق <in TEntity> انجام دهید. این به عنوان وعده ای از طرف شما به کامپایلر عمل می کند که همه اعضای اینترفیس یا به TEntity وابسته نیستند یا با آن به عنوان ورودی (input) و نه خروجی (output) برخورد می کنند. به این Contravariance گفته می شود.
1 - به طور مشابه، حتی اگر void Write(TEntity entity) را از رابط <IReaderWriter<TEntity حذف کنید، کامپایلر -در زمان طراحی- به شما اجازه نمی دهد که TestReadWriter (IReaderWriter<B> param) را با ارسال از یک نمونه از <IReaderWriter<C فراخوانی کنید. شما یک خطای کامپایل دریافت خواهید کرد. این به این دلیل است که کامپایلر - بطور ضمنی به خودی خود - به اعضای تعریف شده در رابط نگاه نمی کند و ببیند که آیا همیشه در زمان اجرا کار می کند یا خیر. شما باید این کار را خودتان از طریق <out TEntity> انجام دهید. این به عنوان قولی از طرف شما به کامپایلر عمل می کند که همه اعضای رابط یا به TEntity وابسته نیستند یا به عنوان یک خروجی و نه ورودی با آن برخورد می کنند. به این Covariance گفته می شود.
2 - بنابراین، افزودن <out > یا <in > باعث می شود کامپایلر برای پاسخگویی به نیازهای ما محدودتر نباشد، آن طور که برخی توسعه دهندگان فکر می کنند محدودتر نیست.
خلاصه مطالب بالا
در این مرحله، شما باید داستان کامل Invariance، Covariance و Contravariance را درک کنید. با این حال، به عنوان یک جمع بندی سریع، می توانید از موارد زیر به عنوان یک برگه تقلب استفاده کنید:
مخلوط بین input و output نوع عمومی => تغییر ناپذیری (Invariance) => محدود کننده ترین => نمی تواند با والدین یا فرزندان جایگزین شود.
Added <in > => only input => Contravariance => itself or replace with parents
Added <out > => only output => Covariance => itself or replace with children
همچنین متوجه شدیم که درک Invariance، Covariance و Contravariance به شما کمک می کند تا موضوعات دیگر را درک کنید و تصمیمات درستی برای طراحی بگیرید.