مقدمه

در درس قبل با کالکشن‌های جنریک مانند List<T> و Dictionary<TKey, TValue> آشنا شدیم. اما معنای دقیق علامت‌های <T> یا <TKey, TValue> چیست؟ این سینتکس، نشان‌دهنده‌ی یکی از قدرتمندترین ویژگی‌های زبان C# یعنی جنریک‌ها (Generics) است. جنریک‌ها به ما اجازه می‌دهند تا کلاس‌ها، اینترفیس‌ها و متدهایی طراحی کنیم که بتوانند با هر نوع داده‌ای کار کنند، بدون اینکه امنیت نوع (type safety) و کارایی را فدا کنیم. در واقع، ما الگوریتم را یک بار می‌نویسیم و نوع داده‌ای که قرار است روی آن کار کند را به عنوان یک پارامتر به آن می‌دهیم. این قابلیت، استفاده مجدد از کد را به سطح جدیدی می‌رساند.

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

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

  1. عدم امنیت نوع (Lack of Type Safety): شما می‌توانستید هر نوع داده‌ای را به یک ArrayList اضافه کنید. این یعنی ممکن بود به اشتباه یک string را در لیستی که انتظار int داشتید قرار دهید و این خطا تنها در زمان اجرا، هنگام تلاش برای استفاده از داده، مشخص می‌شد.
  2. سربار عملکردی (Performance Overhead): وقتی یک نوع مقداری (مانند int) را در یک ArrayList قرار می‌دادید، فرآیندی به نام Boxing رخ می‌داد که در آن، مقدار در یک شیء روی حافظه‌ی Heap بسته‌بندی می‌شد. هنگام خواندن مقدار نیز، فرآیند معکوس یعنی Unboxing و تبدیل نوع (casting) لازم بود که هر دو باعث کاهش کارایی می‌شدند.
Copy Icon Program.cs
using System.Collections;

ArrayList list = new ArrayList();
list.Add(42); // Boxing happens here
list.Add("hello"); // Mixing types is possible, which is risky.

// We need to cast back to int, which is unsafe and requires unboxing.
int myNumber = (int)list[0]; 

// This would cause a runtime error (InvalidCastException).
// int anotherNumber = (int)list[1];

جنریک‌ها به کمک می‌آیند

جنریک‌ها این دو مشکل را به طور همزمان حل می‌کنند. وقتی از یک کالکشن جنریک مانند List<T> استفاده می‌کنیم، T یک پارامتر نوع (Type Parameter) است. این یک جایگاه موقت برای یک نوع واقعی است که ما هنگام تعریف متغیر، آن را مشخص می‌کنیم.

Copy Icon Program.cs
// We specify that this list can ONLY hold integers.
var numbers = new List<int>();
numbers.Add(42); // No boxing needed.

// This line will cause a COMPILE-TIME error. The error is caught early!
// numbers.Add("hello"); // Error: cannot convert from 'string' to 'int'

// No casting is needed when retrieving the item.
int myNumber = numbers[0];

با استفاده از List<int> ما یک کالکشن داریم که هم از نظر نوع امن است (کامپایلر جلوی افزودن داده‌های نامعتبر را می‌گیرد) و هم کارایی بالایی دارد (چون برای انواع مقداری، فرآیند boxing رخ نمی‌دهد).

ایجاد کلاس‌ها و متدهای جنریک

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

کلاس‌های جنریک

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

Copy Icon Program.cs
// A generic class with a type parameter 'T'.
public class DataStore<T>
{
    public T Data { get; set; }
}

// --- Usage ---
// Create an instance for storing a string.
DataStore<string> stringStore = new();
stringStore.Data = "Some important data";

// Create an instance for storing a boolean.
DataStore<bool> boolStore = new();
boolStore.Data = true;

کلاس DataStore<T> می‌تواند برای نگهداری هر نوع داده‌ای استفاده شود و ما در هر مورد، از امنیت نوع کامل برخورداریم.

متدهای جنریک

گاهی اوقات تنها یک متد خاص نیاز به جنریک بودن دارد، نه کل کلاس. یک متد جنریک، پارامتر نوع خود را مستقیماً پس از نامش تعریف می‌کند. یک مثال کلاسیک، متدی برای جابه‌جا کردن مقدار دو متغیر است.

Copy Icon Program.cs
static void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

// --- Usage ---
int x = 5, y = 10;
Swap(ref x, ref y); // T is inferred as int
Console.WriteLine($"x: {x}, y: {y}"); // Output: x: 10, y: 5

string s1 = "Hello", s2 = "World";
Swap(ref s1, ref s2); // T is inferred as string
Console.WriteLine($"s1: {s1}, s2: {s2}"); // Output: s1: World, s2: Hello

متد Swap<T> می‌تواند برای جابه‌جایی هر دو متغیری از هر نوعی به کار رود. در بیشتر موارد، کامپایلر می‌تواند نوع T را از روی آرگومان‌های ورودی تشخیص دهد و نیازی به مشخص کردن صریح آن (مثلاً Swap<int>(...)) نیست.