آموزش کار با Genericها در C#: کدنویسی انعطاف‌پذیر و قابل استفاده مجدد

فهرست مطالب

آموزش کار با Genericها در C#: کدنویسی انعطاف‌پذیر و قابل استفاده مجدد

در دنیای توسعه نرم‌افزار، هدف نهایی همیشه نوشتن کدی است که نه تنها کارا باشد، بلکه قابل نگهداری، مقیاس‌پذیر و مهم‌تر از همه، قابل استفاده مجدد (reusable) باشد. زبان C# با قابلیت‌های فراوان خود، ابزارهای قدرتمندی را برای رسیدن به این اهداف در اختیار توسعه‌دهندگان قرار می‌دهد. یکی از مهم‌ترین و بنیادین‌ترین این ابزارها، مفهوم Genericها (Generics) است.

Genericها به شما این امکان را می‌دهند که کلاس‌ها، متدها، اینترفیس‌ها و دلیگیت‌ها را بدون مشخص کردن نوع دقیق داده در زمان تعریف، ایجاد کنید. به جای آن، شما از پارامترهای نوع (type parameters) استفاده می‌کنید که بعدها در زمان استفاده از آن، با نوع داده مشخصی جایگزین می‌شوند. این رویکرد نه تنها منجر به کدنویسی منعطف‌تر و کمتر تکراری می‌شود، بلکه امنیت نوع (type safety) را نیز به شدت افزایش می‌دهد و از خطاهای زمان اجرا جلوگیری می‌کند.

در این مقاله جامع، ما به عمق مفهوم Genericها در C# شیرجه خواهیم زد. از معرفی ابتدایی و چرایی نیاز به آنها گرفته تا جزئیات پیشرفته‌تر مانند محدودیت‌ها (constraints)، واریانس (variance) و کاربردهای عملی آنها در سناریوهای واقعی. هدف ما این است که شما را به طور کامل با این ویژگی حیاتی C# آشنا کنیم تا بتوانید کدی بنویسید که هم قدرتمند باشد و هم از نظر نگهداری، بهینه. اگر به دنبال ارتقای مهارت‌های C# خود و نوشتن کدهای تمیزتر، کاراتر و قابل استفاده مجدد هستید، این راهنما برای شماست.

۱. چرا به Genericها نیاز داریم؟ بررسی چالش‌های پیش از Genericها

پیش از معرفی و پرداختن به جزئیات Genericها، لازم است که چالش‌هایی را بررسی کنیم که قبل از معرفی این قابلیت در C# (نسخه 2.0) وجود داشتند. درک این چالش‌ها، اهمیت و ارزش Genericها را بیش از پیش آشکار می‌کند.

۱.۱. استفاده از نوع `object` و مشکلات آن

قبل از ظهور Genericها، رایج‌ترین راه برای نوشتن کدی که بتواند با انواع داده‌ای مختلف کار کند، استفاده از نوع پایه `object` بود. از آنجایی که `object` ریشه سلسله مراتب تمام انواع در .NET است، می‌توانست هر نوع داده‌ای را نگهداری کند. به عنوان مثال، کلاس `ArrayList` در فضای نام `System.Collections` به این روش کار می‌کرد:


using System.Collections;
using System;

public class OldApproachExample
{
    public static void Main()
    {
        ArrayList list = new ArrayList();
        list.Add(10); // Boxing: int به object تبدیل می‌شود
        list.Add("Hello"); // string به object تبدیل می‌شود
        list.Add(3.14); // double به object تبدیل می‌شود

        int firstElement = (int)list[0]; // Unboxing: object به int تبدیل می‌شود
        string secondElement = (string)list[1]; // Unboxing: object به string تبدیل می‌شود

        // مشکل اول: عدم امنیت نوع (Type Safety) در زمان کامپایل
        // list.Add(new DateTime(2023, 1, 1));
        // اگر اشتباهاً یک نوع ناسازگار اضافه شود، در زمان کامپایل مشکلی پیش نمی‌آید

        // مشکل دوم: خطاهای زمان اجرا (Runtime Errors)
        try
        {
            // فرض کنید توسعه‌دهنده‌ای اشتباهاً یک عدد را به جای رشته Cast می‌کند
            string wrongElement = (string)list[2]; // Runtime error: InvalidCastException
        }
        catch (InvalidCastException ex)
        {
            Console.WriteLine($"خطا در زمان اجرا: {ex.Message}");
        }

        // مشکل سوم: سربار عملکرد (Performance Overhead)
        // عملیات Boxing (تبدیل نوع مقداری به نوع مرجع) و Unboxing (تبدیل نوع مرجع به نوع مقداری)
        // سربار عملکردی قابل توجهی را به همراه دارند، به خصوص در حلقه‌های بزرگ یا داده‌های حجیم.
        // این عملیات شامل تخصیص حافظه روی Heap و کپی داده‌ها می‌شود.
        Console.WriteLine($"عنصر اول: {firstElement}");
        Console.WriteLine($"عنصر دوم: {secondElement}");
    }
}

همانطور که در مثال بالا مشاهده می‌کنید، استفاده از `object` سه مشکل عمده را به همراه داشت:

  1. عدم امنیت نوع (Lack of Type Safety): کامپایلر هیچ اطلاعی از نوع واقعی داده‌های ذخیره شده در `ArrayList` ندارد. این بدین معنی است که شما می‌توانید بدون هیچ هشداری در زمان کامپایل، انواع داده‌ای مختلف و حتی ناسازگار را به لیست اضافه کنید. خطاهای ناشی از ناسازگاری نوع تنها در زمان اجرا (runtime) آشکار می‌شوند که کشف و رفع آن‌ها دشوارتر و پرهزینه‌تر است.
  2. سربار عملکرد (Performance Overhead): هنگام ذخیره سازی انواع مقداری (value types) مانند `int` یا `double` در `ArrayList`، یک عملیات به نام Boxing رخ می‌دهد. در این عملیات، نوع مقداری به یک `object` (نوع مرجع) تبدیل شده و در حافظه Heap ذخیره می‌شود. هنگام بازیابی این مقادیر، عملیات Unboxing انجام می‌شود که `object` را دوباره به نوع مقداری اصلی تبدیل می‌کند. هر دو عملیات Boxing و Unboxing سربار عملکردی قابل توجهی را به همراه دارند، به خصوص در برنامه‌هایی که با حجم زیادی از داده‌ها سروکار دارند. این سربار شامل تخصیص حافظه (memory allocation) و کپی داده‌ها (data copying) می‌شود.
  3. کد تکراری (Code Duplication): اگر نیاز داشتید که توابعی بنویسید که روی انواع مختلفی از داده‌ها کار کنند اما امنیت نوع را حفظ کنند، چاره‌ای جز نوشتن چندین نسخه از همان تابع، هر کدام برای یک نوع داده خاص، نداشتید. این منجر به کد تکراری و دشواری در نگهداری می‌شد.

Genericها به عنوان راه حلی قدرتمند برای رفع این مشکلات معرفی شدند. آنها به توسعه‌دهندگان اجازه می‌دهند تا کدی بنویسند که هم امنیت نوع را تضمین کند، هم سربار عملکردی را کاهش دهد و هم قابلیت استفاده مجدد را به بالاترین سطح برساند.

۲. معرفی Genericها در C#: انعطاف‌پذیری با امنیت

Genericها در C# به توسعه‌دهندگان این امکان را می‌دهند که کد قابل استفاده مجدد بنویسند که به نوع خاصی از داده محدود نمی‌شود. به جای تعیین یک نوع خاص مانند `int` یا `string` در زمان طراحی، می‌توانید از یک “پارامتر نوع” (Type Parameter) استفاده کنید که معمولاً با حرف `T` (مخفف Type) نمایش داده می‌شود. این پارامتر نوع سپس در زمان استفاده از کلاس، متد، اینترفیس یا دلیگیت، با یک نوع واقعی جایگزین می‌شود.

۲.۱. مفهوم و مزایای کلیدی Genericها

اساساً، یک Generic مانند یک قالب (template) عمل می‌کند. شما یک قالب برای یک کلاس یا متد ایجاد می‌کنید و می‌گویید که این قالب می‌تواند با هر نوعی کار کند، به شرط اینکه آن نوع، الزامات خاصی (که به عنوان محدودیت‌ها شناخته می‌شوند) را برآورده کند. مزایای اصلی استفاده از Genericها عبارتند از:

  1. امنیت نوع در زمان کامپایل (Compile-Time Type Safety): این یکی از مهم‌ترین مزایای Genericها است. برخلاف `object` که خطاها را به زمان اجرا موکول می‌کند، Genericها خطاها را در زمان کامپایل شناسایی می‌کنند. اگر سعی کنید نوع ناسازگاری را به یک مجموعه Generic اضافه کنید یا از آن بازیابی کنید، کامپایلر به شما هشدار می‌دهد. این امر به کاهش باگ‌ها و افزایش پایداری نرم‌افزار کمک شایانی می‌کند.
  2. بهبود عملکرد (Improved Performance): با استفاده از Genericها، نیازی به عملیات Boxing و Unboxing برای انواع مقداری نیست. کامپایلر دقیقاً می‌داند که چه نوعی در حال استفاده است و کد IL (Intermediate Language) بهینه‌ای را تولید می‌کند که مستقیماً با نوع واقعی کار می‌کند. این امر به ویژه برای مجموعه‌های داده بزرگ (مانند `List`) منجر به بهبود عملکرد قابل توجهی می‌شود.
  3. کاهش کد تکراری (Reduced Code Duplication): با Genericها، می‌توانید یک الگوریتم یا ساختار داده را تنها یک بار بنویسید و آن را برای انواع مختلف داده مجدداً استفاده کنید. به عنوان مثال، به جای داشتن `IntList`, `StringList`, `DoubleList` و غیره، فقط یک `List` دارید که می‌تواند با هر نوعی کار کند.
  4. قابلیت استفاده مجدد (Reusability): این ویژگی به طور مستقیم از کاهش کد تکراری ناشی می‌شود. Genericها به شما امکان می‌دهند کتابخانه‌های عمومی و قدرتمندی ایجاد کنید که می‌توانند در پروژه‌های مختلف و با انواع داده‌ای متفاوت مورد استفاده قرار گیرند.
  5. کد خواناتر و تمیزتر (Cleaner, More Readable Code): با حذف نیاز به Castingهای مکرر و کنترل خطاهای زمان اجرا، کدهای نوشته شده با Genericها معمولاً خواناتر و نگهداری از آن‌ها آسان‌تر است.

در ادامه، به بررسی چگونگی پیاده‌سازی Genericها در کلاس‌ها، متدها، اینترفیس‌ها و دلیگیت‌ها خواهیم پرداخت.

۳. کلاس‌های Generic: ساختار داده‌های منعطف

کلاس‌های Generic به شما این امکان را می‌دهند که کلاس‌هایی را تعریف کنید که می‌توانند روی انواع داده‌ای مختلف عمل کنند، بدون اینکه نیاز به کپی کردن کد برای هر نوع باشد. این ویژگی به ویژه برای ساختارهای داده‌ای مانند لیست‌ها، پشته‌ها، صف‌ها و دیکشنری‌ها بسیار مفید است.

۳.۱. تعریف یک کلاس Generic

برای تعریف یک کلاس Generic، نام کلاس را با یک یا چند پارامتر نوع در داخل براکت‌های زاویه‌ای (`<>`) دنبال می‌کنید. معمولاً از `T` به عنوان نام پارامتر نوع استفاده می‌شود، اما می‌توانید از هر نام معتبری استفاده کنید (مثلاً `TKey`, `TValue` برای دیکشنری‌ها).

بیایید یک کلاس Generic ساده برای نگهداری یک جفت (Pair) از مقادیر از هر نوعی ایجاد کنیم:


using System;

// تعریف یک کلاس Generic با دو پارامتر نوع TFirst و TSecond
public class Pair
{
    public TFirst First { get; set; }
    public TSecond Second { get; set; }

    public Pair(TFirst first, TSecond second)
    {
        First = first;
        Second = second;
    }

    public void DisplayInfo()
    {
        Console.WriteLine($"First: {First} (Type: {typeof(TFirst).Name})");
        Console.WriteLine($"Second: {Second} (Type: {typeof(TSecond).Name})");
    }
}

public class GenericClassExample
{
    public static void Main()
    {
        // استفاده از کلاس Pair با انواع مختلف
        Pair scorePair = new Pair(100, "Excellent");
        scorePair.DisplayInfo();
        // خروجی:
        // First: 100 (Type: Int32)
        // Second: Excellent (Type: String)

        Console.WriteLine();

        Pair eventPair = new Pair("Meeting", new DateTime(2023, 12, 25));
        eventPair.DisplayInfo();
        // خروجی:
        // First: Meeting (Type: String)
        // Second: 12/25/2023 12:00:00 AM (Type: DateTime)

        Console.WriteLine();

        Pair booleanDoublePair = new Pair(true, 9.99);
        booleanDoublePair.DisplayInfo();
        // خروجی:
        // First: True (Type: Boolean)
        // Second: 9.99 (Type: Double)
    }
}

در این مثال، `Pair` یک کلاس Generic است که می‌تواند دو مقدار از هر نوعی را در خود نگهداری کند. در زمان ایجاد نمونه، نوع‌های واقعی `int` و `string`، `string` و `DateTime` و `bool` و `double` جایگزین `TFirst` و `TSecond` می‌شوند. کامپایلر این جایگزینی را انجام می‌دهد و تضمین می‌کند که کد شما از نظر نوع امن است.

۳.۲. ساختار داده‌های Generic (مانند `List`)

یکی از متداول‌ترین کاربردهای کلاس‌های Generic، پیاده‌سازی ساختارهای داده است. چارچوب .NET مجموعه‌های Generic قدرتمندی را در فضای نام `System.Collections.Generic` ارائه می‌دهد. `List`، `Dictionary`، `Stack` و `Queue` از جمله پرکاربردترین آن‌ها هستند.

مثال `List` در مقابل `ArrayList`:


using System;
using System.Collections.Generic; // فضای نام برای Generic Collections

public class GenericListExample
{
    public static void Main()
    {
        List numbers = new List();
        numbers.Add(10);
        numbers.Add(20);
        numbers.Add(30);

        // numbers.Add("Hello"); // خطای کامپایل! کامپایلر نوع را بررسی می‌کند.

        foreach (int num in numbers)
        {
            Console.WriteLine(num);
        }

        Console.WriteLine();

        List names = new List();
        names.Add("Alice");
        names.Add("Bob");
        names.Add("Charlie");

        foreach (string name in names)
        {
            Console.WriteLine(name);
        }
    }
}

همانطور که می‌بینید، با `List`، نمی‌توانید یک `string` اضافه کنید، و این خطا در زمان کامپایل (compile time) تشخیص داده می‌شود، نه در زمان اجرا (runtime). این تضمین امنیت نوع و حذف عملیات Boxing/Unboxing، دلیل اصلی برتری `List` نسبت به `ArrayList` است.

۴. متدهای Generic: توابع قابل استفاده مجدد

متدهای Generic به شما این امکان را می‌دهند که متدهایی را تعریف کنید که می‌توانند روی انواع داده‌ای مختلفی عمل کنند، بدون نیاز به نوشتن نسخه‌های متعدد از همان متد. این به ویژه برای توابعی که عملیات مشترکی را روی داده‌ها انجام می‌دهند، مفید است، مانند swap کردن مقادیر، یافتن حداکثر/حداقل، یا توابع کمکی (utility functions).

۴.۱. تعریف یک متد Generic

برای تعریف یک متد Generic، پارامتر نوع را درست بعد از نام متد و قبل از پرانتزهای پارامترهای ورودی، در داخل براکت‌های زاویه‌ای قرار می‌دهید.

مثال ساده Swap:


using System;

public class GenericMethodExample
{
    // تعریف یک متد Generic برای Swap کردن دو مقدار از هر نوعی
    public static void Swap(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }

    public static void Main()
    {
        int x = 10, y = 20;
        Console.WriteLine($"Before swap: x = {x}, y = {y}");
        Swap(ref x, ref y); // Type inference: کامپایلر T را int تشخیص می‌دهد
        Console.WriteLine($"After swap: x = {x}, y = {y}"); // x = 20, y = 10

        Console.WriteLine();

        string s1 = "Hello", s2 = "World";
        Console.WriteLine($"Before swap: s1 = {s1}, s2 = {s2}");
        Swap(ref s1, ref s2); // Type inference: کامپایلر T را string تشخیص می‌دهد
        Console.WriteLine($"After swap: s1 = {s1}, s2 = {s2}"); // s1 = World, s2 = Hello
    }
}

در مثال بالا، متد `Swap` می‌تواند برای هر نوعی (int, string, double, custom objects, etc.) کار کند. کامپایلر C# به لطف قابلیت Type Inference (استنتاج نوع)، می‌تواند نوع `T` را بر اساس نوع آرگومان‌هایی که به متد ارسال می‌کنید، به طور خودکار تشخیص دهد و نیازی به مشخص کردن صریح نوع در زمان فراخوانی نیست (مثلاً `Swap(ref x, ref y)`).

۴.۲. متدهای Generic در کلاس‌های غیر Generic و Generic

یک متد Generic می‌تواند هم در یک کلاس غیر Generic و هم در یک کلاس Generic تعریف شود. اگر در یک کلاس Generic تعریف شود، می‌تواند از پارامترهای نوع کلاس و همچنین پارامترهای نوع خودش استفاده کند.

مثال متد Generic در یک کلاس Generic:


using System;
using System.Collections.Generic;

public class MyGenericProcessor
{
    private List _items;

    public MyGenericProcessor()
    {
        _items = new List();
    }

    public void AddItem(T item)
    {
        _items.Add(item);
    }

    // یک متد Generic در داخل یک کلاس Generic
    // این متد از یک پارامتر نوع جدید U استفاده می‌کند که مستقل از T کلاس است
    public U GetFirstItemAs() where U : T // محدودیت: U باید از T مشتق شده باشد یا همان T باشد
    {
        if (_items.Count > 0)
        {
            if (_items[0] is U convertedItem)
            {
                return convertedItem;
            }
            else
            {
                throw new InvalidCastException($"Cannot cast first item of type {typeof(T).Name} to {typeof(U).Name}.");
            }
        }
        return default(U); // مقدار پیش‌فرض برای U
    }

    // یک متد Generic که از پارامتر نوع T کلاس استفاده می‌کند
    public T GetFirstItem()
    {
        return _items.Count > 0 ? _items[0] : default(T);
    }
}

public class GenericMethodInGenericClassExample
{
    public static void Main()
    {
        MyGenericProcessor processor = new MyGenericProcessor();
        processor.AddItem(100);
        processor.AddItem("Hello");
        processor.AddItem(DateTime.Now);

        Console.WriteLine($"First item as int: {processor.GetFirstItemAs()}"); // Works if first item is int
        Console.WriteLine($"First item: {processor.GetFirstItem()}");
        Console.WriteLine($"First item as string: {processor.GetFirstItemAs()}"); // Throws InvalidCastException

        Console.WriteLine();

        MyGenericProcessor personProcessor = new MyGenericProcessor();
        personProcessor.AddItem(new Student("Alice"));
        personProcessor.AddItem(new Employee("Bob"));

        // GetFirstItemAs works because Student is a Person
        Student firstStudent = personProcessor.GetFirstItemAs();
        Console.WriteLine($"First person (as student): {firstStudent.Name}");
    }
}

public class Person { public string Name { get; set; } }
public class Student : Person { public Student(string name) { Name = name; } }
public class Employee : Person { public Employee(string name) { Name = name; } }
```

در متد `GetFirstItemAs()`، از یک پارامتر نوع جدید `U` استفاده شده است، که نشان می‌دهد متدهای Generic می‌توانند پارامترهای نوع خودشان را داشته باشند، حتی اگر در یک کلاس Generic باشند. محدودیت `where U : T` در این مثال، تضمین می‌کند که `U` باید از `T` مشتق شده باشد یا همان `T` باشد، که به امنیت نوع کمک می‌کند.

۵. اینترفیس‌های Generic: تعریف قراردادهای منعطف

اینترفیس‌های Generic، مفهوم Genericها را به قراردادهای نوع (type contracts) گسترش می‌دهند. این به شما امکان می‌دهد اینترفیس‌هایی را تعریف کنید که متدهای آن‌ها با پارامترهای نوع کار می‌کنند، و تضمین می‌کند که هر کلاسی که این اینترفیس را پیاده‌سازی می‌کند، این کار را به شیوه‌ای امن از نظر نوع انجام دهد. بسیاری از اینترفیس‌های کلیدی در .NET، مانند `IEnumerable`, `IComparable`, `ICollection`, و `IDictionary`، از Genericها استفاده می‌کنند.

۵.۱. تعریف و پیاده‌سازی یک اینترفیس Generic

برای تعریف یک اینترفیس Generic، پارامتر نوع را بعد از نام اینترفیس قرار می‌دهید:


using System;
using System.Collections.Generic;

// تعریف یک اینترفیس Generic برای یک مخزن (Repository)
// که عملیات CRUD (Create, Read, Update, Delete) را برای هر نوع entity ارائه می‌دهد.
public interface IRepository where T : class, IEntity // محدودیت: T باید یک کلاس و پیاده‌ساز IEntity باشد
{
    void Add(T entity);
    T GetById(int id);
    IEnumerable GetAll();
    void Update(T entity);
    void Delete(int id);
}

// یک اینترفیس کمکی برای اعمال محدودیت در IRepository
public interface IEntity
{
    int Id { get; set; }
}

// پیاده‌سازی کلاس Person که IEntity را پیاده‌سازی می‌کند
public class Person : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString()
    {
        return $"Id: {Id}, Name: {Name}, Age: {Age}";
    }
}

// پیاده‌سازی کلاس Product که IEntity را پیاده‌سازی می‌کند
public class Product : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    public override string ToString()
    {
        return $"Id: {Id}, Name: {Name}, Price: {Price:C}";
    }
}

// پیاده‌سازی عمومی (Generic) برای IRepository
public class InMemoryRepository : IRepository where T : class, IEntity, new()
{
    private List _items = new List();
    private int _nextId = 1;

    public void Add(T entity)
    {
        entity.Id = _nextId++;
        _items.Add(entity);
        Console.WriteLine($"Added: {entity}");
    }

    public T GetById(int id)
    {
        return _items.Find(item => item.Id == id);
    }

    public IEnumerable GetAll()
    {
        return _items;
    }

    public void Update(T entity)
    {
        var existing = _items.Find(item => item.Id == entity.Id);
        if (existing != null)
        {
            // در یک سناریوی واقعی، اینجا فیلدها را کپی می‌کنید
            // برای سادگی، فقط جایگزین می‌کنیم
            var index = _items.IndexOf(existing);
            _items[index] = entity;
            Console.WriteLine($"Updated: {entity}");
        }
    }

    public void Delete(int id)
    {
        var itemToRemove = _items.Find(item => item.Id == id);
        if (itemToRemove != null)
        {
            _items.Remove(itemToRemove);
            Console.WriteLine($"Deleted item with Id: {id}");
        }
    }
}

public class GenericInterfaceExample
{
    public static void Main()
    {
        IRepository personRepo = new InMemoryRepository();
        personRepo.Add(new Person { Name = "Alice", Age = 30 });
        personRepo.Add(new Person { Name = "Bob", Age = 25 });

        var alice = personRepo.GetById(1);
        Console.WriteLine($"Retrieved: {alice}");

        alice.Age = 31;
        personRepo.Update(alice);

        Console.WriteLine("All persons:");
        foreach (var p in personRepo.GetAll())
        {
            Console.WriteLine(p);
        }

        Console.WriteLine("\n-------------------\n");

        IRepository productRepo = new InMemoryRepository();
        productRepo.Add(new Product { Name = "Laptop", Price = 1200m });
        productRepo.Add(new Product { Name = "Mouse", Price = 25m });

        var laptop = productRepo.GetById(1);
        Console.WriteLine($"Retrieved: {laptop}");

        laptop.Price = 1150m;
        productRepo.Update(laptop);

        productRepo.Delete(2);

        Console.WriteLine("All products:");
        foreach (var prod in productRepo.GetAll())
        {
            Console.WriteLine(prod);
        }
    }
}
```

در این مثال، `IRepository` یک اینترفیس Generic است که یک قرارداد برای عملیات مخزن داده‌ای (repository operations) تعریف می‌کند. کلاس `InMemoryRepository` این اینترفیس را برای هر نوع `T` که محدودیت‌های `class`, `IEntity`, و `new()` را برآورده کند، پیاده‌سازی می‌کند. این الگو بسیار قدرتمند است و در توسعه سیستم‌های لایه داده و اعمال الگوهای طراحی (مانند Repository Pattern) کاربرد فراوانی دارد.

اینترفیس‌های Generic نقش مهمی در LINQ (Language Integrated Query) و سایر بخش‌های چارچوب .NET ایفا می‌کنند. به عنوان مثال، `IEnumerable` به LINQ اجازه می‌دهد تا با هر نوع مجموعه‌ای به شیوه‌ای یکپارچه و از نظر نوع امن کار کند.

۶. دلیگیت‌های Generic: رویدادها و توابع انعطاف‌پذیر

دلیگیت‌ها (Delegates) در C# اشاره‌گرهای نوع امن (type-safe) به متدها هستند. دلیگیت‌های Generic این مفهوم را گسترش می‌دهند و به شما امکان می‌دهند دلیگیت‌هایی را تعریف کنید که می‌توانند متدهایی با انواع پارامترهای ورودی و/یا خروجی Generic را نمایندگی کنند. این قابلیت به ویژه در سناریوهای مبتنی بر رویداد (event-driven programming)، توابع Callback و LINQ بسیار مفید است.

۶.۱. تعریف و استفاده از دلیگیت‌های Generic

برای تعریف یک دلیگیت Generic، پارامترهای نوع را بعد از نام دلیگیت قرار می‌دهید:


using System;

// تعریف یک دلیگیت Generic که یک عملیات را روی T انجام می‌دهد و چیزی بر نمی‌گرداند
public delegate void GenericAction(T item);

// تعریف یک دلیگیت Generic که دو T را مقایسه می‌کند و یک bool بر می‌گرداند
public delegate bool GenericPredicate(T item1, T item2);

// تعریف یک دلیگیت Generic که یک T را می‌گیرد و یک TResult بر می‌گرداند
public delegate TResult GenericFunc(T item);

public class GenericDelegateExample
{
    // متدی که با GenericAction سازگار است
    public static void PrintNumber(int num)
    {
        Console.WriteLine($"Number: {num}");
    }

    // متدی که با GenericAction سازگار است
    public static void GreetUser(string name)
    {
        Console.WriteLine($"Hello, {name}!");
    }

    // متدی که با GenericPredicate سازگار است
    public static bool IsEven(int num)
    {
        return num % 2 == 0;
    }

    // متدی که با GenericFunc سازگار است
    public static int GetStringLength(string str)
    {
        return str.Length;
    }

    public static void Main()
    {
        // استفاده از GenericAction
        GenericAction printInt = PrintNumber;
        printInt(123); // خروجی: Number: 123

        GenericAction greetString = GreetUser;
        greetString("Alice"); // خروجی: Hello, Alice!

        Console.WriteLine();

        // استفاده از GenericPredicate
        GenericPredicate checkEven = (num1, num2) => (num1 + num2) % 2 == 0; // استفاده از Lambda
        Console.WriteLine($"Are 5 and 7 even together? {checkEven(5, 7)}"); // خروجی: Are 5 and 7 even together? True

        GenericPredicate isSingleEven = IsEven; // استفاده از یک متد نامگذاری شده
        Console.WriteLine($"Is 8 even? {isSingleEven(8, 0)}"); // خروجی: Is 8 even? True (پارامتر دوم نادیده گرفته می‌شود)

        Console.WriteLine();

        // استفاده از GenericFunc
        GenericFunc getLength = GetStringLength;
        Console.WriteLine($"Length of 'Programming': {getLength("Programming")}"); // خروجی: Length of 'Programming': 11

        GenericFunc squareRoot = Math.Sqrt;
        Console.WriteLine($"Square root of 144: {squareRoot(144.0)}"); // خروجی: Square root of 144: 12
    }
}
```

۶.۲. دلیگیت‌های داخلی (Built-in Generic Delegates): `Func` و `Action`

.NET Framework دو نوع دلیگیت Generic بسیار پرکاربرد را ارائه می‌دهد که نیاز به تعریف دلیگیت‌های سفارشی را در اکثر موارد از بین می‌برد:

  • `Action`: یک دلیگیت که به یک متد (با 0 تا 16 پارامتر ورودی) اشاره می‌کند و مقداری را بر نمی‌گرداند (`void`).
    
                Action printMessage = (message) => Console.WriteLine(message);
                printMessage("This is an action.");
    
                Action addAndPrint = (a, b) => Console.WriteLine($"Sum: {a + b}");
                addAndPrint(5, 10);
            
  • `Func`: یک دلیگیت که به یک متد (با 0 تا 16 پارامتر ورودی) اشاره می‌کند و مقداری از نوع `TResult` را بر می‌گرداند. `TResult` همیشه آخرین پارامتر نوع است.
    
                Func add = (a, b) => a + b;
                Console.WriteLine($"Result of add: {add(3, 4)}");
    
                Func startsWithA = (s) => s.StartsWith("A");
                Console.WriteLine($"'Apple' starts with A? {startsWithA("Apple")}");
            

این دلیگیت‌ها به طور گسترده‌ای در LINQ، برنامه‌نویسی موازی (Parallel Programming) و الگوهای مبتنی بر رویداد (event-based patterns) استفاده می‌شوند و انعطاف‌پذیری و قدرت زیادی را به کد شما می‌بخشند.

۷. محدودیت‌ها (Constraints) بر روی پارامترهای نوع Generic

گاهی اوقات، شما نیاز دارید که یک کلاس یا متد Generic روی انواع خاصی از داده‌ها عمل کند، نه هر نوعی. به عنوان مثال، ممکن است نیاز داشته باشید که پارامتر نوع شما فقط یک نوع عددی باشد تا بتوانید عملیات ریاضی روی آن انجام دهید، یا اینکه متدی خاصی را داشته باشد. محدودیت‌ها (Constraints) در C# این امکان را به شما می‌دهند که الزامات خاصی را برای پارامترهای نوع Generic تعریف کنید.

شما محدودیت‌ها را با استفاده از کلمه کلیدی `where` بعد از تعریف پارامترهای نوع قرار می‌دهید. چندین نوع محدودیت وجود دارد:

۷.۱. `where T : struct` (محدودیت نوع مقداری)

این محدودیت تضمین می‌کند که `T` باید یک نوع مقداری باشد (مانند `int`, `double`, `DateTime`, `struct`). انواع Nullable (مانند `int?`) نیز جزو این دسته قرار می‌گیرند.


public class ValueTypeProcessor where T : struct
{
    public T GetDefaultValue()
    {
        return default(T); // 'default' برای value types مقدار پیش فرض 0/false/... را برمی‌گرداند
    }
}
// استفاده:
// var intProcessor = new ValueTypeProcessor(); // OK
// var doubleProcessor = new ValueTypeProcessor(); // OK
// var stringProcessor = new ValueTypeProcessor(); // خطای کامپایل: string یک class است

۷.۲. `where T : class` (محدودیت نوع مرجع)

این محدودیت تضمین می‌کند که `T` باید یک نوع مرجع باشد (مانند `string`, `object`, کلاس‌های تعریف شده توسط کاربر، آرایه‌ها، دلیگیت‌ها).


public class ReferenceTypeContainer where T : class
{
    public T Data { get; set; }
    public bool IsNull()
    {
        return Data == null;
    }
}
// استفاده:
// var stringContainer = new ReferenceTypeContainer(); // OK
// var objectContainer = new ReferenceTypeContainer(); // OK
// var intContainer = new ReferenceTypeContainer(); // خطای کامپایل: int یک struct است

۷.۳. `where T : new()` (محدودیت سازنده بدون پارامتر)

این محدودیت تضمین می‌کند که `T` باید یک سازنده عمومی (public) و بدون پارامتر (parameterless constructor) داشته باشد. این برای زمانی مفید است که نیاز دارید درون کد Generic خود، نمونه‌ای از `T` را ایجاد کنید.


public class Factory where T : new()
{
    public T CreateInstance()
    {
        return new T(); // اکنون می‌توانید یک نمونه جدید از T ایجاد کنید
    }
}

public class MyClassWithConstructor { public MyClassWithConstructor() { } }
public class MyClassNoParameterless { public MyClassNoParameterless(int x) { } }

// استفاده:
// var factory = new Factory();
// MyClassWithConstructor instance = factory.CreateInstance(); // OK

// var factory2 = new Factory(); // خطای کامپایل

۷.۴. `where T : BaseClass` (محدودیت کلاس پایه)

این محدودیت تضمین می‌کند که `T` باید از کلاس `BaseClass` مشتق شده باشد (یا همان `BaseClass` باشد). این امکان دسترسی به اعضای `BaseClass` را از طریق `T` فراهم می‌کند.


public class Animal { public string Name { get; set; } }
public class Dog : Animal { }
public class Cat : Animal { }

public class AnimalProcessor where T : Animal
{
    public void ProcessAnimal(T animal)
    {
        Console.WriteLine($"Processing animal: {animal.Name}"); // دسترسی به Name از BaseClass
    }
}
// استفاده:
// var dogProcessor = new AnimalProcessor();
// dogProcessor.ProcessAnimal(new Dog { Name = "Buddy" }); // OK

// var animalProcessor = new AnimalProcessor();
// animalProcessor.ProcessAnimal(new Cat { Name = "Whiskers" }); // OK

// var stringProcessor = new AnimalProcessor(); // خطای کامپایل: string از Animal مشتق نشده

۷.۵. `where T : IInterface` (محدودیت اینترفیس)

این محدودیت تضمین می‌کند که `T` باید اینترفیس `IInterface` را پیاده‌سازی کند. این امکان فراخوانی متدها و دسترسی به خواص تعریف شده در اینترفیس را فراهم می‌کند.


public interface ISaveable { void Save(); }
public class Document : ISaveable { public void Save() { Console.WriteLine("Document saved."); } }
public class Image : ISaveable { public void Save() { Console.WriteLine("Image saved."); } }

public class Saver where T : ISaveable
{
    public void SaveAll(List items)
    {
        foreach (var item in items)
        {
            item.Save(); // فراخوانی متد Save از ISaveable
        }
    }
}
// استفاده:
// var documentSaver = new Saver();
// documentSaver.SaveAll(new List { new Document(), new Document() }); // OK

// var imageSaver = new Saver();
// imageSaver.SaveAll(new List { new Image() }); // OK

// var intSaver = new Saver(); // خطای کامپایل: int اینترفیس ISaveable را پیاده‌سازی نمی‌کند

۷.۶. `where T : U` (محدودیت پارامتر نوع)

این محدودیت برای متدهای Generic است و تضمین می‌کند که یک پارامتر نوع (`T`) باید از پارامتر نوع دیگری (`U`) مشتق شده باشد (یا همان `U` باشد).


public class DerivedComparer
{
    // این متد دو پارامتر نوع دارد: T و U
    // T باید از U مشتق شده باشد
    public static bool AreSameOrDerived(T derivedObject, U baseObject)
        where T : U
    {
        // می‌توانیم عمل مقایسه را انجام دهیم چون می‌دانیم T با U سازگار است
        return derivedObject.Equals(baseObject);
    }
}

public class Base { }
public class Derived : Base { }

// استفاده:
// Derived d = new Derived();
// Base b = new Base();
// bool result1 = DerivedComparer.AreSameOrDerived(d, b); // OK: Derived از Base مشتق شده
// bool result2 = DerivedComparer.AreSameOrDerived(b, d); // خطای کامپایل: Base از Derived مشتق نشده

۷.۷. ترکیب محدودیت‌ها

شما می‌توانید چندین محدودیت را برای یک پارامتر نوع واحد ترکیب کنید. ترتیب مهم نیست، اما `class` یا `struct` باید اولین محدودیت باشند.


// T باید یک کلاس باشد، یک سازنده بدون پارامتر داشته باشد و ISaveable را پیاده‌سازی کند.
public class ComplexProcessor where T : class, ISaveable, new()
{
    public T CreateAndSave()
    {
        T item = new T(); // از new() استفاده می‌کند
        item.Save(); // از ISaveable استفاده می‌کند
        return item;
    }
}

محدودیت‌ها ابزاری قدرتمند برای کنترل رفتار Genericها و اعمال قوانین در زمان کامپایل هستند، که به نوبه خود به افزایش امنیت و کیفیت کد کمک می‌کنند.

۸. واریانس (Variance): انعطاف‌پذیری بیشتر در Genericها

واریانس (Variance) یک مفهوم پیشرفته‌تر در C# است که به شما امکان می‌دهد با انعطاف‌پذیری بیشتری در خصوص تخصیص (assignment) انواع Generic کار کنید. به طور خاص، این مفهوم به ارتباط بین پارامترهای نوع یک نوع Generic و نوع واقعی آن‌ها در زمان اجرا اشاره دارد. واریانس به دو دسته تقسیم می‌شود: Covariance (همگرایی) و Contravariance (واگرایی).

در C#، واریانس تنها برای اینترفیس‌ها و دلیگیت‌های Generic قابل اعمال است و برای کلاس‌های Generic مجاز نیست.

۸.۱. Covariance (همگرایی) - کلمه کلیدی `out`

Covariance به شما اجازه می‌دهد از یک نوع Generic با پارامتر نوع "مشتق‌شده‌تر" (more derived) استفاده کنید در جایی که یک نوع Generic با پارامتر نوع "اصلی‌تر" (less derived) مورد انتظار است.

از کلمه کلیدی `out` برای مشخص کردن یک پارامتر نوع Covariant در اینترفیس یا دلیگیت استفاده می‌شود. پارامتر نوع `out` فقط می‌تواند به عنوان نوع خروجی متدها (return type) استفاده شود و نمی‌تواند به عنوان نوع ورودی (input parameter) استفاده شود.


using System;
using System.Collections.Generic;

// تعریف یک اینترفیس Covariant
// T فقط به عنوان نوع خروجی استفاده می‌شود
public interface ICovariantProducer
{
    T ProduceItem();
}

public class Fruit { }
public class Apple : Fruit { }

public class AppleProducer : ICovariantProducer
{
    public Apple ProduceItem()
    {
        Console.WriteLine("Producing an Apple.");
        return new Apple();
    }
}

public class CovarianceExample
{
    public static void Main()
    {
        // می‌توانیم یک ICovariantProducer را به یک ICovariantProducer تخصیص دهیم
        // زیرا Apple از Fruit مشتق شده است و ICovariantProducer covariant است.
        ICovariantProducer appleProducer = new AppleProducer();
        ICovariantProducer fruitProducer = appleProducer; // این خط به لطف Covariance مجاز است

        Fruit producedFruit = fruitProducer.ProduceItem();
        Console.WriteLine($"Produced: {producedFruit.GetType().Name}");

        // مثال دیگری از Covariance در .NET Framework
        // IEnumerable یک اینترفیس covariant است.
        List strings = new List { "Hello", "World" };
        IEnumerable stringEnumerable = strings;
        IEnumerable objectEnumerable = stringEnumerable; // مجاز به لطف covariance

        foreach (object obj in objectEnumerable)
        {
            Console.WriteLine(obj);
        }
    }
}

در مثال بالا، `ICovariantProducer` یک اینترفیس Covariant است. این بدان معناست که اگر `Apple` از `Fruit` مشتق شده باشد، پس `ICovariantProducer` می‌تواند به `ICovariantProducer` تبدیل شود. این رفتار منطقی است زیرا هر `Apple` یک `Fruit` است، بنابراین یک تولیدکننده `Apple` می‌تواند به عنوان یک تولیدکننده `Fruit` عمل کند.

۸.۲. Contravariance (واگرایی) - کلمه کلیدی `in`

Contravariance برعکس Covariance است. این به شما اجازه می‌دهد از یک نوع Generic با پارامتر نوع "اصلی‌تر" استفاده کنید در جایی که یک نوع Generic با پارامتر نوع "مشتق‌شده‌تر" مورد انتظار است.

از کلمه کلیدی `in` برای مشخص کردن یک پارامتر نوع Contravariant در اینترفیس یا دلیگیت استفاده می‌شود. پارامتر نوع `in` فقط می‌تواند به عنوان نوع ورودی متدها استفاده شود و نمی‌تواند به عنوان نوع خروجی استفاده شود.


using System;
using System.Collections.Generic;

// تعریف یک اینترفیس Contravariant
// T فقط به عنوان نوع ورودی استفاده می‌شود
public interface IContravariantConsumer
{
    void ConsumeItem(T item);
}

public class Fruit { }
public class Apple : Fruit { }

public class FruitConsumer : IContravariantConsumer
{
    public void ConsumeItem(Fruit item)
    {
        Console.WriteLine($"Consuming a Fruit: {item.GetType().Name}");
    }
}

public class ContravarianceExample
{
    public static void Main()
    {
        // می‌توانیم یک IContravariantConsumer را به یک IContravariantConsumer تخصیص دهیم
        // زیرا Apple از Fruit مشتق شده است و IContravariantConsumer contravariant است.
        IContravariantConsumer fruitConsumer = new FruitConsumer();
        IContravariantConsumer appleConsumer = fruitConsumer; // این خط به لطف Contravariance مجاز است

        appleConsumer.ConsumeItem(new Apple()); // FruitConsumer می‌تواند یک Apple را مصرف کند

        // مثال دیگری از Contravariance در .NET Framework
        // IComparer یک اینترفیس contravariant است.
        IComparer objectComparer = Comparer.Default;
        IComparer stringComparer = objectComparer; // مجاز به لطف contravariance

        // یک مقایسه‌کننده شیء می‌تواند رشته‌ها را مقایسه کند (چون هر رشته‌ای یک شیء است).
        Console.WriteLine($"'Hello' vs 'World': {stringComparer.Compare("Hello", "World")}");
    }
}
```

در مثال بالا، `IContravariantConsumer` یک اینترفیس Contravariant است. اگر `Apple` از `Fruit` مشتق شده باشد، پس `IContravariantConsumer` می‌تواند به `IContravariantConsumer` تبدیل شود. این رفتار نیز منطقی است زیرا یک مصرف‌کننده عمومی `Fruit` می‌تواند هر `Apple` را (که یک نوع خاص از `Fruit` است) مصرف کند.

۸.۳. خلاصه واریانس

  • `out T` (Covariant): فقط می‌تواند به عنوان نوع خروجی (return type) یک متد استفاده شود. امکان تخصیص از نوع مشتق‌شده‌تر به نوع اصلی‌تر (مثلاً `ICovariant` به `ICovariant`).
  • `in T` (Contravariant): فقط می‌تواند به عنوان نوع ورودی (input parameter) یک متد استفاده شود. امکان تخصیص از نوع اصلی‌تر به نوع مشتق‌شده‌تر (مثلاً `IContravariant` به `IContravariant`).

درک واریانس می‌تواند کمی چالش برانگیز باشد، اما برای استفاده کامل از قدرت Genericها و درک عمیق‌تر از کتابخانه‌های اصلی .NET ضروری است.

۹. Genericها و Reflection: کار با انواع در زمان اجرا

Reflection در .NET به شما امکان می‌دهد اطلاعات مربوط به انواع، متدها، فیلدها و خواص یک اسمبلی را در زمان اجرا بررسی کنید و حتی با استفاده از این اطلاعات، نمونه‌هایی از انواع را ایجاد کرده و متدها را فراخوانی کنید. هنگام کار با Genericها، Reflection می‌تواند برای سناریوهای پیشرفته‌ای که در آنها نیاز به ایجاد یا دستکاری انواع Generic به صورت پویا دارید، بسیار مفید باشد.

۹.۱. بررسی انواع Generic با Reflection

شما می‌توانید از کلاس `System.Type` و متدهای آن برای بررسی انواع Generic استفاده کنید:

  • `IsGenericType`: بررسی می‌کند که آیا یک نوع Generic است یا خیر.
  • `IsGenericTypeDefinition`: بررسی می‌کند که آیا یک نوع Generic تعریف‌نشده (مانند `List<>` یا `Dictionary<,>`) است یا خیر.
  • `GetGenericArguments()`: آرایه‌ای از انواع را برمی‌گرداند که پارامترهای نوع واقعی یک نوع Generic ساخته شده را نشان می‌دهند (مثلاً برای `List`، این متد `int` را برمی‌گرداند).
  • `GetGenericTypeDefinition()`: تعریف نوع Generic را برمی‌گرداند (مثلاً برای `List`، این متد `List<>` را برمی‌گرداند).

using System;
using System.Collections.Generic;
using System.Reflection;

public class ReflectionWithGenerics
{
    public static void AnalyzeType(Type type)
    {
        Console.WriteLine($"Analyzing Type: {type.Name}");
        Console.WriteLine($"Is Generic Type: {type.IsGenericType}");
        Console.WriteLine($"Is Generic Type Definition: {type.IsGenericTypeDefinition}");

        if (type.IsGenericType)
        {
            Console.WriteLine("Generic Arguments:");
            foreach (Type arg in type.GetGenericArguments())
            {
                Console.WriteLine($"  - {arg.Name}");
            }
            if (!type.IsGenericTypeDefinition)
            {
                Console.WriteLine($"Generic Type Definition: {type.GetGenericTypeDefinition().Name}");
            }
        }
        Console.WriteLine("--------------------");
    }

    public static void Main()
    {
        AnalyzeType(typeof(List));
        AnalyzeType(typeof(Dictionary));
        AnalyzeType(typeof(List<>)); // Generic type definition
        AnalyzeType(typeof(int)); // Non-generic type
    }
}

۹.۲. ساخت انواع Generic در زمان اجرا

یکی از قدرتمندترین قابلیت‌ها، توانایی ساخت انواع Generic در زمان اجرا است. این کار با استفاده از متد `MakeGenericType` روی یک تعریف نوع Generic انجام می‌شود.


using System;
using System.Collections.Generic;
using System.Reflection;

public class DynamicGenericCreation
{
    public static void Main()
    {
        // 1. دریافت تعریف نوع Generic (مثلاً List<>)
        Type listDefinition = typeof(List<>);
        Console.WriteLine($"List Definition: {listDefinition.Name}, IsGenericTypeDefinition: {listDefinition.IsGenericTypeDefinition}");

        // 2. ساخت یک نوع Generic خاص در زمان اجرا (مثلاً List)
        Type stringListType = listDefinition.MakeGenericType(typeof(string));
        Console.WriteLine($"Constructed Type: {stringListType.Name}, IsGenericType: {stringListType.IsGenericType}");

        // 3. ایجاد یک نمونه از نوع ساخته شده
        object stringListInstance = Activator.CreateInstance(stringListType);

        // 4. فراخوانی متدها روی این نمونه
        MethodInfo addMethod = stringListType.GetMethod("Add");
        addMethod.Invoke(stringListInstance, new object[] { "Dynamic Hello" });
        addMethod.Invoke(stringListInstance, new object[] { "Dynamic World" });

        MethodInfo getCountMethod = stringListType.GetProperty("Count").GetGetMethod();
        int count = (int)getCountMethod.Invoke(stringListInstance, null);
        Console.WriteLine($"Count of dynamic list: {count}");

        MethodInfo getItemMethod = stringListType.GetMethod("get_Item");
        Console.WriteLine($"First item: {getItemMethod.Invoke(stringListInstance, new object[] { 0 })}");

        Console.WriteLine("--------------------");

        // مثال: ایجاد یک دیکشنری Generic به صورت پویا
        Type dictionaryDefinition = typeof(Dictionary<,>);
        Type stringIntDictionaryType = dictionaryDefinition.MakeGenericType(typeof(string), typeof(int));
        object dictionaryInstance = Activator.CreateInstance(stringIntDictionaryType);

        MethodInfo addDictMethod = stringIntDictionaryType.GetMethod("Add");
        addDictMethod.Invoke(dictionaryInstance, new object[] { "One", 1 });
        addDictMethod.Invoke(dictionaryInstance, new object[] { "Two", 2 });

        PropertyInfo countProperty = stringIntDictionaryType.GetProperty("Count");
        Console.WriteLine($"Count of dynamic dictionary: {countProperty.GetValue(dictionaryInstance)}");
    }
}

این قابلیت برای فریم‌ورک‌ها، موتورهای ORM، سیستم‌های پلاگین‌نویسی و سناریوهایی که در زمان کامپایل، نوع دقیق داده‌ها مشخص نیست، بسیار ارزشمند است. با این حال، استفاده از Reflection به دلیل سربار عملکردی (performance overhead) و پیچیدگی، باید با دقت و تنها در صورت لزوم انجام شود.

۱۰. بهترین روش‌ها و ملاحظات عملکردی در استفاده از Genericها

استفاده از Genericها می‌تواند به طور چشمگیری کیفیت و کارایی کد C# شما را بهبود بخشد. با این حال، مانند هر ابزار قدرتمندی، درک بهترین روش‌ها و ملاحظات عملکردی برای استفاده بهینه از آن‌ها ضروری است.

۱۰.۱. چه زمانی از Genericها استفاده کنیم؟

  • ساختارهای داده: برای پیاده‌سازی ساختارهای داده عمومی مانند لیست‌ها، پشته‌ها، صف‌ها، درختان و گراف‌ها که باید با انواع مختلف داده کار کنند (مثلاً `List`, `Dictionary`).
  • الگوریتم‌های عمومی: برای نوشتن الگوریتم‌هایی که عملیات مشترکی را روی انواع مختلفی از داده‌ها انجام می‌دهند، بدون اینکه به نوع خاصی گره خورده باشند (مثلاً توابع مرتب‌سازی، توابع جستجو، متدهای `Swap`).
  • Repository Pattern: در لایه‌های دسترسی به داده برای ایجاد Repositoryهای عمومی که می‌توانند با هر نوع Entity کار کنند.
  • فریم‌ورک‌ها و کتابخانه‌ها: در توسعه کتابخانه‌های عمومی که باید بدون دانستن انواع داده‌ای که در نهایت با آن‌ها کار خواهند کرد، نوشته شوند.
  • پرهیز از Boxing/Unboxing: هرگاه نیاز به کار با مجموعه‌ای از انواع مقداری دارید و می‌خواهید از سربار عملکردی Boxing و Unboxing جلوگیری کنید (مثلاً `List` به جای `ArrayList`).

۱۰.۲. چه زمانی از Genericها استفاده نکنیم؟

  • در صورت عدم نیاز به پارامترهای نوع: اگر عملکرد شما به نوع داده‌ای که پردازش می‌کنید وابسته نیست و می‌توانید با `object` یا اینترفیس‌های غیر Generic کار کنید (که البته نادر است و معمولاً نشانه‌ای از طراحی غیر بهینه است).
  • برای پنهان کردن پیچیدگی‌های غیر ضروری: Genericها می‌توانند کد را پیچیده‌تر کنند اگر به درستی استفاده نشوند. از تعریف پارامترهای نوع اضافی یا محدودیت‌های پیچیده و بی‌مورد خودداری کنید.

۱۰.۳. ملاحظات عملکردی

  • کاهش سربار Boxing/Unboxing: همانطور که قبلاً ذکر شد، یکی از بزرگترین مزایای عملکردی Genericها حذف نیاز به Boxing و Unboxing است که برای انواع مقداری، بهبود چشمگیری به همراه دارد.
  • Generics و JIT Compilation: در C#، کد Generic به محض اولین بار که با یک نوع واقعی (مثلاً `List`) استفاده می‌شود، توسط JIT (Just-In-Time) Compiler کامپایل می‌شود. برای هر ترکیب منحصر به فرد از پارامترهای نوع مقداری (مثلاً `List`, `List`)، JIT یک کد ماشین جداگانه تولید می‌کند. اما برای پارامترهای نوع مرجع (مثلاً `List`, `List`, `List`)، JIT معمولاً یک کد ماشین مشترک تولید می‌کند، زیرا همه انواع مرجع در حافظه به یک شکل مدیریت می‌شوند (به عنوان اشاره‌گر). این بدان معناست که در برخی موارد، برای انواع مقداری، ممکن است چندین نسخه از کد Generic در حافظه وجود داشته باشد، اما این به ندرت به یک مشکل عملکردی جدی تبدیل می‌شود.
  • Reflection و Genericها: استفاده از Reflection برای کار با Genericها در زمان اجرا می‌تواند سربار عملکردی قابل توجهی داشته باشد. تا حد امکان، سعی کنید از کد Generic استاتیک استفاده کنید و Reflection را به موارد ضروری محدود کنید.
  • ۱۰.۴. بهترین روش‌ها

    • نامگذاری پارامترهای نوع: از نامگذاری توصیفی برای پارامترهای نوع استفاده کنید. `T` برای یک پارامتر نوع عمومی، `TKey` و `TValue` برای جفت‌های کلید/مقدار، و `TElement` برای عناصر در یک مجموعه، رایج و خوانا هستند.
    • استفاده از محدودیت‌ها: همیشه از محدودیت‌ها برای اعمال حداقل الزامات بر روی پارامترهای نوع خود استفاده کنید. این کار امنیت نوع را افزایش می‌دهد و به کامپایلر کمک می‌کند تا خطاهای بیشتری را در زمان کامپایل شناسایی کند. همچنین، با اعمال محدودیت‌ها، می‌توانید به متدها و خواص خاصی از نوع دسترسی پیدا کنید که بدون محدودیت ممکن نیست.
    • پرهیز از محدودیت‌های اضافی: از اعمال محدودیت‌های غیر ضروری خودداری کنید، زیرا این کار انعطاف‌پذیری کد شما را کاهش می‌دهد. فقط محدودیت‌هایی را اعمال کنید که برای منطق کد شما لازم هستند.
    • درک واریانس: با واریانس آشنا شوید تا بتوانید به درستی از اینترفیس‌ها و دلیگیت‌های Covariant/Contravariant موجود در .NET استفاده کنید و در صورت لزوم، خودتان آن‌ها را تعریف کنید.
    • مستندسازی: اگر Genericهای پیچیده با محدودیت‌های متعدد ایجاد می‌کنید، حتماً آن‌ها را به خوبی مستند کنید تا توسعه‌دهندگان دیگر بتوانند به راحتی آن‌ها را درک و استفاده کنند.

    با رعایت این نکات، می‌توانید از Genericها به بهترین شکل ممکن برای نوشتن کدهای C# قوی، انعطاف‌پذیر و کارآمد بهره ببرید.

    ۱۱. کاربردهای پیشرفته و سناریوهای واقعی

    Genericها به قدری در C# اساسی هستند که تقریباً در هر بخش از توسعه .NET، از برنامه‌های کاربردی دسکتاپ گرفته تا وب‌سرویس‌ها و میکرو سرویس‌ها، حضور دارند. در اینجا به برخی از کاربردهای پیشرفته و سناریوهای واقعی که Genericها نقش کلیدی در آن‌ها ایفا می‌کنند، اشاره می‌کنیم:

    ۱۱.۱. Dependency Injection (DI) Frameworks

    فریم‌ورک‌های تزریق وابستگی مانند .NET Core's built-in DI، Autofac، Ninject و StructureMap به شدت بر Genericها متکی هستند. آنها از Genericها برای ثبت و حل وابستگی‌ها به صورت نوع امن و قابل استفاده مجدد استفاده می‌کنند. به عنوان مثال، ثبت یک سرویس ممکن است به این شکل باشد:

    
    services.AddScoped<IMyService, MyServiceImpl>();
    

    این سینتکس Generic به فریم‌ورک DI اجازه می‌دهد تا بداند که وقتی `IMyService` درخواست شد، یک نمونه از `MyServiceImpl` باید ارائه شود.

    ۱۱.۲. LINQ و Expression Trees

    تمام متدهای Extension در LINQ، مانند `Where`, `Select`, `OrderBy`, `GroupBy` و غیره، از Genericها استفاده می‌کنند. این امکان را فراهم می‌کند که توابع Query (پرس و جو) برای هر نوع `IEnumerable` یا `IQueryable` کار کنند، بدون اینکه نیاز به کدهای تکراری برای هر نوع باشد. `Expression Trees` که در LINQ to SQL/Entities استفاده می‌شوند، نیز از Genericها برای ساخت پرس و جوهای دینامیک استفاده می‌کنند.

    ۱۱.۳. ORM Frameworks (مانند Entity Framework Core)

    فریم‌ورک‌های Object-Relational Mapping (ORM) مانند Entity Framework Core به شدت به Genericها وابسته هستند. آن‌ها از `DbSet` برای نمایش مجموعه‌ای از موجودیت‌ها در پایگاه داده استفاده می‌کنند که `TEntity` یک نوع Generic است که موجودیت شما را نشان می‌دهد. این به ORM اجازه می‌دهد تا عملیات CRUD را برای هر موجودیتی که تعریف می‌کنید، بدون نیاز به نوشتن کد خاص برای هر جدول، انجام دهد.

    
    public class ApplicationDbContext : DbContext
    {
        public DbSet<Product> Products { get; set; }
        public DbSet<Customer> Customers { get; set; }
        // ...
    }
    

    ۱۱.۴. Message Queues و Event Buses

    در معماری‌های مبتنی بر پیام (message-driven architectures)، اغلب از Genericها برای تعریف انواع پیام و Handlerهای آن‌ها استفاده می‌شود. به عنوان مثال، یک اینترفیس Generic برای یک Message Handler می‌تواند به این صورت تعریف شود:

    
    public interface IHandleMessage<TMessage> where TMessage : IMessage
    {
        void Handle(TMessage message);
    }
    

    این الگو به شما امکان می‌دهد سیستم‌های پیام‌رسانی قوی و قابل توسعه ایجاد کنید.

    ۱۱.۵. Unit Testing Frameworks

    فریم‌ورک‌های تست واحد مانند NUnit و XUnit نیز از Genericها استفاده می‌کنند. به عنوان مثال، متدهای `Assert.Throws()` یا `CollectionAssert.AreEquivalent()` به شما امکان می‌دهند تست‌های نوع‌امن بنویسید.

    ۱۱.۶. طراحی الگوهای قابل استفاده مجدد (Reusable Design Patterns)

    Genericها به شما اجازه می‌دهند تا بسیاری از الگوهای طراحی (Design Patterns) را به صورت نوع‌امن و قابل استفاده مجدد پیاده‌سازی کنید. الگوهایی مانند Strategy، Factory، Decorator، Observer و Builder می‌توانند با استفاده از Genericها به روشی بسیار منعطف‌تر پیاده‌سازی شوند.

    این کاربردها تنها بخش کوچکی از تأثیر گسترده Genericها در اکوسیستم C# و .NET هستند. آن‌ها به توسعه‌دهندگان امکان می‌دهند کدی با کیفیت بالاتر، قابل نگهداری‌تر و کارآمدتر بنویسند.

    ۱۲. آینده Genericها در C#

    تکامل Genericها در C# با معرفی ویژگی‌های جدید ادامه دارد. به عنوان مثال، C# 11 مفهوم Generic Math را معرفی کرد که به توسعه‌دهندگان اجازه می‌دهد تا متدهایی Generic بنویسند که بتوانند عملیات ریاضی را روی انواع عددی مختلف (مانند `int`, `double`, `decimal`) بدون نیاز به Overload کردن متد برای هر نوع، انجام دهند. این قابلیت از اینترفیس‌های استاتیک انتزاعی (static abstract interfaces) در Genericها بهره می‌برد.

    مثال Generic Math (نیاز به C# 11 یا بالاتر):

    
    using System;
    using System.Numerics; // فضای نام برای Generic Math interfaces
    
    public static class MathOperations
    {
        // این متد می‌تواند برای هر نوع T که IAdditionOperators را پیاده‌سازی می‌کند، کار کند.
        // به این معنی که می‌تواند عملگر + را پشتیبانی کند.
        public static T Add<T>(T left, T right) where T : IAdditionOperators<T, T, T>
        {
            return left + right;
        }
    
        // مثال دیگر: یافتن حداکثر
        public static T Max<T>(T left, T right) where T : IComparable<T>
        {
            return left.CompareTo(right) > 0 ? left : right;
        }
    
        // با Generic Math، می‌توانیم عملیات حسابی را Generic کنیم.
        // T باید IAdditionOperators, IMultiplyOperators و غیره را پیاده‌سازی کند.
        public static T CalculateExpression<T>(T a, T b, T c)
            where T : IAdditionOperators<T, T, T>,
                      IMultiplyOperators<T, T, T>,
                      ISubtractionOperators<T, T, T>,
                      IDivisionOperators<T, T, T>
        {
            // (a + b) * c - (a / b)
            return (a + b) * c - (a / b);
        }
    }
    
    public class GenericMathExample
    {
        public static void Main()
        {
            Console.WriteLine($"Add(5, 7): {MathOperations.Add(5, 7)}");
            Console.WriteLine($"Add(3.5, 2.1): {MathOperations.Add(3.5, 2.1)}");
            Console.WriteLine($"Add(new Complex(1,2), new Complex(3,4)): {MathOperations.Add(new Complex(1,2), new Complex(3,4))}");
    
            Console.WriteLine($"Max(10, 20): {MathOperations.Max(10, 20)}");
            Console.WriteLine($"Max(15.5m, 12.3m): {MathOperations.Max(15.5m, 12.3m)}");
    
            Console.WriteLine($"CalculateExpression(10, 5, 2): {MathOperations.CalculateExpression(10, 5, 2)}");
            Console.WriteLine($"CalculateExpression(10.0, 5.0, 2.0): {MathOperations.CalculateExpression(10.0, 5.0, 2.0)}");
        }
    }
    

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

    نتیجه‌گیری: قدرت بی‌پایان Genericها در C#

    همانطور که در این مقاله به تفصیل بررسی کردیم، Genericها یکی از قدرتمندترین و اساسی‌ترین ویژگی‌های زبان C# هستند که رویکرد شما را به کدنویسی به طور کامل متحول می‌کنند. آنها با ارائه مکانیزمی برای نوشتن کد نوع‌امن، قابل استفاده مجدد و با کارایی بالا که مستقل از انواع داده خاص است، به شما امکان می‌دهند تا باگ‌ها را در زمان کامپایل شناسایی کرده، سربار عملکردی ناشی از Boxing/Unboxing را حذف کنید و در نهایت، کدی تمیزتر و قابل نگهداری‌تر بنویسید.

    از کلاس‌های Generic که ساختارهای داده‌ای منعطف ایجاد می‌کنند تا متدهای Generic که الگوریتم‌های جهانی را ممکن می‌سازند، و اینترفیس‌ها و دلیگیت‌های Generic که قراردادهای نوع‌امن را تعریف می‌کنند، این قابلیت در تمام بخش‌های اکوسیستم .NET حضور پررنگی دارد. درک عمیق محدودیت‌ها و واریانس، به شما امکان می‌دهد تا Genericها را برای سناریوهای پیچیده‌تر بهینه سازی کرده و از تمام پتانسیل آن‌ها بهره‌مند شوید.

    با معرفی Generic Math در C# 11، شاهد ادامه تکامل این قابلیت هستیم که مرزهای کدنویسی عمومی را باز هم گسترش می‌دهد. تسلط بر Genericها برای هر توسعه‌دهنده جدی C# یک مهارت ضروری است. با به‌کارگیری صحیح آن‌ها، شما نه تنها بهره‌وری خود را افزایش می‌دهید، بلکه کدی تولید می‌کنید که برای سال‌های آتی قوی، مقیاس‌پذیر و پایدار خواهد بود. اکنون زمان آن است که دانش خود را به عمل تبدیل کرده و شروع به ساخت راه‌حل‌های بهتر با Genericها در C# کنید.

    “تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

    قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

    "تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

    "با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

    ویژگی‌های کلیدی:

    بدون نیاز به تجربه قبلی برنامه‌نویسی

    زیرنویس فارسی با ترجمه حرفه‌ای

    ۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان

    سبد خرید