کلاس‌ها و اشیاء در C#: درک مفاهیم اساسی

فهرست مطالب

کلاس‌ها و اشیاء در C#: درک مفاهیم اساسی

برنامه‌نویسی شی‌ءگرا (Object-Oriented Programming یا به اختصار OOP) ستون فقرات توسعه نرم‌افزار مدرن است و زبان C# یکی از برجسته‌ترین و قدرتمندترین زبان‌هایی است که این پارادایم را به طور کامل پیاده‌سازی می‌کند. در قلب OOP، مفاهیم کلاس (Class) و شیء (Object) قرار دارند که اساس طراحی، سازماندهی و توسعه برنامه‌های پیچیده و مقیاس‌پذیر را تشکیل می‌دهند. درک عمیق این مفاهیم، فراتر از شناخت سینتکس، برای هر توسعه‌دهنده C# حیاتی است. این مقاله به بررسی جامع و تخصصی کلاس‌ها و اشیاء در C# می‌پردازد، از تعاریف پایه گرفته تا جزئیات پیشرفته و بهترین شیوه‌های طراحی.

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

مقدمه‌ای بر برنامه‌نویسی شی‌ءگرا (OOP) و جایگاه C#

برنامه‌نویسی شی‌ءگرا یک پارادایم برنامه‌نویسی است که بر اساس مفهوم “اشیاء” بنا شده است. این اشیاء می‌توانند حاوی داده‌ها به شکل “فیلد” (Field) یا “ویژگی” (Attribute) باشند و کد را به شکل “متد” (Method) یا “روال” (Procedure) در خود نگهداری کنند. هدف اصلی OOP، سازماندهی کد به گونه‌ای است که مدل‌سازی دنیای واقعی را تسهیل کند، قابلیت استفاده مجدد (Reusability) را افزایش دهد و نگهداری از کد را ساده‌تر کند.

در دنیای نرم‌افزار، پارادایم‌های متعددی وجود دارند، از جمله برنامه‌نویسی رویه‌ای (Procedural Programming)، تابعی (Functional Programming)، منطقی (Logic Programming) و شی‌ءگرا. هر کدام از این پارادایم‌ها رویکرد متفاوتی برای حل مسائل برنامه‌نویسی ارائه می‌دهند. برنامه‌نویسی شی‌ءگرا با معرفی مفاهیمی مانند کپسوله‌سازی (Encapsulation)، وراثت (Inheritance)، چندریختی (Polymorphism) و تجرید (Abstraction) که به عنوان چهار ستون اصلی OOP شناخته می‌شوند، انقلابی در نحوه توسعه نرم‌افزار ایجاد کرد.

C# (سی شارپ) که توسط مایکروسافت توسعه یافته است، یک زبان برنامه‌نویسی مدرن، شی‌ءگرا و نوع‌امن (Type-safe) است که برای ساخت طیف وسیعی از برنامه‌ها، از جمله برنامه‌های وب (ASP.NET)، دسکتاپ (WPF, WinForms), موبایل (Xamarin, MAUI), بازی (Unity) و سرویس‌های ابری (Azure) استفاده می‌شود. C# از زمان معرفی خود در سال 2000، به طور مداوم تکامل یافته و ویژگی‌های جدیدی را برای پشتیبانی بهتر از الگوهای برنامه‌نویسی مدرن، از جمله LINQ، Async/Await، Records و Nullable Reference Types، اضافه کرده است.

در C#، تقریباً هر چیزی یک شیء است یا به گونه‌ای با اشیاء تعامل دارد. حتی انواع داده‌های اولیه (مانند `int` یا `bool`) نیز ساختارهایی (structs) هستند که از کلاس `System.ValueType` ارث‌بری می‌کنند و امکان فراخوانی متدها بر روی آن‌ها وجود دارد. این رویکرد یکپارچه، C# را به یک زبان ایده‌آل برای پیاده‌سازی اصول OOP تبدیل کرده است.

چهار ستون اصلی OOP به شرح زیر هستند که هر کدام در ادامه در ارتباط با کلاس‌ها و اشیاء توضیح داده خواهند شد:

  • کپسوله‌سازی (Encapsulation): بسته‌بندی داده‌ها و متدهای مرتبط با آن‌ها در یک واحد (کلاس) و پنهان‌سازی جزئیات پیاده‌سازی از دنیای خارج.
  • وراثت (Inheritance): امکان ایجاد کلاس‌های جدید (کلاس‌های مشتق شده) بر اساس کلاس‌های موجود (کلاس‌های پایه)، به ارث بردن ویژگی‌ها و رفتارها و افزایش قابلیت استفاده مجدد از کد.
  • چندریختی (Polymorphism): قابلیت اشیاء از کلاس‌های مختلف برای پاسخگویی به یک پیام یا فراخوانی متد به روش‌های مختلف.
  • تجرید (Abstraction): پنهان‌سازی پیچیدگی‌های غیرضروری و نمایش تنها جزئیات مربوطه به کاربر یا توسعه‌دهنده.

کلاس چیست؟ تعریف، ساختار و اعضای کلاس

در برنامه‌نویسی شی‌ءگرا، یک کلاس را می‌توان به عنوان یک طرح اولیه (Blueprint) یا قالب (Template) برای ایجاد اشیاء در نظر گرفت. کلاس، ساختار و رفتار مشترک گروهی از اشیاء را تعریف می‌کند. به عبارت دیگر، کلاس یک نوع داده تعریف شده توسط کاربر است که مجموعه‌ای از داده‌ها (فیلدها) و توابع (متدها) را در یک واحد منطقی کپسوله می‌کند.

برای مثال، اگر بخواهیم مفهوم “ماشین” را در برنامه‌نویسی مدل‌سازی کنیم، “ماشین” یک کلاس خواهد بود. این کلاس می‌تواند ویژگی‌هایی مانند “رنگ”، “مدل”، “سال ساخت” و “سرعت” (به عنوان فیلدها یا پراپرتی‌ها) و رفتارهایی مانند “روشن کردن”، “خاموش کردن”، “شتاب گرفتن” و “ترمز کردن” (به عنوان متدها) را داشته باشد.

ساختار کلی یک کلاس در C# به صورت زیر است:


[AccessModifier] class ClassName [: BaseClassOrInterface]
{
    // اعضای کلاس (Members)
    // - فیلدها (Fields)
    // - پراپرتی‌ها (Properties)
    // - متدها (Methods)
    // - سازنده‌ها (Constructors)
    // - رویدادها (Events)
    // - ایندکسرها (Indexers)
    // - اپراتورها (Operators)
    // - کلاس‌های تو در تو (Nested Classes)
}

بیایید به جزئیات اعضای اصلی یک کلاس بپردازیم:

فیلدها (Fields)

فیلدها متغیرهایی هستند که داده‌های مربوط به حالت (State) یک شیء را ذخیره می‌کنند. آن‌ها معمولاً برای نگهداری داده‌های داخلی کلاس استفاده می‌شوند و اغلب به عنوان `private` تعریف می‌شوند تا از کپسوله‌سازی پشتیبانی کنند.


public class Car
{
    private string _model; // فیلد خصوصی
    private int _year;     // فیلد خصوصی
    public string Color;   // فیلد عمومی (کمتر توصیه می‌شود)
}

پراپرتی‌ها (Properties)

پراپرتی‌ها راهی امن و کپسوله‌شده برای دسترسی به فیلدهای یک کلاس فراهم می‌کنند. آن‌ها شامل یک متد `get` برای خواندن مقدار و یک متد `set` برای نوشتن مقدار هستند. استفاده از پراپرتی‌ها به جای فیلدهای عمومی، به شما امکان می‌دهد منطق اعتبارسنجی (Validation) یا پردازش اضافی را هنگام خواندن یا نوشتن داده‌ها اعمال کنید.


public class Car
{
    private string _model;
    public string Model // پراپرتی کامل (Full Property)
    {
        get { return _model; }
        set
        {
            if (string.IsNullOrWhiteSpace(value))
            {
                throw new ArgumentException("Model cannot be empty.");
            }
            _model = value;
        }
    }

    // پراپرتی پیاده‌سازی خودکار (Auto-Implemented Property)
    // C# به طور خودکار یک فیلد خصوصی برای آن ایجاد می‌کند.
    public int Year { get; set; }

    // پراپرتی فقط خواندنی (Read-only Property)
    public string Description { get; } = "A vehicle for transportation.";
}

متدها (Methods)

متدها توابعی هستند که رفتارهای (Behaviors) یک شیء را تعریف می‌کنند. آن‌ها عملیاتی را انجام می‌دهند که می‌تواند بر روی داده‌های شیء (فیلدها) تأثیر بگذارد یا با اشیاء دیگر تعامل داشته باشد.


public class Car
{
    public void StartEngine()
    {
        Console.WriteLine("Engine started.");
    }

    public void Accelerate(int speedIncrease)
    {
        // منطق افزایش سرعت
        Console.WriteLine($"Accelerating by {speedIncrease} km/h.");
    }

    public int GetCurrentSpeed()
    {
        // منطق بازگرداندن سرعت فعلی
        return 100; // مثال
    }
}

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

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


public class Car
{
    public string Model { get; set; }
    public int Year { get; set; }

    // سازنده بدون پارامتر (Default Constructor)
    public Car()
    {
        Model = "Unknown";
        Year = DateTime.Now.Year;
    }

    // سازنده با پارامتر
    public Car(string model, int year)
    {
        Model = model;
        Year = year;
    }
}

رویدادها (Events)

رویدادها اعضایی هستند که به یک کلاس امکان می‌دهند به کدهای دیگر اطلاع دهند که اتفاق خاصی رخ داده است. آن‌ها به الگوی Observer (ناظر) یا Publisher/Subscriber (ناشر/مشترک) امکان می‌دهند که در آن اشیاء می‌توانند به رویدادهای اشیاء دیگر مشترک شوند و زمانی که رویداد رخ می‌دهد، از آن مطلع شوند.


public class Button
{
    public event EventHandler Click; // تعریف یک رویداد

    public void SimulateClick()
    {
        // زمانی که دکمه کلیک می‌شود، رویداد را فراخوانی کن
        OnClick(EventArgs.Empty);
    }

    protected virtual void OnClick(EventArgs e)
    {
        Click?.Invoke(this, e); // فراخوانی رویداد
    }
}

اصلاح‌کننده‌های دسترسی (Access Modifiers)

این اصلاح‌کننده‌ها کنترل می‌کنند که کدام اعضای یک کلاس (فیلدها، متدها، پراپرتی‌ها و …) از خارج از کلاس قابل دسترسی باشند. اینها نقش مهمی در پیاده‌سازی کپسوله‌سازی دارند:

  • public: قابل دسترسی از هر مکان.
  • private: قابل دسترسی فقط از داخل کلاسی که در آن تعریف شده‌اند.
  • protected: قابل دسترسی از داخل کلاسی که در آن تعریف شده‌اند و از کلاس‌های مشتق شده.
  • internal: قابل دسترسی فقط از داخل اسمبلی (Assembly) فعلی.
  • protected internal: قابل دسترسی از داخل اسمبلی فعلی و از کلاس‌های مشتق شده در هر اسمبلی.
  • private protected: قابل دسترسی از داخل کلاسی که در آن تعریف شده‌اند و از کلاس‌های مشتق شده در همان اسمبلی.

شیء چیست؟ نمونه‌سازی، چرخه حیات و ارتباط شیء با کلاس

در حالی که کلاس یک طرح اولیه است، شیء (Object) یک نمونه (Instance) واقعی از آن کلاس است. هر شیء، یک موجودیت ملموس است که در حافظه کامپیوتر فضای خاص خود را اشغال می‌کند و دارای مقادیر منحصر به فردی برای فیلدها و پراپرتی‌های تعریف شده در کلاس خود است. در مثال “ماشین”، یک “ماشین قرمز مدل 2023” یا یک “ماشین آبی مدل 2020” هر کدام یک شیء (اینستنس) از کلاس `Car` هستند.

اشیاء در C# از نوع ارجاعی (Reference Type) هستند. این بدان معناست که وقتی یک شیء را ایجاد می‌کنید، فضای آن در هیپ (Heap) حافظه اختصاص می‌یابد و متغیری که شما برای نگهداری شیء استفاده می‌کنید، در واقع یک ارجاع (Reference) یا آدرس به آن مکان در هیپ را ذخیره می‌کند.

نمونه‌سازی (Instantiation)

فرایند ایجاد یک شیء از یک کلاس را نمونه‌سازی می‌نامند. این کار معمولاً با استفاده از کلمه کلیدی `new` و فراخوانی یکی از سازنده‌های کلاس انجام می‌شود:


public class Car
{
    public string Model { get; set; }
    public int Year { get; set; }

    public Car(string model, int year)
    {
        Model = model;
        Year = year;
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        // نمونه‌سازی یک شیء از کلاس Car
        Car myCar = new Car("Toyota Camry", 2023);
        Console.WriteLine($"My car is a {myCar.Year} {myCar.Model}.");

        // ایجاد شیء دیگری از همان کلاس
        Car anotherCar = new Car("Honda Civic", 2020);
        Console.WriteLine($"Another car is a {anotherCar.Year} {anotherCar.Model}.");

        // هر دو myCar و anotherCar اشیاء مجزا با حالت‌های جداگانه در حافظه هستند.
    }
}

در مثال بالا، `myCar` و `anotherCar` دو شیء مجزا از کلاس `Car` هستند. هر کدام از آن‌ها فیلدهای `Model` و `Year` خود را دارند که می‌توانند مقادیر متفاوتی داشته باشند.

چرخه حیات شیء (Object Lifecycle)

چرخه حیات یک شیء در C# شامل مراحل زیر است:

  1. ایجاد (Creation): با استفاده از عملگر `new` و فراخوانی سازنده، فضای لازم در هیپ اختصاص می‌یابد و فیلدها مقداردهی اولیه می‌شوند.
  2. استفاده (Usage): شیء در برنامه برای انجام وظایف خود، دسترسی به داده‌ها و فراخوانی متدها استفاده می‌شود.
  3. عدم دسترسی (Inaccessibility): زمانی که دیگر هیچ ارجاعی به شیء در کد وجود ندارد (مثلاً متغیری که آن را نگه می‌داشته از دامنه خارج شده یا به `null` تنظیم شده است)، شیء به حالت “قابل جمع‌آوری زباله” (Garbage Collectible) در می‌آید.
  4. جمع‌آوری زباله (Garbage Collection): سیستم زمان اجرای دات نت (CLR) دارای یک جمع‌آوری‌کننده زباله (GC) خودکار است که به طور دوره‌ای حافظه اشغال شده توسط اشیاء غیرقابل دسترسی را بازیابی می‌کند. این فرآیند غیرقطعی (Non-deterministic) است، به این معنی که نمی‌توانید دقیقا زمان وقوع آن را پیش‌بینی کنید.
  5. نهایی‌سازی (Finalization – اختیاری): قبل از اینکه GC حافظه یک شیء را آزاد کند، اگر شیء دارای یک Finalizer (یا مخرب) باشد، آن فراخوانی می‌شود. این برای آزادسازی منابع غیرمدیریتی (مانند فایل هندل‌ها، اتصالات دیتابیس) استفاده می‌شود. با این حال، استفاده از `IDisposable` و الگوی `using` برای آزادسازی منابع غیرمدیریتی بسیار توصیه شده‌تر است.

درک ارتباط بین کلاس و شیء اساسی است: کلاس‌ها طرح‌های ایستا هستند که در زمان کامپایل وجود دارند و ساختار را تعریف می‌کنند، در حالی که اشیاء نمونه‌های پویا هستند که در زمان اجرا (Runtime) ایجاد می‌شوند و داده‌های واقعی را نگهداری می‌کنند و رفتارها را انجام می‌دهند.

اصول چهارگانه برنامه‌نویسی شی‌ءگرا و پیاده‌سازی آن‌ها در C#

چهار ستون اصلی OOP – کپسوله‌سازی، وراثت، چندریختی و تجرید – راهنمای قدرتمندی برای طراحی و سازماندهی کد به صورت شی‌ءگرا هستند. C# از تمام این اصول به طور کامل پشتیبانی می‌کند.

کپسوله‌سازی (Encapsulation): پنهان‌سازی اطلاعات و کنترل دسترسی

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

در C#، کپسوله‌سازی عمدتاً از طریق موارد زیر پیاده‌سازی می‌شود:

  • اصلاح‌کننده‌های دسترسی (Access Modifiers): استفاده از private برای فیلدها و public برای پراپرتی‌ها و متدهایی که قرار است از خارج از کلاس قابل دسترسی باشند.
  • پراپرتی‌ها (Properties): به جای اینکه فیلدها را مستقیماً عمومی (public) کنیم، از پراپرتی‌ها استفاده می‌کنیم. پراپرتی‌ها به ما این امکان را می‌دهند که هنگام دسترسی یا تغییر یک مقدار، منطق سفارشی (مانند اعتبارسنجی) اضافه کنیم.

public class Account
{
    private decimal _balance; // فیلد خصوصی، از بیرون کلاس قابل دسترسی نیست.

    public decimal Balance // پراپرتی عمومی، دسترسی کنترل شده به _balance
    {
        get { return _balance; }
        private set // ست کردن فقط از داخل کلاس ممکن است.
        {
            if (value < 0)
            {
                throw new ArgumentOutOfRangeException("Balance cannot be negative.");
            }
            _balance = value;
        }
    }

    public Account(decimal initialBalance)
    {
        Balance = initialBalance; // فراخوانی ست Property
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
        {
            throw new ArgumentOutOfRangeException("Deposit amount must be positive.");
        }
        Balance += amount; // فراخوانی ست Property
    }

    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
        {
            throw new ArgumentOutOfRangeException("Withdraw amount must be positive.");
        }
        if (Balance < amount)
        {
            throw new InvalidOperationException("Insufficient funds.");
        }
        Balance -= amount; // فراخوانی ست Property
    }
}

// استفاده
// Account myAccount = new Account(1000);
// myAccount.Deposit(500);
// Console.WriteLine(myAccount.Balance); // 1500
// myAccount.Balance = -100; // خطا، زیرا ست private است و مقدار منفی نامعتبر

کپسوله‌سازی به شما کمک می‌کند تا یکپارچگی داده‌های شیء را حفظ کرده و پیچیدگی را با پنهان‌سازی جزئیات پیاده‌سازی از کاربران کلاس کاهش دهید.

وراثت (Inheritance): استفاده مجدد از کد و ایجاد سلسله‌مراتب

وراثت مکانیسمی است که به یک کلاس جدید (کلاس مشتق شده یا Subclass) اجازه می‌دهد تا ویژگی‌ها (فیلدها و پراپرتی‌ها) و رفتارها (متدها) را از یک کلاس موجود (کلاس پایه یا Base Class/Superclass) به ارث ببرد. این اصل، قابلیت استفاده مجدد از کد را افزایش می‌دهد و امکان ایجاد یک سلسله‌مراتب از کلاس‌ها را فراهم می‌کند که روابط "Is-A" (یک نوع از) را نشان می‌دهد.

در C#، یک کلاس می‌تواند فقط از یک کلاس پایه به ارث ببرد (تک وراثت). برای وراثت از یک کلاس، از علامت دونقطه (:) استفاده می‌شود:


// کلاس پایه
public class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        Name = name;
    }

    public virtual void MakeSound() // متد virtual اجازه بازنویسی در کلاس‌های مشتق شده را می‌دهد
    {
        Console.WriteLine("Animal makes a sound.");
    }
}

// کلاس مشتق شده
public class Dog : Animal // Dog از Animal ارث می‌برد
{
    public string Breed { get; set; }

    public Dog(string name, string breed) : base(name) // فراخوانی سازنده کلاس پایه
    {
        Breed = breed;
    }

    public override void MakeSound() // بازنویسی متد MakeSound از کلاس پایه
    {
        Console.WriteLine("Woof! Woof!");
    }

    public void Fetch()
    {
        Console.WriteLine($"{Name} fetches the ball.");
    }
}

// کلاس مشتق شده دیگر
public class Cat : Animal
{
    public Cat(string name) : base(name) { }

    public override void MakeSound()
    {
        Console.WriteLine("Meow!");
    }
}

// استفاده
// Animal myDog = new Dog("Buddy", "Golden Retriever");
// myDog.MakeSound(); // Woof! Woof! (چندریختی در عمل)
// Animal myCat = new Cat("Whiskers");
// myCat.MakeSound(); // Meow!

کلمات کلیدی مهم در وراثت:

  • virtual: متدی در کلاس پایه را مشخص می‌کند که می‌تواند در کلاس‌های مشتق شده بازنویسی (override) شود.
  • override: متدی در کلاس مشتق شده که متد virtual کلاس پایه را بازنویسی می‌کند.
  • base: برای دسترسی به اعضای کلاس پایه (مانند فراخوانی سازنده پایه یا متدهای پایه) استفاده می‌شود.
  • sealed: کلاسی را مشخص می‌کند که نمی‌توان از آن ارث برد، یا متدی را مشخص می‌کند که نمی‌توان آن را بیشتر بازنویسی کرد.

چندریختی (Polymorphism): قابلیت‌های متنوع با یک رابط واحد

چندریختی به معنای "اشکال بسیار" است و در OOP به قابلیت اشیاء از کلاس‌های مختلف اشاره دارد که می‌توانند به یک پیام یا فراخوانی متد به روش‌های متفاوت پاسخ دهند. این امکان، کد را انعطاف‌پذیرتر و قابل توسعه‌تر می‌کند.

در C#، چندریختی از طریق دو نوع اصلی پشتیبانی می‌شود:

  1. چندریختی زمان کامپایل (Compile-time Polymorphism) - سربارگذاری متد (Method Overloading):

    به شما اجازه می‌دهد چندین متد با نام یکسان در یک کلاس داشته باشید، به شرطی که امضای آن‌ها (تعداد یا نوع پارامترها) متفاوت باشد. کامپایلر بر اساس آرگومان‌های ارائه شده، متد صحیح را در زمان کامپایل انتخاب می‌کند.

    
    public class Calculator
    {
        public int Add(int a, int b)
        {
            return a + b;
        }
    
        public double Add(double a, double b) // سربارگذاری متد Add
        {
            return a + b;
        }
    
        public int Add(int a, int b, int c) // سربارگذاری دیگر
        {
            return a + b + c;
        }
    }
    // Calculator calc = new Calculator();
    // calc.Add(1, 2); // فراخوانی متد int
    // calc.Add(1.5, 2.5); // فراخوانی متد double
            
  2. چندریختی زمان اجرا (Run-time Polymorphism) - بازنویسی متد (Method Overriding):

    این نوع چندریختی از طریق وراثت و استفاده از کلمات کلیدی virtual و override محقق می‌شود. به یک متد در کلاس پایه اجازه می‌دهد که در کلاس‌های مشتق شده با پیاده‌سازی خاص خود بازنویسی شود. در زمان اجرا، شیء واقعی (و نه نوع ارجاعی آن) تعیین می‌کند که کدام نسخه از متد فراخوانی شود.

    
    // همان مثال Animal و Dog/Cat از بخش وراثت
    // List<Animal> animals = new List<Animal>();
    // animals.Add(new Dog("Buddy", "Golden Retriever"));
    // animals.Add(new Cat("Whiskers"));
    
    // foreach (Animal animal in animals)
    // {
    //     animal.MakeSound(); // در زمان اجرا، متد MakeSound مناسب برای Dog یا Cat فراخوانی می‌شود
    // }
            

    این مثال نشان می‌دهد که چگونه یک متغیر از نوع کلاس پایه (Animal) می‌تواند به اشیاء از کلاس‌های مشتق شده (Dog و Cat) ارجاع دهد و متد MakeSound را به طور چندریختی فراخوانی کند.

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

تجرید (Abstraction): تمرکز بر "چه" به جای "چگونه"

تجرید به معنای پنهان‌سازی پیچیدگی‌های غیرضروری و نمایش تنها جزئیات مربوطه (آنچه که یک شیء انجام می‌دهد) به کاربر یا توسعه‌دهنده است، بدون اینکه "چگونه" آن را انجام می‌دهد. هدف تجرید، مدیریت پیچیدگی و ارائه یک رابط کاربری ساده و واضح است.

در C#، تجرید عمدتاً از طریق:

  • کلاس‌های انتزاعی (Abstract Classes):

    یک کلاس انتزاعی نمی‌تواند به طور مستقیم نمونه‌سازی شود. این کلاس ممکن است شامل متدهای انتزاعی (بدون پیاده‌سازی) و متدهای غیرانتزاعی (با پیاده‌سازی) باشد. کلاس‌های مشتق شده باید تمام متدهای انتزاعی را پیاده‌سازی کنند. کلاس‌های انتزاعی معمولاً برای ایجاد یک پایه مشترک برای گروهی از کلاس‌های مرتبط استفاده می‌شوند که یک رابطه "Is-A" قوی دارند.

    
    public abstract class Shape // کلاس انتزاعی
    {
        public abstract double GetArea(); // متد انتزاعی، بدون پیاده‌سازی
        public virtual void Display() // متد غیرانتزاعی (با پیاده‌سازی پیش‌فرض)
        {
            Console.WriteLine("This is a shape.");
        }
    }
    
    public class Circle : Shape
    {
        public double Radius { get; set; }
        public Circle(double radius) { Radius = radius; }
        public override double GetArea() // پیاده‌سازی متد انتزاعی
        {
            return Math.PI * Radius * Radius;
        }
        public override void Display()
        {
            Console.WriteLine($"This is a circle with radius {Radius}.");
        }
    }
    
    // Shape s = new Shape(); // خطا: نمی‌توان یک کلاس انتزاعی را نمونه‌سازی کرد
    // Shape c = new Circle(5);
    // Console.WriteLine(c.GetArea()); // 78.53...
    // c.Display();
            
  • اینترفیس‌ها (Interfaces):

    یک اینترفیس فقط یک "قرارداد" را تعریف می‌کند، یعنی مجموعه‌ای از متدها، پراپرتی‌ها و رویدادهایی که یک کلاس باید پیاده‌سازی کند. اینترفیس‌ها هیچ پیاده‌سازی یا فیلدی ندارند (تا C# 8 که متدهای پیش‌فرض اضافه شد). آن‌ها برای تعریف قابلیت‌ها یا رفتارها استفاده می‌شوند و یک رابطه "Can-Do" (می‌تواند انجام دهد) را نشان می‌دهند. یک کلاس می‌تواند چندین اینترفیس را پیاده‌سازی کند.

    
    public interface ILogger // اینترفیس
    {
        void LogInfo(string message);
        void LogError(string message);
    }
    
    public class ConsoleLogger : ILogger // پیاده‌سازی اینترفیس
    {
        public void LogInfo(string message)
        {
            Console.WriteLine($"[INFO] {message}");
        }
    
        public void LogError(string message)
        {
            Console.Error.WriteLine($"[ERROR] {message}");
        }
    }
    
    // ILogger logger = new ConsoleLogger();
    // logger.LogInfo("Application started.");
            

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

سازنده‌ها (Constructors)، مخرب‌ها (Finalizers) و مدیریت حافظه در C#

مدیریت صحیح چرخه حیات شیء و حافظه در C# برای توسعه برنامه‌های پایدار و کارآمد ضروری است. سازنده‌ها و مخرب‌ها نقش‌های متفاوتی در این فرآیند ایفا می‌کنند.

سازنده‌ها: تضمین حالت اولیه شیء

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

انواع سازنده‌ها در C#:

  1. سازنده پیش‌فرض (Default Constructor):

    اگر هیچ سازنده‌ای در کلاس تعریف نکنید، C# به طور خودکار یک سازنده عمومی (public) بدون پارامتر (default constructor) برای شما ایجاد می‌کند. این سازنده تمام فیلدها را به مقادیر پیش‌فرضشان (0 برای اعداد، `null` برای ارجاعات، `false` برای بولین‌ها) مقداردهی اولیه می‌کند.

  2. سازنده پارامتردار (Parameterized Constructor):

    شما می‌توانید سازنده‌هایی با یک یا چند پارامتر تعریف کنید تا مقادیر اولیه برای فیلدها را از خارج کلاس دریافت کنند. این به شما امکان می‌دهد اشیاء را با حالت‌های اولیه خاصی ایجاد کنید.

    
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
    
        public Person(string name, int age) // سازنده پارامتردار
        {
            Name = name;
            Age = age;
        }
    }
            
  3. سربارگذاری سازنده (Constructor Overloading):

    مانند متدها، می‌توانید چندین سازنده با امضاهای مختلف (تعداد یا نوع پارامترها) تعریف کنید تا انعطاف‌پذیری بیشتری در هنگام نمونه‌سازی فراهم شود.

    
    public class Product
    {
        public string Name { get; set; }
        public decimal Price { get; set; }
    
        public Product(string name) : this(name, 0) // فراخوانی سازنده دیگر (constructor chaining)
        {
            // Name = name; // توسط سازنده دیگر انجام می‌شود
            // Price = 0;
        }
    
        public Product(string name, decimal price)
        {
            Name = name;
            Price = price;
        }
    }
            

    در مثال بالا، : this(name, 0) نشان‌دهنده "زنجیره‌ای کردن سازنده‌ها" (Constructor Chaining) است. این به شما امکان می‌دهد که یک سازنده، سازنده دیگری از همان کلاس را فراخوانی کند، که به جلوگیری از تکرار کد کمک می‌کند.

  4. سازنده خصوصی (Private Constructor):

    یک سازنده می‌تواند private باشد. این کار از نمونه‌سازی مستقیم کلاس از خارج جلوگیری می‌کند. این الگو اغلب در پیاده‌سازی الگوی طراحی Singleton استفاده می‌شود که تضمین می‌کند فقط یک نمونه از یک کلاس در کل برنامه وجود دارد.

    
    public class SingletonLogger
    {
        private static SingletonLogger _instance;
    
        private SingletonLogger() // سازنده خصوصی
        {
            Console.WriteLine("SingletonLogger instance created.");
        }
    
        public static SingletonLogger GetInstance()
        {
            if (_instance == null)
            {
                _instance = new SingletonLogger();
            }
            return _instance;
        }
    
        public void Log(string message)
        {
            Console.WriteLine($"Log: {message}");
        }
    }
    // SingletonLogger logger1 = SingletonLogger.GetInstance();
    // SingletonLogger logger2 = SingletonLogger.GetInstance();
    // // logger1 و logger2 هر دو به یک شیء اشاره می‌کنند.
            
  5. سازنده استاتیک (Static Constructor):

    سازنده استاتیک برای مقداردهی اولیه به فیلدهای استاتیک یا انجام عملیات یک‌باره برای یک کلاس استفاده می‌شود. این سازنده بدون پارامتر، بدون اصلاح‌کننده دسترسی (public, private و ...) و فقط یک بار در طول عمر برنامه (هنگامی که کلاس برای اولین بار بارگذاری می‌شود یا اولین عضو استاتیک یا اینستنس از آن دسترسی پیدا می‌کند) فراخوانی می‌شود.

    
    public class AppConfig
    {
        public static string ConnectionString { get; private set; }
        public static int MaxRetries { get; private set; }
    
        static AppConfig() // سازنده استاتیک
        {
            Console.WriteLine("Static constructor called.");
            ConnectionString = "Data Source=server;Initial Catalog=db;Integrated Security=True";
            MaxRetries = 3;
        }
    
        public static void DisplayConfig()
        {
            Console.WriteLine($"Connection: {ConnectionString}, Retries: {MaxRetries}");
        }
    }
    // AppConfig.DisplayConfig(); // سازنده استاتیک اینجا فراخوانی می‌شود
            

مخرب‌ها (Finalizers) و جمع‌آوری زباله (Garbage Collection): مدیریت منابع نمدیریتی

بر خلاف زبان‌هایی مانند C++ که توسعه‌دهنده مسئول تخصیص و آزادسازی دستی حافظه است، C# از یک سیستم جمع‌آوری زباله خودکار (Automatic Garbage Collection یا GC) استفاده می‌کند. این سیستم مسئول بازیابی حافظه اشغال شده توسط اشیائی است که دیگر در برنامه قابل دسترسی نیستند. این ویژگی پیچیدگی مدیریت حافظه را از دوش برنامه‌نویس برمی‌دارد و از خطاهایی مانند Memory Leak (نشت حافظه) یا Dangling Pointer جلوگیری می‌کند.

Finalizers (مخرب‌ها):
مخرب‌ها (که در C# به آن‌ها Finalizers گفته می‌شود و با نام کلاس و پیشوند `~` تعریف می‌شوند) متدهایی هستند که توسط GC درست قبل از آزاد کردن حافظه یک شیء فراخوانی می‌شوند. آن‌ها برای آزادسازی منابع "غیرمدیریتی" (Unmanaged Resources) مانند فایل هندل‌ها، سوکت‌های شبکه، اتصالات دیتابیس یا حافظه تخصیص یافته توسط کدهای C++ که GC نمی‌تواند آن‌ها را مدیریت کند، استفاده می‌شوند.


public class FileReader : IDisposable
{
    private StreamReader _reader;

    public FileReader(string filePath)
    {
        _reader = new StreamReader(filePath);
        Console.WriteLine("FileReader created.");
    }

    // Finalizer - علامت ~
    ~FileReader()
    {
        Console.WriteLine("FileReader Finalizer called.");
        Dispose(false); // آزادسازی منابع غیرمدیریتی
    }

    public void Dispose() // پیاده‌سازی IDisposable
    {
        Console.WriteLine("FileReader Dispose called.");
        Dispose(true);
        GC.SuppressFinalize(this); // جلوگیری از فراخوانی Finalizer
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // آزاد کردن منابع مدیریتی
            if (_reader != null)
            {
                _reader.Dispose();
                _reader = null;
            }
        }
        // آزاد کردن منابع غیرمدیریتی (اگر وجود داشته باشد)
    }

    public string ReadLine()
    {
        return _reader?.ReadLine();
    }
}

مشکلات Finalizer:
استفاده از Finalizer ها معمولاً توصیه نمی‌شود زیرا:

  • غیرقطعی هستند: شما نمی‌توانید تضمین کنید که Finalizer دقیقا چه زمانی فراخوانی می‌شود، زیرا GC در زمان‌های نامشخصی اجرا می‌شود. این می‌تواند منجر به مشکلات پیش‌بینی نشده در آزادسازی منابع شود.
  • کاهش عملکرد: اشیاء دارای Finalizer باید در یک صف خاص توسط GC قرار گیرند که این کار سربار عملکردی (overhead) دارد.
  • پیچیدگی: پیاده‌سازی صحیح الگوی Dispose (که شامل Finalizer و IDisposable است) می‌تواند پیچیده باشد.

IDisposable و الگوی Using: راه حل ترجیحی برای آزادسازی منابع
برای آزادسازی قطعی (Deterministic) منابع غیرمدیریتی، C# الگوی `IDisposable` را ارائه می‌دهد. کلاس‌هایی که منابع غیرمدیریتی را نگه می‌دارند، باید این اینترفیس را پیاده‌سازی کنند و متد `Dispose()` را برای آزادسازی منابع فراهم کنند.

کلمه کلیدی `using` (statement) به طور خودکار متد `Dispose()` را در انتهای بلوک `using` فراخوانی می‌کند، حتی اگر استثنایی رخ دهد. این بهترین راه برای اطمینان از آزادسازی به موقع منابع است.


// استفاده از IDisposable و using
// using (FileReader reader = new FileReader("example.txt"))
// {
//     string line = reader.ReadLine();
//     Console.WriteLine(line);
// } // Dispose به طور خودکار در اینجا فراخوانی می‌شود

بنابراین، تمرکز اصلی بر روی `IDisposable` و `using` است و Finalizer ها فقط به عنوان یک مکانیزم "فرصت آخر" برای آزادسازی منابعی که به درستی `Dispose` نشده‌اند، در نظر گرفته می‌شوند.

اعضای استاتیک (Static Members): اشتراک‌گذاری بین تمام اشیاء و استفاده‌های خاص

در C#، اعضای یک کلاس می‌توانند به دو دسته اصلی تقسیم شوند: اعضای اینستنس (Instance Members) و اعضای استاتیک (Static Members).

  • اعضای اینستنس: این اعضا به یک شیء خاص از کلاس تعلق دارند. هر شیء دارای کپی‌های مخصوص به خود از فیلدهای اینستنس است و متدهای اینستنس بر روی آن شیء خاص عمل می‌کنند.
  • اعضای استاتیک: این اعضا به خود کلاس تعلق دارند، نه به یک شیء خاص. تنها یک کپی از فیلدهای استاتیک وجود دارد که بین تمام اشیاء آن کلاس به اشتراک گذاشته می‌شود. متدهای استاتیک نیز بدون نیاز به نمونه‌سازی کلاس قابل فراخوانی هستند.

کلمه کلیدی static برای تعریف اعضای استاتیک استفاده می‌شود.

فیلدهای استاتیک (Static Fields)

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


public class Configuration
{
    public static string DatabaseName { get; set; } = "DefaultDB"; // فیلد استاتیک

    static Configuration() // سازنده استاتیک برای مقداردهی اولیه پیچیده‌تر
    {
        Console.WriteLine("Static Configuration initialized.");
        // DatabaseName = ReadFromConfigFile(); // مثال از مقداردهی از فایل
    }
}
// Configuration.DatabaseName = "ProductionDB"; // دسترسی مستقیم به فیلد استاتیک
// Console.WriteLine(Configuration.DatabaseName);

متدهای استاتیک (Static Methods)

متدهای استاتیک عملیاتی را انجام می‌دهند که نیازی به حالت یک شیء خاص ندارند. آن‌ها نمی‌توانند به اعضای اینستنس (فیلدها یا پراپرتی‌های غیر استاتیک) دسترسی داشته باشند مگر اینکه یک شیء از کلاس را به عنوان پارامتر دریافت کنند. متدهای کمکی (Utility Methods) و توابعی که بر روی ورودی‌ها عمل می‌کنند و خروجی تولید می‌کنند، اغلب استاتیک تعریف می‌شوند.


public static class MathUtility // کلاس استاتیک
{
    public static int Add(int a, int b) // متد استاتیک
    {
        return a + b;
    }

    public static double CalculateAverage(params double[] numbers) // متد استاتیک
    {
        if (numbers == null || numbers.Length == 0) return 0;
        return numbers.Average();
    }
}
// int sum = MathUtility.Add(5, 3);
// double avg = MathUtility.CalculateAverage(1.0, 2.0, 3.0);

پراپرتی‌های استاتیک (Static Properties)

مشابه فیلدهای استاتیک، پراپرتی‌های استاتیک نیز داده‌های استاتیک را کپسوله می‌کنند و دسترسی کنترل شده‌ای به آن‌ها ارائه می‌دهند. آن‌ها برای مقادیر پیکربندی سراسری یا داده‌هایی که یک بار مقداردهی می‌شوند و در طول عمر برنامه ثابت می‌مانند، مفید هستند.


public class GlobalSettings
{
    public static string AppVersion { get; } = "1.0.0"; // پراپرتی فقط خواندنی استاتیک
    public static int MaxUsers { get; set; } = 100; // پراپرتی استاتیک خواندنی/نوشتنی
}
// Console.WriteLine(GlobalSettings.AppVersion);
// GlobalSettings.MaxUsers = 150;

کلاس‌های استاتیک (Static Classes)

اگر تمام اعضای یک کلاس استاتیک باشند، می‌توانید خود کلاس را نیز static تعریف کنید. یک کلاس استاتیک:

  • نمی‌تواند نمونه‌سازی شود (یعنی نمی‌توانید از آن شیء ایجاد کنید).
  • نمی‌تواند شامل اعضای اینستنس (غیر استاتیک) باشد.
  • نمی‌تواند از کلاس دیگری ارث ببرد (اما می‌تواند از اینترفیس‌ها ارث ببرد).
  • اغلب برای مجموعه‌ای از متدهای کمکی یا ابزاری (Utility Methods) استفاده می‌شود که نیازی به حالت شیء ندارند، مانند کلاس System.Math یا System.Console.

public static class UtilityHelpers
{
    public static string CapitalizeFirstLetter(string input)
    {
        if (string.IsNullOrEmpty(input)) return input;
        return char.ToUpper(input[0]) + input.Substring(1);
    }
}
// string capitalized = UtilityHelpers.CapitalizeFirstLetter("hello");

چه زمانی از `static` استفاده کنیم؟

  • هنگامی که یک عضو (فیلد، متد، پراپرتی) به طور مستقل از هر شیء از کلاس عمل می‌کند.
  • برای داده‌ها یا عملکردهایی که به صورت سراسری در سطح برنامه (نه یک شیء خاص) مورد نیاز هستند.
  • برای پیاده‌سازی الگوهایی مانند Singleton (با سازنده خصوصی استاتیک).
  • برای ایجاد کلاس‌های کمکی (Helper Classes) که فقط شامل متدهای کاربردی هستند.

چه زمانی از `static` استفاده نکنیم؟

  • هنگامی که عضو نیاز به دسترسی به حالت یک شیء (فیلدهای اینستنس) دارد.
  • هنگامی که قصد دارید از اصول OOP مانند وراثت یا چندریختی استفاده کنید. (اعضای استاتیک نمی‌توانند virtual یا override شوند).
  • استفاده بیش از حد از `static` می‌تواند منجر به کد سفت و سخت (Tightly Coupled) و دشوار برای تست شود، زیرا وابستگی‌ها پنهان می‌شوند و تزریق وابستگی (Dependency Injection) دشوار می‌شود.

مقایسه عمیق: کلاس‌های انتزاعی، اینترفیس‌ها و رکوردها

با درک اصول چهارگانه OOP، اکنون می‌توانیم به مقایسه عمیق‌تری بین سه مفهوم کلیدی در C# بپردازیم که هر کدام راهی برای تعریف رفتارها و ساختارها ارائه می‌دهند: کلاس‌های انتزاعی، اینترفیس‌ها و (اخیرتر) رکوردها.

کلاس‌های انتزاعی (Abstract Classes)

یک کلاس انتزاعی، کلاسی است که برای نمونه‌سازی مستقیم در نظر گرفته نشده است، بلکه به عنوان یک کلاس پایه برای سایر کلاس‌ها عمل می‌کند. کلاس‌های انتزاعی برای تعریف یک پایه مشترک و رفتارهای پیش‌فرض برای گروهی از کلاس‌های مرتبط که یک رابطه "Is-A" (یک نوع از) را به اشتراک می‌گذارند، استفاده می‌شوند.

ویژگی‌ها:

  • با کلمه کلیدی abstract تعریف می‌شوند.
  • نمی‌توانند مستقیماً نمونه‌سازی شوند.
  • می‌توانند شامل متدهای انتزاعی (بدون پیاده‌سازی) باشند که کلاس‌های مشتق شده باید آن‌ها را پیاده‌سازی کنند.
  • می‌توانند شامل متدهای غیرانتزاعی (با پیاده‌سازی) باشند.
  • می‌توانند شامل فیلدها، سازنده‌ها، پراپرتی‌ها و سایر اعضای کلاس معمولی باشند.
  • یک کلاس فقط می‌تواند از یک کلاس پایه انتزاعی ارث ببرد (تک وراثت).

public abstract class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public Employee(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public void DisplayFullName()
    {
        Console.WriteLine($"{FirstName} {LastName}");
    }

    public abstract decimal CalculateSalary(); // متد انتزاعی
}

public class FullTimeEmployee : Employee
{
    public decimal MonthlySalary { get; set; }

    public FullTimeEmployee(string firstName, string lastName, decimal monthlySalary)
        : base(firstName, lastName)
    {
        MonthlySalary = monthlySalary;
    }

    public override decimal CalculateSalary()
    {
        return MonthlySalary;
    }
}
// Employee emp = new FullTimeEmployee("Ali", "Rezaei", 5000);
// emp.DisplayFullName();
// Console.WriteLine(emp.CalculateSalary());

اینترفیس‌ها (Interfaces)

یک اینترفیس یک "قرارداد" را تعریف می‌کند. اینترفیس مجموعه‌ای از امضای متدها، پراپرتی‌ها و رویدادها را مشخص می‌کند که کلاس پیاده‌سازی‌کننده متعهد به پیاده‌سازی آن‌ها است. اینترفیس‌ها یک رابطه "Can-Do" (می‌تواند انجام دهد) را نشان می‌دهند.

ویژگی‌ها:

  • با کلمه کلیدی interface تعریف می‌شوند.
  • نمی‌توانند مستقیماً نمونه‌سازی شوند.
  • تا C# 8، فقط می‌توانستند شامل امضای اعضا باشند (بدون پیاده‌سازی). از C# 8 به بعد، می‌توانند شامل پیاده‌سازی‌های پیش‌فرض برای متدها باشند.
  • نمی‌توانند شامل فیلد (instance fields) باشند (فیلدهای استاتیک از C# 8 مجاز شدند).
  • نمی‌توانند شامل سازنده باشند.
  • یک کلاس می‌تواند چندین اینترفیس را پیاده‌سازی کند (وراثت چندگانه از قراردادها).

public interface IPrintable
{
    void Print();
}

public interface IEditable
{
    void Edit();
}

public class Document : IPrintable, IEditable // پیاده‌سازی چندین اینترفیس
{
    public void Print()
    {
        Console.WriteLine("Printing document...");
    }

    public void Edit()
    {
        Console.WriteLine("Editing document...");
    }
}
// IPrintable doc = new Document();
// doc.Print();
// IEditable doc2 = new Document();
// doc2.Edit();

تفاوت‌های کلیدی بین کلاس انتزاعی و اینترفیس:

ویژگی کلاس انتزاعی اینترفیس
هدف پایه مشترک برای کلاس‌های مرتبط با رابطه "Is-A". تعریف قراردادها و قابلیت‌ها با رابطه "Can-Do".
پیاده‌سازی متد هم متدهای انتزاعی (بدون پیاده‌سازی) و هم متدهای غیرانتزاعی (با پیاده‌سازی). فقط امضا (تا C# 8). از C# 8: امضا و متدهای پیش‌فرض.
فیلدها می‌تواند شامل فیلدهای عادی (اینستنس و استاتیک) باشد. نمی‌تواند شامل فیلدهای اینستنس باشد (فقط استاتیک از C# 8).
سازنده‌ها می‌تواند شامل سازنده‌ها باشد. نمی‌تواند شامل سازنده‌ها باشد.
وراثت فقط تک وراثت (یک کلاس می‌تواند از یک کلاس انتزاعی ارث ببرد). وراثت چندگانه (یک کلاس می‌تواند چندین اینترفیس را پیاده‌سازی کند).

رکوردها (Records) در C# 9 و بالاتر: یک رویکرد جدید به کلاس‌ها و اشیاء

رکوردها که در C# 9 معرفی شدند، یک نوع ارجاعی (reference type) هستند که برای سناریوهایی طراحی شده‌اند که نیاز به تعریف کلاس‌های تغییرناپذیر (Immutable) و داده‌محور (Data-centric) داریم. آن‌ها سینتکس مختصرتری برای تعریف کلاس‌هایی با هدف اصلی ذخیره داده‌ها ارائه می‌دهند و برخی ویژگی‌های پیش‌فرض مفید را برای آن‌ها فراهم می‌کنند.

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

  • تغییرناپذیری پیش‌فرض (Immutability by Default): پراپرتی‌ها به طور پیش‌فرض init-only هستند (فقط در زمان مقداردهی اولیه یا از طریق سازنده قابل تنظیم هستند) که ایجاد اشیاء تغییرناپذیر را آسان‌تر می‌کند.
  • برابری مبتنی بر مقدار (Value-based Equality): به طور پیش‌فرض، دو رکورد برابر در نظر گرفته می‌شوند اگر تمام پراپرتی‌های عمومی آن‌ها مقادیر یکسانی داشته باشند، بر خلاف کلاس‌ها که برابری آن‌ها مبتنی بر ارجاع (Reference Equality) است.
  • پشتیبانی از with expression (Non-destructive Mutation): به شما امکان می‌دهد یک کپی از یک رکورد را ایجاد کنید، در حالی که برخی از پراپرتی‌های آن را در زمان کپی تغییر می‌دهید، بدون اینکه شیء اصلی را تغییر دهید.
  • سازنده‌های اصلی (Primary Constructors): سینتکس بسیار مختصر برای تعریف پراپرتی‌ها و سازنده.
  • متد ToString() بهبود یافته: پیاده‌سازی پیش‌فرض ToString() تمام پراپرتی‌های عمومی را نمایش می‌دهد.
  • پشتیبانی از وراثت: رکوردها می‌توانند از یکدیگر ارث ببرند.

// تعریف یک رکورد با سازنده اصلی
public record PersonRecord(string FirstName, string LastName);

// تعریف یک رکورد با پراپرتی‌های init
public record ProductRecord
{
    public int Id { get; init; } // init-only property
    public string Name { get; init; }
    public decimal Price { get; init; }
}

public class ProgramRecords
{
    public static void Main(string[] args)
    {
        // نمونه‌سازی رکورد
        PersonRecord person1 = new PersonRecord("John", "Doe");
        PersonRecord person2 = new PersonRecord("John", "Doe");
        PersonRecord person3 = new PersonRecord("Jane", "Doe");

        // برابری مبتنی بر مقدار
        Console.WriteLine($"person1 == person2: {person1 == person2}"); // True
        Console.WriteLine($"person1 == person3: {person1 == person3}"); // False

        // استفاده از with expression (Non-destructive mutation)
        PersonRecord personWithNewLastName = person1 with { LastName = "Smith" };
        Console.WriteLine(personWithNewLastName); // PersonRecord { FirstName = John, LastName = Smith }
        Console.WriteLine(person1); // PersonRecord { FirstName = John, LastName = Doe }

        // کلاس‌ها در مقابل
        // public class PersonClass(string FirstName, string LastName); // This is not a record, this is a C# 12 primary constructor for a class.
        // PersonClass class1 = new PersonClass("John", "Doe");
        // PersonClass class2 = new PersonClass("John", "Doe");
        // Console.WriteLine($"class1 == class2: {class1 == class2}"); // False (Reference equality)
    }
}

چه زمانی از رکوردها استفاده کنیم؟
رکوردها برای مدل‌های داده (Data Models) که عمدتاً برای نگهداری و انتقال داده‌ها استفاده می‌شوند و انتظار تغییرناپذیری و برابری مبتنی بر مقدار دارند، ایده‌آل هستند. آن‌ها به ویژه در سناریوهایی مانند DTO (Data Transfer Objects)، Value Objects و Domain Models در معماری‌های رویداد محور (Event-driven) یا Functional Programming مفید هستند.

بهترین شیوه‌ها و الگوهای طراحی (Design Patterns) در طراحی کلاس و شیء

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

1. اصل مسئولیت واحد (Single Responsibility Principle - SRP)

یکی از اصول SOLID (مجموعه‌ای از پنج اصل راهنما برای طراحی شی‌ءگرا) که توسط رابرت سی. مارتین (Uncle Bob) معرفی شده است. SRP بیان می‌کند که "یک کلاس باید تنها یک دلیل برای تغییر داشته باشد." به عبارت دیگر، هر کلاس باید مسئولیت واحدی داشته باشد. این به معنای عدم ترکیب وظایف نامرتبط در یک کلاس است.

مزایا: کاهش پیچیدگی، افزایش خوانایی، تسهیل تست، کاهش تأثیر تغییرات.

مثال نامناسب: یک کلاس Order که هم داده‌های سفارش را نگه می‌دارد، هم اعتبار سنجی می‌کند، هم به دیتابیس ذخیره می‌کند و هم ایمیل تأیید می‌فرستد.

مثال مناسب:

  • Order: فقط داده‌های سفارش را نگهداری می‌کند.
  • OrderValidator: مسئول اعتبار سنجی سفارش.
  • OrderRepository: مسئول ذخیره و بازیابی سفارش از دیتابیس.
  • EmailService: مسئول ارسال ایمیل.

2. اصل باز/بسته (Open/Closed Principle - OCP)

OCP بیان می‌کند که "موجودیت‌های نرم‌افزاری (کلاس‌ها، ماژول‌ها، توابع) باید برای توسعه باز باشند، اما برای تغییر بسته باشند." این بدان معناست که شما باید بتوانید رفتارهای جدید را به یک سیستم اضافه کنید بدون اینکه کد موجود را تغییر دهید. این معمولاً از طریق استفاده از اینترفیس‌ها و کلاس‌های انتزاعی و چندریختی به دست می‌آید.

مثال: به جای داشتن یک متد `CalculateArea` در کلاس `Shape` با کلی `if-else` برای هر نوع شکل، یک اینترفیس `IShape` با متد `GetArea()` تعریف کنید و هر شکل (Circle، Rectangle) آن را پیاده‌سازی کند. با افزودن یک شکل جدید، نیازی به تغییر کد موجود نیست.

3. اصل وارونگی وابستگی (Dependency Inversion Principle - DIP)

DIP بیان می‌کند که "ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند؛ هر دو باید به انتزاعیات وابسته باشند. انتزاعیات نباید به جزئیات وابسته باشند؛ جزئیات باید به انتزاعیات وابسته باشند." این اصل اساس الگوی "تزریق وابستگی" (Dependency Injection - DI) است.

به جای اینکه یک کلاس به طور مستقیم یک کلاس دیگر را نمونه‌سازی کند، باید از طریق یک اینترفیس به آن وابسته باشد و پیاده‌سازی واقعی از خارج (معمولاً توسط یک کانتینر DI) به آن تزریق شود. این باعث کاهش وابستگی‌های سخت (Tight Coupling) و افزایش انعطاف‌پذیری و قابلیت تست می‌شود.


// بدون DIP (وابستگی سخت)
// public class OrderProcessor
// {
//     private Logger _logger = new Logger(); // وابستگی مستقیم به پیاده‌سازی خاص
//     public void ProcessOrder() { _logger.Log("Processing order..."); }
// }

// با DIP (وابستگی به انتزاع)
public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message) { Console.WriteLine($"Log: {message}"); }
}

public class OrderProcessor
{
    private readonly ILogger _logger; // وابسته به انتزاع

    public OrderProcessor(ILogger logger) // تزریق وابستگی از طریق سازنده
    {
        _logger = logger;
    }

    public void ProcessOrder()
    {
        _logger.Log("Processing order...");
    }
}
// OrderProcessor processor = new OrderProcessor(new ConsoleLogger());

4. ترکیب بر وراثت (Composition over Inheritance)

این یک توصیه طراحی است که بیان می‌کند بهتر است از "ترکیب" اشیاء برای دستیابی به قابلیت استفاده مجدد از کد و انعطاف‌پذیری استفاده کنیم تا از "وراثت". وراثت یک رابطه "Is-A" ایجاد می‌کند، در حالی که ترکیب یک رابطه "Has-A" ایجاد می‌کند.

مثال: به جای اینکه Duck از Bird ارث ببرد و متد Fly() را پیاده‌سازی کند (مشکل پرندگان بدون پرواز مانند پنگوئن)، بهتر است Duck یک شیء IFlyBehavior را "داشته باشد" و پرواز را به آن واگذار کند.


public interface IFlyBehavior { void Fly(); }
public class CanFly : IFlyBehavior { public void Fly() { Console.WriteLine("Flying!"); } }
public class CannotFly : IFlyBehavior { public void Fly() { Console.WriteLine("Cannot fly."); } }

public class Duck
{
    private IFlyBehavior _flyBehavior;
    public Duck(IFlyBehavior flyBehavior) { _flyBehavior = flyBehavior; }
    public void PerformFly() { _flyBehavior.Fly(); }
}
// Duck mallard = new Duck(new CanFly());
// Duck rubberDuck = new Duck(new CannotFly());

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

تا جایی که ممکن است، اشیاء را تغییرناپذیر (Immutable) طراحی کنید. یک شیء تغییرناپذیر پس از ایجاد، نمی‌تواند تغییر کند. این کار به افزایش امنیت رشته‌ای (Thread Safety)، سادگی کد و کاهش باگ‌ها کمک می‌کند، زیرا وضعیت شیء همیشه ثابت است.

پیاده‌سازی: استفاده از پراپرتی‌های init (برای رکوردها) یا پراپرتی‌های فقط get با مقداردهی در سازنده.


public class ImmutablePoint
{
    public int X { get; }
    public int Y { get; }

    public ImmutablePoint(int x, int y)
    {
        X = x;
        Y = y;
    }
    // هیچ متدی برای تغییر X یا Y پس از ساخت وجود ندارد.
}

6. نام‌گذاری (Naming Conventions)

رعایت استانداردهای نام‌گذاری (مانند CamelCase، PascalCase) برای کلاس‌ها، متدها، فیلدها و پراپرتی‌ها، خوانایی کد را به شدت افزایش می‌دهد. برای مثال، نام کلاس‌ها و متدها با PascalCase، فیلدهای خصوصی با CamelCase با پیشوند `_` و پراپرتی‌ها با PascalCase نام‌گذاری می‌شوند.

7. کوچک نگه‌داشتن کلاس‌ها و فوکوس بر یک وظیفه

کلاس‌ها باید کوچک و متمرکز بر یک وظیفه باشند. این با اصل SRP همسو است. کلاس‌های بزرگ و چند کاره (God Objects) نگهداری، تست و درک آن‌ها دشوار است. سعی کنید کلاس‌ها را به اجزای کوچک‌تر و با مسئولیت‌های مشخص تقسیم کنید.

نتیجه‌گیری

کلاس‌ها و اشیاء مفاهیم بنیادین و حیاتی در برنامه‌نویسی شی‌ءگرا و به ویژه در زبان قدرتمند C# هستند. درک عمیق از اینکه کلاس‌ها چگونه به عنوان طرح‌های اولیه برای اشیاء عمل می‌کنند، چگونه اشیاء در حافظه نمونه‌سازی می‌شوند و چگونه چرخه حیات آن‌ها مدیریت می‌شود، برای هر توسعه‌دهنده C# ضروری است.

ما به طور جامع به تشریح ساختار یک کلاس، اعضای آن مانند فیلدها، پراپرتی‌ها و متدها پرداختیم. همچنین، اهمیت سازنده‌ها در تضمین حالت اولیه معتبر اشیاء و نقش Finalizer ها و الگوی IDisposable در مدیریت منابع غیرمدیریتی را بررسی کردیم. اعضای استاتیک را نیز به عنوان راهی برای اشتراک‌گذاری داده‌ها و رفتارها در سطح کلاس، نه در سطح شیء، شناختیم.

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

نهایتاً، رعایت بهترین شیوه‌ها و الگوهای طراحی مانند SRP، OCP، DIP و ترجیح ترکیب بر وراثت، کلید نوشتن کدهای C# حرفه‌ای و با کیفیت است. با تمرین و اعمال این مفاهیم، می‌توانید مهارت‌های خود را در برنامه‌نویسی شی‌ءگرا ارتقا داده و راه‌حل‌های نرم‌افزاری قوی‌تر و پایدارتری ایجاد کنید. دنیای C# و OOP دائماً در حال تکامل است و تعهد به یادگیری مداوم برای همگام ماندن با آخرین پیشرفت‌ها و بهترین شیوه‌ها حیاتی است.

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

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

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

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

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

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

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

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