مقدمه

در درس قبل، با قدرت جنریک‌ها و مزایای آن‌ها در ایجاد کدهای امن از نظر نوع و قابل استفاده مجدد آشنا شدیم. دیدیم که چگونه کالکشن‌های استاندارد مانند List<T> از این ویژگی بهره می‌برند. اکنون زمان آن رسیده که یک قدم فراتر رفته و یاد بگیریم چگونه انواع جنریک سفارشی خودمان را، شامل اینترفیس‌ها و کلاس‌ها، طراحی و پیاده‌سازی کنیم. ساخت انواع جنریک سفارشی به ما این امکان را می‌دهد تا الگوهای طراحی (Design Patterns) قدرتمندی مانند الگوی Repository را پیاده‌سازی کرده و منطق برنامه را از نوع داده‌ی خاصی که روی آن کار می‌کند، جدا کنیم.

تعریف اینترفیس‌های جنریک

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

برای مثال، بیایید یک اینترفیس برای یک "مخزن داده" یا Repository تعریف کنیم. یک مخزن، عملیات پایه‌ای داده مانند افزودن، خواندن و حذف را کپسوله می‌کند. ما می‌خواهیم این اینترفیس برای هر نوع موجودیتی (مانند Student، Product یا Order) قابل استفاده باشد.

Copy Icon Program.cs
// A generic interface for repository operations.
// 'T' is the type parameter representing the entity type.
public interface IRepository<T>
{
    T GetById(int id);
    void Add(T entity);
    void Update(T entity);
    void Delete(int id);
    IEnumerable<T> GetAll();
}

این اینترفیس IRepository<T> یک قرارداد بسیار انعطاف‌پذیر است. هر کلاسی که آن را پیاده‌سازی کند، باید این پنج متد را برای نوع خاصی که مشخص می‌کند، فراهم نماید.

پیاده‌سازی اینترفیس‌های جنریک

اکنون که قرارداد خود را داریم، می‌توانیم یک کلاس مشخص (concrete) بسازیم که این اینترفیس را برای یک نوع خاص، مثلاً Student، پیاده‌سازی کند.

Copy Icon Program.cs
public class Student { public int Id { get; set; } public string Name { get; set; } }

// This class implements the generic interface for the 'Student' type.
public class StudentRepository : IRepository<Student>
{
    private readonly List<Student> _students = new();
    private int _nextId = 1;

    public void Add(Student entity)
    {
        entity.Id = _nextId++;
        _students.Add(entity);
    }

    public Student GetById(int id)
    {
        return _students.FirstOrDefault(s => s.Id == id);
    }

    // Implementation for other methods (Update, Delete, GetAll)...
    public void Update(Student entity) { /*...*/ }
    public void Delete(int id) { /*...*/ }
    public IEnumerable<Student> GetAll() { return _students; }
}

در این مثال، کلاس StudentRepository متعهد شده است که عملیات CRUD (Create, Read, Update, Delete) را به طور مشخص برای اشیاء Student فراهم کند. این روش بسیار سازمان‌یافته است، اما اگر بخواهیم همین منطق را برای Product هم داشته باشیم، باید یک کلاس ProductRepository جداگانه بنویسیم که کد آن بسیار شبیه به StudentRepository خواهد بود. اینجا جایی است که می‌توانیم با ایجاد یک کلاس جنریک، کد خود را باز هم قابل استفاده‌تر کنیم.

ایجاد یک کلاس جنریک کامل

ما می‌توانیم یک کلاس جنریک بسازیم که اینترفیس جنریک IRepository<T> را پیاده‌سازی کند. این کلاس می‌تواند منطق پایه‌ای را برای هر نوع `T` فراهم کند و به عنوان یک مخزن داده‌ی عمومی عمل کند.

Copy Icon Program.cs
// A generic class implementing the generic interface.
// This repository can work with ANY type T.
public class GenericRepository<T> : IRepository<T>
{
    protected readonly List<T> _items = new();

    public void Add(T entity)
    {
        _items.Add(entity);
        Console.WriteLine($"Added a {typeof(T).Name} to the repository.");
    }
    
    public IEnumerable<T> GetAll()
    {
        return _items;
    }

    // Other methods would need more complex logic.
    public T GetById(int id) { return default; /* Simplified */ }
    public void Update(T entity) { /* ... */ }
    public void Delete(int id) { /* ... */ }
}

اکنون ما یک کلاس GenericRepository<T> داریم که می‌توانیم از آن برای هر نوعی استفاده کنیم:

Copy Icon Program.cs
// Use the generic repository for Students.
IRepository<Student> studentRepo = new GenericRepository<Student>();
studentRepo.Add(new Student { Name = "John" });

// Use the SAME generic repository class for Products.
public class Product { public string ProductName { get; set; } }
IRepository<Product> productRepo = new GenericRepository<Product>();
productRepo.Add(new Product { ProductName = "Laptop" });

این سطح از استفاده مجدد از کد، بدون جنریک‌ها امکان‌پذیر نبود. البته پیاده‌سازی متدهایی مانند GetById در یک مخزن جنریک واقعی پیچیده‌تر است و معمولاً نیازمند محدود کردن پارامتر نوع T است که در درس بعدی به آن خواهیم پرداخت.

کلمه‌ی کلیدی default در جنریک‌ها

هنگام کار با یک پارامتر نوع جنریک T، شما نمی‌دانید که آیا T یک نوع مقداری (مانند int) خواهد بود یا یک نوع ارجاعی (مانند یک class). این موضوع زمانی اهمیت پیدا می‌کند که می‌خواهید یک مقدار پیش‌فرض برای T برگردانید. کلمه‌ی کلیدی default این مشکل را حل می‌کند. default(T) (یا در نسخه‌های جدیدتر C#، به سادگی default) مقدار پیش‌فرض صحیح را برای هر نوعی برمی‌گرداند: 0 برای انواع عددی، false برای bool، و null برای انواع ارجاعی.

public T GetFirstOrDefault(List<T> items)
{
    if (items.Count > 0)
    {
        return items[0];
    }
    else
    {
        // Returns null for reference types, 0 for int, etc.
        return default;
    }
}