مقدمه

در زبان C# و فریم‌ورک .NET، تمام انواع داده به دو دسته‌ی اصلی تقسیم می‌شوند: نوع‌های مقداری (Value Types) و نوع‌های ارجاعی (Reference Types). درک تفاوت بین این دو دسته یکی از اساسی‌ترین و مهم‌ترین مفاهیم در برنامه‌نویسی C# است، زیرا نحوه‌ی ذخیره‌سازی داده‌ها در حافظه، مدیریت حافظه، عملکرد برنامه و حتی رفتار کد شما را عمیقاً تحت تأثیر قرار می‌دهد. در درس قبل با struct و enum آشنا شدیم که هر دو نمونه‌هایی از نوع‌های مقداری هستند. در مقابل، انواع پرکاربردی مانند کلاس‌ها از نوع ارجاعی هستند. در این درس، این دو دسته را به تفصیل بررسی کرده و تفاوت‌های بنیادین آن‌ها را با مثال‌های عملی روشن خواهیم کرد.

نوع‌های مقداری (Value Types)

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

رفتار در هنگام تخصیص (کپی کردن مقدار)

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

Copy Icon Program.cs
// Using the Point struct from the previous lesson
public struct Point { public int X, Y; }

// Create a Point variable (value type)
Point p1 = new Point();
p1.X = 10;

// Assign p1 to p2. The value is COPIED.
Point p2 = p1;

Console.WriteLine($"Before change: p1.X = {p1.X}, p2.X = {p2.X}");

// Change p2. It does NOT affect p1.
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 اشاره خواهند کرد.

Copy Icon Program.cs
// A simple class (reference type)
public class PointClass { public int X, Y; }

// Create a PointClass instance (reference type)
PointClass pc1 = new PointClass();
pc1.X = 10;

// Assign pc1 to pc2. The REFERENCE is copied.
PointClass pc2 = pc1;

Console.WriteLine($"Before change: pc1.X = {pc1.X}, pc2.X = {pc2.X}");

// Change pc2. It DOES affect pc1 because they point to the SAME object.
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)

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

Copy Icon 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 اشاره می‌کند، متد می‌تواند از طریق آن، شیء اصلی را تغییر دهد.

Copy Icon 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 نیز داشته باشند.