9 تفاوت کلیدی Struct و Class در #C (همراه با مثال‌های عملی)

در زبان برنامه‌نویسی C#، هم class (کلاس) و هم struct (ساختار) به عنوان "بلوک‌های سازنده" یا "طرح‌های اولیه" (blueprints) برای ایجاد اشیاء عمل می‌کنند. هر دو می‌توانند شامل فیلدها، خصوصیات (Properties)، متدها و رویدادها باشند. در نگاه اول، سینتکس (syntax) تعریف آن‌ها بسیار شبیه‌ به هم به نظر می‌رسد، اما در زیر این شباهت ظاهری، تفاوت‌های بنیادین و بسیار مهمی نهفته است.
کینگتو - آموزش برنامه نویسی تخصصصی - دات نت - سی شارپ - بانک اطلاعاتی و امنیت

9 تفاوت کلیدی Struct و Class در #C (همراه با مثال‌های عملی)

57 بازدید 0 نظر ۱۴۰۴/۰۸/۰۱

تفاوت‌های گفته شده در چکیده، صرفاً آکادمیک نیستند؛ آن‌ها مستقیماً بر نحوه مدیریت حافظه، کارایی (Performance) و رفتار (Behavior) برنامه شما تأثیر می‌گذارند. درک عمیق این تفاوت‌ها یکی از نشانه‌های یک توسعه‌دهنده باتجربه C# است. انتخاب اشتباه بین struct و class می‌تواند منجر به باگ‌های پنهان یا مشکلات جدی در بهینه‌سازی برنامه شود.

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

 

1. تفاوت بنیادین: نوع مقداری (Value Type) در برابر نوع مرجع (Reference Type)

این مهم‌ترین و اساسی‌ترین تفاوت است و منشأ تمام تفاوت‌های دیگر می‌باشد.

  • Struct (ساختار) یک نوع مقداری (Value Type) است:

    • وقتی شما یک متغیر از نوع struct ایجاد می‌کنید، آن متغیر خودِ داده‌ها را در خود نگه می‌دارد.

    • مانند انواع داده‌ای اولیه (primitive types) مثل int, double, bool.

  • Class (کلاس) یک نوع مرجع (Reference Type) است:

    • وقتی شما یک متغیر از نوع class ایجاد می‌کنید، آن متغیر داده‌ها را مستقیماً نگه نمی‌دارد؛ بلکه آدرس (یا مرجعی) به محل قرارگیری داده‌ها در حافظه را نگه می‌دارد.

    • مانند string یا آرایه‌ها.

مثال مفهومی:

  • Struct (نوع مقداری): آن را مانند یک دفترچه یادداشت فیزیکی در نظر بگیرید. اگر شما دفترچه خود را (که حاوی اطلاعات است) به دوستتان بدهید، در واقع یک کپی کامل (فتوکپی) از آن را به او می‌دهید. اگر دوست شما در آن کپی چیزی بنویسد، دفترچه اصلی شما تغییری نمی‌کند.

  • Class (نوع مرجع): آن را مانند یک لینک گوگل داک (Google Doc) در نظر بگیرید. اگر شما لینک سند را برای دوستتان بفرستید، شما یک کپی از سند را نفرستاده‌اید، بلکه یک کپی از لینک را فرستاده‌اید. اکنون هر دوی شما به یک سند واحد دسترسی دارید. اگر دوست شما آن را ویرایش کند، شما نیز تغییرات را خواهید دید، زیرا هر دو به یک مکان واحد در حافظه (سرور) اشاره می‌کنید.

 

2. تخصیص حافظه: پشته (Stack) در برابر هیپ (Heap)

این تفاوت مستقیماً از مورد اول ناشی می‌شود.

  • Struct (پشته - Stack):

    • از آنجایی که structها انواع مقداری هستند، معمولاً (اگر متغیر محلی در یک متد باشند) در پشته (Stack) ذخیره می‌شوند.

    • پشته یک ناحیه از حافظه با مدیریت بسیار سریع (LIFO - Last-In, First-Out) است. تخصیص و آزادسازی حافظه در پشته بسیار کم‌هزینه است و به سادگی با جابجا کردن یک اشاره‌گر انجام می‌شود. حافظه به طور خودکار پس از خروج از متد آزاد می‌شود.

    • نکته: اگر یک struct عضوی (فیلدی) از یک class باشد، آنگاه همراه با آن class در هیپ (Heap) ذخیره می‌شود (اما به صورت درون‌خطی (inline) درون خود آبجکت کلاس).

  • Class (هیپ - Heap):

    • اشیاء ایجاد شده از classها (با کلمه کلیدی new) در ناحیه حافظه هیپ (Heap) ذخیره می‌شوند.

    • هیپ برای داده‌هایی است که طول عمر طولانی‌تری دارند و نمی‌توان پیش‌بینی کرد چه زمانی باید آزاد شوند.

    • تخصیص حافظه در هیپ کندتر از پشته است.

    • مهم‌تر از آن، مدیریت حافظه در هیپ بر عهده زباله‌روب (Garbage Collector - GC) است. GC باید به صورت دوره‌ای هیپ را اسکن کند تا اشیائی را که دیگر مرجعی به آن‌ها وجود ندارد، پیدا و حذف کند. این فرآیند می‌تواند باعث ایجاد "وقفه‌های" کوچک (GC Pauses) در اجرای برنامه شود.

 

3. رفتار کپی‌برداری (Assignment)

این تفاوت را با یک مثال عملی بررسی می‌کنیم:

مثال با Class:

فرض کنید یک کلاس ساده PointClass داریم:

public class PointClass
{
    public int X, Y;
}

// ... در متد Main ...
PointClass p1 = new PointClass { X = 10, Y = 20 };
PointClass p2 = p1; // **کپی کردن مرجع**

// تغییر p2، باعث تغییر p1 هم می‌شود
p2.X = 100;

Console.WriteLine($"p1.X: {p1.X}"); // خروجی: p1.X: 100
Console.WriteLine($"p2.X: {p2.X}"); // خروجی: p2.X: 100

تحلیل: p1 و p2 هر دو به یک آبجکت واحد در هیپ اشاره می‌کنند. تغییر یکی، دیگری را نیز تحت تأثیر قرار می‌دهد.

مثال با Struct:

حالا همان مثال با PointStruct:

public struct PointStruct
{
    public int X, Y;
}

// ... در متد Main ...
PointStruct s1 = new PointStruct { X = 10, Y = 20 };
PointStruct s2 = s1; // **کپی کردن تمام داده‌ها**

// تغییر s2، هیچ تأثیری بر s1 ندارد
s2.X = 100;

Console.WriteLine($"s1.X: {s1.X}"); // خروجی: s1.X: 10
Console.WriteLine($"s2.X: {s2.X}"); // خروجی: s2.X: 100

تحلیل: وقتی s2 = s1 اجرا شد، یک کپی کامل از داده‌های s1 در متغیر s2 ایجاد شد. s1 و s2 دو موجودیت کاملاً مستقل در پشته هستند.

 

4. ارسال به متدها (Passing to Methods)

این تفاوت مشابه مورد قبلی است و عواقب مهمی دارد.

  • وقتی یک Class را به متد پاس می‌دهید، مرجع آن (آدرس) کپی و ارسال می‌شود. متد می‌تواند آبجکت اصلی را تغییر دهد.

  • وقتی یک Struct را به متد پاس می‌دهید، یک کپی کامل از آن ساخته و به متد ارسال می‌شود. متد فقط روی کپی کار می‌کند و آبجکت اصلی دست‌نخورده باقی می‌ماند.

مثال:

public static void ChangeClass(PointClass p)
{
    p.X = 500; // آبجکت اصلی را تغییر می‌دهد
}

public static void ChangeStruct(PointStruct s)
{
    s.X = 500; // فقط کپی محلی را تغییر می‌دهد
}

// ... در متد Main ...
PointClass p_main = new PointClass { X = 10 };
PointStruct s_main = new PointStruct { X = 10 };

ChangeClass(p_main);
ChangeStruct(s_main);

Console.WriteLine($"Class: {p_main.X}");   // خروجی: Class: 500
Console.WriteLine($"Struct: {s_main.X}"); // خروجی: Struct: 10

نکته: برای تغییر struct اصلی در یک متد، باید صراحتاً آن را با کلمات کلیدی ref یا out ارسال کنید که در این صورت به جای کپی، مرجع آن ارسال می‌شود.

 

5. وراثت (Inheritance)

این یک تفاوت ساختاری بزرگ است.

  • Classes: به طور کامل از وراثت پیاده‌سازی (Implementation Inheritance) پشتیبانی می‌کنند. یک کلاس می‌تواند از یک کلاس پایه دیگر ارث‌بری کند (اما نه از چند کلاس).

    public class Employee { ... }
    public class Manager : Employee { ... } // معتبر
    
  • Structs: از وراثت پیاده‌سازی پشتیبانی نمی‌کنند. یک struct نمی‌تواند از struct یا class دیگری ارث‌بری کند.

    public struct Shape { ... }
    public struct Rectangle : Shape { ... } // خطا در زمان کامپایل
    

    (تمام structها به طور ضمنی از System.ValueType ارث می‌برند که آن هم از System.Object ارث می‌برد).

  • نکته مهم (اینترفیس‌ها): هم class و هم struct می‌توانند اینترفیس‌ها (Interfaces) را پیاده‌سازی کنند.

    public interface IDrawable { void Draw(); }
    public class Circle : IDrawable { ... } // معتبر
    public struct Square : IDrawable { ... } // معتبر
    

 

6. مقدار پیش‌فرض و null

  • Classes: به عنوان انواع مرجع، می‌توانند مقدار null داشته باشند. null یعنی متغیر به هیچ آبجکتی در هیپ اشاره نمی‌کند.

    PointClass p = null; // کاملاً معتبر
    if (p == null)
    {
        // ...
    }
    
  • Structs: به عنوان انواع مقداری، نمی‌توانند null باشند. آن‌ها همیشه یک مقدار دارند.

    PointStruct s = null; // خطا در زمان کامپایل
    

    مقدار پیش‌فرض یک struct (مثلاً با new PointStruct()) حالتی است که تمام فیلدهای آن با مقدار پیش‌فرض خودشان (0 برای اعداد، false برای bool و null برای رفرنس‌ها) پر شده‌اند.

  • راه حل (Nullable Types): اگر نیاز دارید که یک struct قابلیت null شدن داشته باشد، باید از Nullable یا سینتکس ? استفاده کنید.

    PointStruct? s_nullable = null; // معتبر
    

 

7. سازنده‌ها (Constructors)

در اینجا تفاوت‌های ظریفی وجود دارد (که با C# 10 کمی تغییر کرد).

  • Classes: می‌توانند یک سازنده پیش‌فرض (بدون پارامتر) صریح داشته باشند. اگر نسازید، کامپایلر یکی برایتان می‌سازد.

    public class MyClass
    {
        public MyClass() { Console.WriteLine("Constructor!"); }
    }
    
  • Structs:

    • قبل از C# 10: شما نمی‌توانستید یک سازنده بدون پارامتر صریح تعریف کنید. کامپایلر همیشه یک سازنده پیش‌فرض (که تمام فیلدها را صفر می‌کرد) ارائه می‌داد.

    • از C# 10 به بعد: شما می‌توانید یک سازنده بدون پارامتر صریح تعریف کنید.

    • قانون مهم: اگر در یک struct هر نوع سازنده‌ای (با پارامتر) تعریف کنید، باید در آن سازنده، تمام فیلدهای struct را مقداردهی اولیه کنید. کلاس‌ها چنین الزامی ندارند (فیلدهای مقداردهی نشده، مقدار پیش‌فرض می‌گیرند).

    public struct Report
    {
        public int ID;
        public string Title;
    
        public Report(int id) // خطا! Title مقداردهی نشده
        {
            this.ID = id;
            // this.Title = "Default"; // باید این خط را اضافه کرد
        }
    }
    

 

8. تغییرناپذیری (Immutability)

  • Structs: برای نمایش داده‌های تغییرناپذیر (Immutable) بسیار ایده‌آل هستند. از آنجایی که آن‌ها کپی می‌شوند، شما به طور تصادفی اصل داده را در جای دیگری از کد تغییر نمی‌دهید. این ویژگی به ایمنی کد (Thread-Safety) و قابل پیش‌بینی بودن آن کمک می‌کند.

  • در C# می‌توان با کلمه کلیدی readonly struct، تغییرناپذیری را در سطح کامپایلر تضمین کرد.

مثال عالی برای readonly struct:

public readonly struct Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    // متدهایی که "تغییر" ایجاد می‌کنند، یک نمونه جدید برمی‌گردانند
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Currencies must match.");
        
        // 'this' را تغییر نمی‌دهد، یک struct جدید برمی‌گرداند
        return new Money(this.Amount + other.Amount, this.Currency);
    }
}
  • Classes: هم می‌توانند تغییرناپذیر طراحی شوند (با readonly کردن فیلدها و خصوصی کردن set ها)، اما struct ها به طور طبیعی تمایل بیشتری به این الگو دارند.

 

 

9. کارایی و سربار (Performance & Overhead)

اینجاست که انتخاب درست، اهمیت حیاتی پیدا می‌کند.

  • Struct (جوانب مثبت):

    1. بدون فشار بر GC: اگر در پشته تخصیص یابند، هیچ فشاری بر زباله‌روب (GC) وارد نمی‌کنند. این برای حلقه‌هایی که میلیون‌ها آبجکت کوتاه‌عمر می‌سازند (مثل شبیه‌سازی فیزیک یا سیستم ذرات در بازی) حیاتی است.

    2. حافظه پیوسته (Cache Locality): آرایه‌ای از structها (مثلاً PointStruct[1000]) یک بلوک حافظه پیوسته است. پردازنده (CPU) می‌تواند این داده‌ها را به سرعت در کش خود بارگذاری کند. اما آرایه‌ای از classها (PointClass[1000]) در واقع آرایه‌ای از اشاره‌گرها است و خود آبجکت‌ها در نقاط مختلف هیپ پراکنده هستند که منجر به "Cache Miss" های زیاد و کاهش سرعت می‌شود.

  • Struct (جوانب منفی):

    1. سربار کپی کردن: اگر struct شما بزرگ باشد (مثلاً بیش از 16 بایت، اگرچه این یک قانون قطعی نیست) و شما آن را مکرراً به متدها پاس دهید، هزینه کپی کردن تمام آن داده‌ها می‌تواند از هزینه کپی کردن یک مرجع 64 بیتی (در کلاس) بیشتر شود.

    2. Boxing و Unboxing: اگر مجبور شوید یک struct را در جایی استفاده کنید که به یک object نیاز است (مثلاً در نسخه‌های قدیمی ArrayList یا فراخوانی متدی با پارامتر object)، پدیده Boxing رخ می‌دهد. یعنی سیستم یک "جعبه" (یک آبجکت کلاس) در هیپ می‌سازد و داده‌های struct را در آن کپی می‌کند. این فرآیند بسیار پرهزینه است.

  • Class:

    • سربار GC: نقطه ضعف اصلی کلاس‌ها، فشار بر GC است.

    • ارسال ارزان: ارسال آن‌ها به متدها همیشه ارزان است (فقط کپی یک آدرس).

 

چه زمانی از Struct استفاده کنیم و چه زمانی از Class؟ (راهنمای نهایی)

پس از این 9 تفاوت، به یک قانون سرانگشتی می‌رسیم:

قانون اول: در صورت تردید، از Class استفاده کنید.

Class انتخاب پیش‌فرض و ایمن در C# است. رفتار آن قابل پیش‌بینی‌تر است و اکثر توسعه‌دهندگان با آن آشناتر هستند.

 

از Class استفاده کنید اگر:

  • آبجکت شما نشان‌دهنده یک "موجودیت" (Entity) با "هویت" (Identity) مشخص است (مانند Customer, Order, DatabaseConnection). شما می‌خواهید چندین مرجع به همان مشتری داشته باشید.

  • آبجکت شما داده‌های زیادی دارد و بزرگ است.

  • شما به وراثت نیاز دارید.

  • آبجکت شما باید تغییرپذیر (Mutable) باشد و تغییرات آن در همه‌جا دیده شود.

  • طول عمر آبجکت طولانی است.

 

از Struct استفاده کنید اگر (و فقط اگر):

  • آبجکت شما کوچک و ساده است (مانند یک نوع داده اولیه).

  • مفهوم آبجکت بیشتر شبیه "مقدار" است تا "هویت" (مانند Point, Color, Vector3, DateTime, KeyValuePair).

  • آبجکت شما تغییرناپذیر (Immutable) است یا باید باشد.

  • شما در حال ایجاد تعداد بسیار زیادی از این آبجکت‌ها هستید (مثلاً میلیون‌ها) و می‌خواهید از فشار بر GC جلوگیری کنید (این یک بهینه‌سازی پیشرفته است).

 

نتیجه‌گیری

تفاوت Struct و Class در C# یکی از عمیق‌ترین مفاهیم این زبان است. این تفاوت فراتر از سینتکس و مربوط به معماری بنیادی نحوه مدیریت داده و حافظه در دات‌نت است.

  • Class یک نوع مرجع (Reference Type) است: مانند یک لینک گوگل داک، ارزان ارسال می‌شود، اما تغییرات از همه‌جا قابل مشاهده است و در هیپ (Heap) زندگی می‌کند.

  • Struct یک نوع مقداری (Value Type) است: مانند یک کپی فیزیکی از سند، در زمان ارسال کپی می‌شود، ایمن و ایزوله است و معمولاً در پشته (Stack) زندگی می‌کند.

انتخاب هوشمندانه بین این دو، کلید نوشتن کدهایی است که نه تنها به درستی کار می‌کنند، بلکه بهینه، کارآمد و قابل نگهداری هستند.

 
 
لینک استاندارد شده: EzBbVSX

0 نظر

    هنوز نظری برای این مقاله ثبت نشده است.
جستجوی مقاله و آموزش
دوره‌ها با تخفیفات ویژه