مقدمه
در درس قبل، با قدرت جنریکها و مزایای آنها در ایجاد کدهای امن از نظر نوع و قابل استفاده مجدد
آشنا شدیم. دیدیم که چگونه کالکشنهای استاندارد مانند List<T>
از این ویژگی بهره میبرند. اکنون
زمان آن رسیده که یک قدم فراتر رفته و یاد بگیریم چگونه انواع جنریک سفارشی خودمان را، شامل
اینترفیسها و کلاسها، طراحی و پیادهسازی کنیم. ساخت انواع جنریک سفارشی به ما این امکان را
میدهد تا الگوهای طراحی (Design Patterns) قدرتمندی مانند الگوی Repository را پیادهسازی کرده
و منطق برنامه را از نوع دادهی خاصی که روی آن کار میکند، جدا کنیم.
تعریف اینترفیسهای جنریک
درست مانند کلاسها، اینترفیسها نیز میتوانند جنریک باشند. یک اینترفیس جنریک، یک قرارداد را
تعریف میکند که به یک پارامتر نوع `T` وابسته است. این به ما اجازه میدهد تا یک مجموعهی
استاندارد از عملیات را تعریف کنیم که میتواند توسط کلاسهای مختلف برای انواع دادههای متفاوت
پیادهسازی شود.
برای مثال، بیایید یک اینترفیس برای یک "مخزن داده" یا Repository تعریف کنیم. یک مخزن، عملیات
پایهای داده مانند افزودن، خواندن و حذف را کپسوله میکند. ما میخواهیم این اینترفیس برای هر نوع
موجودیتی (مانند Student، Product یا Order) قابل
استفاده باشد.
Program.cs
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، پیادهسازی کند.
Program.cs
public class Student { public int Id { get; set; } public string Name { get; set; } }
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);
}
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` فراهم کند و به عنوان یک مخزن دادهی عمومی عمل کند.
Program.cs
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;
}
public T GetById(int id) { return default; }
public void Update(T entity) { }
public void Delete(int id) { }
}
اکنون ما یک کلاس GenericRepository<T> داریم که میتوانیم از
آن برای هر نوعی استفاده کنیم:
Program.cs
IRepository<Student> studentRepo = new GenericRepository<Student>();
studentRepo.Add(new Student { Name = "John" });
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
{
return default;
}
}