در نسخههای اولیه #C (پیش از ارائه #C 2.0)، توسعهدهندگان برای طراحی کلاسها و متدهای انعطافپذیری که بتوانند با انوع دادههای مختلف کار کنند، ناچار به استفاده از شیء پایه System.Object یا الگوی Reflection بودند. این رویکرد دو چالش بحرانی به همراه داشت:
فقدان Type Safety در زمان کامپایل: خطاهای مربوط به عدم تطابق نوع تنها در زمان اجرا (Runtime) کشف میشدند.
افت شدید کارایی (Performance Overhead): تبدیل انوع دادهای ارزشمحور (Value Types) به نوع مرجع (Reference Type) و برعکس، فرایند هزینه بر Boxing و Unboxing را تحمیل میکرد.
معرفی Generics در #C 2.0 پارادایم برنامهنویسی شیءگرا و متادیتا محور را دگرگون کرد. این قابلیت به برنامهنویس اجازه میدهد تا الگوی ساختاری کد (کلاس، اینترفیس، متد، یا دلیگیت) را با پارامترهای نوع (Type Parameters) تعریف کند و تعیین نوع واقعی داده را تا زمان فراخوانی یا تعریف نمونه به تاخیر بیندازد.
// رویکرد قدیمی - بدون Generics (غیر ایمن و با افت کارایی)
public class LegacyArrayList
{
private object[] _items = new object[10];
public void Add(object item) => _items[0] = item;
public object Get(int index) => _items[index];
}
// رویکرد مدرن - با Generics (Type-Safe و بهینه)
public class GenericList<T>
{
private T[] _items = new T[10];
public void Add(T item) => _items[0] = item;
public T Get(int index) => _items[index];
}
درک نحوه مدیریت Generics توسط Common Language Runtime (CLR) برای طراحی برنامههای با مقیاس بالا الزامی است. برخلاف templates در زبان ++C که برای هر نوع داده مشخص یک نسخه کاملاً مجزا از کد را در زمان کامپایل تولید میکند (Template Instantiation)، #C و CLR از ترکیبی از متادیتا و کد IL (Intermediate Language) استفاده میکنند.
فرایند Specialized Code Generation
برای Reference Types (مانند string یا کلاسهای اختصاصی): CLR تنها یک نسخه از کدهای بومی (Native Machine Code) را بین تمامی انواع مرجع به اشتراک میگذارد. چرا که تمامی اشارهگرها در معماری ۶۴ بیتی دارای اندازهای یکسان (۸ بایت) هستند.
برای Value Types (مانند int، struct یا DateTime): CLR به ازای هر Value Type منحصر به فرد، یک نسخه خاص و بهینهسازی شده از کد بومی را در زمان اجرا (JIT Compilation) تولید میکند تا از احتیاج به Boxing جلوگیری کند.
قابلیت استفاده مجدد (Reusability) به معنای نوشتن یکباره کد و بهرهبرداری از آن در سناریوهای مختلف بدون دستکاری منطق داخلی است. Generics این هدف را از سه مسیر اصلی محقق میسازد:
انتزاع منطق دادهورز (Abstracting Data-centric Logic)
یکپارچگی در معماری نرمافزار (Architectural Consistency)
برای درک عمقی نقش Generics در پروژههای واقعی، پیادهسازی سه الگوی کلیدی سیستمهای enterprise بررسی میشود:
1. الگوی Generic Repository Pattern
در برنامههای متکی بر پایگاهداده، عملیات CRUD برای تمام موجودیتها (Entities) یکسان است. بدون Generics باید به ازای هر Entity یک Repository مجزا ایجاد کرد.
public interface IRepository<TEntity, TId> where TEntity : class
{
Task<TEntity?> GetByIdAsync(TId id);
Task<IEnumerable<TEntity>> GetAllAsync();
Task AddAsync(TEntity entity);
void Update(TEntity entity);
void Delete(TEntity entity);
}
public class GenericRepository<TEntity, TId> : IRepository<TEntity, TId> where TEntity : class
{
protected readonly DbContext _context;
protected readonly DbSet<TEntity> _dbSet;
public GenericRepository(DbContext context)
{
_context = context;
_dbSet = _context.Set<TEntity>();
}
public async Task<TEntity?> GetByIdAsync(TId id) => await _dbSet.FindAsync(id);
public async Task<IEnumerable<TEntity>> GetAllAsync() => await _dbSet.ToListAsync();
public async Task AddAsync(TEntity entity) => await _dbSet.AddAsync(entity);
public void Update(TEntity entity) => _dbSet.Update(entity);
public void Delete(TEntity entity) => _dbSet.Remove(entity);
}
2. الگوی Result/Response Pattern (کپسولهسازی پاسخها)
یکسانسازی خروجی APIها و سرویسها یکی از معیارهای اصالت معماری است.
public class Result<T>
{
public bool IsSuccess { get; }
public T? Data { get; }
public string? ErrorMessage { get; }
private Result(bool isSuccess, T? data, string? errorMessage)
{
IsSuccess = isSuccess;
Data = data;
ErrorMessage = errorMessage;
}
public static Result<T> Success(T data) => new(true, data, null);
public static Result<T> Failure(string message) => new(false, default, message);
}
گاهی اوقات برای افزایش کاربرد مجدد کد، نیاز است مطمئن شویم نوع ورودی دارای رفتارها یا ویژگیهای خاصی است. این کار از طریق کلمه کلیدی where انجام میشود:
| قید (Constraint) | شرح کارکرد |
| where T : struct | پارامتر نوع باید حتماً یک Value Type باشد. |
| where T : class | پارامتر نوع باید حتماً یک Reference Type باشد. |
| where T : new() | نوع مربوطه باید دارای یک سازنده بدون پارامتر (Parameterless Constructor) باشد. |
| where T : BaseClass | پارامتر نوع باید از کلاس پایه مشخصی ارثبری کرده باشد. |
| where T : ISomeInterface | نوع ورود باید اینترفیس مشخصشده را پیادهسازی کرده باشد. |
| where T : notnull | نوع ورودی نباید قابل پذیرش مقدار Null باشد (معرفی شده در #C 8). |
// نمونه استفاده ترکیبی از قیود
public class BusinessEngine<TEntity, TDto>
where TEntity : class, IEntity, new()
where TDto : IDto
{
public TEntity MapToEntity(TDto dto)
{
var entity = new TEntity();
// منطق نگاشت
return entity;
}
}
یکی از پیشرفتهترین مفاهیم مرتبط با بازاستفاده کدهای مبتنی بر اینترفیس و دلیگیتهای ژنریک، موضوع Variance است که با کلیدواژههای out و in مدیریت میشود.
1. همتغییری (Covariance - out)
اجازه میدهد یک نوع مشتقه (Derived) به جای نوع پایه (Base) اختصاص داده شود (صرفاً برای خروجیها/Read-only).
public interface IReadOnlyRepository<out T>
{
T GetById(int id); // T فقط در موقعیت خروجی قرار دارد
}
// تخصیص معتبر به لطف Covariance:
IReadOnlyRepository<string> strRepo = null!;
IReadOnlyRepository<object> objRepo = strRepo;
2. فراتغییری (Contravariance - in)
اجازه میدهد یک نوع پایه به جای نوع مشتقه اختصاص یابد (صرفاً برای ورودیها/Write-only).
public interface IReceiver<in T>
{
void Process(T item); // T فقط در موقعیت ورودی قرار دارد
}
// تخصیص معتبر به لطف Contravariance:
IReceiver<object> objReceiver = null!;
IReceiver<string> strReceiver = objReceiver;
| معیار مقایسه | Generics | System.Object / Cast | Reflection |
| ایمنی نوع (Type Safety) | زمان کامپایل (حداکثر) | زمان اجرا (ضعیف) | زمان اجرا (ضعیف) |
| کارایی (Performance) | بسیار بالا (بدون Boxing) | پایین (به دلیل Boxing) | بسیار پایین (کُند) |
| خوانایی کد | بالا | متوسط | پایین |
| خطایابی (Debugging) | ساده | پیچیده | دشوار |
Generics در #C تنها یک ابزار برنامهنویسی نیست، بلکه یکی از ستونهای اصلی انعطافپذیری و کارایی در اکوسیستم .NET است. این قابلیت با انتقال بررسیهای ایمنی نوع به زمان کامپایل، حذف هزینههای Boxing/Unboxing و فراهم کردن امکان تعریف الگوی منطقی فارغ از جنس داده، قابلیت استفاده مجدد کد را به حداکثر میرساند و زیربنای کدهای تمیز (Clean Code) و معماریهای نگهداریپذیر (Maintainable Architectures) را تشکیل میدهد.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.