مقدمه

در درس‌های گذشته دیدیم که جنریک‌ها چقدر در ایجاد کدهای قابل استفاده مجدد و امن از نظر نوع، قدرتمند هستند. ما می‌توانیم کلاس یا متدی بنویسیم که با هر نوع داده‌ای (T) کار کند. اما گاهی اوقات، "هر نوع داده‌ای" بیش از حد عمومی است. ممکن است منطق جنریک ما نیاز داشته باشد که به یک متد یا پراپرتی خاص دسترسی داشته باشد که فقط روی انواع خاصی وجود دارد. برای مثال، اگر بخواهیم دو شیء از نوع T را با هم مقایسه کنیم، از کجا بدانیم که نوع T دارای متد CompareTo است؟ اینجاست که تحدیدها (Constraints) وارد عمل می‌شوند. تحدیدها قوانینی هستند که ما بر روی پارامترهای نوع جنریک اعمال می‌کنیم تا به کامپایلر بگوییم که نوع T باید دارای چه قابلیت‌ها یا ویژگی‌هایی باشد.

مشکل جنریک‌های بیش از حد عمومی

بیایید به مثال GenericRepository<T> از درس قبل برگردیم. ما می‌خواستیم متد GetById را پیاده‌سازی کنیم. یک پیاده‌سازی ساده می‌تواند به این شکل باشد:

Copy Icon Program.cs
public T GetById(int id)
{
    // This will cause a compile-time error!
    // Error: 'T' does not contain a definition for 'Id'...
    return _items.FirstOrDefault(item => item.Id == id);
}

این کد کامپایل نمی‌شود. کامپایلر کاملاً حق دارد؛ او هیچ تضمینی ندارد که هر نوع T که در آینده استفاده خواهد شد، حتماً یک پراپرتی به نام Id خواهد داشت. ممکن است کسی بخواهد از GenericRepository<string> استفاده کند و رشته‌ها پراپرتی Id ندارند. برای حل این مشکل، باید به کامپایلر تضمین دهیم که T همیشه یک نوع با قابلیت داشتن Id خواهد بود.

عبارت where: اعمال تحدیدها

برای اعمال تحدید بر روی یک پارامتر نوع، از کلمه‌ی کلیدی where پس از تعریف امضای کلاس یا متد استفاده می‌کنیم. سینتکس کلی آن به این صورت است:

public class MyGenericClass<T> where T : /* constraint */ { }

انواع مختلفی از تحدیدها وجود دارند که می‌توانیم از آن‌ها استفاده کنیم.

تحدید کلاس پایه یا اینترفیس

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

بیایید مشکل GenericRepository را حل کنیم. ابتدا یک اینترفیس ساده تعریف می‌کنیم که قرارداد "داشتن یک Id" را مشخص کند.

Copy Icon Program.cs
public interface IEntity
{
    int Id { get; }
}

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

Copy Icon Program.cs
public class GenericRepository<T> where T : IEntity
{
    protected readonly List<T> _items = new();
    
    public T GetById(int id)
    {
        // This now works perfectly! The compiler knows T has an Id property.
        return _items.FirstOrDefault(item => item.Id == id);
    }
    // ... other methods
}

انواع دیگر تحدیدها

علاوه بر تحدید به کلاس پایه یا اینترفیس، چند نوع تحدید دیگر نیز وجود دارد:

  • where T : class: این تحدید تضمین می‌کند که T باید یک نوع ارجاعی (کلاس، اینترفیس، delegate یا آرایه) باشد. این به شما اجازه می‌دهد تا مثلاً T را با `null` مقایسه کنید.
  • where T : struct: این تحدید تضمین می‌کند که T باید یک نوع مقداری غیرپوچ‌پذیر باشد.
  • where T : new(): این تحدید تضمین می‌کند که T باید یک سازنده‌ی عمومی و بدون پارامتر داشته باشد. این به شما اجازه می‌دهد تا از داخل کد جنریک خود، نمونه‌های جدیدی از T بسازید (new T()). این تحدید باید همیشه در انتهای لیست تحدیدها قرار گیرد.

ترکیب چندین تحدید

شما می‌توانید چندین تحدید را برای یک پارامتر نوع با استفاده از کاما ترکیب کنید.

Copy Icon Program.cs
public class SomeGenericClass<T> where T : class, IEntity, new()
{
    public T CreateNewItem()
    {
        // We can do this because of the 'new()' constraint.
        T newItem = new T(); 
        
        // We can access 'Id' because of the 'IEntity' constraint.
        Console.WriteLine(newItem.Id);
        
        return newItem;
    }
}

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