حسین احمدی
بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

مفاهیم Covariance و Contra-Variance در سی شارپ

از نسخه 4، دات نت تغییراتی را در interface های جنریک و delegate های جنریک اعمال کرد: Covariance و Contra-Variance. قبلاً در مورد جنریک ها در این لینک صحبت کردیم و با این مفهوم آشنا شدیم. هدف از این مطلب آشنایی با این دو مفهوم در دات نت است. Covariance و Contra-Variance به طور مستقیم برای تبدیل نوع های که برای پارامترهای ورودی و همچنین مقادیر بازگشتی استفاده می شوند کاربرد دارند. برای مثال دو کلاس زیر را در نظر بگیرید:

دوره های شبکه، برنامه نویسی، مجازی سازی، امنیت، نفوذ و ... با برترین های ایران
سرفصل های این مطلب
  1. کلمه کلیدی out
  2. کلمه کلیدی in
public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public override string ToString()
    {
        return string.Format("{0}: {1} {2}", Id, FirstName, LastName);
    }
}

public class Teacher : Person
{

}

فرض کنید متدی دارید که به عنوان ورودی، پارامتری از نوع Person را قبول می کند:

public static void DisplayPerson(Person person)
{
    Console.WriteLine(person.ToString());
}

طبیعی است که می توانید هر شئ ای که از نوع هایی که از کلاس Person مشتق شده اند را برای کلاس DisplayPerson استفاده کنید:

DisplayPerson(new Teacher() {Id = 1, FirstName = "Hossein", LastName = "Ahmadi"});

حال متدی را در نظر بگیرید که مقدار بازگشتی آن از نوع Person است:

public static Person CreatePerson()
{
    return new Teacher();
}

زمان استفاده از این متد، شما نمی توانید خروجی آن را به صورت مستقیم داخل متغیری از نوع Teacher قرار دهید و با پیغام خطا مواجه خواهید شد. کد زیر مشکل داشته و اجرا نخواهد شد:

Teacher teacher = CreatePerson();

این موضوع به این خاطر است که لزوماً، همیشه خروجی متد CreatePerson از نوع Teacher نیست و می تواند هر نوعی که از کلاس Person مشتق شده باشد را برگرداند. اما عکس این موضوع صادق است، یعنی خروجی متدی که نوع بازگشتی آن از نوع مثلاً Teacher است را داخل متغیری از نوع Person قرار داد:

public static Teacher CreatePerson()
{
    return new Teacher();
}

و نحوه استفاده از متد بالا که بدون مشکل اجرا خواهد شد:

Person teacher = CreatePerson();

این موارد در حالت عادی موضوعیت داشت و قبل از نسخه 4 دات نت، نمی توانستیم از این مفاهیم برای پارامترهای جنریک استفاده کنیم. به لطف قابلیت های Covariance و Contra-Variance می توان نوع های جنریک را به صورتی تعریف کرد که این قابلیت ها در آن ها قابل استفاده باشد. در اینجا ما با دو کلمه کلیدی in و out که زمان تعریف interface های جنریک و یا delegate های جنریک استفاده می شوند آشنا می شویم.

کلمه کلیدی out

کلمه کلیدی out زمانی که برای پارامتر های جنریک یک interface استفاده می شود، مشخص می کند که پارامتر مشخص شده Covariance است، همچنین با قرار دادن کلمه کلیدی out، برای یک پارامتر جنریک، تنها می توان آن را برای مقادیر بازگشتی مورد استفاده قرار داد. به مثال زیر توجه کنید:

public interface ICollection<out T>
{
    T this[int index] { get; }
    int Count { get; }
}

کد بالا بدون مشکل کار می کند، اما در صورتی که interface بالا را به صورت زیر تغییر دهیم کد نوشته شده کامپایل نخواهد شد، زیرا پارامتر T را برای ورودی یک متد استفاده کردیم، در حالی که این پارامتر از نوع out یا Covariance است:

public interface ICollection<out T>
{
    T this[int index] { get; }
    int Count { get; }
    void Add(T person);
}

حال کلاسی با نام TeacherCollection به صورت تعریف می کنیم که ICollection را پیاده سازی می کند:

public class TeacherCollection : ICollection<Teacher>
{
    private Teacher[] teachers = new Teacher[]
    {
        new Teacher(),
        new Teacher(),
        new Teacher(),
    };

    public Teacher this[int index]
    {
        get { return teachers[index]; }
    }

    public int Count { get { return teachers.Length; } }
}

اما تاثیر کلمه کلیدی out در زمان تعریف ICollection، زمانی مشخص می شود که بخواهیم از TeacherCollection استفاده کنیم. فرض کنید متدی داریم به صورت زیر:

public static void ProcessPersons(ICollection<Person> persons)
{
            
}

و به صورت زیر از این متد استفاده می کنیم:

ICollection<Teacher> teachers = new TeacherCollection();

ProcessPersons(teachers);

همانطور که مشاهده می کنید، پارامتر ورودی ProcessPersons از نوع <ICollection<Person است، اما زمان فراخوانی متد، شئ ای از نوع TeacherCollection به آن پاس می دهیم و کد ما هم بدون مشکل اجرا می شود، دلیل این موضوع استفاده از کلمه کلیدی out در زمان تعریف پارامتر جنریک T در ICollection است. اگر ما کلمه کلیدی out را برداریم، interface ما دیگر Covariance نبوده و زمان کامپایل کد با پیغام خطا مواجه می شویم.

کلمه کلیدی in

این کلمه کلیدی، بر عکس کلمه کلیدی out، به ما این اجازه را می دهد تا قابلیت Contra-Variance را بر روی پارامترهای جنریک interface ها استفاده کنیم. برای مثال کد زیر را در نظر بگیرید:

public interface IDisplay<in T>
{
    void Display(T input);
}

public class PersonDisplay : IDisplay<Person>
{
    public void Display(Person input)
    {
        Console.WriteLine("{0} {1}", input.FirstName, input.LastName);
    }
}

پارامتر جنریک T برای IDisplay از نوع in تعریف شده و به همین خاطر، پارامتر T را تنها می توان به عنوان پارامتر ورودی در تعریف interface استفاده کرد، سپس کلاسی تعریف کردیم با نام PersonDisplay که IDisplay را پیاده سازی کرده است. در ادامه متدی تعریف می کنیم که دو پارامتر ورودی به صورت زیر قبول می کند:

public static void DisplayTeachers(IDisplay<Teacher> items, ICollection<Person> persons)
{
            
}

پارامتر اول کلاسی را قبول می کند که IDisplay را پیاده سازی کرده و پارامتر دوم از نوع ICollection است. به صورت زیر از این متد استفاده می کنیم:

ICollection<Teacher> teachers = new TeacherCollection();
IDisplay<Person> display = new PersonDisplay();

DisplayTeachers(display, teachers);

همانطور که مشاهده می کنید، شئ PersonDisplay داخل متغیری از نوع <IDisplay<Person ریخته شده است، اما پارامتر ورودی متد <IDisplay<Teacher است. در صورتی که کلمه کلیدی in را برای تعریف پارامتر جنریک IDisplay استفاده نمی کردیم با پیغام خطا زمان کامپایل کد مواجه می شدیم، اما به خاطر اینکه پارامتر T در IDisplay از نوع Contra-Variance تعریف شده مشکلی برای اجرای کد نخواهیم داشت.

در دات نت interface های زیادی هستند که به صورت Covariance یا Contra-variance تعریف شده اند، برای مثال، IEnumerable یکی از این interface هاست که در مثال زیر این موضوع را مشاهده می کنید:

IEnumerable<string> strings = new[] {"Hossein", "Ahmadi", "ITPro.ir"};
IEnumerable<object> objects = strings;

به این دلیل که پارامتر حنریک T برای IEnumerable از نوع out تعریف شده، کد بالا بدون مشکل اجرا خواهد شد. یا برای مثالی دیگر، <Action<T که یکی از delegate های موجود در دات نت است به صورت Contra-Variance تعریف شده است، یعنی پارامتر T برای این delegate با کلمه کلیدی in مشخص شده و باعث می شود که کد زیر بدون مشکل ایجاد شود:

Action<object> process = new Action<object>(o => Console.WriteLine(o));
Action<string> prcessString = process;

در انتها تعریف کلی که از Convariance و Contra-Variance می توان ارائه کرد به صورت زیر است:

  1. Covariance: پارامتر جنریکی که با کلمه کلیدی out مشخص می شود و مشخص می کند که یک interface یا delegate از نوع کلاس فرزند، می تواند به همان interface یا delegate اما از نوع پدر به صورت implicit تبدیل شود.
  2. Contra-Variance: پارامتر جنریکی که با کلمه کلیدی in مشخص می شود و مشخص می کند که یک interface یا delegate از نوع کلاس پدر، می تواند به همان interface یا delegate اما از نوع فرزند به صورت implicit تبدیل شود.

حسین احمدی
حسین احمدی

بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

حسین احمدی ، بنیانگذار TOSINSO ، توسعه دهنده وب و برنامه نویس ، بیش از 12 سال سابقه فعالیت حرفه ای در سطح کلان ، مشاور ، مدیر پروژه و مدرس نهادهای مالی و اعتباری ، تخصص در پلتفرم دات نت و زبان سی شارپ ، طراحی و توسعه وب ، امنیت نرم افزار ، تحلیل سیستم های اطلاعاتی و داده کاوی ...

نظرات