مقدمه
در درس قبل با کالکشنهای جنریک مانند List<T> و Dictionary<TKey, TValue> آشنا شدیم. اما معنای
دقیق علامتهای <T> یا <TKey,
TValue> چیست؟ این سینتکس، نشاندهندهی یکی از
قدرتمندترین ویژگیهای زبان C# یعنی جنریکها
(Generics) است. جنریکها به ما اجازه میدهند تا کلاسها،
اینترفیسها و متدهایی طراحی کنیم که بتوانند با هر نوع دادهای کار کنند، بدون
اینکه امنیت نوع (type safety) و کارایی را فدا کنیم. در واقع، ما الگوریتم را یک
بار مینویسیم و نوع دادهای که قرار است روی آن کار کند را به عنوان یک پارامتر به
آن میدهیم. این قابلیت، استفاده مجدد از کد را به سطح جدیدی میرساند.
مشکل قبل از دوران جنریکها
برای درک بهتر ارزش جنریکها، باید نگاهی به دنیای قبل از آن بیندازیم. در نسخههای اولیهی
.NET، کالکشنها در فضای نام System.Collections قرار داشتند (مانند ArrayList) و
جنریک نبودند. این کالکشنها برای اینکه بتوانند هر نوع دادهای را در خود ذخیره کنند، با نوع
پایهی object کار میکردند. این رویکرد دو مشکل اساسی داشت:
- عدم امنیت نوع (Lack of Type Safety): شما میتوانستید هر نوع دادهای را به
یک ArrayList اضافه کنید. این یعنی ممکن بود به اشتباه یک string را در لیستی که انتظار
int داشتید قرار دهید و این خطا تنها در زمان اجرا، هنگام تلاش برای استفاده از داده، مشخص
میشد.
- سربار عملکردی (Performance Overhead): وقتی یک نوع مقداری (مانند int) را
در یک ArrayList قرار میدادید، فرآیندی به نام Boxing رخ میداد که در آن،
مقدار در یک شیء روی حافظهی Heap بستهبندی میشد. هنگام خواندن مقدار نیز، فرآیند معکوس یعنی
Unboxing و تبدیل نوع (casting) لازم بود که هر دو باعث کاهش کارایی میشدند.
Program.cs
using System.Collections;
ArrayList list = new ArrayList();
list.Add(42);
list.Add("hello");
int myNumber = (int)list[0];
جنریکها به کمک میآیند
جنریکها این دو مشکل را به طور همزمان حل میکنند. وقتی از یک کالکشن جنریک مانند List<T>
استفاده میکنیم، T یک پارامتر نوع (Type Parameter) است. این یک جایگاه
موقت برای یک نوع واقعی است که ما هنگام تعریف متغیر، آن را مشخص میکنیم.
Program.cs
var numbers = new List<int>();
numbers.Add(42);
int myNumber = numbers[0];
با استفاده از List<int> ما یک کالکشن داریم که هم از نظر نوع امن است (کامپایلر جلوی افزودن
دادههای نامعتبر را میگیرد) و هم کارایی بالایی دارد (چون برای انواع مقداری، فرآیند boxing
رخ نمیدهد).
ایجاد کلاسها و متدهای جنریک
زیبایی جنریکها این است که ما محدود به استفاده از کالکشنهای استاندارد نیستیم. ما میتوانیم
کلاسها، اینترفیسها و متدهای جنریک خودمان را بسازیم.
کلاسهای جنریک
یک کلاس جنریک، کلاسی است که با یک یا چند پارامتر نوع تعریف میشود. این به ما اجازه میدهد تا
کلاسی بنویسیم که منطق آن مستقل از نوع دادهای است که نگهداری میکند.
Program.cs
public class DataStore<T>
{
public T Data { get; set; }
}
DataStore<string> stringStore = new();
stringStore.Data = "Some important data";
DataStore<bool> boolStore = new();
boolStore.Data = true;
کلاس DataStore<T> میتواند برای نگهداری هر نوع دادهای استفاده شود و ما در هر مورد، از امنیت
نوع کامل برخورداریم.
متدهای جنریک
گاهی اوقات تنها یک متد خاص نیاز به جنریک بودن دارد، نه کل کلاس. یک متد جنریک، پارامتر نوع خود را
مستقیماً پس از نامش تعریف میکند. یک مثال کلاسیک، متدی برای جابهجا کردن مقدار دو متغیر است.
Program.cs
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
int x = 5, y = 10;
Swap(ref x, ref y);
Console.WriteLine($"x: {x}, y: {y}");
string s1 = "Hello", s2 = "World";
Swap(ref s1, ref s2);
Console.WriteLine($"s1: {s1}, s2: {s2}");
متد Swap<T> میتواند برای جابهجایی هر دو متغیری از هر نوعی به کار رود. در بیشتر موارد،
کامپایلر میتواند نوع T را از روی آرگومانهای ورودی تشخیص دهد و نیازی به مشخص کردن صریح آن
(مثلاً Swap<int>(...)) نیست.