مقدمه
در زبان C# و فریمورک .NET، تمام انواع داده به دو دستهی اصلی تقسیم
میشوند: نوعهای مقداری (Value Types) و نوعهای ارجاعی (Reference
Types). درک تفاوت بین این دو دسته یکی از اساسیترین و مهمترین مفاهیم در
برنامهنویسی C# است، زیرا نحوهی ذخیرهسازی دادهها در حافظه، مدیریت حافظه، عملکرد
برنامه و حتی رفتار کد شما را عمیقاً تحت تأثیر قرار میدهد. در درس قبل با struct و enum آشنا شدیم که هر دو
نمونههایی از نوعهای مقداری هستند. در مقابل، انواع پرکاربردی مانند کلاسها از
نوع ارجاعی هستند. در این درس، این دو دسته را به تفصیل بررسی کرده و تفاوتهای بنیادین آنها را با
مثالهای
عملی روشن خواهیم کرد.
نوعهای مقداری (Value Types)
نوعهای مقداری، همانطور که از نامشان پیداست، متغیرهایی هستند که مقدار داده را
مستقیماً در خود جای میدهند. وقتی یک متغیر از نوع مقداری تعریف میکنید، بخشی از
حافظه به آن اختصاص داده میشود که خود داده در آن قرار میگیرد. این متغیرها معمولاً در بخشی از
حافظه به نام Stack ذخیره میشوند که دسترسی به آن بسیار سریع است.
رفتار در هنگام تخصیص (کپی کردن مقدار)
مهمترین ویژگی نوعهای مقداری، رفتار آنها در زمان تخصیص (assignment) است. وقتی شما مقدار یک
متغیر از نوع مقداری را به متغیر دیگری اختصاص میدهید، یک کپی کامل از مقدار اصلی
ایجاد شده و در متغیر جدید قرار میگیرد. در نتیجه، دو متغیر کاملاً مستقل از هم خواهیم داشت و
تغییر در یکی، هیچ تأثیری بر دیگری نخواهد داشت.
Program.cs
public struct Point { public int X, Y; }
Point p1 = new Point();
p1.X = 10;
Point p2 = p1;
Console.WriteLine($"Before change: p1.X = {p1.X}, p2.X = {p2.X}");
p2.X = 99;
Console.WriteLine($"After change: p1.X = {p1.X}, p2.X = {p2.X}");
اگر این کد را اجرا کنید، خروجی به شکل زیر خواهد بود:
Before change: p1.X = 10, p2.X = 10
After change: p1.X = 10, p2.X = 99
همانطور که مشاهده میکنید، پس از اینکه p2 = p1 اجرا شد، یک کپی از مقادیر p1 در
p2 قرار
گرفت. به همین دلیل، تغییر مقدار p2.X هیچ تأثیری بر p1.X نداشت، زیرا این دو متغیر
به دو مکان
کاملاً مجزا در حافظه اشاره دارند.
تمام structها (مانند نوعهای پایهای عددی مثل int و double) و enumها
از نوع مقداری هستند.
نوعهای ارجاعی (Reference Types)
در مقابل نوعهای مقداری، نوعهای ارجاعی قرار دارند. یک متغیر از نوع ارجاعی، خود داده را ذخیره
نمیکند. در عوض، یک ارجاع (reference) یا به بیان سادهتر، یک آدرس حافظه را در
خود نگه میدارد که به مکانی اشاره میکند که دادهی اصلی در آنجا ذخیره شده است. دادهی اصلی (که
به آن شیء یا object میگوییم) در بخشی از حافظه به نام Heap
نگهداری میشود.
رفتار در هنگام تخصیص (کپی کردن ارجاع)
رفتار نوعهای ارجاعی در زمان تخصیص، تفاوت بنیادین آنها با نوعهای مقداری را آشکار میکند. وقتی
یک متغیر از نوع ارجاعی را به متغیر دیگری اختصاص میدهید، خود شیء کپی نمیشود؛ بلکه فقط
ارجاع (آدرس حافظه) آن کپی میشود. در نتیجه، هر دو متغیر به یک شیء
واحد در حافظهی Heap اشاره خواهند کرد.
Program.cs
public class PointClass { public int X, Y; }
PointClass pc1 = new PointClass();
pc1.X = 10;
PointClass pc2 = pc1;
Console.WriteLine($"Before change: pc1.X = {pc1.X}, pc2.X = {pc2.X}");
pc2.X = 99;
Console.WriteLine($"After change: pc1.X = {pc1.X}, pc2.X = {pc2.X}");
خروجی این کد شما را شگفتزده خواهد کرد:
Before change: pc1.X = 10, pc2.X = 10
After change: pc1.X = 99, pc2.X = 99
در این حالت، چون pc1 و pc2 هر دو به یک شیء در حافظه اشاره میکنند، تغییر شیء از
طریق pc2،
از طریق pc1 نیز قابل مشاهده است. این مفهوم برای درک نحوهی کار با اشیاء در C#
بسیار
حیاتی است.
تمام کلاسها (class)، اینترفیسها (interface) و نمایندهها (delegate) از
نوع ارجاعی هستند.
حافظهی Stack و Heap
برای درک کاملتر تفاوت این دو نوع، باید با دو ناحیه اصلی حافظه که برنامه از آنها استفاده میکند
آشنا شویم: Stack و Heap.
مقایسه Stack و Heap
- Stack (پشته): این بخش از حافظه برای ذخیرهسازی دادههای ایستا (static
memory allocation) به کار میرود. Stack ساختاری از نوع LIFO (Last-In, First-Out) یا
"آخرین ورودی، اولین خروجی" دارد. هرگاه یک متد فراخوانی میشود، یک "فریم" برای آن روی
Stack ایجاد میشود که پارامترها و متغیرهای محلی آن متد را در خود جای میدهد.
نوعهای مقداری و ارجاعها (آدرسها) به اشیاء، در Stack
ذخیره میشوند. تخصیص و آزادسازی حافظه در Stack بسیار سریع است و به صورت خودکار با ورود و
خروج از متدها مدیریت میشود.
- Heap (توده): این بخش برای تخصیص حافظهی پویا (dynamic memory
allocation) استفاده میشود. اشیاء (نمونههای کلاسها) در Heap ذخیره
میشوند. تخصیص حافظه در Heap کندتر از Stack است. مدیریت حافظهی Heap بر عهدهی یک فرآیند
خودکار به نام Garbage Collector (GC) یا "زبالهروب" است که به صورت
دورهای اشیائی را که دیگر هیچ ارجاعی به آنها وجود ندارد، از حافظه پاک میکند.
به طور خلاصه، وقتی یک متغیر از نوع مقداری (مثلاً int x = 5;) تعریف
میکنید، مقدار 5 مستقیماً روی Stack قرار میگیرد. اما وقتی یک متغیر از نوع ارجاعی (مثلاً PointClass p = new PointClass();) تعریف میکنید، شیء
PointClass در
Heap ایجاد میشود و متغیر p که روی Stack قرار دارد، فقط آدرس آن شیء را در خود نگه
میدارد.
ارسال پارامتر به متدها
تفاوت بین نوعهای مقداری و ارجاعی، نحوهی ارسال پارامترها به متدها را نیز تحت تأثیر قرار میدهد.
ارسال نوعهای مقداری (Pass by Value)
وقتی یک نوع مقداری را به عنوان پارامتر به یک متد ارسال میکنید، یک کپی از آن
مقدار به متد فرستاده میشود. هر تغییری که متد روی این کپی اعمال کند، هیچ تأثیری بر متغیر اصلی در
خارج از متد نخواهد داشت.
Program.cs
static void IncrementValue(int number)
{
number = number + 1;
Console.WriteLine($"Value inside method: {number}");
}
int myValue = 5;
Console.WriteLine($"Value before calling method: {myValue}");
IncrementValue(myValue);
Console.WriteLine($"Value after calling method: {myValue}");
خروجی کد بالا نشان میدهد که متغیر اصلی دستنخورده باقی مانده است:
Value before calling method: 5
Value inside method: 6
Value after calling method: 5
ارسال نوعهای ارجاعی
وقتی یک نوع ارجاعی را به متد ارسال میکنید، اتفاقی که میافتد این است که یک کپی از
ارجاع به متد ارسال میشود. از آنجایی که این ارجاعِ کپیشده هنوز به همان شیء
اصلی در Heap اشاره میکند، متد میتواند از طریق آن، شیء اصلی را تغییر دهد.
Program.cs
static void ChangeName(Person person)
{
person.Name = "Jane Doe";
}
public class Person { public string Name; }
Person myPerson = new Person { Name = "John Doe" };
Console.WriteLine($"Name before: {myPerson.Name}");
ChangeName(myPerson);
Console.WriteLine($"Name after: {myPerson.Name}");
خروجی این کد نشان میدهد که شیء اصلی تغییر کرده است:
Name before: John Doe
Name after: Jane Doe
خلاصه تفاوتها
جدول زیر تفاوتهای کلیدی بین نوعهای مقداری و ارجاعی را خلاصه میکند:
ویژگی |
نوعهای مقداری (Value Types) |
نوعهای ارجاعی (Reference Types) |
محتوای متغیر |
خود داده |
ارجاع (آدرس) به داده |
محل ذخیرهسازی |
معمولاً Stack |
شیء در Heap، ارجاع در Stack |
رفتار در تخصیص |
کپی کردن مقدار |
کپی کردن ارجاع |
مدیریت حافظه |
خودکار (با خروج از محدوده) |
توسط Garbage Collector (GC) |
مقدار پیشفرض |
صفر برای اعداد، false برای bool |
null (پوچ) |
مثالها |
int, double, bool, char, struct, enum |
class, interface, delegate, string, object, array |
در این درس، یاد گرفتید که نوعهای مقداری و ارجاعی در C# چه تفاوتهایی دارند، هرکدام
چگونه در
حافظه ذخیره میشوند و رفتارشان هنگام تخصیص و ارسال به متدها چگونه است. درک این مفاهیم برای نوشتن
کدهای بهینه و جلوگیری از خطاهای رایج بسیار مهم است.
در درس بعدی، با نوعهای پوچپذیر (Nullable Types) در C# آشنا خواهید شد و یاد میگیرید چگونه
میتوان متغیرهای مقداری را به گونهای تعریف کرد که بتوانند مقدار null نیز داشته باشند.