یکی از مفاهیمی که بنظر پیچیده میآمد و هر دفعه موقع مطالعه از آن فرار میکردم، همین بحث COVARIANCE و CONTRAVARIANCE بود. در اینجا قصد دارم به زبان ساده این مفاهیم را شرح دهم.
کد زیر کامپایل نخواهد شد:
دلیل اینکه کد فوق کامپایل نمیشود، در کد زیر آورده شده است:
اگر کامپایل انجام میشد، کد بالا در زمان اجرا خطا صادر میکرد؛ چرا که نوع واقعی animals، در واقع <Stack<Bear بوده و نمیتوان به آن، شیء ای از جنس Camel اضافه کرد. عدم پشتیبانی از کوواریانس، به هرحال مانع از امکان استفاده مجدد (re-usability) خواهد شد. برای مثال فرض کنید میخواهیم متدی بنویسیم که وظیفه آن صادر کردن دستور شستن حیوانات موجود در پشته باشد:
فراخوانی متد Wash با پارامتری از جنس <Stack<Bear در زمان کامپایل خطا خواهد داد (اعمال این محدودیت منطقی است. برای مثال ممکن است مثلا در بدنه متد Wash با استفاده از متد Pop کلاس Stack یک Animal برداشته شده و به Camel کست گردد که با توجه به نوع اصلی آن (Bear) خطای run-time صادر خواهد شد. اما به هرحال محدودیت ایجاد شده، جلوی خطاهایی که ممکن است در run-time اتفاق بیافتد را میگیرد).
با کد فوق میتوان متد Wash را به صورت زیر فراخوانی نمود:
کامپایلر، ورژن جنریک متد Wash را کامپایل میکند. در این حالت میتوان با چک کردن نوع واقعی T و کست کردن به آن نوع، عملیات را بدون خطا انجام داد.
این مورد باعث ایجاد قابلیت استفاده مجدد میشود؛ به قیمت اینکه ممکن است چنین خطاهایی ایجاد شوند:
کلمه کلیدی out نشان میدهد که T فقط در موقعیت خروجی مورد استفاده واقع میگردد (برای مثال نوع برگشتی یک متد). این مورد سبب میشود تا پارامتر covariant باشد و کد زیر کامپایل گردد:
در اینجا کامپایلر اجازه تبدیل bears را به animals میدهد. چرا که موردی که کامپایلر از آن جلوگیری میکرد (Push کردن Camel به Stack با اعضایی از جنس Bear) در اینجا نمیتواند رخ دهد. چرا که در اینجا پارامتر T فقط میتواند به عنوان خروجی استفاده گردد و امکان Push کردن وجود ندارد.
میتوانیم کد زیر را بنویسیم:
هیچ عضوی از اینترفیس IPushable خروجی T را بر نمیگرداند و لذا با casting اشتباه، مواجه نخواهیم شد (برای نمونه از طریق این اینترفیس راهی برای Pop کردن نداریم).
از آنجاییکه T در اینجا contravariant است میتوان از <IComparer<object برای مقایسه دو string استفاده نمود:
برای مطالعهی بیشتر
Covariant and Contravariant
Covariance
A را در نظر بگیرید که قابل تبدیل به B باشد. در اینصورت X، دارای پارامتر کواریانس است اگر <X<A قابل تبدیل به <X<B باشد. بدون ذکر مثال شاید این تعریف خیلی ملموس نباشد. پس بهتر است با ذکر مثال به تشریح مفاهیم بپردازیم.
نکته: در اینجا منظور از قابل تبدیل بودن، قابل تبدیل بودن به صورت ضمنی (implicit) میباشد. برای مثال A از B ارث بری داشته باشد و یا A، تایپ B را پیاده سازی کند (در صورتی که B یک اینترفیس باشد). تبدیلات عددی، Boxing و تبدیلات کاستوم مجاز نیستند.
برای نمونه نوع <IFoo<T پارامتر کوواریانس T دارد، اگر کد زیر معتبر باشد:
IFoo<string> s = ...; IFoo<object> b = s;
از C# 4.0، اینترفیسها و delegateها مجاز به استفاده از پارامتر کوواریانس T هستند؛ اما در مورد کلاسها اینطور نیست. آرایهها نیز مجاز هستند که در ادامه تشریح خواهند شد (اگر A قابل تبدیل به B باشد در اینصورت []A قابل تبدیل به []B خواهد بود. هر چند ممکن است به run-time exception منجر گردد که ظاهرا این پشتیبانی آرایهها از پارامترهای کوواریانس دلایل تاریخی دارد!).
Variance is not automatic
برای حصول اطمینان از static type safety، پارامترها به صورت پیش فرض variant نمیباشند:
class Animal {} class Bear : Animal {} class Camel : Animal {} public class Stack<T> { int position; T[] data = new T[100]; public void Push (T obj) => data[position++] = obj; public T Pop() => data[--position]; }
Stack<Bear> bears = new Stack<Bear>(); Stack<Animal> animals = bears; // Compile-time error
animals.Push (new Camel()); // Trying to add Camel to bears
public class ZooCleaner { public static void Wash (Stack<Animal> animals) {...} }
یک راه حل برای این موضوع، تعریف متد Wash به صورت جنریک و با constraint است:
class ZooCleaner { public static void Wash<T> (Stack<T> animals) where T : Animal { ... } }
Stack<Bear> bears = new Stack<Bear>(); ZooCleaner.Wash(bears);
نکته: اگر reusable بودن مد نظر نبود، باید برای هر sub-type از Animal یک متد جداگانه Wash مینوشتیم (یکی برای Bear، یکی برای Camel،...).
راه حل دیگر این است که کلاس <Stack<T یک اینترفیس با پارامتر covariant پیاده سازی نماید که در ادامه به این مورد بازخواهیم گشت.
Arrays
آرایهها از covariance پشتیبانی میکنند. برای مثال:
Bear[] bears = new Bear[3]; Animal[] animals = bears; // OK
animals[0] = new Camel(); // Runtime error
Declaring a covariant type parameter
از C# 4.0 و بالاتر، پارامترهای اینترفیسها و delegateها میتوانند با استفاده از کلمه کلیدی out از covariance پشتیبانی کنند؛ یا به زبان سادهتر covariant گردند. در این صورت برخلاف آرایهها از type safety اطمینان کامل خواهیم داشت.
برای نشان دادن این مورد، در کلاس <Stack<T اینترفیس زیر را پیاده سازی میکنیم:
public interface IPoppable<out T> { T Pop(); }
var bears = new Stack<Bear>(); bears.Push (new Bear()); // Bears implements IPoppable<Bear>. We can convert to IPoppable<Animal>: IPoppable<Animal> animals = bears; // Legal Animal a = animals.Pop();
نکته: پارامترهای متدی که مزین به کلمه کلیدی out شدهاند، واجد شرایط covariant بودن نمیباشند (به دلیل وجود محدودیتی در CLR).
با استفاده از کد زیر قابلیت استفاده مجددی که در ابتدا بحث کردیم فراهم میشود:
public class ZooCleaner { public static void Wash (IPoppable<Animal> animals) { ... } //cast covariantly to solve the reusability problem }
نکته: Covariance (و contravariance) فقط در موارد تبدیل ارجاعی کار میکنند (نه تبدیل boxing). بنابراین اگر متدی داشته باشیم که دارای پارامتری از جنس IPoppa
<ble<object باشد، امکان فراخوانی آن متد با ورودی از جنس <IPoppable<string وجود دارد؛ اما پاس دادن متغیر از جنس <IPoppable<int امکانپذیر نمیباشد.
Contravariance
در تعریف covaraince داشتیم: A را در نظر بگیرید که قابل تبدیل به B باشد. در اینصورت X، دارای پارامتر کواریانس است اگر <X<A قابل تبدیل به <X<B باشد. Contravariance
زمانی است که تبدیل در جهت عکس صورت گیرد (تبدیل از <X<B به <X<A). این مورد فقط برای پارامترهای ورودی صحیح است و با کلمه کلیدی in تعیین میگردد. با استفاده از پیاده سازی اینترفیس:
public interface IPushable<in T> { void Push (T obj); }
IPushable<Animal> animals = new Stack<Animal>(); IPushable<Bear> bears = animals; // Legal bears.Push (new Bear());
توجه: کلاس <Stack<T هر دو اینترفیس <IPushable<T و <IPoppable<T را پیاده سازی کرده است (با وجود اینکه T هم out است و هم in). اما این مورد مشکلی ایجاد نمیکند. زیرا قبل از تبدیل، ارجاعی فقط به یکی از اینترفیسها صورت میگیرد (نه همزمان به هردو!). این مورد نشان میدهد که چرا classها از پارامترهای variant پشتیبانی نمیکنند.
برای مثال اینترفیس زیر را در نظر بگیرید:
public interface IComparer<in T> { // Returns a value indicating the relative ordering of a and b int Compare (T a, T b); }
var objectComparer = Comparer<object>.Default; // objectComparer implements IComparer<object> IComparer<string> stringComparer = objectComparer; int result = stringComparer.Compare ("Hashem", "hashem");
برای مطالعهی بیشتر
Covariant and Contravariant