متغیرها و انواع داده در C#: یک آموزش جامع

فهرست مطالب

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

C#، به عنوان یک زبان برنامه‌نویسی قویاً تایپ شده (strongly-typed)، توسعه‌دهندگان را ملزم می‌کند تا نوع داده هر متغیر را در زمان کامپایل مشخص کنند. این ویژگی به کاهش خطاهای زمان اجرا کمک کرده و خوانایی و قابلیت نگهداری کد را افزایش می‌دهد. سیستم نوع قوی C# به کامپایلر اجازه می‌دهد تا بررسی‌های نوع (type checks) دقیقی را انجام دهد و تضمین می‌کند که عملیات روی داده‌ها به درستی و ایمن انجام شوند. این امر به ویژه در پروژه‌های بزرگ با تیم‌های متعدد برنامه‌نویسی، که در آن هماهنگی و کاهش خطا اهمیت حیاتی دارد، مزیت بزرگی محسوب می‌شود.

در این آموزش جامع، به بررسی دقیق و موشکافانه متغیرها و انواع داده در C# خواهیم پرداخت. از مبانی اعلان و مقداردهی اولیه گرفته تا تفاوت‌های پیچیده بین انواع Value و Reference، تبدیل نوع داده‌ها، و انتخاب بهترین نوع برای سناریوهای مختلف، همه و همه پوشش داده خواهند شد. هدف این است که شما نه تنها با سینتکس آشنا شوید، بلکه درک عمیقی از مکانیزم‌های زیربنایی آن‌ها نیز پیدا کنید تا بتوانید کدی بهینه، خوانا و بدون خطا بنویسید. این آموزش به گونه‌ای طراحی شده که هم برای مبتدیانی که می‌خواهند پایه‌های C# را محکم بنا نهند و هم برای توسعه‌دهندگان باتجربه‌تر که به دنبال تعمیق دانش خود در مفاهیم پیشرفته‌تر مانند Boxing/Unboxing و مدیریت حافظه هستند، مفید باشد.

مبانی متغیرها در C#

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

اعلان و مقداردهی اولیه متغیرها

اعلان متغیر فرآیند تعیین نام و نوع داده برای یک متغیر است. این عمل به کامپایلر اطلاع می‌دهد که چه نوع فضایی در حافظه برای این متغیر رزرو کند. سینتکس پایه برای اعلان یک متغیر به صورت زیر است:


dataType variableName;

در اینجا، dataType نوع داده‌ای است که متغیر قرار است ذخیره کند (مثلاً int برای اعداد صحیح، string برای متن، bool برای مقادیر منطقی)، و variableName نامی است که شما به متغیر می‌دهید. هر دستور اعلان با یک سمی‌کولن (;) خاتمه می‌یابد. به عنوان مثال:


int age; // اعلان متغیری برای ذخیره سن از نوع عدد صحیح
string name; // اعلان متغیری برای ذخیره نام از نوع رشته
bool isActive; // اعلان متغیری برای ذخیره وضعیت فعال بودن از نوع منطقی

مقداردهی اولیه (Initialization) به فرآیند تخصیص یک مقدار اولیه به متغیر در زمان اعلان یا پس از آن گفته می‌شود. این کار اطمینان می‌دهد که متغیر قبل از استفاده حاوی یک مقدار معتبر باشد. توصیه اکید می‌شود که همیشه متغیرها را قبل از استفاده مقداردهی اولیه کنید تا از خطاهای پیش‌بینی نشده (مانند استفاده از مقادیر زباله در حافظه) جلوگیری شود. اگر یک متغیر محلی (local variable) را بدون مقداردهی اولیه استفاده کنید، کامپایلر C# به طور معمول یک خطای زمان کامپایل (compile-time error) صادر می‌کند.


int age = 30; // اعلان و مقداردهی اولیه متغیر age به 30 در یک خط
string name = "علی احمدی"; // اعلان و مقداردهی اولیه متغیر name
bool isActive = true; // اعلان و مقداردهی اولیه متغیر isActive

// می‌توان متغیر را پس از اعلان و در خطوط بعدی مقداردهی اولیه کرد:
double price;
price = 99.99; // مقداردهی اولیه متغیر price
Console.WriteLine($"Price: {price}"); // استفاده از متغیر پس از مقداردهی اولیه

// مثال از خطای استفاده از متغیر محلی بدون مقداردهی اولیه:
// int uninitializedVariable;
// Console.WriteLine(uninitializedVariable); // این خط منجر به خطای زمان کامپایل خواهد شد.

نکته مهم در مورد متغیرهای عضو (fields) در کلاس‌ها و متغیرهای استاتیک (static fields) این است که C# به طور خودکار آن‌ها را با مقادیر پیش‌فرض (default values) مناسب برای نوعشان مقداردهی اولیه می‌کند. به عنوان مثال، برای انواع عددی (مانند int، double) مقدار پیش‌فرض 0 است، برای bool مقدار false و برای انواع ارجاعی (مانند string، کلاس‌ها) مقدار null است. با این حال، این رفتار برای متغیرهای محلی (local variables) درون یک متد یا بلوک کد صدق نمی‌کند و مسئولیت مقداردهی اولیه آن‌ها کاملاً بر عهده برنامه‌نویس است.


public class Product
{
    // این یک فیلد (field) است - به طور خودکار با مقدار پیش‌فرض 0 مقداردهی اولیه می‌شود
    private int productId; 
    // این یک فیلد است - به طور خودکار با مقدار پیش‌فرض null مقداردهی اولیه می‌شود
    private string productName; 

    public void DisplayInfo()
    {
        // این یک متغیر محلی (local variable) است - باید قبل از استفاده مقداردهی اولیه شود
        int quantity; 
        // Console.WriteLine(quantity); // اگر این خط uncomment شود، منجر به خطای کامپایل می‌شود

        quantity = 10; // مقداردهی اولیه متغیر محلی
        Console.WriteLine($"Product ID: {productId}, Name: {productName}, Quantity: {quantity}");
    }
}

همچنین C# امکان اعلان و مقداردهی اولیه چندین متغیر از یک نوع در یک خط را نیز فراهم می‌کند، که می‌تواند کد را فشرده‌تر کند:


int x = 10, y = 20, z = 30; // اعلان و مقداردهی اولیه سه متغیر int در یک خط

قوانین نام‌گذاری متغیرها

انتخاب نام مناسب برای متغیرها در C# نه تنها یک الزام سینتکسی، بلکه یک اصل مهم در نوشتن کد خوانا، قابل درک و قابل نگهداری است. نام‌گذاری مناسب به برنامه‌نویسان دیگر (و خودتان در آینده) کمک می‌کند تا هدف و کاربرد متغیر را به سرعت درک کنند. C# از دستورالعمل‌های خاصی برای نام‌گذاری پیروی می‌کند که عمدتاً توسط مایکروسافت توصیه شده‌اند و بخشی از چارچوب .NET هستند:

  • شروع با حرف یا زیرخط: نام متغیرها باید با یک حرف (a-z, A-Z) یا یک زیرخط (_) شروع شود. اعداد مجاز نیستند که در ابتدای نام قرار گیرند.
  • کاراکترهای مجاز: پس از حرف اول، می‌توان از حروف، اعداد (0-9) و زیرخط استفاده کرد. هیچ کاراکتر خاص دیگری مانند @، #، $ و غیره به صورت عادی مجاز نیستند (به جز @ برای کلمات کلیدی، که در ادامه توضیح داده می‌شود).
  • عدم استفاده از کلمات کلیدی: نمی‌توان از کلمات کلیدی رزرو شده C# (مانند int, class, public, void, for) به عنوان نام متغیر استفاده کرد. با این حال، اگر واقعاً نیاز به استفاده از یک کلمه کلیدی به عنوان نام دارید، می‌توانید با پیشوند @ آن را از یک کلمه کلیدی متمایز کنید (مثلاً string @class = "Mathematics";). این کار معمولاً توصیه نمی‌شود مگر در موارد خاص مانند همخوانی با APIهای دیگر.
  • حساسیت به حروف کوچک و بزرگ (Case-Sensitive): C# به حروف کوچک و بزرگ حساس است، بنابراین myVar و MyVar دو متغیر مجزا محسوب می‌شوند. این ویژگی با بسیاری از زبان‌های دیگر (مانند جاوا) مشترک است.

علاوه بر قوانین سینتکسی، قراردادهای نام‌گذاری (Naming Conventions) نیز در C# برای بهبود خوانایی و استانداردسازی کد اهمیت دارند. این قراردادها اگرچه اجباری نیستند، اما رعایت آن‌ها به شدت توصیه می‌شود و بخش مهمی از “نوشتن کد تمیز” (Clean Code) محسوب می‌شود:

  • CamelCase برای متغیرهای محلی و پارامترها: در این شیوه، حرف اول کلمه اول کوچک و حرف اول کلمات بعدی بزرگ نوشته می‌شود (مثلاً myLocalVariable، inputParameter، calculateSum). این شیوه رایج‌ترین قرارداد برای متغیرهایی است که در داخل متدها یا به عنوان پارامتر متدها تعریف می‌شوند.
  • PascalCase برای متغیرهای عمومی (Public Fields/Properties)، نام کلاس‌ها، نام متدها و Enumerationها: در این شیوه، حرف اول تمام کلمات بزرگ نوشته می‌شود (مثلاً ProductId، CustomerName، CalculateTotal، MyClass، DayOfWeek). این شیوه برای اعضای عمومی و نام‌های نوع استفاده می‌شود تا نشان‌دهنده دسترسی گسترده‌تر آن‌ها باشد.
  • استفاده از نام‌های توصیفی: نام متغیر باید به وضوح نشان‌دهنده هدف و محتوای آن باشد (مثلاً numberOfStudents به جای num، customerAddress به جای ca). این کار باعث می‌شود کد خود-توضیح‌دهنده باشد و نیاز به کامنت‌های اضافی را کاهش دهد.
  • پرهیز از مخفف‌های مبهم: از مخفف‌هایی که ممکن است برای دیگران ناواضح باشند، پرهیز کنید. اگر مخففی رایج و قابل فهم است (مانلاً ID برای Identifier)، استفاده از آن مشکلی ندارد.
  • عدم استفاده از Underscore در ابتدای نام: استفاده از _ در ابتدای نام متغیرها معمولاً برای فیلدهای خصوصی کلاس‌ها (Private Fields) استفاده می‌شود (مثلاً _myPrivateField). برای متغیرهای محلی یا پارامترها، از آن پرهیز کنید.

// مثال‌هایی از نام‌گذاری صحیح و متداول
int studentAge; // CamelCase for local variable
string firstName; 
bool isDataValid;

public class User // PascalCase for class name
{
    public int UserId { get; set; } // PascalCase for public property
    public string UserName { get; set; }

    private string _email; // Underscore for private field

    public void DisplayUserInfo(int userId) // PascalCase for method, CamelCase for parameter
    {
        // ...
    }
}

// مثال‌هایی از نام‌گذاری نادرست (یا نامناسب)
// int 1stNumber; // خطا: شروع با عدد
// string class; // خطا: استفاده از کلمه کلیدی رزرو شده
// int x; // نام نامناسب، غیرتوصیفی، در یک پروژه واقعی باید نام بهتری داشته باشد.

رعایت این قوانین و قراردادها نه تنها به کامپایل شدن کد شما کمک می‌کند، بلکه باعث می‌شود کد شما قابل خواندن‌تر، قابل نگهداری‌تر و در نهایت، با کیفیت‌تر باشد. این یک سرمایه‌گذاری برای آینده پروژه و تیم شماست.

ثابت‌ها در C#

ثابت‌ها (Constants) مقادیری هستند که در طول اجرای برنامه تغییر نمی‌کنند. در C#، ثابت‌ها با استفاده از کلمه کلیدی const تعریف می‌شوند. یک ثابت باید در زمان اعلان مقداردهی اولیه شود و پس از آن نمی‌توان مقدار آن را تغییر داد. مقادیر ثابت باید در زمان کامپایل (compile-time) مشخص باشند؛ یعنی نمی‌توانند نتیجه یک عملیات زمان اجرا (runtime operation) باشند یا از متغیرها یا توابع پویا مقداری را دریافت کنند.

ویژگی‌های مهم ثابت‌ها:

  • با کلمه کلیدی const اعلان می‌شوند.
  • باید در زمان اعلان مقداردهی اولیه شوند و این مقدار باید یک ثابت کامپایل-تایم باشد (یک مقدار لیترال یا یک ثابت دیگر).
  • مقدار آن‌ها در طول عمر برنامه ثابت می‌ماند و قابل تغییر نیست.
  • به صورت ضمنی static هستند (بدون نیاز به کلمه کلیدی static) و می‌توان بدون ایجاد شیء از کلاس به آن‌ها دسترسی داشت (مثلاً MathConstants.PI).
  • برای انواع عددی، bool، char، string، و انواع enum قابل استفاده هستند.
  • نمی‌توانند static readonly باشند، زیرا این دو مفهوم متضادند: const به معنای ثابت بودن در زمان کامپایل است، در حالی که readonly به معنای تخصیص یک‌باره در زمان اجرا یا در سازنده است.

public class MathConstants
{
    // ثابت زمان کامپایل برای عدد پی
    public const double PI = 3.14159; 
    // ثابت زمان کامپایل برای حداکثر مقدار
    public const int MaxValue = 1000; 
    // این یک ثابت رشته ای است.
    public const string CompanyName = "Tech Solutions Inc.";

    public void CalculateCircumference(double radius)
    {
        double circumference = 2 * PI * radius; // استفاده از ثابت PI
        Console.WriteLine($"The circumference is: {circumference}");
    }
}

// استفاده از ثابت‌ها در خارج از کلاس:
// double area = MathConstants.PI * radius * radius;
// Console.WriteLine($"Company: {MathConstants.CompanyName}");

تفاوت const با readonly مهم است و اغلب باعث سردرگمی می‌شود:

const (Compile-Time Constant):

  • مقدار آن در زمان کامپایل تعیین می‌شود و در تمام نقاط استفاده، مستقیماً با مقدار واقعی جایگزین می‌شود.
  • فقط برای انواع پایه و string و enum قابل استفاده است.
  • به طور ضمنی static است و نمی‌تواند static صریح داشته باشد.
  • تغییر مقدار const در کد، نیاز به کامپایل مجدد تمام Assemblyهایی دارد که از آن const استفاده کرده‌اند، زیرا مقدار آن کپی شده است.

readonly (Runtime Constant):

  • یک فیلد است که فقط در زمان اعلان یا در سازنده کلاس (constructor) مقداردهی اولیه می‌شود و پس از آن قابل تغییر نیست.
  • می‌تواند برای هر نوع داده‌ای استفاده شود (Value Type یا Reference Type).
  • می‌تواند مقداری را از زمان اجرا دریافت کند (مثلاً از یک تابع یا ورودی کاربر).
  • باید به صورت static readonly (برای ثابت‌های در سطح کلاس که بین تمام نمونه‌ها مشترک است) یا فقط readonly (برای ثابت‌های در سطح نمونه که برای هر شیء متفاوت است) اعلان شود.

public class Config
{
    // این یک readonly field است. مقدار آن می تواند در زمان اجرا تعیین شود.
    // به عنوان مثال، از یک فایل تنظیمات خوانده شود.
    public readonly string Version = "1.0.0"; 

    // یک static readonly field. مقدار آن برای همه نمونه‌های کلاس ثابت است.
    public static readonly DateTime StartTime = DateTime.Now; 

    public Config(string version)
    {
        Version = version; // می توان در سازنده مقداردهی اولیه کرد.
    }

    // public const DateTime CompileTime = DateTime.Now; // خطا: DateTime.Now یک ثابت زمان کامپایل نیست.
}

// استفاده از readonly
// Console.WriteLine($"App Version: {new Config("2.0").Version}");
// Console.WriteLine($"App Started At: {Config.StartTime}");

بنابراین، const برای مقادیری که هرگز تغییر نمی‌کنند و در زمان کامپایل شناخته شده‌اند (مانند PI)، و readonly برای مقادیری که یک بار در زمان اجرا (مثلاً هنگام بارگذاری برنامه یا ایجاد یک شیء) مقداردهی اولیه می‌شوند و سپس ثابت باقی می‌مانند، مناسب است. انتخاب صحیح بین این دو بر عملکرد و انعطاف‌پذیری کد شما تأثیر می‌گذارد.

درک سیستم انواع داده در C#

یکی از ستون‌های اصلی و مهم C#، سیستم انواع داده آن است که به طور دقیق مشخص می‌کند که چگونه داده‌ها در حافظه ذخیره می‌شوند و چگونه می‌توان با آن‌ها تعامل داشت. C# یک زبان Strongly-Typed (قویاً تایپ شده) است، به این معنی که هر متغیر و هر بیان (expression) در زمان کامپایل یک نوع مشخص دارد. این ویژگی به کامپایلر اجازه می‌دهد تا بسیاری از خطاها را قبل از اجرای برنامه تشخیص دهد، که منجر به کدی با پایداری بیشتر، خطاهای کمتر در زمان اجرا و دیباگ آسان‌تر می‌شود. این رویکرد به ویژه در توسعه نرم‌افزارهای بزرگ و پیچیده که دقت و قابلیت اطمینان حیاتی است، بسیار ارزشمند است.

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

انواع Value و Reference: تفاوت‌های بنیادی

تفاوت اصلی بین انواع Value و Reference در نحوه مدیریت حافظه و کپی شدن مقادیر آن‌ها است. این تفاوت در پشت صحنه اتفاق می‌افتد و بر عملکرد و منطق برنامه شما تأثیر چشمگیری دارد، به ویژه زمانی که با عملیات انتساب (assignment) و ارسال آرگومان‌ها به متدها سروکار دارید.

انواع Value (Value Types)

انواع Value مستقیماً مقادیر خود را ذخیره می‌کنند و در زمان کپی شدن، یک کپی کامل و مستقل از داده ایجاد می‌شود.

  • ذخیره‌سازی در Stack: انواع Value به طور معمول مقادیر خود را مستقیماً در حافظه Stack ذخیره می‌کنند. Stack یک ناحیه حافظه بسیار سریع است که برای ذخیره‌سازی متغیرهای محلی، پارامترهای متد و اطلاعات مربوط به فراخوانی متدها استفاده می‌شود. زمانی که یک متد فراخوانی می‌شود، یک “Stack Frame” برای آن ایجاد می‌شود و متغیرهای Value Type در آن فریم ذخیره می‌شوند. پس از اتمام اجرای متد، Stack Frame از بین رفته و حافظه آزاد می‌شود.
  • کپی شدن مقدار (Copy by Value): وقتی یک متغیر از نوع Value به متغیر دیگری اختصاص داده می‌شود (مانند int b = a;) یا به عنوان پارامتر به یک متد پاس داده می‌شود، یک کپی کامل از مقدار آن ساخته می‌شود. این به این معنی است که هر دو متغیر مستقل از یکدیگر عمل می‌کنند و تغییر در یکی، دیگری را تحت تأثیر قرار نمی‌دهد. هر متغیر Value Type یک کپی جداگانه و مستقل از داده خود را در حافظه دارد.
  • شامل چه مواردی می‌شوند؟ تمام انواع عددی توکار (sbyte, byte, short, ushort, int, uint, long, ulong, float, double, decimalbool، char، struct (ساختارها)، و enum (شمارش‌ها). همچنین، DateTime و Guid نیز Value Type هستند.

int a = 10;
int b = a; // مقدار a (یعنی 10) در b کپی می شود. b اکنون دارای کپی مستقل از 10 است.

Console.WriteLine($"a: {a}, b: {b}"); // خروجی: a: 10, b: 10

a = 20; // تغییر در a، b را تحت تأثیر قرار نمی دهد.
Console.WriteLine($"a: {a}, b: {b}"); // خروجی: a: 20, b: 10

// مثال با struct (یک نوع Value تعریف شده توسط کاربر)
public struct Point
{
    public int X;
    public int Y;
}

Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1; // p1 در p2 کپی می شود. p2 اکنون یک کپی مستقل از p1 است.

p1.X = 100; // تغییر در p1، بر p2 تأثیری ندارد.

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

انواع Reference (Reference Types)

انواع Reference مستقیماً داده را ذخیره نمی‌کنند، بلکه یک ارجاع (آدرس حافظه) به مکان داده در Heap را نگهداری می‌کنند. هنگام کپی شدن، فقط ارجاع کپی می‌شود.

  • ذخیره‌سازی در Heap: انواع Reference، مقدار واقعی داده را مستقیماً ذخیره نمی‌کنند. در عوض، آن‌ها یک ارجاع (reference) یا آدرس حافظه را ذخیره می‌کنند که به مکانی در حافظه Heap اشاره دارد. Heap ناحیه حافظه‌ای است که برای اشیاء ایجاد شده در زمان اجرا استفاده می‌شود و مدیریت آن توسط Garbage Collector (GC) دات‌نت انجام می‌شود. تخصیص حافظه در Heap و مدیریت GC سربار عملکردی بیشتری نسبت به Stack دارد.
  • کپی شدن ارجاع (Copy by Reference / Copy of Reference): وقتی یک متغیر از نوع Reference به متغیر دیگری اختصاص داده می‌شود یا به عنوان پارامتر به یک متد پاس داده می‌شود، فقط ارجاع (آدرس حافظه) کپی می‌شود، نه خود شیء. این بدان معنی است که هر دو متغیر به یک مکان واحد در حافظه Heap اشاره می‌کنند. بنابراین، تغییر در شیء از طریق یکی از متغیرها، از طریق متغیر دیگر نیز قابل مشاهده خواهد بود، زیرا هر دو به یک منبع مشترک اشاره می‌کنند.
  • شامل چه مواردی می‌شوند؟ class (کلاس‌ها)، interface (واسط‌ها)، delegate (نمایندگان)، string (رشته‌ها)، و array (آرایه‌ها). همچنین object که ریشه سلسله مراتب انواع در C# است، یک نوع ارجاعی است.

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Person p1 = new Person { Name = "احمد", Age = 25 }; // یک شیء Person در Heap ایجاد می شود، و p1 ارجاعی به آن است.
Person p2 = p1; // ارجاع p1 (آدرس حافظه شیء) در p2 کپی می شود. p1 و p2 اکنون به یک شیء واحد در Heap اشاره دارند.

Console.WriteLine($"p1.Name: {p1.Name}, p2.Name: {p2.Name}"); // خروجی: p1.Name: احمد, p2.Name: احمد

p1.Name = "رضا"; // تغییر در شیء از طریق p1. این تغییر روی شیء مشترک اعمال می شود.
Console.WriteLine($"p1.Name: {p1.Name}, p2.Name: {p2.Name}"); // خروجی: p1.Name: رضا, p2.Name: رضا (p2 نیز تغییر را می بیند)

درک این تفاوت‌ها برای جلوگیری از رفتارهای غیرمنتظره در برنامه‌های پیچیده حیاتی است. به عنوان مثال، اگر شما یک لیست از اشیاء (مثلاً List) را به یک متد ارسال کنید و آن متد یکی از اشیاء را تغییر دهد، تغییرات در لیست اصلی نیز منعکس خواهد شد، زیرا شما در حال کار با ارجاع به آن اشیاء هستید. اما اگر یک متغیر int را ارسال کنید (که Value Type است)، متد روی یک کپی از آن int کار می‌کند و تغییرات آن تأثیری بر int اصلی نخواهد داشت.

این تمایز در نحوه عملکرد Garbage Collector دات‌نت نیز نقش دارد. انواع Value به محض خروج از Scope (پایان یافتن بلوک کد یا متد مربوطه)، از Stack حذف می‌شوند و مدیریت حافظه آن‌ها ساده است، در حالی که اشیاء در Heap (انواع Reference) تا زمانی که هیچ ارجاعی به آن‌ها نباشد (یا ارجاعات موجود به null تنظیم شوند)، توسط Garbage Collector جمع‌آوری نمی‌شوند. این فرآیند جمع‌آوری زباله سربار خاص خود را دارد و می‌تواند در برخی سناریوهای عملکردی مهم باشد.

انواع داده Value در C#

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

انواع عددی صحیح (Integral Numeric Types)

این انواع برای ذخیره اعداد صحیح (بدون بخش اعشاری) استفاده می‌شوند و بر اساس اندازه (تعداد بایت‌های مورد نیاز برای ذخیره‌سازی) و علامت (قابلیت ذخیره اعداد منفی) دسته‌بندی می‌شوند. انتخاب نوع صحیح می‌تواند بر مصرف حافظه و گاهی عملکرد تأثیر بگذارد.

  • sbyte: عدد صحیح ۸ بیتی علامت‌دار (Signed 8-bit integer). محدوده: از -128 تا 127.
  • byte: عدد صحیح ۸ بیتی بدون علامت (Unsigned 8-bit integer). محدوده: از 0 تا 255. مناسب برای داده‌های بایت یا مقادیر کوچک مثبت.
  • short: عدد صحیح ۱۶ بیتی علامت‌دار (Signed 16-bit integer). محدوده: از -32,768 تا 32,767.
  • ushort: عدد صحیح ۱۶ بیتی بدون علامت (Unsigned 16-bit integer). محدوده: از 0 تا 65,535.
  • int: عدد صحیح ۳۲ بیتی علامت‌دار (Signed 32-bit integer). محدوده: از -2,147,483,648 تا 2,147,483,647. این رایج‌ترین و پیش‌فرض‌ترین نوع برای اعداد صحیح در C# است و برای اکثر سناریوها مناسب است.
  • uint: عدد صحیح ۳۲ بیتی بدون علامت (Unsigned 32-bit integer). محدوده: از 0 تا 4,294,967,295.
  • long: عدد صحیح ۶۴ بیتی علامت‌دار (Signed 64-bit integer). محدوده: از -9,223,372,036,854,775,808 تا 9,223,372,036,854,775,807. برای اعداد بسیار بزرگ که از محدوده int فراتر می‌روند. لیترال‌های long معمولاً با پسوند L یا l مشخص می‌شوند.
  • ulong: عدد صحیح ۶۴ بیتی بدون علامت (Unsigned 64-bit integer). محدوده: از 0 تا 18,446,744,073,709,551,615.
  • nint و nuint: این انواع از C# 9 و .NET 5 معرفی شدند و “Native-sized integers” هستند. اندازه آن‌ها در زمان اجرا (runtime) تعیین می‌شود و می‌تواند 32 یا 64 بیتی باشد که با اندازه پوینتر در سیستم عامل (32-bit یا 64-bit) مطابقت دارد. این‌ها برای سناریوهای پیشرفته مانند P/Invoke (Platform Invoke) و عملیات‌های سطح پایین که نیاز به همخوانی با معماری پردازنده دارند، مفید هستند.

int myInt = 100;
long bigNumber = 123456789012345L; // پسوند L برای مشخص کردن long literal
byte smallByte = 200;
// uint negativeUint = -5; // خطا: uint نمی تواند مقادیر منفی را ذخیره کند.
Console.WriteLine($"Int Max Value: {int.MaxValue}");
Console.WriteLine($"Long Min Value: {long.MinValue}");

انواع عددی اعشاری (Floating-Point Numeric Types)

این انواع برای ذخیره اعداد حقیقی (با بخش اعشاری) استفاده می‌شوند و تفاوت آن‌ها در دقت و محدوده مقادیری است که می‌توانند نگهداری کنند.

  • float: عدد اعشاری ۳۲ بیتی با دقت پایین (Single-precision floating-point). دقت آن حدود ۷ رقم اعشار است. برای دقت کمتر و مصرف حافظه کمتر، مناسب است. لیترال‌های float باید با پسوند f یا F مشخص شوند.
  • double: عدد اعشاری ۶۴ بیتی با دقت بالا (Double-precision floating-point). دقت آن حدود ۱۵-۱۶ رقم اعشار است. این نوع رایج‌ترین برای اعداد اعشاری در C# است و پیش‌فرض برای لیترال‌های اعشاری محسوب می‌شود.
  • decimal: عدد اعشاری ۱۲۸ بیتی با دقت بسیار بالا (۲۸-۲۹ رقم اعشار) و دقت پایه ۱۰ (Base 10 precision). این نوع برای محاسبات مالی و پولی که نیاز به دقت بالا و جلوگیری از خطاهای گرد کردن (rounding errors) ذاتی در نمایش اعداد اعشاری مبنای ۲ (مانند float و double) دارند، ایده‌آل است. لیترال‌های decimal باید با پسوند m یا M مشخص شوند.

float temperature = 23.5f; // پسوند f برای float literal
double pi = 3.1415926535; // پیش فرض double
decimal amount = 199.99m; // پسوند m برای decimal literal

// مثال تفاوت دقت بین double و decimal:
double d1 = 0.1 + 0.2; 
Console.WriteLine($"Double sum of 0.1 and 0.2: {d1}"); // ممکن است 0.30000000000000004 را نشان دهد.
decimal d2 = 0.1m + 0.2m; 
Console.WriteLine($"Decimal sum of 0.1 and 0.2: {d2}"); // خروجی دقیقاً 0.3m.

نوع منطقی (Boolean Type)

  • bool: یک نوع ساده برای ذخیره‌سازی مقادیر منطقی true (درست) یا false (غلط). این نوع در عملیات‌های منطقی و کنترل جریان برنامه (مانند عبارات شرطی if و حلقه‌های for، while) به طور گسترده‌ای استفاده می‌شود. bool نمی‌تواند به طور مستقیم به انواع عددی تبدیل شود.

bool isLoggedIn = true;
bool hasPermission = false;

if (isLoggedIn && hasPermission)
{
    Console.WriteLine("کاربر وارد شده و دارای مجوز است.");
}
else
{
    Console.WriteLine("دسترسی رد شد.");
}

نوع کاراکتری (Character Type)

  • char: یک کاراکتر یونیکد ۱۶ بیتی را ذخیره می‌کند. این نوع برای ذخیره یک حرف، عدد یا نماد استفاده می‌شود. می‌توان آن را با یک کاراکتر تکی در داخل ‘تک نقل قول’ (single quotes) مقداردهی اولیه کرد. C# از یونیکد (UTF-16) پشتیبانی می‌کند، بنابراین می‌تواند کاراکترهای زبان‌های مختلف را نگهداری کند.

char initial = 'A'; // یک کاراکتر انگلیسی
char persianChar = 'ک'; // یک کاراکتر فارسی
char unicodeChar = '\u0041'; // 'A' در فرمت یونیکد

Console.WriteLine($"Initial: {initial}, Persian Char: {persianChar}, Unicode Char: {unicodeChar}");

Structs (ساختارها)

struct یک نوع Value تعریف شده توسط کاربر است. بر خلاف کلاس‌ها که انواع Reference هستند، ساختارها مستقیماً مقادیر خود را در Stack (یا درون شیئی دیگر در Heap اگر عضوی از یک نوع Reference باشند) ذخیره می‌کنند. آن‌ها برای مدل‌سازی اشیاء کوچک و سبک که دارای مجموعه‌ای از داده‌های مرتبط هستند و معمولاً تغییر ناپذیرند، مناسب هستند. structها اغلب برای افزایش عملکرد در سناریوهایی که تعداد زیادی نمونه از یک نوع کوچک ایجاد می‌شود و سربار Garbage Collector برای آن‌ها نامطلوب است، استفاده می‌شوند.

ویژگی‌های کلیدی Structs:

  • انواع Value هستند: بنابراین هنگام کپی شدن (انتساب یا ارسال به متدها)، یک کپی کامل از تمام داده‌های Struct ایجاد می‌شود. این بدان معنی است که هر نمونه از Struct دارای داده‌های مستقل خود است.
  • نمی‌توانند از Structs دیگر ارث‌بری کنند: اما می‌توانند از رابط‌ها (interfaces) پیاده‌سازی کنند (مانند IComparable یا IEquatable).
  • به طور پیش‌فرض، سازنده بدون پارامتر (parameterless constructor) دارند: اما از C# 10 به بعد، شما می‌توانید سازنده‌های بدون پارامتر و همچنین سازنده‌های پارامتردار را تعریف کنید.
  • مقادیر پیش‌فرض برای فیلدها: فیلدهای یک Struct همیشه با مقادیر پیش‌فرض خود مقداردهی اولیه می‌شوند (بر خلاف فیلدهای کلاس که می‌توانند null باشند).
  • عملکرد: استفاده از Struct می‌تواند به بهبود عملکرد در سناریوهای خاص (به دلیل کاهش سربار Garbage Collector و بهینه‌سازی دسترسی به حافظه) کمک کند، اما برای اشیاء بزرگ یا اشیایی که نیاز به تغییرات مکرر دارند، کلاس‌ها ترجیح داده می‌شوند زیرا کپی کردن Structهای بزرگ می‌تواند خود پرهزینه باشد.
  • تغییرناپذیری (Immutability): Structها معمولاً به صورت تغییرناپذیر طراحی می‌شوند (تمام فیلدها readonly هستند) تا رفتار آن‌ها قابل پیش‌بینی‌تر باشد، زیرا کپی شدن آن‌ها می‌تواند منجر به سردرگمی در مورد حالت‌های مختلف شود.

public struct Coordinates
{
    public double Latitude;
    public double Longitude;

    public Coordinates(double lat, double lon) // سازنده با پارامتر
    {
        Latitude = lat;
        Longitude = lon;
    }

    public void Display()
    {
        Console.WriteLine($"Latitude: {Latitude}, Longitude: {Longitude}");
    }
}

// استفاده از Struct
Coordinates c1 = new Coordinates(35.6892, 51.3890); // Tehran coordinates
Coordinates c2 = c1; // مقدار c1 (همه فیلدهای آن) در c2 کپی می شود.

c1.Latitude = 40.7128; // تغییر در c1، بر c2 تأثیری ندارد (چون c2 یک کپی مستقل است).

c1.Display(); // خروجی: Latitude: 40.7128, Longitude: 51.389
c2.Display(); // خروجی: Latitude: 35.6892, Longitude: 51.389

Enums (شمارش‌ها)

Enum (enumeration) یک نوع Value است که شامل مجموعه‌ای از ثابت‌های نام‌گذاری شده است. این ثابت‌ها به طور پیش‌فرض از نوع int هستند (که از 0 شروع می‌شود و با هر عضو بعدی 1 واحد افزایش می‌یابد) اما می‌توانند به انواع عددی صحیح دیگری مانند byte، short، long و غیره نیز تغییر کنند. Enumها برای بهبود خوانایی و حفظ کد، به ویژه زمانی که با مجموعه‌ای از گزینه‌های ثابت و محدود سروکار دارید (مانند روزهای هفته، حالت‌های یک برنامه، یا سطوح دسترسی) بسیار مفید هستند. آن‌ها به جای استفاده از “اعداد جادویی” (magic numbers) در کد، نام‌های معنی‌دار و خوانا را فراهم می‌کنند.


public enum DayOfWeek
{
    Sunday,    // به طور پیش‌فرض 0
    Monday,    // به طور پیش‌فرض 1
    Tuesday,   // به طور پیش‌فرض 2
    Wednesday, // به طور پیش‌فرض 3
    Thursday,  // به طور پیش‌فرض 4
    Friday,    // به طور پیش‌فرض 5
    Saturday   // به طور پیش‌فرض 6
}

public enum Status : byte // می‌توان نوع پایه را به صراحت مشخص کرد (مثلاً byte)
{
    Active = 1,  // می‌توان مقادیر را به صورت دستی تعیین کرد
    Inactive = 2,
    Pending = 4,
    Deleted = 8
}

// استفاده از Enum
DayOfWeek today = DayOfWeek.Wednesday;
Console.WriteLine($"Today is {today}. Its underlying value is {(int)today}"); // خروجی: Today is Wednesday. Its underlying value is 3

Status userStatus = Status.Active;
if (userStatus == Status.Active)
{
    Console.WriteLine("User is active.");
}

// تبدیل از رشته به Enum و بالعکس
string statusName = Status.Pending.ToString(); // "Pending"
Status parsedStatus = (Status)Enum.Parse(typeof(Status), "Inactive"); // Status.Inactive
Console.WriteLine($"Parsed Status: {parsedStatus}");

Nullable Value Types (انواع Value با قابلیت Null)

به طور پیش‌فرض، انواع Value (مانند int، bool، DateTime) نمی‌توانند null باشند؛ یعنی آن‌ها همیشه باید یک مقدار معتبر داشته باشند. این رفتار، اگرچه در بسیاری از موارد منطقی است، اما در سناریوهایی که ممکن است یک متغیر Value به طور موقت یا دائمی فاقد مقدار باشد (مثلاً یک فیلد اختیاری در پایگاه داده، یا ورودی کاربر که ممکن است خالی بماند)، مشکل‌ساز می‌شود.

برای حل این مشکل، C# مفهوم “Nullable Value Types” را با استفاده از Nullable<T> یا سینتکس کوتاه‌شده ? (علامت سوال) معرفی کرده است. T در Nullable<T> باید یک Value Type باشد.


int? nullableInt = null; // یک int که می‌تواند null باشد
double? nullableDouble = 12.34; // یک double که می‌تواند مقدار داشته باشد یا null باشد
bool? nullableBool = true;

Console.WriteLine($"nullableInt: {nullableInt ?? -1}"); // استفاده از Null-coalescing operator (??) برای ارائه مقدار پیش‌فرض در صورت null بودن
Console.WriteLine($"nullableDouble: {nullableDouble.Value}"); // دسترسی مستقیم به مقدار (اگر null نباشد)

// بررسی اینکه آیا یک Nullable Value Type مقدار دارد یا خیر
if (nullableInt.HasValue) // HasValue یک خصوصیت bool است.
{
    Console.WriteLine($"nullableInt has value: {nullableInt.Value}");
}
else
{
    Console.WriteLine("nullableInt is null.");
}

// متد GetValueOrDefault():
// این متد یک راه امن برای دسترسی به مقدار است. اگر متغیر null باشد، مقدار پیش‌فرض نوع را برمی‌گرداند.
int actualInt = nullableInt.GetValueOrDefault(); // اگر nullableInt null باشد، 0 را برمی‌گرداند (پیش‌فرض int)
int customDefault = nullableInt.GetValueOrDefault(100); // اگر nullableInt null باشد، 100 را برمی‌گرداند

Console.WriteLine($"actualInt (from null nullableInt): {actualInt}");
Console.WriteLine($"customDefault (from null nullableInt): {customDefault}");

// مثال با یک Nullable که مقدار دارد:
int? anotherInt = 50;
Console.WriteLine($"anotherInt.Value: {anotherInt.Value}");
Console.WriteLine($"anotherInt.GetValueOrDefault(): {anotherInt.GetValueOrDefault()}");

Nullable Value Types به شما امکان می‌دهند تا به طور صریح اعلام کنید که یک متغیر Value Type ممکن است هیچ مقداری نداشته باشد، که این امر به بهبود خوانایی کد و جلوگیری از خطاهای NullReference (که معمولاً با انواع Reference رخ می‌دهند) در زمان اجرا کمک می‌کند. از C# 8.0 به بعد، با معرفی ویژگی “Nullable Reference Types” (که به طور پیش‌فرض در پروژه‌های جدید خاموش است)، سیستم نوع C# برای تشخیص و هشدار درباره خطاهای Null Reference در زمان کامپایل نیز تکامل یافته است. با این حال، مفهوم اصلی int? یا bool? همچنان برای انواع Value کاربرد دارد و مستقل از ویژگی Nullable Reference Types عمل می‌کند.

انواع داده Reference در C#

انواع Reference، بر خلاف انواع Value، ارجاعی به داده‌های خود را ذخیره می‌کنند که در حافظه Heap قرار دارند. این بدان معناست که چندین متغیر می‌توانند به یک شیء واحد در حافظه Heap اشاره کنند و تغییرات اعمال شده از طریق یک متغیر، از طریق سایر متغیرها نیز قابل مشاهده است. این مفهوم برای ساختاردهی داده‌های پیچیده، مدیریت حافظه و پیاده‌سازی الگوهای برنامه‌نویسی شی‌گرا حیاتی است.

String (رشته‌ها)

string یک نوع Reference ویژه در C# است که برای ذخیره‌سازی دنباله‌ای از کاراکترهای یونیکد (متن) استفاده می‌شود. اگرچه string یک نوع Reference است، اما در بسیاری از جهات مانند یک نوع Value رفتار می‌کند، به دلیل ویژگی تغییرناپذیری (Immutability) آن. این ویژگی آن را از بسیاری دیگر از انواع Reference متمایز می‌کند.

  • تغییرناپذیری: به این معنی است که یک بار که یک شیء string ایجاد می‌شود، نمی‌توان محتوای آن را تغییر داد. هر عملیاتی که به نظر می‌رسد یک string را تغییر می‌دهد (مانند الحاق، جایگزینی کاراکترها و غیره)، در واقع یک شیء string جدید در حافظه Heap ایجاد می‌کند و ارجاع به آن را برمی‌گرداند. شیء string اصلی دست نخورده باقی می‌ماند و در نهایت توسط Garbage Collector جمع‌آوری می‌شود اگر هیچ ارجاعی به آن باقی نماند.
  • لیترال‌ها: رشته‌ها می‌توانند با استفاده از لیترال‌های رشته‌ای (کاراکترها در داخل “دو نقل قول”) مقداردهی اولیه شوند. C# همچنین از رشته‌های verbatim (با پیشوند @) برای نادیده گرفتن کاراکترهای escape و رشته‌های اینترپولیت‌شده (interpolated strings) با پیشوند $ برای جایگذاری آسان متغیرها پشتیبانی می‌کند.
  • عملیات رایج: C# متدهای قدرتمندی را برای کار با رشته‌ها فراهم می‌کند، از جمله Length (طول رشته)، ToUpper() (تبدیل به حروف بزرگ)، ToLower() (تبدیل به حروف کوچک)، IndexOf() (یافتن موقعیت کاراکتر/زیررشته)، Substring() (استخراج زیررشته)، Replace() (جایگزینی کاراکترها/زیررشته‌ها)، Trim() (حذف فضای خالی از ابتدا و انتها) و بسیاری دیگر.

string message = "سلام جهان"; // ایجاد یک شیء رشته
string name = "سارا";
string greeting = "خوش آمدید، " + name + "!"; // ایجاد یک رشته جدید با الحاق (نه تغییر رشته اصلی)

Console.WriteLine(message);
Console.WriteLine(greeting);

string original = "C# Programming";
string modified = original.Replace("C#", "VB.NET"); // یک رشته جدید ایجاد می شود، original تغییر نمی کند.

Console.WriteLine($"Original: {original}"); // خروجی: Original: C# Programming
Console.WriteLine($"Modified: {modified}"); // خروجی: Modified: VB.NET Programming

// مقایسه رشته ها:
// عملگر == برای رشته ها محتوا را مقایسه می کند نه ارجاع را (برخلاف کلاس های دیگر).
// متد Equals() نیز محتوا را مقایسه می کند و برای کنترل حساسیت به حروف (با StringComparison) توصیه می شود.
string s1 = "hello";
string s2 = "hello";
string s3 = new string(new char[] { 'h', 'e', 'l', 'l', 'o' }); // ایجاد یک شیء جدید

Console.WriteLine(s1 == s2); // true (مقایسه محتوا)
Console.WriteLine(s1 == s3); // true (مقایسه محتوا)
Console.WriteLine(object.ReferenceEquals(s1, s2)); // ممکن است true باشد به دلیل "string interning" (ذخیره سازی تک نمونه از رشته های یکسان در یک pool)
Console.WriteLine(object.ReferenceEquals(s1, s3)); // false (دو شیء مختلف در حافظه)

// استفاده از رشته‌های verbatim و interpolated:
string path = @"C:\MyDocuments\MyFile.txt"; // رشته verbatim: \ به عنوان کاراکتر escape تفسیر نمی شود.
Console.WriteLine(path);

string userName = "آرش";
int age = 28;
string userInfo = $"نام: {userName}, سن: {age}"; // رشته interpolated
Console.WriteLine(userInfo);

ویژگی تغییرناپذیری رشته‌ها در C# منجر به الگوهای طراحی خاصی می‌شود. در سناریوهایی که نیاز به تغییرات مکرر رشته‌ها دارید (مانند ساخت یک رشته طولانی در حلقه)، استفاده از کلاس System.Text.StringBuilder به جای الحاق رشته‌ها با عملگر + برای بهبود عملکرد توصیه می‌شود، زیرا StringBuilder امکان تغییر رشته‌ها را بدون ایجاد شیء جدید در هر عملیات فراهم می‌کند و تنها در انتها یک شیء string نهایی تولید می‌کند.

Class (کلاس‌ها)

class (کلاس) اصلی‌ترین بلوک سازنده در برنامه‌نویسی شی‌گرا (Object-Oriented Programming – OOP) در C# است. کلاس‌ها قالب‌هایی (blueprints) برای ایجاد اشیاء (instances) هستند که می‌توانند شامل داده‌ها (فیلدها، خصوصیات/Properties) و رفتارها (متدها، رویدادها) باشند. کلاس‌ها انواع Reference هستند، به این معنی که متغیرهای نوع کلاس، ارجاعی به شیء واقعی را در حافظه Heap نگهداری می‌کنند. این ساختار امکان مدل‌سازی پیچیده‌ترین مفاهیم دنیای واقعی را در برنامه فراهم می‌کند.

ویژگی‌های کلیدی کلاس‌ها:

  • انواع Reference: هنگامی که کپی می‌شوند (با عملگر انتساب یا ارسال به عنوان پارامتر متد)، فقط ارجاع (آدرس حافظه شیء در Heap) کپی می‌شود، نه خود شیء. این به معنای اشتراک‌گذاری شیء بین چندین ارجاع است.
  • پشتیبانی از ارث‌بری (Inheritance): کلاس‌ها می‌توانند از کلاس‌های دیگر ارث‌بری کنند (با استفاده از کلمه کلیدی :)، که امکان استفاده مجدد از کد، ایجاد سلسله مراتب نوعی و تعریف روابط “is-a” (مثل یک سگ یک حیوان است) را فراهم می‌کند.
  • پشتیبانی از چندریختی (Polymorphism): امکان رفتار متفاوت اشیاء از یک نوع پایه را فراهم می‌کند. این به این معنی است که یک متغیر از نوع پایه می‌تواند به اشیاء از انواع مشتق شده اشاره کند و متدهای Overridden یا Implemented روی شیء واقعی فراخوانی شوند.
  • کپسوله‌سازی (Encapsulation): با استفاده از Modifierهای دسترسی (مانند public, private, protected) می‌توان دسترسی به اعضای کلاس (فیلدها، متدها، خصوصیات) را کنترل کرد و منطق داخلی کلاس را از جهان خارج پنهان کرد.
  • Object Lifecycle: اشیاء کلاس‌ها (که در Heap ایجاد می‌شوند) توسط Garbage Collector (GC) دات‌نت مدیریت می‌شوند. توسعه‌دهنده به طور مستقیم حافظه را آزاد نمی‌کند، بلکه GC به طور خودکار اشیایی را که دیگر قابل دسترسی نیستند، شناسایی و حافظه آن‌ها را بازیابی می‌کند.
  • سازنده‌ها (Constructors): متدهای خاصی برای مقداردهی اولیه اشیاء در زمان ایجاد آن‌ها.

public class Car
{
    public string Make { get; set; } // Property for car manufacturer
    public string Model { get; set; } // Property for car model
    public int Year { get; set; } // Property for manufacturing year

    public Car(string make, string model, int year) // Constructor to initialize a new Car object
    {
        Make = make;
        Model = model;
        Year = year;
        Console.WriteLine($"A new car ({Make} {Model}) has been created.");
    }

    public void DisplayCarInfo() // Method to display car information
    {
        Console.WriteLine($"Make: {Make}, Model: {Model}, Year: {Year}");
    }
}

// ایجاد شیء از کلاس (instantiation)
Car myCar = new Car("Toyota", "Camry", 2020); // یک شیء Car جدید در Heap ایجاد و ارجاع آن به myCar اختصاص می یابد.
myCar.DisplayCarInfo(); // خروجی: Make: Toyota, Model: Camry, Year: 2020

Car yourCar = myCar; // yourCar نیز به همان شیء ای که myCar به آن اشاره می کند، اشاره می کند. ارجاع کپی می شود.
yourCar.Model = "Corolla"; // تغییر از طریق yourCar، بر شیء مشترک اثر می گذارد.

myCar.DisplayCarInfo(); // خروجی: Make: Toyota, Model: Corolla, Year: 2020 (تغییر در myCar نیز دیده می شود زیرا هر دو به یک شیء اشاره دارند)
Console.WriteLine(object.ReferenceEquals(myCar, yourCar)); // خروجی: True (هر دو به یک شیء اشاره دارند)

// ایجاد یک شیء جدید و مستقل
Car anotherCar = new Car("Honda", "Civic", 2022);
anotherCar.DisplayCarInfo();
Console.WriteLine(object.ReferenceEquals(myCar, anotherCar)); // خروجی: False (اشاره به اشیاء متفاوت)

کلاس‌ها ستون فقرات اکثر برنامه‌های کاربردی C# هستند و برای مدل‌سازی موجودیت‌های پیچیده، سیستم‌های بزرگ، و پیاده‌سازی منطق کسب‌وکار مورد استفاده قرار می‌گیرند. انتخاب بین کلاس و Struct در C# یک تصمیم طراحی مهم است که باید با توجه به اندازه داده، نیاز به تغییرپذیری، و الزامات عملکردی اتخاذ شود.

Interface (واسط‌ها)

interface (واسط) یک نوع Reference است که مجموعه‌ای از امضاهای متدها، خصوصیات (properties)، رویدادها (events) یا ایندکسرها (indexers) را تعریف می‌کند اما پیاده‌سازی آن‌ها را ندارد. واسط‌ها مانند یک قرارداد (contract) عمل می‌کنند: کلاس‌ها یا Structها (که به آن‌ها “implementers” گفته می‌شود) می‌توانند یک یا چند واسط را پیاده‌سازی کنند و متعهد به ارائه پیاده‌سازی برای تمام اعضای تعریف شده در آن واسط شوند. واسط‌ها برای تعریف قراردادها، دستیابی به قابلیت چندریختی (Polymorphism) بدون نیاز به ارث‌بری از پیاده‌سازی پایه، و پیاده‌سازی الگوهای طراحی مانند Dependency Injection استفاده می‌شوند.


public interface IShape // تعریف یک واسط برای اشکال هندسی
{
    double GetArea(); // امضای متد برای محاسبه مساحت
    double GetPerimeter(); // امضای متد برای محاسبه محیط
}

public class Circle : IShape // کلاس Circle واسط IShape را پیاده‌سازی می‌کند
{
    public double Radius { get; set; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public double GetArea() // پیاده‌سازی متد GetArea
    {
        return Math.PI * Radius * Radius;
    }

    public double GetPerimeter() // پیاده‌سازی متد GetPerimeter
    {
        return 2 * Math.PI * Radius;
    }
}

public class Rectangle : IShape // کلاس Rectangle نیز واسط IShape را پیاده‌سازی می‌کند
{
    public double Width { get; set; }
    public double Height { get; set; }

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    public double GetArea()
    {
        return Width * Height;
    }

    public double GetPerimeter()
    {
        return 2 * (Width + Height);
    }
}

// استفاده از Interface برای دستیابی به چندریختی
List shapes = new List();
shapes.Add(new Circle(5));
shapes.Add(new Rectangle(4, 6));

foreach (IShape shape in shapes)
{
    Console.WriteLine($"Shape Area: {shape.GetArea()}, Perimeter: {shape.GetPerimeter()}");
}
// خروجی:
// Shape Area: 78.53981633974483, Perimeter: 31.41592653589793
// Shape Area: 24, Perimeter: 20

از C# 8.0 به بعد، واسط‌ها می‌توانند شامل پیاده‌سازی‌های پیش‌فرض (Default Interface Methods) نیز باشند، که به توسعه‌دهندگان اجازه می‌دهد تا متدها را به واسط اضافه کنند بدون اینکه کلاس‌های پیاده‌ساز موجود را مجبور به تغییر کنند.

Delegate (نمایندگان)

delegate (نماینده) یک نوع Reference است که ارجاعی به یک متد (یا متدهایی) را نگهداری می‌کند. آن‌ها اساساً “اشاره‌گرهای تایپ شده به متدها” هستند و برای پیاده‌سازی الگوهای رویداد (event handling) و Callbacks (فراخوانی برگشتی) در C# استفاده می‌شوند. یک delegate می‌تواند به هر متدی (استاتیک یا نمونه) که امضای (signature – شامل نوع بازگشتی و پارامترها) مشابهی با آن delegate دارد، ارجاع دهد. این مفهوم پایه و اساس رویدادها (events) در دات‌نت است.


// تعریف یک delegate که به متدی با یک پارامتر string و بدون مقدار بازگشتی اشاره می کند.
public delegate void PrintMessage(string message);

public class Logger
{
    public void LogInfo(string msg) // متد نمونه
    {
        Console.WriteLine($"Info: {msg}");
    }

    public static void LogWarning(string msg) // متد استاتیک
    {
        Console.WriteLine($"Warning: {msg}");
    }
}

// استفاده از Delegate
PrintMessage printer1 = new Logger().LogInfo; // ایجاد یک نمونه از delegate که به متد LogInfo اشاره می کند
printer1("این یک پیام اطلاعاتی است."); // فراخوانی متد از طریق delegate

PrintMessage printer2 = Logger.LogWarning; // ایجاد یک نمونه از delegate که به متد استاتیک LogWarning اشاره می کند
printer2("این یک پیام هشدار است.");

// Delegateها می توانند به چندین متد اشاره کنند (Multicast Delegates)
PrintMessage multicastPrinter = printer1 + printer2; // اضافه کردن متدها
multicastPrinter("این یک پیام چند پخشی است."); // هر دو متد فراخوانی می شوند.

multicastPrinter -= printer1; // حذف یک متد از لیست
multicastPrinter("پیام بعد از حذف"); // فقط printer2 فراخوانی می شود.

Func<TResult>، Action<T> و Predicate<T> انواع delegateهای عمومی (Generic Delegates) هستند که به طور گسترده در .NET استفاده می‌شوند و نیاز به تعریف delegateهای سفارشی را کاهش می‌دهند.

Array (آرایه‌ها)

Array (آرایه) یک نوع Reference است که برای ذخیره مجموعه‌ای از عناصر از یک نوع داده خاص و با اندازه ثابت استفاده می‌شود. آرایه‌ها پس از ایجاد، دارای اندازه ثابتی هستند و عناصر آن‌ها توسط یک ایندکس عددی (از 0 شروع می‌شود) قابل دسترسی هستند. آرایه‌ها یکی از ابتدایی‌ترین و پرکاربردترین ساختارهای داده برای ذخیره مجموعه‌های مرتب‌شده از داده‌های هم‌نوع هستند.

  • تک بعدی (Single-dimensional): رایج‌ترین نوع آرایه، برای ذخیره یک لیست خطی از عناصر.
  • چند بعدی (Multidimensional / Rectangular): آرایه‌هایی با بیش از یک بعد (مثلاً ماتریس‌ها یا جداول). تعداد ابعاد در زمان اعلان ثابت است.
  • آرایه‌های دندانه‌دار (Jagged Arrays): آرایه‌هایی که عناصر آن‌ها خود آرایه هستند و طول هر آرایه داخلی می‌تواند متفاوت باشد. این‌ها اساساً “آرایه‌ای از آرایه‌ها” هستند.

// آرایه تک بعدی
int[] numbers = new int[5]; // اعلان آرایه‌ای با 5 عنصر از نوع int، با مقادیر پیش‌فرض 0.
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40;
numbers[4] = 50;

// دسترسی به عناصر
Console.WriteLine($"First number: {numbers[0]}"); // خروجی: First number: 10
Console.WriteLine($"Array length: {numbers.Length}"); // خروجی: Array length: 5

// مقداردهی اولیه در زمان اعلان
string[] names = { "علی", "سارا", "رضا" }; // اندازه آرایه به طور خودکار تعیین می شود.
Console.WriteLine($"Second name: {names[1]}"); // خروجی: Second name: سارا

// آرایه چند بعدی (2D Array - Matrix)
int[,] matrix = new int[2, 3] { { 1, 2, 3 }, { 4, 5, 6 } }; // 2 ردیف، 3 ستون
Console.WriteLine($"Element at [0,1]: {matrix[0, 1]}"); // خروجی: 2
Console.WriteLine($"Number of dimensions: {matrix.Rank}"); // خروجی: 2

// آرایه دندانه دار (Array of Arrays)
int[][] jaggedArray = new int[3][]; // آرایه ای از 3 آرایه int (هنوز خود آرایه های داخلی null هستند)

jaggedArray[0] = new int[] { 1, 2 }; // اولین آرایه داخلی با 2 عنصر
jaggedArray[1] = new int[] { 3, 4, 5, 6 }; // دومین آرایه داخلی با 4 عنصر
jaggedArray[2] = new int[] { 7, 8, 9 }; // سومین آرایه داخلی با 3 عنصر

Console.WriteLine($"Element in jagged array: {jaggedArray[1][2]}"); // خروجی: 5

با وجود پرکاربرد بودن آرایه‌ها، به دلیل اندازه ثابت آن‌ها، در سناریوهایی که نیاز به تغییر اندازه پویا دارید، از کلاس‌های Collection مانند List<T> یا ArrayList (البته List<T> توصیه می‌شود) استفاده می‌شود.

Object (شیء پایه)

object (همچنین با نام System.Object شناخته می‌شود) نوع پایه نهایی برای تمام انواع داده در C# است. به عبارت دیگر، هر نوع داده‌ای در C#، چه Value Type باشد (مانند int، struct) و چه Reference Type (مانند string، class)، به طور ضمنی از object ارث‌بری می‌کند. این به این معنی است که می‌توانید هر متغیری را به یک متغیر از نوع object اختصاص دهید و از این طریق، اشیاء مختلف را به صورت عمومی‌تر (polymorphically) مدیریت کنید.


int myInt = 10;
string myString = "Hello";
MyCustomClass myObjectInstance = new MyCustomClass(); // فرض کنید MyCustomClass تعریف شده است.

object obj1 = myInt; // Boxing اتفاق می افتد.
object obj2 = myString;
object obj3 = myObjectInstance;

Console.WriteLine($"obj1: {obj1.GetType().Name}, Value: {obj1}");
Console.WriteLine($"obj2: {obj2.GetType().Name}, Value: {obj2}");
Console.WriteLine($"obj3: {obj3.GetType().Name}, Value: {obj3}");

استفاده از object در C# به دو مفهوم مهم و مرتبط منجر می‌شود که بر عملکرد برنامه تأثیرگذار هستند: Boxing و Unboxing.

  • Boxing (باکس کردن): فرآیند تبدیل یک Value Type (مانند int، struct) به یک Reference Type (object یا هر واسطی که توسط Value Type پیاده‌سازی شده است) است. وقتی یک Value Type باکس می‌شود، یک شیء جدید در Heap ایجاد می‌شود و یک کپی از مقدار Value Type در آن شیء ذخیره می‌شود. ارجاع به این شیء جدید به متغیر object اختصاص داده می‌شود. این فرآیند از نظر عملکردی پرهزینه است زیرا شامل تخصیص حافظه در Heap، کپی کردن داده‌ها و سپس نیاز به مدیریت حافظه توسط Garbage Collector است.
  • Unboxing (آن‌باکس کردن): فرآیند معکوس Boxing است، یعنی تبدیل یک object (که قبلاً یک Value Type باکس شده بود) به Value Type اصلی خود. این فرآیند نیز پرهزینه است زیرا شامل بررسی نوع (type checking) در زمان اجرا برای اطمینان از سازگاری نوع و سپس کپی کردن داده‌ها از Heap به Stack است. اگر نوع داده اصلی با نوعی که قرار است آن‌باکس شود مطابقت نداشته باشد، یک استثنای System.InvalidCastException در زمان اجرا پرتاب خواهد شد.

// Boxing مثال:
int num = 123; // Value Type
object obj = num; // Boxing: یک شیء در Heap ایجاد می شود و 123 در آن کپی می شود. obj ارجاعی به این شیء است.

Console.WriteLine($"Original Value: {num}");
Console.WriteLine($"Boxed Object: {obj}");

// Unboxing مثال:
int unboxedNum = (int)obj; // Unboxing: مقدار از شیء در Heap به یک int جدید در Stack کپی می شود.
Console.WriteLine($"Unboxed Value: {unboxedNum}");

// مثال خطای زمان اجرا در Unboxing (InvalidCastException):
object wrongObj = "Hello"; // این شیء string است (یک Reference Type)
// int invalidUnbox = (int)wrongObj; // این خط منجر به System.InvalidCastException می شود، زیرا wrongObj از ابتدا int نبود.

// مثال دیگر: اگر Value Type با نوع آنباکس شده مطابقت نداشته باشد، حتی اگر هر دو عددی باشند
object boxedDouble = 123.45; // Boxing یک double
// int unboxedFromDouble = (int)boxedDouble; // InvalidCastException: نمی توان double boxed را مستقیماً به int unbox کرد.
// ابتدا باید به double unbox کرد و سپس تبدیل نوع (cast) کرد.
double correctlyUnboxedDouble = (double)boxedDouble;
int castedInt = (int)correctlyUnboxedDouble; // صحیح: ابتدا unbox، سپس cast.
Console.WriteLine($"Correctly unboxed and casted: {castedInt}");

به دلیل سربار عملکردی قابل توجه ناشی از Boxing و Unboxing، توصیه می‌شود تا حد امکان از تبدیل‌های صریح به/از object برای انواع Value خودداری شود. بهترین راه حل برای کار با انواع مختلف داده به صورت تایپ شده و بدون سربار Boxing/Unboxing، استفاده از Genericها (مانند List<T>، Dictionary<TKey, TValue> یا متدهای Generic) است. Genericها اجازه می‌دهند تا کد را با انواع داده مختلف به روشی ایمن و با عملکرد بالا بنویسید.

تبدیل نوع داده در C#

در برنامه‌نویسی، اغلب لازم است که داده‌ها را از یک نوع به نوع دیگر تبدیل کنیم. C# روش‌های مختلفی برای انجام این کار ارائه می‌دهد، از تبدیل‌های ضمنی (Implicit Conversions) و صریح (Explicit Conversions/Casting) گرفته تا عملگرهای خاص و کلاس‌های کمکی. درک این روش‌ها برای نوشتن کد صحیح، انعطاف‌پذیر و جلوگیری از خطاهای زمان اجرا بسیار مهم است.

تبدیل ضمنی (Implicit Conversion)

تبدیل ضمنی توسط کامپایلر C# به صورت خودکار و بدون نیاز به سینتکس خاصی انجام می‌شود. این نوع تبدیل تنها زمانی مجاز است که:

  • هیچ گونه از دست دادن داده‌ای (loss of data) وجود نداشته باشد.
  • نوع مقصد (Target Type) توانایی نگهداری تمام مقادیر نوع مبدأ (Source Type) را داشته باشد.

به عنوان مثال، تبدیل یک int (32 بیتی) به long (64 بیتی) یا یک float (32 بیتی) به double (64 بیتی) تبدیل ضمنی هستند زیرا نوع مقصد بزرگتر یا دقیق‌تر است و می‌تواند مقدار نوع کوچکتر را بدون از دست دادن اطلاعات در خود جای دهد. این تبدیل‌ها همیشه ایمن (safe) هستند.


int myInt = 10;
long myLong = myInt; // تبدیل ضمنی int به long. 10 به صورت خودکار در یک long ذخیره می شود.
Console.WriteLine($"myLong: {myLong}"); // خروجی: 10

float myFloat = 12.34f;
double myDouble = myFloat; // تبدیل ضمنی float به double. 12.34f به صورت خودکار در یک double ذخیره می شود.
Console.WriteLine($"myDouble: {myDouble}"); // خروجی: 12.34

// سلسله مراتب تبدیل ضمنی برای انواع عددی صحیح (به ترتیب از کوچکترین به بزرگترین ظرفیت):
// sbyte -> short -> int -> long
// byte -> ushort -> uint -> ulong
// char -> ushort -> int -> long
// همچنین، تبدیل از انواع صحیح به انواع اعشاری:
// int -> long -> float -> double
// uint -> ulong -> float -> double
// float -> double

// توجه: تبدیل به decimal فقط از int, long, float, double با تبدیل صریح انجام می شود.

تبدیل صریح (Explicit Conversion / Casting)

تبدیل صریح، که به آن Casting (ریخته‌گری) نیز گفته می‌شود، زمانی استفاده می‌شود که احتمال از دست دادن داده یا خطا (مانند سرریز/overflow) وجود داشته باشد. این تبدیل باید به صورت دستی توسط برنامه‌نویس با استفاده از عملگر پرانتز () جلوی متغیر یا مقدار، مشخص شود. این کار به کامپایلر اعلام می‌کند که برنامه‌نویس آگاهانه مسئولیت از دست دادن احتمالی داده‌ها را می‌پذیرد.

اگر تبدیل صریح ناموفق باشد (مثلاً تلاش برای تبدیل یک رشته نامعتبر به عدد، یا تبدیل یک نوع ناسازگار)، یک استثنا (Exception) مانند InvalidCastException یا FormatException در زمان اجرا پرتاب خواهد شد که باید توسط کد مدیریت شود.

مثال‌ها:

  • تبدیل double به int (از دست دادن بخش اعشاری).
  • تبدیل یک نوع عددی بزرگتر به کوچکتر (اگر مقدار خارج از محدوده نوع کوچکتر باشد، سرریز (overflow) اتفاق می‌افتد).

double value = 123.45;
int truncatedValue = (int)value; // تبدیل صریح double به int. بخش اعشاری حذف می شود.
Console.WriteLine($"truncatedValue: {truncatedValue}"); // خروجی: 123

long bigLong = 2000000000000L;
// int smallInt = (int)bigLong; // اگر bigLong بزرگتر از int.MaxValue باشد، سرریز رخ می دهد و مقدار غیرمنتظره ای حاصل می شود.
// Console.WriteLine($"smallInt: {smallInt}"); // ممکن است منجر به داده های غلط یا استثنا شود اگر checked باشد.

// مثال سرریز با checked context:
// checked
// {
//     int overflowInt = (int)bigLong; // در checked context، این خط System.OverflowException پرتاب می کند.
// }

// برای انجام محاسبات با دقت اعشاری، نیاز به cast صریح است:
int numA = 10;
int numB = 3;
double result = (double)numA / numB; // حداقل یکی از عملوندها باید به double تبدیل شود تا تقسیم اعشاری انجام شود.
Console.WriteLine($"result: {result}"); // خروجی: 3.3333333333333335

برای تبدیل‌های پیچیده‌تر، C# کلاس Convert و متدهای Parse()/TryParse() را ارائه می‌دهد:

  • کلاس System.Convert: این کلاس متدهای استاتیک مختلفی برای تبدیل بین انواع پایه .NET (مانند Convert.ToInt32(), Convert.ToString(), Convert.ToBoolean()) را فراهم می‌کند. این متدها برای تبدیل‌های عمومی، به ویژه از و به string، و در هنگام مدیریت null (که null را به مقدار پیش‌فرض نوع هدف تبدیل می‌کنند) مفید هستند.
  • متدهای Parse() و TryParse(): این متدها در انواع ساختاری (مانند int, double, DateTime) موجودند و به طور خاص برای تبدیل string به نوع داده مربوطه استفاده می‌شوند.
    • Parse(): اگر رشته ورودی نامعتبر باشد یا نتواند به نوع هدف تبدیل شود، یک استثنا (معمولاً FormatException) پرتاب می‌کند. این متد زمانی مناسب است که مطمئن هستید ورودی همیشه معتبر خواهد بود.
    • TryParse(): یک روش امن‌تر و توصیه شده‌تر برای تبدیل رشته‌ها است که استثنا پرتاب نمی‌کند. این متد یک مقدار bool را برمی‌گرداند که نشان می‌دهد تبدیل موفقیت‌آمیز بوده است یا خیر، و اگر موفق باشد، مقدار تبدیل شده را در یک پارامتر out قرار می‌دهد. این متد برای اعتبارسنجی ورودی کاربر و جلوگیری از Crash شدن برنامه بسیار مناسب است.

string strNumber = "123";
int convertedInt = Convert.ToInt32(strNumber); // تبدیل رشته به int با استفاده از کلاس Convert
Console.WriteLine($"Converted using Convert: {convertedInt}");

string strDouble = "98.76";
double parsedDouble = double.Parse(strDouble); // تبدیل رشته به double با متد Parse
Console.WriteLine($"Parsed using Parse: {parsedDouble}");

string invalidStr = "abc";
// int failedParse = int.Parse(invalidStr); // اگر این خط اجرا شود، FormatException پرتاب می کند.

int tryParsedInt;
bool success = int.TryParse(invalidStr, out tryParsedInt); // تلاش برای تبدیل امن
if (success)
{
    Console.WriteLine($"TryParse successful: {tryParsedInt}");
}
else
{
    Console.WriteLine("TryParse failed. Invalid input for integer.");
}

string nullString = null;
int defaultInt = Convert.ToInt32(nullString); // Convert.ToInt32(null) برمی گرداند 0 (پیش فرض int)
Console.WriteLine($"Convert.ToInt32(null): {defaultInt}");

// int parsedNull = int.Parse(nullString); // FormatException (Parse برای null هم استثنا می دهد)
// int tryParsedNull;
// bool successNull = int.TryParse(nullString, out tryParsedNull); // TryParse برای null برمی گرداند false

عملگرهای `as` و `is` (The `as` and `is` Operators)

این دو عملگر به طور خاص برای کار با انواع Reference در زمان اجرا و انجام عملیات Casting امن طراحی شده‌اند. آن‌ها نقش مهمی در کنترل جریان برنامه بر اساس نوع واقعی یک شیء ایفا می‌کنند و به جلوگیری از InvalidCastException کمک می‌کنند.

  • عملگر is: برای بررسی اینکه آیا یک شیء با یک نوع داده خاص سازگار است یا خیر، استفاده می‌شود. اگر شیء قابل تبدیل به آن نوع باشد (یعنی یک نمونه از آن نوع یا از نوع مشتق شده آن باشد)، true و در غیر این صورت false را برمی‌گرداند. این عملگر استثنا پرتاب نمی‌کند.
  • عملگر is با Pattern Matching (از C# 7.0 به بعد): این یک ویژگی قدرتمند است که به شما اجازه می‌دهد تا همزمان نوع یک شیء را بررسی کنید و اگر مطابقت داشت، آن را به یک متغیر از نوع مورد نظر (با نام جدید) اختصاص دهید. این ترکیب کد را کوتاه‌تر و خواناتر می‌کند.
  • عملگر as: تلاش می‌کند یک شیء را به نوع داده مشخص شده تبدیل کند. اگر تبدیل موفقیت‌آمیز باشد، شیء تبدیل شده را برمی‌گرداند؛ در غیر این صورت، null را برمی‌گرداند و هیچ استثنایی پرتاب نمی‌کند. این عملگر تنها برای انواع Reference و Nullable Value Types قابل استفاده است. اگر برای یک Value Type غیر Nullable استفاده شود، خطای زمان کامپایل رخ می‌دهد.

object myObject = "Hello C#"; // myObject از نوع object است اما محتوای آن یک رشته است.

// استفاده از 'is' برای بررسی نوع
if (myObject is string)
{
    Console.WriteLine("myObject is compatible with string.");
}

// استفاده از 'is' با Pattern Matching (C# 7.0+ syntax)
if (myObject is string strMessage) // اگر myObject یک string باشد، به strMessage اختصاص داده می شود.
{
    Console.WriteLine($"Casting with 'is' and Pattern Matching: Length = {strMessage.Length}");
}
else if (myObject is int intValue)
{
    Console.WriteLine($"MyObject is an integer with value: {intValue}");
}
else
{
    Console.WriteLine("MyObject is neither string nor int.");
}

// استفاده از 'as'
string castedString = myObject as string; // تلاش برای تبدیل myObject به string. اگر موفق باشد، مقدار را برمی‌گرداند، در غیر این صورت null.
if (castedString != null)
{
    Console.WriteLine($"Casting with 'as': {castedString}");
}

// مثال زمانی که 'as' مقدار null برمی‌گرداند:
object anotherObject = 123; // یک int (Value Type) که Boxing شده به object
string nullString = anotherObject as string; // nullString = null زیرا anotherObject یک string نیست.
if (nullString == null)
{
    Console.WriteLine("anotherObject is not a string, 'as' returned null.");
}

// مثال: استفاده از 'as' برای Nullable Value Types
int? nullableInt = 5;
object boxedNullableInt = nullableInt; // Boxing یک Nullable به object

int? unboxedNullableInt = boxedNullableInt as int?; // تبدیل object به Nullable
if (unboxedNullableInt.HasValue)
{
    Console.WriteLine($"Unboxed Nullable Int: {unboxedNullableInt.Value}");
}

استفاده از as و is برای Casting امن در سناریوهای پلیمورفیسم (Polymorphism) و زمانی که با اشیاء از نوع object یا از سلسله مراتب ارث‌بری سروکار دارید، بسیار توصیه می‌شود، زیرا از پرتاب استثناهای ناخواسته InvalidCastException جلوگیری می‌کنند و کد را پایدارتر می‌سازند. این عملگرها به شما کنترل بیشتری بر روی منطق تبدیل نوع در زمان اجرا می‌دهند.

انتخاب نوع داده مناسب

انتخاب نوع داده مناسب برای متغیرها و ساختارهای داده در C# فراتر از صرفاً “کار کردن” کد است. این یک تصمیم مهندسی است که می‌تواند تأثیرات عمیقی بر عملکرد برنامه، مصرف حافظه، خوانایی کد، و مقیاس‌پذیری آن داشته باشد. انتخاب صحیح نوع داده در شروع پروژه می‌تواند از مشکلات جدی در آینده جلوگیری کند. در اینجا برخی از ملاحظات کلیدی برای انتخاب بهترین نوع داده آورده شده است:

محدوده و دقت (Range and Precision)

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

  • اعداد صحیح: اگر با اعداد صحیح سروکار دارید، کوچکترین نوعی را انتخاب کنید که بتواند تمام مقادیر مورد نیاز شما را بدون سرریز شدن (overflow) در خود جای دهد. به عنوان مثال، برای سن (که معمولاً بین 0 تا 120 است)، byte یا sbyte از نظر تئوری کافی هستند و کمترین حافظه را اشغال می‌کنند، اما int (32 بیتی) رایج‌تر و ایمن‌تر است و برای اکثر سناریوهای عمومی عددی کفایت می‌کند. برای شناسه‌های پایگاه داده (مانند IDها) یا مقادیر بسیار بزرگ، long ممکن است لازم باشد.
  • اعداد اعشاری:
    • برای محاسبات علمی، گرافیکی یا فیزیکی که دقت کمتری (حدود 7-15 رقم اعشار) قابل قبول است و عملکرد (سرعت محاسبات) مهم است، از float یا double استفاده کنید. double دقت بیشتری دارد و برای اکثر محاسبات علمی پیش‌فرض است.
    • برای محاسبات پولی و مالی (مانند قیمت‌ها، مبالغ بانکی) که دقت دقیق اعشار (بدون خطاهای گرد کردن مبنای 2) حیاتی است، همیشه از decimal استفاده کنید. decimal اعداد را بر اساس مبنای 10 ذخیره می‌کند و از مشکلاتی که float و double با اعداد اعشاری دقیق دارند، جلوگیری می‌کند.
  • کاراکترها و رشته‌ها: از char برای یک کاراکتر و از string برای دنباله‌ای از کاراکترها (متن) استفاده کنید. به خاطر داشته باشید که string تغییرناپذیر است؛ برای ساخت رشته‌های بزرگ یا انجام تغییرات مکرر، StringBuilder را به خاطر عملکرد بهتر ترجیح دهید.
  • تاریخ و زمان: برای مدیریت تاریخ و زمان، از نوع DateTime (برای لحظات خاص در زمان) و TimeSpan (برای بازه‌های زمانی) استفاده کنید.

مصرف حافظه (Memory Footprint)

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

  • انواع عددی: byte (1 بایت) < short (2 بایت) < int (4 بایت) < long (8 بایت). همچنین float (4 بایت) < double (8 بایت) < decimal (16 بایت).
  • انواع Value در مقابل Reference:
    • انواع Value مستقیماً داده‌های خود را در Stack ذخیره می‌کنند و سربار کمتری برای Garbage Collector دارند. آن‌ها به محض خروج از Scope آزاد می‌شوند.
    • انواع Reference در Heap ذخیره می‌شوند و نیاز به تخصیص حافظه و جمع‌آوری توسط Garbage Collector دارند که می‌تواند سربار عملکردی ایجاد کند. اگر شیء شما کوچک، تغییرناپذیر، و اغلب کپی می‌شود (مانند Point یا Color)، struct (Value Type) ممکن است انتخاب بهتری از class (Reference Type) باشد زیرا سربار Heap و GC را کاهش می‌دهد.
  • با این حال، اگر Structها بیش از حد بزرگ شوند (مثلاً شامل 16 بایت یا بیشتر باشند)، کپی شدن آن‌ها می‌تواند خود باعث افت عملکرد شود. در این صورت، استفاده از کلاس (Reference Type) و پاس دادن ارجاع ممکن است بهینه‌تر باشد.

// مثال: انتخاب نادرست و بهینه سازی حافظه
// اگر نمره دانش آموز بین 0 تا 100 باشد، استفاده از long برای StudentScore بیهوده است و 8 بایت حافظه را اشغال می کند.
// long studentScore = 95; // 8 بایت

// انتخاب مناسب تر:
byte studentScore = 95; // تنها 1 بایت حافظه اشغال می کند.

عملکرد (Performance)

همانطور که ذکر شد، انتخاب نوع داده و نحوه استفاده از آن می‌تواند بر عملکرد برنامه تأثیر بگذارد:

  • Boxing/Unboxing: از تبدیل Value Type به object (Boxing) و بالعکس (Unboxing) تا حد امکان خودداری کنید، زیرا این عملیات‌ها پرهزینه هستند (شامل تخصیص حافظه در Heap و کپی کردن داده‌ها). به جای آن، برای کار با انواع مختلف داده به صورت تایپ شده و با عملکرد بالا، از Genericها (مانند List<T> یا متدهای Generic) استفاده کنید.
  • رشته‌ها: الحاق مکرر رشته‌ها با عملگر + می‌تواند به دلیل ایجاد اشیاء string جدید در هر عملیات، ناکارآمد باشد. برای سناریوهای سنگین که تعداد زیادی الحاق رشته در یک حلقه انجام می‌شود، System.Text.StringBuilder را ترجیح دهید.
  • انواع Value در مقابل Reference: برای اشیاء کوچک و بسیار پرکاربرد، Structها (Value Types) می‌توانند عملکرد بهتری داشته باشند زیرا سربار حافظه Heap و GC را کاهش می‌دهند و دسترسی به داده‌های آن‌ها سریع‌تر است. اما برای اشیاء بزرگتر، کلاس‌ها مناسب‌ترند.

خوانایی و قابلیت نگهداری (Readability and Maintainability)

انتخاب نوع داده‌ای که به وضوح هدف آن را بیان کند، کد شما را برای خودتان و دیگران قابل درک‌تر می‌کند. این جنبه اغلب به اندازه عملکرد اهمیت دارد.

  • استفاده از نام‌های توصیفی: این امر نه تنها برای متغیرها، بلکه برای انواع داده‌های تعریف شده توسط کاربر (class، struct، enum) نیز بسیار مهم است. نام متغیر و نوع آن باید مکمل یکدیگر باشند و مفهوم را منتقل کنند.
  • استفاده از enum: برای مجموعه‌ای از مقادیر ثابت و از پیش تعریف شده، enum را به جای استفاده از اعداد جادویی (magic numbers) یا رشته‌های ثابت ترجیح دهید. این کار کد را خواناتر، قابل نگهداری‌تر و کمتر مستعد خطا می‌کند، زیرا کامپایلر می‌تواند خطاهای مرتبط با مقادیر نامعتبر را تشخیص دهد.
  • Nullable Types: برای متغیرهایی که ممکن است مقداری نداشته باشند، از Nullable Value Types (مثلاً int?، DateTime?) استفاده کنید. این کار به وضوح نشان می‌دهد که متغیر می‌تواند null باشد و به شما کمک می‌کند تا خطاهای NullReference را در زمان کامپایل (با استفاده از Nullable Reference Types در C# 8+) یا در زمان اجرا به طور ایمن مدیریت کنید.
  • var (Implicitly Typed Local Variables): از کلمه کلیدی var (معرفی شده در C# 3.0) برای اعلان متغیرهای محلی استفاده کنید که کامپایلر نوع آن‌ها را از روی مقدار اولیه استنتاج می‌کند. این می‌تواند کد را کوتاه‌تر و خواناتر کند، به خصوص زمانی که نوع داده طولانی یا پیچیده باشد (مثلاً Dictionary<string, List<Customer>>). با این حال، از var در جایی که نوع متغیر بلافاصله از روی سمت راست مشخص نیست، اجتناب کنید، زیرا این کار می‌تواند خوانایی کد را کاهش دهد و ابهام ایجاد کند.

// استفاده مناسب از var
var customer = new Customer { Name = "Ali", Id = 1 }; // نوع Customer از سمت راست واضح است
var numbersList = new List { 1, 2, 3 }; // نوع List از سمت راست واضح است

// استفاده نامناسب از var (ممکن است خوانایی را کاهش دهد)
// var result = SomeComplexMethod(); // اگر نوع بازگشتی متد SomeComplexMethod بلافاصله واضح نباشد، استفاده از var نامناسب است.
// در این صورت، بهتر است نوع صریح را مشخص کنید:
// MyComplexReturnType result = SomeComplexMethod();

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

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

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

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان