حسین احمدی
بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

برنامه نویسی شی گرا در سی شارپ (C#) | آموزش مفاهیم OOP

زبان سی شارپ یک زبان شی گرا می باشد و در آموزش برنامه نویسی شی گرا در سی شارپ قصد داریم با مفاهیم شی گرایی (OOP) در سی شارپ آشنا بشیم. اما برنامه نویسی شی گرا چیست؟ چرا ما از زبان های شی گرا استفاده می کنیم؟ مفاهیمی مانند شی و کلاس در زبان های شی گرا چه کاربردی دارند و چندین سوال دیگر که در ادامه سری آموزشی سی شارپ قصد داریم به این سوالات پاسخ داده و شما را با برنامه نویسی شی گرا و مفاهیم های مرتبط با آن آشنا کنیم. پس در ادامه دوره آموزشی سی شارپ با من همراه باشید.

دوره های شبکه، برنامه نویسی، مجازی سازی، امنیت، نفوذ و ... با برترین های ایران
سرفصل های این مطلب
  1. تعریف شی گرایی
  2. مفاهیم اساسی در برنامه نویسی شی گرا
    1. دید انتزاعی (Abstraction)
    2. پنهان سازی (Encapsulation)
    3. وراثت (Inheritance)
    4. Polymorphism چیست؟
  3. کلاس و شی
  4. فیلد و متد
    1. آشنایی با Field ها
    2. تعریف رفتار یا Method برای کلاس
    3. مثال عملی: کلاس Calculator
  5. Property ها
    1. استفاده از Property ها
    2. Automatic Properties چیست؟
  6. سازنده ها یا Contructors
    1. مقدار دهی اولیه با کمک Object Initialization
    2. سازنده ها یا Constructors
    3. زنجیره سازنده ها یا Constrcutor Chaining
    4. نوع های بدون نام یا Anonymous Types
  7. وراثت
    1. تعریف وراثت یا Inheritance و پیاده سازی آن در زبان C#
    2. کلمه کلیدی base
    3. تبدیل کلاس های مشتق شده به کلاس والد
    4. کلاس Object
  8. Polymorphism
    1. متدهای virtual
  9. abstract و sealed
    1. کلاس ها و اعضاء abstract
    2. کلاس ها و اعضاء sealed
  10. مباحث تکمیلی در وراثت
    1. سازنده هه در کلاس های فرزند و پدر
    2. سطح دسترسی protected
    3. مخفی سازی اعضاء بوسیله کلمه کلیدی new
    4. فیلدهای readonly
  11. Exntesion Method ها
    1. کلاس ها و اعضای static
    2. سازنده های static
    3. Extension Method ها
    4. کلاس های partial
  12. Value Type و Reference Type
    1. Value Types
    2. Reference Types چیست؟
    3. تفاوت Value Type و Reference Type هنگام استفاده
    4. تعریف Value Type ها با کمک struct
    5. رشته ها Reference Type هستند یا Value Type
    6. آشنایی با null و متغیرهای nullable
  13. Interface ها
    1. تعریف interface ها
    2. پیاده سازی interface ها به صورت implicite و explicit
  14. Dependency Injection
  15. تبدیل نوع یا Type Casting
    1. انواع Type Casting در زبان سی شارپ
    2. کلمات کلیدی checked و unchecked
    3. استفاده از کلاس های Helper برای تبدیل نوع داده ها
    4. کلمات کلیدی is و as
  16. Operator Overloading
    1. تعریف Cast های دلخواه
  17. Boxing و Unboxing
    1. Boxing چیست؟
    2. Unboxing چیست؟
  18. جنریک ها (Generics)
  19. List و Dictionary
    1. آشنایی با کلاس جنریک List
    2. کلاس Dictionary
  20. مدیریت خطاها
    1. خطاهای دلخواه و دستور throw

اگر تازه به دنیای برنامه نویسی وارد شدید و هیچ آشنایی مقدماتی با مفاهیم برنامه نویسی و زبان های برنامه نویسی ندارید قبل از مطالعه این مطلب، مطالعه مطلب زیر می تواند برای شما مفید باشد:

همچنین در صورتی که با مقدمات زبان برنامه نویسی سی شارپ آشنایی ندارید، بهتر است قبل از مطالعه این مطلب مطلب زیر را مطالعه کنید:

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

  1. آموزش برنامه نویسی شبکه در سی شارپ
  2. آموزش برنامه نویسی موازی در سی شارپ
  3. آموزش LINQ در سی شارپ
  4. کاملترین دوره آموزش سی شارپ صفر تا صد در دنیا (دوره آموزشی ویدیویی)

تعریف شی گرایی

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

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

  • خصوصیات یا Properties: خصوصیات مجموعه ای از صفات هستند که یک شی را توصیف می کنند. برای مثال شی ای با نام انسان را در نظر بگیرید، این شی یکسری خصوصیات دارد مانند رنگ مو، قد، وزن، رنگ چشم و غیره. تمامی این پارامترها که به توصیف یک شی می پردازند تحت عنوان خصوصیت یا Property شناخته می شوند.
  • رفتارها یا Behaviors: هر شی علاوه بر خصوصیات، شامل یکسری رفتارها می باشد، این رفتارها در حقیقت کاریست که یک شی می تواند انجام دهد. دوباره شی انسان را در نظر بگیرید، این شی می تواند نگاه کند، صحبت کند یا بشنود. رفتارها با خصوصیات تفاوت دارند و به کاری گفته می شوند که یک شی می تواند انجام دهد.

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

  1. عضو کتابخانه
  2. اپراتور نرم افزار کتابخانه
  3. دسته بندی کتاب (که همان قفسه هایی که کتاب ها در آن دسته بندی می شوند می باشد)
  4. کتاب

پس از شناسایی موجودیت ها باید خصوصیت ها و رفتارهای آن ها را شناسایی کنیم. برای مثال شی عضو کتابخانه را در نظر بگیرید. این شی شامل یکسری خصوصیت ها به شرح زیر می باشد:

  1. کد عضویت
  2. نام
  3. نام خانوداگی
  4. شماره ملی
  5. نام پدر
  6. جنسیت

همچنین هر عضو یکسری رفتارهایی دارد که مختص به عملیات های کتابخانه می باشد. برای مثال عضو کتابخانه می تواند رفتارهای زیر را داشته باشد:

  1. دریافت کتاب
  2. پس دادن کتاب
  3. ورود به کتابخانه
  4. خروج از کتابخانه

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

مفاهیم اساسی در برنامه نویسی شی گرا

برای ادامه مباحث مربوط به آموزش برنامه نویسی شی گرا در سی شارپ، لازم است که با چهار مفهوم اساسی در زبان های برنامه شی گرا آشنا شویم. این چهار مفهوم، ارکان اساسی و ستون های برنامه نویسی شی گرا می باشند که در زیر به بررسی هر یک از انها خواهیم پرداخت:

دید انتزاعی (Abstraction)

زمانی که تصمیم داریم برنامه ای را به صورت شی گرا بنویسیم، باید شروع به تحلیل سیستم و شناسایی موجودیت های آن کنیم. در بالا مثالی را در مورد برنامه کتابخانه بررسی کردیم. شی عضو را در نظر بگیرید، شاید این عضو خصوصیت های بسیاری داشته باشد، مانند رنگ چشم، رنگ مو، قد، وزن، رنگ پوست و ... .

اما آیا تمامی این خصوصیات در سیستم به کار می آید؟ در مورد رفتارهای یک شی نیز همین موضوع صدق می کند. مفهوم Abstraction به ما می گوید زمان بررسی یک موجودیت، تنها خصوصیات و رفتارهایی باید در تعریف موجودیت لحاظ شوند که مستقیماً در سیستم کاربرد دارند. در حقیقت Abstraction مانند فیلتری عمل می کنند که تنها خصوصیات و رفتارهای مورد استفاده در برنامه ای که قصد نوشتن آن را داریم از آن عبور می کنند.

پنهان سازی (Encapsulation)

فرض کنید ماشین جدیدی خریداری کرده اید، پشت فرمان ماشین می نشینید و ماشین را استارت می زنید. استارت زدن ماشین خیلی ساده است، قرار دادن سوئیچ و چرخاندن آن و روشن شدن ماشین. اما آیا پروسه ای که داخل ماشین طی شده برای روشن شدن نیز همینقدر ساده است؟

صد در صد، عملیات های بسیار دیگری اتفاق می افتد تا ماشین روشن شود. اما شما تنها سوئیچ را چرخانده و ماشین را روشن میکنید. در حقیقت پیچیدگی عملیات روشن شدن ماشین از راننده ماشین پنهان شده است. به این عملیات Encapsulation یا پنهان سازی پیچیدگی پیاده سازی عملیات های درون یک شی می گویند.

وراثت (Inheritance)

می توان گفت Inheritance یا وراثت اصلی ترین مفهوم در برنامه نویسی شی گرا است. زمانی که شما خوب این مفهوم را درک کنید 70 درصد از مفاهیم برنامه نویسی شی گرا را درک کرده اید. برای درک بهتر این مفهوم مثالی میزنیم. تمامی انسان های متولد شده بر روی کره خاکی از یک پدر و مادر متولد شده اند.

در حقیقت این پدر و مادر والدین انسان هستند. زمانی که انسانی متولد می شود یکسری خصوصیات و ویژگی ها را از والدین خود به ارث می برد، مانند رنگ چشم، رنگ پوست یا برخی ویژگی های رفتاری. در برنامه نویسی شی گرا به زبان سی شارپ نیز به همین صورت می باشد.

زمانی که شما موجودیت را طراحی می کنید، می توانید برای آن یک کلاس Base یا والد در نظر بگیرید که شی فرزند تمامی خصوصیات و رفتارهای شی والد را به ارث خواهد برد. مهمترین ویژگی وراثت، استفاده مجدد از کدهای نوشته شده است که حجم کدهای نوشته شده را به صورت محسوسی کاهش می دهد. در بخش های بعدی در مورد این ویژگی به صورت کامل توضیح خواهیم داد.

Polymorphism چیست؟

در فرهنگ لغت این واژه به معنای چند ریختی ترجمه شده است. اما در برنامه نویسی شی گرا چطور؟ خیلی از افراد با این مفهوم مشکل دارند و درک صحیحی از آن پیدا نمی کنند. مفهوم Polymorphism رابطه مستقیمی با Inheritance دارد. یعنی شما ابتدا نیاز دارید مفهوم وراثت را خوب درک کرده و سپس به یادگیری Polymorphism بپردازید.

باز هم برای درک مفهوم Polymorphism یک مثال از دنیای واقعی میزنیم. در کره خاکی ما انسان های مختلفی در کشور های مختلف و شهر های مختلف با گویش های مختلف زندگی می کنند. اما تمامی این ها انسان هستند. در اینجا انسان را به عنوان یک شی والد و انسان چینی، انسان ایرانی و انسان آمریکایی را به عنوان اشیاء فرزند که از شی انسان مشتق شده اند یا والد آنها کلاس انسان می باشد را در نظر بگیرید.

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

این کار مفهوم مستقیم Polymorphism می باشد. در زبان های برنامه نویسی شی گرا، Polymorphism به تغییر رفتار یک شی در اشیاء فرزند آن گفته می شود. در زبان سی شارپ این کار با کمک تعریف متدها به صورت virtual و override کردن آنها در کلاس های فرزند انجام می شود.

همچنین Polymorphism با کمک Interface ها قابل پیاده سازی است که در بخش های بعدی در مورد این ویژگی ها به صورت کامل صحبت خواهیم کرد.در این بخش مقدمه ای بر مفاهیم اولیه برنامه نویسی شی گرا داشتیم، در لیست زیر مباحثی که در طول دوره برنامه نویسی شی گرا با آنها آشنا خواهیم شد را مشاهده می کنید:

  1. آشنایی با class ها و object ها
  2. تعریف فیلد ها و Property ها در کلاس ها
  3. نحوه تعریف رفتار ها برای کلاس ها با کمک متدها
  4. تعریف سازنده ها یا Constructor برای کلاس ها
  5. فیلد های readonly
  6. نوع های بدون نام (Anonymous Types)
  7. آشنایی با structs و تفاوت آن با کلاس
  8. آشنایی با Reference Types و Value Types تفاوت آنها و حافظه های Stack و Heap
  9. وراثت و کاربرد آن در برنامه نویسی شی گرا
  10. انواع مختلف وراثت (Is-A و Has-A)
  11. کلمات کلیدی this و base
  12. متدهای virtual و مبحث Polymorphism
  13. کلاس های Abstract و Sealed
  14. آشنایی با Access Modifiers
  15. تعریف Interface ها و کاربرد آنها در برنامه نویسی شی گرا
  16. بررسی برخی از Interface های موجود در کتابخانه دات نت
  17. سایر مباحث (Indexer ها، Extension Method ها، بررسی کلاس Object و آشنایی با مفاهیم Boxing و UnBoxing)

با اتمام مباحث ذکر شده، وارد قسمت Generics که در نسخه دوم دات نت به آن اضافه شده است خواهیم شد. امیدوارم که تا انتهای این دوره آموزشی با بنده همراه باشید.

کلاس و شی

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

همانطور که در قسمت قبل گفتیم، زمانی که قصد نوشتن برنامه ای به صورت شی گرا را داریم، باید موجودیت های مورد استفاده را در برنامه مدل سازی کنیم. این موجودیت ها همان اشیاء هستند که در سیستم مورد استفاده قرار میگیرند. اما شیوه مدل سازی و استفاده از اشیاء چگونه خواهد بود؟ در اینجا باید با دو مفهوم آشنا شویم: 1. کلاس ها و 2. اشیاء.

  1. کلاس: نمونه ای از یک شی که داخل برنامه طراحی می شود را کلاس می گویند. برای اینکه با مفهوم کلاس بیشتر آشنا شوید یک مثال از دنیای واقعی می زنیم. فرض کنید تصمیم به ساخت یک خانه دارید. اولین چیزی که به آن نیاز خواهید داشت نقشه خانه ایست که تصمیم دارید بسازید. نقشه یک طرح اولیه و مفهومی از ساختمان به شما می دهد و بعد از روی نقشه اقدام به ساخت خانه می کنید. نقشه شامل تمامی بخش های خانه است، اطاق پذیرایی، آشپزخانه، حمام، سرویس بهداشتی و سایر بخش ها. اما فقط یک نقشه در اختیار دارید. نمی توانید از اطاق پذیرایی داخل نقشه استفاده کنید. کلاس دقیقاً معادل نقشه ای است که شما برای ساختمان خود کشیده اید. کلاس یک نمونه اولیه از موجودیت ایست که باید اشیاء از روی آن ساخته شوند.
  2. شی: باز هم به سراغ مثال قبلی می رویم. بعد از کشیدن نقشه ساختمان شما باید اقدام به ساخت خانه کنید.

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

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

همچنین می توان از روی یک کلاس، یک یا چندین شی تعریف کرد.حال که با مفاهیم اولیه کلاس و شی آشنا شدید، بهتر است با نحوه تعریف کلاس و ساخت شی آشنا شویم. تعریف کلاس بوسیله کلمه کلیدی class در زبان C# انجام می شود. ساختار کلی این دستور به صورت زیر است:

{access-modifier} class {name}
{
}

قسمت access-modifier سطح دسترسی به کلاس را تعیین می کند. ما زمانی که اقدام به تعریف کلاس یا هر قطعه کدی در زبان c# می کنیم، می توانیم سطح دسترسی به آن کد را تعیین کنیم. اما سطح دسترسی به چه معناست؟ در قسمت های اولیه آموزش گفتیم که زمان ایجاد یک پروژه به زبان C#، برای شما یک solution ایجاد شده که هر solution می تواند شامل چندین پروژه باشد. برای مثال، کلاس یا اعضای یک کلاس را تعریف می کنیم، می توانیم مشخص کنیم که این کلاس از کدام قسمت های پروژه قابل دسترس باشد. سطوح دسترسی زیر در زبان سی شارپ تعریف شده اند:

  1. private: این سطح دسترسی مشخص می کند که قطعه کد تعریف شده تنها داخل خود پروژه یا Scope مربوطه قابل دسترس باشند. برای مثال کلاسی که به صورت private تعریف شده باشد، تنها داخل همان پروژه قابل دسترس بوده و از سایر پروژه هایی که در solution تعریف شده قابل دسترس نخواهد بود، یا اعضای کلاسی که به صورت private تعریف شده اند، تنها در Scope همان کلاس که بین علامت های {} می باشد قابل دسترس خواهند بود.
  2. public: کدهایی که با این سطح دسترسی مشخص شده باشند، در تمامی قسمت های پروژه و سایر پروژه ها قابل دسترس خواهند بود.
  3. internal: سطوح دسترسی internal، تنها داخل همان پروژه قابل دسترس بوده و سایر پروژه ها به آنها دسترسی نخواهند داشت. این سطح دسترسی برای اعضای کلاس ها کاربرد زیادی دارد.
  4. protected: این سطح دسترسی زمانی که از مفهوم inheritance استفاده کنیم کاربرد دارد. در قسمت وراثت این سطح دسترسی را به تفصیل مورد بررسی قرار خواهیم داد.
  5. internal protected: همانند قسمت protected، این دسترسی نیز در قسمت وراثت توضیح داده خواهد شد که تلفیقی از دسترسی های internal و protected می باشد.

بعد از access-modifier، با کلمه کلیدی class می گوییم که قصد تعریف یک کلاس را داریم و بعد از کلمه کلیدی class در قسمت name نام کلاس را مشخص می کنیم. نام کلاس باید همیشه بر اساس قاعده PascalCase نام گذاری شود. ما دو شیوه نام گذاری داریم:

  1. camelCase: در این شیوه نام گذاری، کاراکتر ابتدای هر کلمه باید با حروف بزرگ نوشته شود غیر از کلمه اول. مانند: newEmployee، sampleDictionary.
  2. PascalCase: در این شیوه نام گذاری، کاراکتر ابتدای هر کلمه باید با حروف بزرگ نوشته شود، مانند: SampleDictionary، NewEmployee

حال، تصمیم داریم یک کلاس با نام Person تعریف کنیم. در قسمت های بعدی به این کلاس خصوصیات و رفتارهای مورد نظر را اضافه خواهیم کرد. برای تعریف کلاس، بر روی نام پروژه در پنجره Solution Explorer، با موس راست کلیک کرده و از منوی ظاهر شده از قسمت گزینه Class... را انتخاب می کنیم:


i1

بعد از انتخاب این گزینه، نام کلاس مورد نظر را در پنجره Add New Item وارد کرده و روی دکمه Add کلیک می کنیم. در اینجا نام Person را وارد می کنیم. بعد از انجام این کار، فایل جدیدی با نام Person.cs به پروژه ما اضافه می شود:


i2


i3

اگر بر روی فایل Person.cs دوبار کلیک کنیم، محتویات فایل مورد نظر به صورت زیر نمایش داده خواهد شد:


i4

نکته: در قسمت معرفی ابزارهای این مجموعه آموزشی، درباره ابزاری به نام Resharper صحبت کردیم. در صورتی که این ابزار را نصب کرده باشید، برای تعریف کلاس جدید، کافیست پروژه ای که قصد تعریف کلاس داخل آن را درید، در پنجره Solution Explorer انتخاب کرده و کلیدهای Alt+Insert را فشار دهید. با اینکار منوی زیر نمایش داده می شود:


i5

بعد از انتخاب گزینه کلاس از منوی ظاهر شده، نام کلاس از شما پرسیده شده و کلاس به پروژه شما اضافه می شود.

به سراغ محتویات فایل اضافه شده برویم:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CSharpOOP
{
    class Person
    {
    }
}

قسمت using مربوط به استفاده کلاس هایی است که در namespace های دیگر تعریف شده اند. namespace ها برای دسته بندی کدهای پروژه مورد استفاده قرار میگیرند، در حقیقت شما می توانید از لحاظ کاربردی کدهای خود را در زبان سی شارپ بوسیله namespace تقسیم بندی کنید.

برای مثال، در کد بالا، کلاس Person، در namespace یا فضای نام CSharpOOP تعریف شده است. زمانی که پروژه ای ایجاد می کنید، فضای نام پیش فرض بر اساس نام پروژه ایجاد شده و تمام کدهای شما داخل این فضای نام تعریف خواهند شد.

همچنین کلیه کلاس هایی که به صورت پیش فرض در دات نت تعریف شده اند، در فضای نام System قرار دارند. در حقیقت System فضای نام پایه برای کلیه کلاس های موجود در دات نت می باشد. همچنین می توان برای هر فضای نام یک فضای نام زیر مجموعه تعریف کرد که این جداسازی بوسیله کاراکتر . انجام می شود. برای مثال، برای فضای نام CSharpOOP می خواهیم یک فضای نام زیر مجموعه با نام DataTools تعریف کنیم:

namespace CSharpOOP.DataTools
{
}

به کد کلاس Person برگردیم. در ادامه کد، فضای نام CSharpOOP مشخص شده که داخل آن کلاس Person تعریف شده است. اگر دقت کنید، این کلاس access-modifier ندارد. کدهایی که برای آنها access-modifier مشخص نشده باشد، به صورت پیش فرض private در نظر گرفته می شوند.

بعد از تعریف کلاس بوسیله {} محدوده کلاس مشخص شده است که کدهای مربوط به کلاس داخل آن نوشته می شوند.خوب تا اینجا، ما با شیوه تعریف یک کلاس ساده آشنا شدیم. در مرحله بعد، باید از روی این کلاس یک شی بسازیم. ساختار کلی تعریف شی به صورت زیر است:

{class-name} {object-name} = new {class-name}();

در قسمت class-name، نام کلاس را مشخص می کنیم، برای مثال Person و در قسمت object-name، نام شی مورد نظر را مشخص می کنیم. در حقیقت object-name یک متغیر است که به شی ما اشاره می کند. بعد از علامت انتساب یا = باید عملیات ساخت شی را انجام دهیم.

بوسیله کلمه کلیدی new می گوییم که تصمیم به ساخت یک شی جدید داریم و در مقال آن نام کلاسی که می خواهیم از روی آن شی بسازیم را می نویسیم. دقت کنید که بعد از نوشتن نام کلاس در مقال کلمه کلیدی new باید () حتماً نوشته شود، در غیر اینصورت با پیغام خطا مواجه خواهید شد. با توضیحات بالا، می توان گفت عملیات ساخت شی در دو مرحله انجام می شود:

  1. تعریف متغیری که شی داخل آن نگهداری می شود (دستورات قبل از عملیات انتساب).
  2. ساخت شی و قرار دادن آن داخل متغیر مربوطه (دستورات بعد از عملیات انتساب).

حال از روی کلاس Person یک شی ایجاد می کنیم. کد متد Main در فایل Program.cs را به صورت زیر تغییر دهید:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CSharpOOP
{
    class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person();
        }
    }
}

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

Person person1 = new Person();
Person person2 = new Person();
Person person3 = new Person();

دقت کنید، کلاس Program در فایل Program.cs نیز داخل فضای نام CSharpOOP قرار دارد. شما می توانید در فایل های متفاوت فضای نام همنام داشته باشید، بدین معنی که کلیه کدها در همان فضای نام قرار خواهند گرفت. در صورتی که شما در متد Main نام فضای نام CSharpOOP را تایپ کنید و پس از آن کلید . را بزنید، لیستی که از محتویات آن فضای نام برای شما نمایش داده خواهد شد:


i6

اما فرض کنید، کد ما در فایل Person.cs، در فضای نام دیگری با نام CSharpOOP.Entities تعریف شده بود:

namespace CSharpOOP.Entities
{
    class Person
    {
    }
}

در این حالت، زمانی که شما در فایل Program.cs و متد Main، تصمیم دارید از روی کلاس Person شی بسازید، باید آدرس کامل فضای نام را نیز هنگام ساخت شی مشخص کنید، زیرا فضای نام کلاس های Program و Person دیگر یکسان نیستند:

CSharpOOP.Entities.Person person = new CSharpOOP.Entities.Person();

اما در اینجا نکته ای وجود دارد، چون ابتدای فضای نام کلاس های Program و کلاس Person یکسان می باشد، یعنی فضای نام Entities زیر مجموعه CSharpOOP قرار دارد و کلاس Program نیز در فضای نام CSharpOOP تعریف شده، می توان از نوشتن قسمت اول فضای نام یعنی CSharpOOP خودداری کرد:

Entities.Person person = new Entities.Person();

دقت کنید، اگر فضای نام را برای ایجاد شی ننویسیم، با پیغام خطا مواجه خواهیم شد. اما راهی وجود دارد که آدرس کامل کلاس را ننویسیم، برای اینکار از دستور using استفاده می کنیم که در بالا نیز به آن اشاره شد. دستور using کلیه کدهای داخل یک فضای نام را داخل فضای نام جاری قابل دسترس می کند. برای مثال بالا، کافیست در قسمت using فایل Program.cs، دستور زیر را بنویسیم:

using CSharpOOP.Entities;

با نوشتن دستور بالا، دیگر نیازی به نوشتن آدرس فضای نام هنگام ساخت شی نخواهد بود. نمونه دیگر استفاده از دستور using، استفاده از دستورات کلاس Console می باشد که در قسمت های قبل با آن زیاد کار کردیم. کلاس Console داخل فضای نام System که فضای نام پایه کلیه کلاس های دات نت می باشد تعریف شده.

اما بدلیل اینکه در ابتدای فایل Program.cs دستور using System; نوشته شده است، کافیست تنها نام کلاس Console را بنویسیم و نیازی به نوشتن آدرس کامل آن به صورت System.Console نمی باشد.شما می توانید داخل یک فایل چندین کلاس را تعریف کنید. برای مثال، در فایل Program.cs می توانید بعد از اتمام کد کلاس Program.cs، اقدام به تعریف کلاس Person نمایید:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CSharpOOP.Entities;

namespace CSharpOOP
{
    class Program
    {
        static void Main(string[] args)
        {
            Entities.Person person = new Entities.Person();
        }
    }

    class Person
    {
        
    }
}

اما بهتر است برای هر کلاس، یک فایل جداگانه در نظر بگیرید تا ساختار مناسب برای پروژه ای که تصمیم به انجام آن دارید حفظ شود.یکی دیگر از قابلیت های موجود در Solution Explorer، قابلیت پوشه بندی فایل ها داخل پروژه می باشد. برای مثال، می توانید کلاس های مربوط به موجودیت های برنامه را داخل یک پوشه قرار دهید.

برای اینکار، بر روی پروژه راست کلیک کرده، از قسمت Add گزینه New Folder را انتخاب کنید. با اینکار پوشه جدیدی به پروژه شما اضافه می شود که می توانید برای آن یک نام دلخواه انتخاب کنید:


i7

در صورتی که ابزار Resharper را نصب کرده باشید، با زدن کلید های Alt+Insert بر روی پروژه داخل Solution Explorer از منوی ظاهر شده گزینه New Folder را برای افزودن پوشه جدید انتخاب کنید.پس از تعریف پوشه، با انتخاب آن و تکرار مراحل قبلی برای ایجاد کلاس، می توانید داخل آن پوشه یک فایل جدید ایجاد کنید. برای مثال، پوشه ای با نام Entities داخل پروژه تعریف کرده و کلاسی با نام Car داخل آن تعریف کنید. بعد از اینکار محتویات فایل شما به صورت زیر خواهد بود:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace CSharpOOP.Entities
{
    public class Car
    {
         
    }
}

به یک نکته توجه کنید که فضای نام یا namespace کلاس Car به صورت خودکار CSharpOOP.Entities انتخاب شده است، زیرا کلاس داخل پوشه Entities که در پروژه CSharpOOP قرار دارد اضافه شده. در صورتی که شما داخل یک پوشه، پوشه جدیدی اضافه کرده و داخل آن یک کلاس اضافه کنید، آدرس فضای نام مبتنی بر نام آن پوشه انتخاب خواهد شد.

پس نکته بعدی که باید مد نظر داشته باشید، زمانی که قصد دارید کدهای خود را بوسیله فضاهای نام دسته بندی کنید، حدالامکان برای آنها پوشه ایجاد کنید، اجباری به اینکار نیست، اما برای حفظ ساختار و نظم پروژه اینکار توصیه می شود.در این قسمت از سری آموزش سی شارپ، با مفاهیم کلاس، شی، فضاهای نام، دستور using و پوشه بندی فایل ها داخل پروژه آشنا شدیم. در قسمت بعدی آموزش با نحوه تعریف خصوصیت و رفتار برای کلاس ها و شیوه استفاده از آنها بوسیله اشیاء ساخته شده آشنا خواهیم شد.

فیلد و متد

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

public class Person
{
}

بعد از ایجاد کلاس باید از روی آن یک شی یا اصطلاحاً Instance یا نمونه بسازیم:

Person person = new Person();

دقت کنید، می توانیم از کلمه کلیدی var که در قسمت های ابتدایی این آموزش با آن آشنا شدیم استفاده کنیم:

var person = new Person();

کلمه کلیدی var را می توان هنگام ایجاد متغیرها و ایجاد شی از روی کلاس ها استفاده کرد. ما تا اینجا تنها یک کلاس تعریف کردیم. اما این کلاس هیچ خصوصیت یا رفتاری ندارد. زمانی که کلاسی تعریف می کنید باید اعضا یا Member های آن کلاس را مشخص کنید. هر کلاس به طور کلی می تواند شامل موارد زیر باشد:

  1. Field
  2. Property
  3. Method
  4. Indexer
  5. Event
  6. Nested Type

آشنایی با Field ها

در ابتدا به بررسی مفهوم Field پرداخته و شیوه دسترسی به اعضای کلاس را خواهیم گفت. فیلد متغیری است که داخل یک کلاس تعریف شده و امکان ذخیره مقداری را به ما می دهد. این متغیر می تواند از نوع داده های اولیه دات نت یا کلاسی باشد که تعریف کرده ایم. شیوه تعریف Field دقیقاً مشابه تعریف متغیر می باشد. با این تفاوت که ما زمان تعریف یک Field امکان استفاده از کلمه کلیدی var را نداریم. برای کلاس Person دو فیلد با نام های FirstName و LastName تعریف می کنیم:

public class Person
{
    public string FirstName;
    public string LastName;
}

دقت کنید که قبل از مشخص کردن نوع داده فیلد، سطح دسترسی آن مشخص شده است. در اینجا ما فیلدها را از نوع public تعریف کردیم. یعنی این فیلدها خارج از scope کلاس و بیرون از پروژه نیز قابل دسترس میی باشند. حال که فیلدهای مورد نظر را داخل کلاس تعریف کردیم

می توانیم بعد از ایجاد شی از کلاس، به این فیلدها دسترسی داشته باشیم. برای دسترسی به فیلدها بعد از نوشتن نام شی از کاراکتر . استفاده می کنیم تا لیستی از اعضای آن کلاس برای نمایش داده شود. تصویر زیر مربوط به کد ما در متد Main است:


وب سایت توسینسو

کد Main را به صورت زیر تغییر دهید:

var person = new Person();

person.FirstName = "Hossein";
person.LastName = "Ahmadi";

Console.WriteLine(person.Firstname + " " + LastName);

کد بالا از سه بخش نشکیل شده است.

  1. ایجاد یک شی از روی کلاس Person
  2. مقدار دهی فیلد ها
  3. استفاده از فیلدها برای نمایش خروجی

در همه شرایط، زمانی که میخواهیم به یک عضو کلاس دسترسی داشته باشیم، باید از کاراکتر . استفاده کنیم، برای مثال در فیلدهای بالا، چه زمانی که قصد مقدار دهی فیلد را داریم، چه زمانی که می خواهیم مقداری از فیلد را بخوانیم.بوسیله فیلدها می توان خصوصیات یک کلاس را تعریف کرد، در مورد خصوصیات در قسمت قبل توضیحاتی دادیم.

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

می توانیم رفتارهای یک کلاس را static تعریف کنیم، مانند کاری در کلاس Program در قسمت های قبلی می کردیم، اما در حال حاضر، متدها به صورت static تعریف نمی شوند. با اعضای static کلاس ها در قسمت های بعد به صورت کامل آشنا خواهیم شد

اما به صورت خلاصه تفاوت اعضای static و غیر static در این است که برای دسترسی به اعضای static نیازی به ایجاد شی از روی کلاس نیست، مانند کلاس Console که برای استفاده از اعضای آن مانند WriteLine شی ای از کلاس Console ایجاد نمی کنیم، اما برای استفاده از اعضای غیر static باید حتماً از روی کلاس یک شی ایجاد شود.

تعریف رفتار یا Method برای کلاس

در ادامه می خواهیم برای کلاس Person یک رفتار تعریف کنیم. موجودیت Person چه کارهایی می تواند انجام دهد؟ برای مثال یک شخص می تواند صحبت کند، پس رفتاری با نام Speak تعریف می کنیم و در آن یک پیغام مناسب در خروجی چاپ می کنیم:

public class Person
{
    public string FirstName;
    public string LastName;

    public void Speak()
    {
        Console.WriteLine("Hello, my name is " + FirstName + " " + LastName + ".");
    }
}

در کد بالا، متد یا رفتار Speak پیغامی را به همراه نام و نام خانوداگی مشخص شده توسط فیلدها در خروجی نمایش می دهد. دقت کنید، داخل کلاس هم ما می توانیم از اعضای یک کلاس استفاده کنیم. خیلی وقت ها رفتارهای یک کلاس وابسته به مقادیر خصوصیات یک کلاس هستند.

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

var person = new Person();
person.FirstName = "Hossein";
person.LastName = "Ahmadi";
person.Speak();
Console.ReadLine();

مثال عملی: کلاس Calculator

در ادامه یک مثال واقعی تر را بررسی می کنیم. کلاسی می نویسیم با نام Calculator که دو فیلد با نام های FirstNumber و SecondNumber دارد. همچنین چهار متد با نام های Sum و Substract و Multiply و Divide تعریف می کنیم که بر اساس مقادیر FirstNumber و SecondNumber، در خروجی حاصل جمع، تفریق، ضرب یا تقسیم را نمایش دهد. کلاس Calculator را به صورت زیر تعریف کنید:

public class Calculator
{
    public int FirstNumber;
    public int SecondNumber;

    public int Sum()
    {
        return FirstNumber + SecondNumber;
    }

    public int Substract()
    {
        return FirstNumber - SecondNumber;
    }

    public int Multiply()
    {
        return FirstNumber * SecondNumber;
    }

    public int Divide()
    {
        return FirstNumber / SecondNumber;
    }
}

دقت کنید، رفتارهای کلاس همگی مقدار بازگشتی از int دارند. در ادامه متد Main را به صورت زیر تغییر دهید:

var calculator = new Calculator();
calculator.FirstNumber = 12;
calculator.SecondNumber = 17;
Console.WriteLine("Sum: " + calculator.Sum());
Console.WriteLine("Substract: " + calculator.Substract());
Console.WriteLine("Multiply: " + calculator.Multiply());
Console.WriteLine("Divide: " + calculator.Divide());
Console.ReadLine();

کد بالا، خروجی حاصل جمع، تفریق، ضرب و تقسیم اعداد 12 و 17 را در خروجی چاپ می کند. تا اینجا با مبحث رفتارها و خصوصیات در کلاس ها آشنا شدیم. در بخش بعدی در باره شیوه کنترل دسترسی به مقادیر یک خصوصیات در کلاس ها یعنی مبحث Property ها صحبت خواهیم کرد

Property ها

در قسمت قبلی آموزش، در مورد فیلدها و نحوه تعریف آنها در کلاس آشنا شدیم، اما گاهی اوقات نیاز داریم که بر روند مقداردهی و گرفتن مقدار یک فیلد نظارت داشته باشیم. زمانی که یک فیلد تعریف می کنیم، عملیات انتساب مقدار و گرفتن مقدار به صورت مستقیم از داخل فیلد انجام شده و امکان انجام هیچ گونه نظارتی بر روی این عملیات ها وجود ندارد. برای رفع این مشکل، دو راه وجود دارد:

  1. استفاده از متدها برای ست کردن و گرفتن مقدار از فیلد.
  2. استفاده از Property ها

افرادی که با زبان جاوا آشنا هستند با متدهای get و set در کلاس ها آشنایی دارند. این متدها عملیات خواندن و نوشتن در فیلدها را برای ما انجام می دهند. مثالی از زبان سی شارپ می زنیم. کلاس Person را در نظر بگیرید:

public class Person
{
    public string FirstName;
    public string LastName;
}

این کلاس، ما زمانی که یک شی از این کلاس می سازیم به صورت مستقیم فیلدها را مقدار دهی کرده یا مقدار آنها را می خوانیم. اما برای کنترل دسترسی به فیلدها، ابتدا باید سطح دسترسی فیلدها را به private تغییر بدیم. زمانی که یک عضو کلاس که در اینجا فیلدها هستند را به private تغییر می دهیم، آن عضو تنها داخل همان کلاس قابل دسترس خواهد بود. برای اولین قدم، کلاس Person را به صورت زیر تغییر می دهیم:

public class Person
{
    private string firstName;
    private string lastName;
}

به نام گذاری فیلدها دقت کنید، زمانی که فیلدها به private تغییر کردند، نام گذاری بر اساس قاعده camelCase انجام می شود. این قاعده برای کلیه فیلدهای private کلاس ها حکم می کند. البته الزامی به این کار نیست، اما برای رعایت اصول کد نویسی بهتر است از این قواعد پیروی کنیم.

بعضی از برنامه نویس ها ابتدای نام فیلدهای private از کاراکتر __ استفاده می کنند. این موضوع کاملاً دلخواه می باشد، اما سعی کنید در نام گذاری فیلدها public و private تفاوت قایل شوید.در قدم بعدی باید بتوانیم در خارج از کلاس، عملیات خواندن و مقدار دهی فیلد را انجام دهیم. در روش اول گفتیم که از متدهای get و set برای اینکار استفاده می کنیم. کلاس Person را به صورت زیر تغییر می دهیم:

public class Person
{
    private string firstName;
    private string lastName;

    public string GetFirstName()
    {
        return firstName;            
    }

    public void SetFirstName(string value)
    {
        firstName = value;
    }

    public string GetLastName()
    {
        return lastName;
    }

    public void SetLastName(string value)
    {
        lastName = value;
    }
}

در تصویر زیر، لیست نمایش داده شده برای شی ای از کلاس Person را مشاهده می کنید:


i1

حال می توانیم عملیات خواندن و نوشتن فیلدها را در خارج از کلاس بوسیله این دو متد انجام دهیم:

var person = new Person();

person.SetFirstName("Hossein");
person.SetLastName("Ahmadi");

Console.WriteLine(person.GetFirstName() + " " + person.GetLastName());
Console.ReadLine();

حال فرض کنید، یک فیلد باید تنها خواندنی باشد، برای اینکار کافیست بخش Set را از کلاس حذف کنید یا فرض کنید عملیات نوشتن باید تنها در داخل خود کلاس انجام شود و از بیرون کلاس دسترسی نوشتن باید بسته شود، برای این کار کافیست که دسترسی متد Set برای فیلد مورد نظر را به private تغییر دهیم. در مثال زیر عملیات نوشتن برای فیلد firstName به صورت private تعریف شده و فیلد lastName قابلیت نوشتن ندارد:

public class Person
{
    private string firstName;
    private string lastName;

    public string GetFirstName()
    {
        return firstName;            
    }

    private void SetFirstName(string value)
    {
        firstName = value;
    }

    public string GetLastName()
    {
        return lastName;
    }
}

استفاده از Property ها

اما در زبان سی شارپ به صورت دیگری می توان این کنترل را انجام داد و آن استفاده از Property ها می باشند. ساختار کلی property ها به صورت زیر می باشد:

{access-modifier} {data-type} {property-name}
{
    [access-modifier] get
    {
        // body for get value
    }
    [access-modifier] set
    {
        // body for set value
    }
}

اما بررسی هر یک از قسمت های ساختار Property:

  1. access-modifier: سطح دسترسی به Property را تعیین می کند. Property نیز مانند فیلد می تواند سطح دسترسی داشته باشد.
  2. data-type: نوع Property که یکی از Data Type های دات نت یا کلاسی که به صورت دستی نوشته شده باشد.
  3. property-name: نام Property، برای نام گذاری Property ها همیشه از قاعده PascalCase استفاده کنید، سطح دسترسی تفاوتی ندارد، همیشه PascalCase تعریف کنید.
  4. بدنه get: این بدنه، دقیقاً معادل متد Get ایست که در قسمت قبلی تعریف کردیم. شما داخل بدنه get هر دستوری را می توانید بنویسید، در حقیقت این بدنه مانند یک متد عمل کرده و زمانی که شما مقدار Property را می خوانید (مثلاً برای چاپ با دستور

WriteLine) بدنه get اجرا می شود. دقت کنید بدنه get حتماً باید مقداری را با دستور return بر گرداند. همچنین این بدنه می تواند دارای access-modifier باشد، یعنی سطح دسترسی خواندن مقدار را مشخص می کند. در صورتی که سطح دسترسی را مشخص نکنید به صورت پیش فرض public در نظر گرفته می شود.

  1. بدنه set: این بدنه، دقیقاً معادل متد Set در مثال قبلی است. زمانی که شما مقداری را داخل Property ست می کنید، بدنه set اجرا می شود. داخل بدنه set پارامتر پیش فرضی وجود دارد به نام value که مقدار ست شده داخل Property داخل آن قرار

گرفته و شما می توانید به آن از داخل بدنه set دسترسی داشته باشید. همچنین می توان برای بدنه set سطح دسترسی را مشخص کرد. در صورتی که سطح دسترسی را مشخص نکنید به صورت پیش فرض public در نظر گرفته می شود.به مثال کلاس Person بر می گردیم. تصمیم داریم عملیات هایی که در بوسیله متدهای Get و Set انجام دادیم را با Property ها پیاده سازی کنیم. کلاس Person را به صورت زیر تغییر دهید:

public class Person
{
    private string firstName;
    private string lastName;

    public string FirstName
    {
        get { return firstName; }
        set { firstName = value; }
    }

    public string LastName
    {
        get { return lastName; }
        set { lastName = value; }
    }
}

در کد بالا، دو Property با نام های FirstName و LastName تعریف کردیم که عملیات خواندن و نوشتن از فیلدهای مربوطه را انجام می دهند. نتیجه اعضای نمایش داده شده برای کلاس Person هنگام استفاده از نمونه یا Instance آن را در تصویر زیر مشاهده

می کنید:


i2

در ادامه کد متد Main را به صورت زیر تغییر دهید:

var person = new Person();

person.FirstName = "Hossein";
person.LastName = "Ahmadi";

Console.WriteLine(person.FirstName + " " + person.LastName);
Console.ReadKey();

در کد بالا، زمانی که مقدار Hossein را داخل فیلد FirstName ست می کنیم، بدنه set مربوط به خاصیت FirstName اجرا می شود که در این بدنه ما مقدار value که همان مقدار ست شده در هنگام استفاده از کلاس است را داخل فیلد firstName قرار می دهیم. برای LastName هم به همین صورت است. شاید بپرسید پارامتر value کجا تعریف شده است؟

این پارامتر به صورت پیش فرض برای بدنه set وجود دارد که نگه دارنده مقدار ست شده داخل Property است.همچنین زمانی که داخل دستور WriteLine مقدار FirstName یا LastName را می خوانیم، بدنه get مربوط به همان Property اجرا می شود. در حقیقت Property ها واسطی میان فیلدها و استفاده کننده از کلاس ها هستند.

مانند یک انبار دار که عملیات کنترل ورود و خروج از انبار را کنترل می کند و عملیات تحویل دادن کالا یا گرفتن کالا و قرار دادن آن در انبار را انجام می دهد.برای نوشتن Property ها، حتماً نیازی به تعریف Field برای آنها نیست، شما می توانید هر کدی را برای بدنه get یا set بنویسید. برای مثال، می خواهیم به کلاس Person یک Property تعریف کنیم که نام کامل شخص را بر گرداند. نام این خاصیت را FullName می گذاریم:

public class Person
{
    private string firstName;
    private string lastName;

    public string FirstName
    {
        get { return firstName; }
        set { firstName = value; }
    }

    public string LastName
    {
        get { return lastName; }
        set { lastName = value; }
    }

    public string FullName
    {
        get { return FirstName + " " + LastName; }
    }
}

حال کد Main قسمت قبلی را می توان به صورت زیر تغییر داده و از Property جدیدی که تعریف کردیم استفاده کنیم:

var person = new Person();

person.FirstName = "Hossein";
person.LastName = "Ahmadi";

Console.WriteLine(person.FullName);
Console.ReadKey();

نگاهی دوباره به کلاس Person و خاصیت FullName می کنیم، اگر دقت کرده باشید این خاصیت تنها بدنه get را دارد و بدنه set را برای آن ننوشتیم. دلیل این امر آن است که FullName تنها برای ترکیبی از firstName و lastName را بر میگرداند. در صورتی که بخواهیم مقداری داخل FullName بریزیم، با پیغام خطا مواجه می شویم.


i3

Property هایی که بدنه get را ندارند Write-Only و آنهایی که بدنه set را ندارند Read-Only می گوییم. همچنین همانطور که قبلاً هم گفتیم می توانیم علاوه بر خود Property برای هر یک از بدنه های get و set نیز سطح دسترسی مشخص کنیم. برای مثال میخواهیم خاصیت FirstName تنها داخل خود کلاس قابلیت نوشتن داشته باشد، برای اینکار کافیست بدنه set را به صورت private تعریف کنیم:

public string FirstName
{
    get { return firstName; }
    private set { firstName = value; }
}

Automatic Properties چیست؟

گاهی اوقات، Property که تعریف می کنیم تنها عملیات خواندن و نوشتن یک فیلد را کنترل می کند. برای مثال، کلاس Person را در نظر بگیرید:

public class Person
{
    private string firstName;

    public string FirstName
    {
        get { return firstName; }
        set { firstName = value; }
    }
}

کد مربوط به خصوصیت بالا را می توان به شکل زیر نیز نوشت:

public class Person
{
    public string FirstName { get; set; }
}

کامپایلر بعد از کامپایل کد بالا، به صورت خودکار یه فیلد برای خاصیت نوشته شده تعریف کرده و بدنه get و set آن را به صورت خودکار می نویسد. از مزیت های Auto-Property ها حجم کد کمتر و البته قابلیت کنترل دسترسی به عملیات های خواندن و نوشتن Property ها می باشد. مثال بالا را جوری تغییر می دهیم که خاصیت FirstName تنها داخل کلاس قابل نوشتن باشد:

public class Person
{
    public string FirstName { get; private set; }
}

به این نکته توجه داشته باشید، زمانی که از Auto-Property ها استفاده می کنید، حتماً باید get و set را بنویسید، در غیر اینصورت پیغام خطا دریافت خواهید کرد. البته این مشکل در نسخه 6 زبان سی شارپ برطرف شده است.در این قسمت با Property ها آشنا شدیم و متوجه شدیم که چگونه می توان عملیات خواندن و نوشتن مقادیر یک کلاس را کنترل کرد. در قسمت بعدی با مفهوم سازنده ها در کلاس ها آشنا شده و به بررسی حالت های مختلف ایجاد شی از روی کلاس ها خواهیم پرداخت

سازنده ها یا Contructors

در قسمت قبلی آموزش در مورد خصوصیات یا Property ها و نحوه صحیح استفاده از آنها در کلاس ها صحبت کردیم. در این بخش در مورد سازنده ها یا Constructors، مقدار دهی اولیه اشیاء (Object Initialization) و نوع های بدون نام (Anonymous Types) صحبت می کنیم.زمانی که شما کلاسی را تعریف می کنید

این کلاس حاوی یکسری خصوصیات و یکسری رفتارها یا همان متدها می باشد. شما بعد از ایجاد شی، خصوصیات را مقدار دهی کرده و از شی استفاده می کنید. اما چندین راه دیگر برای مقدار دهی خصوصیات و آماده سازی اولیه کلاس وجود دارد. ما در این قسمت دو روش مختلف را بررسی می کنیم:

  1. مقدار دهی اولیه شی یا Object Initialization
  2. استفاده از سازنده ها یا Constructors

مقدار دهی اولیه با کمک Object Initialization

در این روش، شما زمانی که اقدام به ایجاد یک شی می کنید، می توانید مقادیر خصوصیات و فیلدهای آن را مشخص کنید. کلاس Person را در نظر بگیرید:

public class Person
{
    public string FirstName;
    public string LastName;
}

به صورت پیش فرض، شما یک شی از کلاس ساخته و خصوصیات آن را مقدار دهی می کنید:

var person = new Person();
person.FirstName = "Hossein";
person.LastName = "Ahmadi";

مقدار دهی اولیه شی کار ساده ایست، کافیست پس از نوشتن () بعد از نام کلاس در قسمت new بین علامت های {} مقادیر خصوصیات را مشخص کنیم:

var person = new Person()
{
    FirstName = "Hossein",
    LastName = "Ahmadi"
};

با این کار مقادیر FirstName و LastName در کلاس Person مقدار دهی اولیه خواهند شد. می توانید در این حالت، از نوشتن () صرفنظر کنید:

var person = new Person
{
    FirstName = "Hossein",
    LastName = "Ahmadi"
};

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

سازنده ها یا Constructors

اگر دقت کرده باشید، زمانی که شی ای را تعریف می کنیم، بعد از نوشتن نام کلاس بعد از کلمه new از () استفاده می کنیم، مشابه زمانی که تصمیم به صدا زدن یک متد دارید. دلیل اینکار، پروسه ایست که سی شارپ برای ایجاد کردن کلاس ها انجام می دهد.

زمانی که شما شی ای از یک کلاس ایجاد می کنید، سی شارپ قسمتی با نام سازنده یا Constructor را برای آن کلاس صدا می زند. این سازنده یک متد می باشد که می تواند بدون پارامتر یا با پارامتر باشد و داخل آن کدی نوشته می شود که می خواهیم در هنگام ایجاد شی اجرا شود.

با یک مثال ساده سازنده ها را بررسی می کنیم. کلاس Person را در نظر بگیرید، برای این کلاس یک سازنده تعریف می کنیم که مقادیر FirstName و LastName را به عنوان ورودی گرفته و خصوصیات مربوطه را مقدار دهی می کند:

public class Person
{
    public Person(string firstName, string lastName)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
}

در کد بالا، قسمت سانده برای کلاس Person با دو پارامتر تعریف شده است. به شیوه تعریف سازنده دقت کنید، ابتدا سطح دسترسی به سازنده مشخص شده، سپس نام کلاس نوشته شده که برای سازنده ها، این نام دقیقاً باید معادل نام کلاس باشد، سپس پارامترهای مورد نظر و بعد از آن ها بدنه سازنده. دقت کنید سازنده ها مقدار بازگشتی ندارند. با توضیحات گفته شده می توان ساختار کلی سازنده را به صورت زیر بیان کرد:

{access-modifier} {class-name}([parameters])
{
    // constructor body
}

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

var person = new Person("Hossein", "Ahmadi");

با اجرای کد بالا، خصوصیت های FirstName و LastName هنگام ایجاد شی، مقدار دهی خواهند شد. اما باید به یک نکته در اینجا توجه داشت، زمانی که شما سازنده ای به همراه پارامتر برای یک کلاس تعریف می کنید، دیگر نمی توانید از کلاس بدون ارسال پارامتر در سازنده شی بسازید.

دلیل این موضوع، عدم وجود سازنده ای به نام سازنده پیش فرض یا Default Constructor می باشد. سازنده پیش فرض، سازنده ایست که هیچ پارامتری را به عنوان ورودی نمی گیرد. زمانی که شما سازنده ای برای یک کلاس تعریف نکرده اید، آن کلاس به صورت پیش Default Constrcutor برایش تعریف شده است.

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

public class Person
{
    public Person()
    {
    }

    public Person(string firstName, string lastName)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
}

شما می توانید سطح دسترسی به سازنده ها را مشخص کنید، برای مثال، حالتی پیش می آید که می خواهد یک سازنده فقط داخل همان کلاس در دسترس باشد، یعتی شما از یک کلاس داخل خودش، مثلاً داخل یک رفتار، می خواهید یک شی بسازید. برای این کار، می توانید سطح دسترسی سازنده مد نظر را private تعریف کنید. برای مثال:

public class Person
{
    public Person()
    {
    }

    private Person(string firstName, string lastName)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }

    public Person CreateObject(string firstName)
    {
        return new Person(firstName, null);
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
}

در کد بالا، یک متد یا رفتار برای کلاس تعریف کردیم با نام CreateObject. این رفتار یک شی از روی خود کلاس Person می سازد و پارامتر ارسالی به متد CreateObject را به سازنده ارسال می کند. اما خارج از شی، دیگر نمی توانیم از سازنده ای که دو پارامتر را به عنوان ورودی می گیرد استفاده کنیم، زیرا این سازنده با سطح دسترسی private تعریف شده است.همانند متدها، سازنده ها می توانند overload داشته باشند، یعنی چند سازنده با signature های متفاوت. برای مثال، کلاس Person را به صورت زیر تغییر می دهیم:

public class Person
{
    public Person()
    {
    }

    public Person(string firstName)
    {
        this.FirstName = firstName;
    }

    public Person(string firstName, string lastName)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
}

در اینجا دو سازنده برای کلاس تعریف کردیم که اولی یک پارامتر گرفته و دومی با دو پارامتر صدا زده می شود.

زنجیره سازنده ها یا Constrcutor Chaining

زمانی که شما چندین سازنده دارید، می توانید از کد های نوشته شده داخل یک سازنده در سازنده دیگر استفاده کنید. برای اینکار از قابلیت constructor chaining استفاده می شود. با یک مثال ادامه می دهیم، در کد قبلی سه سازنده داشتیم، سازنده پیش فرض، سازنده ای که تنها FirstName را می گرفت و سازنده ای که FirstName و LastName را به عنوان پارامتر می گرفت. در سازنده دوم، می توان از سازنده سوم جهت مقدار دهی استفاده کرد. برای این کار، کد بالا را به صورت زیر تغییر می دهیم:

public class Person
{
    public Person()
    {
    }

    public Person(string firstName) : this(firstName,null)
    {
    }

    public Person(string firstName, string lastName)
    {
        this.FirstName = firstName;
        this.LastName = lastName;
    }

    public string FirstName { get; set; }
    public string LastName { get; set; }
}

سازنده دوم ما به صورت زیر تغییر کرده است:

public Person(string firstName) : this(firstName,null)
{
}

دقت کنید، بعد از بستن پرانتز پس از علامت : از کلمه کلیدی this مانند یک متد استفاده کرده ایم، در این روش، سازنده کلاس صدا زده شده و به عنوان پارامتر اول، firstName که در سازنده تعریف شده را ارسال کرده و عنوان پارامتر دوم مقدار null را ارسال کرده ایم.

قابلیت constrcutor chaining، در کاهش تعداد خطوط نوشته در برنامه کمک زیادی به ما می کند.به این نکته توجه داشته باشید که نمی تواند سازنده را به صورت متد از داخل کلاس جایی غیر از خود سازنده ها صدا زد. همچنین سازنده ها تنها برای مقدار دهی خصوصیات استفاده نمی شوند، شما می توانید هر کدی را داخل سازنده بنویسید.

نوع های بدون نام یا Anonymous Types

نوع های بدون نام، به ما این امکان را می دهند تا شی ای بدون تعریف کلاس ایجاد کنیم. این شی تنها می تواند شامل خصوصیات باشد و قابلیت تعریف رفتار برای آن را نخواهیم داشت. در مثال زیر یک شی بدون نام ایجاد کرده ایم که سه خصوصیت با نام FirstName و LastName و Age دارد:

var anonymous = new
{
    FirstName = "Hossein",
    LastName = "Ahmadi",
    Age = 29
};

Console.WriteLine(anonymous.FirstName + " " + anonymous.LastName);

همانطور که در کد مشاهده می کنید، کافیست بعد از کلمه کلیدی new بلافاصه به سراغ عملیات Object Initialization برویم و نیازی به نوشتن نام کلاس نیست. نوع های بدون نام کاربردهای زیادی در بخش LINQ دارند که در همین وب سایت دوره آموزشی LINQ توسط بنده نوشته شده و در این دوره آموزشی نیز مروری کوتاه بر این قابلیت خواهیم داشت. در بخش بعدی آموزش، مبحث وراثت یا Inheritance که مهمترین مبحث در زمینه برنامه نویسی شی گرا می باشد را آغاز خواهیم کرد.

وراثت

یکی از مباحث بسیار مهم در برنامه نویسی شی گرا، مبحث وراثت یا Inheritance است. در قسمت مقدمه برنامه نویسی شی گرا، در مورد این مبحث به صورت مختصر صحبت کردیم. در ادامه تصمیم داریم که مبحث را بیشتر مورد بررسی قرار دهیم و با نحوه کاربرد آن در زبان سی شارپ بیشتر آشنا شویم. مبحث وراثت از این نظر مهم است که به شما کمک می کند از کدهای نوشته شده مجدداً استفاده کنید و در نتیجه حجم کده نوشته شده در برنامه شما به صورت محسوسی کاهش پیدا کند.

تعریف وراثت یا Inheritance و پیاده سازی آن در زبان C#

همانطور که در مقدمه مبحث برنامه نویسی شی گرا خدمت دوستان توضیح دادم، وراثت به معنی به ارث بردن یکسری خصوصیات و رفتار بوسیله فرزند از والد است. در برنامه نویسی شی گرا، زمانی که صحبت از وراثت می کنیم، در حقیقت می خواهیم برای یک کلاسی، یک کلاس والد مشخص کنیم. وراثت در برنامه نویسی شی گرا کاربردهای بسیاری دارد، به صورتی که اصلی ترین و بنیادی ترین قابلیت در برنامه نویسی شی گرا نام برده می شود. قبل از شروع به نکته زیر توجه کنید:

زمانی که کلاس A به عنوان والد کلاس B معرفی می شود، یعنی کلاس B فرزند کلاس A می باشد، می گوییم کلاس B از کلاس A مشتق شده است. در طول این دوره از واژه مشتق شده به تکرار استفاده خواهیم کرد.در ابتدا با شیوه کلی استفاده از وراثت در کلاس ها آشنا می شویم. فرض کنید کلاسی داریم با نام A:

public class A
{
        
}

حال تصمیم داریم کلاسی تعریف کنیم با نام B که از کلاس A مشتق شده است، یعنی تمامی خصوصیات و رفتارهای کلاس A را به ارث می برد. برای اینکار کلاس B را به صورت زیر تعریف می کنیم:

public class B : A
{
}

بوسیله دستور بالا، کلاس A به عنوان کلاس والد کلاس B در نظر گرفته خواهد شد. گفتیم یکی از مزایای استفاده از وراثت در برنامه نویسی شی گرا، استفاده مجدد از کدهایی است که در کلاس والد تعریف شده است. در مثال بالا، کد کلاس A خصوصیتی با نام Item1 و Item2 تعریف می کنیم:

public class A
{
    public string Item1 { get; set; }
    public string Item2 { get; set; }
}

به دلیل اینکه کلاس B از کلاس A مشتق شده است، می توانیم از خصوصیت های Item1 و Item2 برای کلاس B استفاده کنیم:

B obj = new B();
obj.Item1 = "Hossein Ahmadi";
obj.Item2 = "ITPro.ir";

حال، کلاس سومی تعریف می کنیم با نام C. این کلاس نیز از کلاس A مشتق می شود:

public class C : A
{
}

زمانی که شی ای از کلاس C بسازیم، میبینیم که خصوصیات Item1 و Item2 برای این شی کلاس C نیز وجود دارند، در حقیقت ما این خصوصیات را تنها یکبار در کلاس A تعریف کردیم و با قابلیت وراثت از این کدها برای کلاس های B و C مجدداً استفاده کردیم. زمانی که کلاسی یک یک کلاس والد مشتق می شود، علاوه بر اینکه دارای خصوصیات و رفتارهای کلاس های والد می باشد، می توان برای کلاس فرزند خصوصیات و رفتارهای جدید تعریف کرد. کلاس C را که در بالا تعریف کردیم به صورت زیر تغییر می دهیم:

public class C : A
{
    public string Item3 { get; set; }
}

حال زمانی که ما شی ای از روی کلاس C بسازیم علاوه بر خصوصیت های Item1 و Item2 که در کلاس A تعریف شده اند، به خصوصیت دیگری نیز نام Item3 که در کلاس C تعریف شده دسترسی خواهیم داشت:

var instanceOfC = new C();
instanceOfC.Item1 = "Hossein Ahmadi";
instanceOfC.Item2 = "ITPro.ir";
instanceOfC.Item3 = "C# Course";

وراثت در زبان سی شارپ، به صورت درختی می باشد، یعنی زمانی که کلاس C از کلاس A مشتق شد می توان کلاس D را نوشت که از کلاس C مشتق شده است:

public class D : C
{
    public string Item4 { get; set; }
}

در مثال بالا، کلاس D علاوه بر خصوصیات کلاس A و کلاس C خصوصیات مربوط به خودش را نیز شامل می شود. در حقیقت زنجیره وراثت را در این مثال مشاهده می کنید. در مثال های بالا، کلاس ها تنها شامل Property بودند، زمانی که شما برای کلاسی یک رفتار تعریف می کنید، کلاس های فرزند آن رفتار را نیز به ارث می برند:

public class A
{
    public string Item1 { get; set; }
    public string Item2 { get; set; }
    
    public void PrintItem1()
    {
        Console.WriteLine(Item1);
    }
}

حال شی ای از کلاس D می سازیم و رفتار PrintItem1 را صدا می زنیم:

var obj = new D();
d.PrintItem1();

در قسمت های قبلی دیدیم که کلاس D از کلاس C مشتق شده است و خود کلاس C از کلاس A. پس کلیه خصوصیات و رفتارهای کلاس A برای سطوح پایین تر وراثت قابل دسترس هستند.

کلمه کلیدی base

در قسمت های قبلی، در مورد کلمه کلیدی this توضیح دادیم و گفتیم که این کلمه کلیدی به شی ای اشاره می کند که از روی کلاس ساخته شده. کلمه کلیدی دیگری وجود دارد با نام base که اشاره به کلاس والد دارد. برای مثال، کلاس B را به صوورت زیر تغییر می دهیم:

public class B : A
{
    public void PrintParentItems()
    {
        Console.WriteLine(base.Item1 + " " + base.Item2);
    }
}

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

تبدیل کلاس های مشتق شده به کلاس والد

زمانی که شما از روی یک کلاس، شی ای می سازید باید نوع آن کلاس را مشخص کنید یا از کلمه کلیدی var استفاده کنید:

B obj = new B();

زمانی که کلاسی از یک شی مشتق شده باشد، می توان هنگام تعریف شی از روی آن کلاس، نوع متغیر را به جای خود کلاس، کلاس وارد قرار داد. برای مثال، در مثال زیر ما یک شی از روی کلاس C می سازیم:

A obj = new C();

دقت کنید که نوع متغیر obj را از نوع A در نظر گرفتیم، اما شی ای از نوع C داخل آن ریختیم. دلیل این امر آن است که کلاس C از کلاس A مشتق شده و به نوعی قابل تبدیل به کلاس A می باشد. اما به این نکته توجه داشته باشید، زمانی که نوع متغیر را از نوع کلاس والد در نظر می گیریم

هنگام استفاده از شی ساخته شده، تنها خصوصیات و رفتارهایی هایی قابل استفاده هستند که در کلاس والد تعریف شده اند. به عنوان مثال، کد زیر صحیح نمی باشد، به این خاطر که Item3 داخل کلاس C تعریف شده و ما تنها به Item1 و Item2 که داخل A تعریف شده اند دسترسی داریم.

A instanceOfC = new C();
instanceOfC.Item3 = "C# Course";

یکی از مهمترین کاربردهای استفاده از نوع داده کلاس والد، مبحث Polymorphism می باشد که در بخش های بعدی با این مفهوم بیشتر آشنا خواهیم شد.

کلاس Object

در کتابخانه دات نت، کلاسی وجود دارد به نام Object یا شی. در دات نت، کلیه نوع های داده و کلاس ها، چه آنهایی که به صورت دستی می نویسیم و چه آنهایی که در کتابخانه دات نت وجود دارند، از کلاس object مشتق شده اند، به جز کلاس هایی که برای آنها کلاس والد را مشخص کرده ایم.

حتی کلاس هایی که برای آنها کلاس والد مشخص شده، باز هم شاخه اصلی زنجیره وراثت به کلاس object ختم می شود. پس به این صورت می گوییم که کلاس object، کلاس پایه ای برای کلیه کلاس های دات نت می باشد. یکسری رفتارها برای کلاس Object تعریف شده اند که در تمامی کلاس ها در دسترس هستند، زیرا کلیه کلاس ها از کلاس object مشتق شده اند. این رفتارها به شرح زیر می باشند:

  1. Equals: این رفتار بررسی می کند که دو شی با یکدیگر برابر هستند یا خیر.
  2. GetHashCode: این رفتار عددی را برمی گرداند که شناسه شی ایجاد شده می باشد.
  3. GetType: نوع یا Type شی را بر میگرداند. این متد را در بخش Reflection بیشتر بررسی خواهیم کرد.
  4. ToString: زمانی که این رفتار را برای یک شی صدا می زنید، رشته ای مرتبط با آن شی را بر میگرداند که به صورت پیش فرض Type Name یا نام نوع آن کلاس را بر میگرداند. در بخش Polymorphism با این متد بیشتر آشنا می شوید.

گفتیم زمان تعریف کردن یک شی، نوع داده والد را به جای خود کلاس برای متغیر در نظر گرفت:

object number = 12;
object name = "Hossein Ahmadi";
object instance = new A();

همینطور که مشاهده می کنید فرقی نمی کند مقدار متغیر چه باشد، هر مقداری را می توان در متغیر از نوع object ذخیره کرد. در این قسمت سعی کردیم مقدماتی از مبحث وراثت را با هم مرور کنیم. در بخش بعدی بحث وراثت را با بررسی مفهوم Polymorphism ادامه خواهیم داد.

Polymorphism

همانطور که در بخش قبل گفتیم وراثت یکی از اصلی ترین مباحث برنامه نویسی شی گرا می باشد. در بخش قبلی با شیوه ارث بری از کلاس ها آشنا شدیم. یکی از مفاهیمی که در برنامه نویسی شی گرا خیلی کاربرد دارد و وابسته به مفهوم وراثت است، چند ریختی یا Polymorphism است.

در قسمت مقدمه برنامه نویسی شی گرا با تعریف کلی Polymorphism آشنا شدیم و در این بخش تصمیم داریم به صورت عملی با نحوه پیاده سازی این قابلیت در زبان سی شارپ آشنا شویم.همانطور که در قسمت مقدمه گفتیم، Polymorphism به معنای قابلیت تعریف مجدد رفتار یک موجودیت در کلاس های فرزند می باشد. Polymorphism در زبان سی شارپ به سه روش قابل پیاده سازی است:

  1. استفاده از متد های virtual و override کردن آنها در کلاس های فرزند
  2. استفاده از رفتارهای abstract در کلاس والد
  3. استفاده از قابلیت interface ها

در این قسمت، حالت اول را بررسی می کنیم و حالت دوم و سوم، یعنی استفاده از متدهای abstract و interface ها را در بخش های بعدی توضیح خواهیم داد.

متدهای virtual

همانطور که گفتیم یکی از روش های پیاده سازی Polymorphism استفاده از متدهای virtual و override کردن آنها در کلاس فرزند است. برای مثال، فرض کنیم کلاس پایه ای داریم با عنوان Shape که در آن رفتاری با نام Draw تعریف کردیم. رفتار Draw وظیفه ترسیم شی را بر عهده دارد.

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

public class Shape
{
    public void Draw()
    {
        Console.WriteLine("Drawing the shape!");
    }
}

حالا باید کلاس های فرزند را تعریف کنیم. ما سه کلاس به نام های Rectangle، Triangle و Circle که وظیفه ترسیم مستطیل، مثلث و دایره را بر عهده دارند تعریف می کنیم که هر سه از کلاس Shape مشتق شده اند:

public class Rectangle : Shape
{
        
}

public class Triangle : Shape
{
        
}

public class Circle : Shape
{
        
}

هر سه کلاسی که در بالا تعریف کردیم، شامل متد Draw هستند، زیرا این متد در کلاس پایه یعنی Shape تعریف شده است. حال از هر یک، شی ای ساخته و متد Draw را صدا می زنیم:

var rect = new Rectangle();
var tri = new Triangle();
var circ = new Circle();

rect.Draw();
tri.Draw();
circ.Draw();

Console.ReadKey();

خروجی دستورات بالا به صورت زیر می باشد:

Drawing the shape!
Drawing the shape!
Drawing the shape!

اما خروجی مدنظر ما تولید نشده است. ما می خواهیم هر کلاس رفتار مربوط به خود را داشته باشد. درست است که رفتار Draw در کلاس پایه تعریف شده، اما باید بتوانیم این رفتار را برای کلاس های فرزند تغییر دهیم. برای اینکار باید در کلاس پایه مشخص کنیم که کدم رفتار را می خواهیم تغییر دهیم. برای اینکار، کافیست رفتار مورد نظر را از نوع virtual تعریف کنیم. اعضای virtual به ما این اجازه را می دهند تا در کلاس فرزند مجدد آنها را تعریف کنیم. برای اینکار متد Draw در کلاس Shape را به صورت زیر تغییر می دهیم:

public virtual void Draw()
{
    Console.WriteLine("Drawing the shape!");
}

دقت کنید، کلمه کلیدی virtual قبل از نوع بازگشتی متد نوشته می شود. حالا باید در کلاس فرزند رفتار Draw را مجدداً تعریف کنیم. برای اینکار باید متدی که از نوع virtual تعریف شده است را override کنیم. ابتدا رفتار Draw را برای کلاس Rectangle تغییر می دهیم. کد کلاس Rectangle را به صورت زیر تغییر دهید:

public class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing rectangle!");
    }
}

حالا با اجرای مجدد کد خروجی به صورت زیر تغییر می کند:

Drawing rectangle!
Drawing the shape!
Drawing the shape!

زمانی که شما داخل کلاس Rectangle شروع به تایپ می کنید، بعد از نوشتن کلمه کلیدی override و زدن کلید space لیستی از متدهایی که قابل override شدن هستند برای شما نمایش داده می شوند:


img1

همچنین در صورتی که ابزار Resharper را نصب کرده باشید، وارد scope کلاس شده و کلید های Alt+Insert را فشار دهید، با اینکار منوی Generate برای شما نمایش داده می شود، از طریق این منو و انتخاب گزینه Overriding members لیستی از تمامی اعضای قابل override شدن به شما نمایش داده می شود:


img2


img3

بعد از انتخاب عضو مورد نظر و فشار دادن کلید Finish متد مورد نظر برای شما override می شود. مهمترین کاربرد این ویژگی، زمانی است که شما تصمیم دارید چندین ویژگی را با هم override کنید. به یک نکته توجه داشته باشید، چه با روش اول متد را override کنید، چه با روش دوم، کدی برای شما به صورت خودکار درج می شود که به صورت زیر است:

public override void Draw()
{
    base.Draw();
}

در قسمت قبل با کلمه کلیدی base آشنا شدید، در کد بالا که به صورت پیش فرض نوشته می شود، با فراخوانی متد Draw از شی ای که متد داخل آن override شده، نسخه کلاس پایه از رفتار فراخوانی می شود که شما باید بر اساس نیاز خود کد مورد نظر را برای رفتار override شده بنویسید.کلاس های Triangle و Circle را نیز به صورت زیر تغییر می دهیم:

public class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Draw triangle!");
    }        
}

public class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Draw circle!");
    }        
}

بعد از اجرای برنامه، خروجی باید به صورت زیر باشد:

Drawing rectangle!
Drawing triangle!
Drawing circle!

شاید خیلی از دوستان این سوال برایشان پیش بیاید که دلیل اینکار چیست؟ ما که متدها را در هر کلاس نوشتیم، چرا باید از کلاس پایه و override کردن آنها استفاده می کردیم؟ مهمترین خاصیت استفاده از Polymorphism استفاده از کلاس پدر برای کارهاست.

برای درک بهتر، فرض کنید میخواهیم آرایه ای از اشیاء داشته باشیم. همانطور که می دانید، ما سه کلاس مختلف داریم و در صورت استفاده نکردن از قابلیت وراثت باید برای هر یک از کلاس ها یک آرایه تعریف کنیم. اما با قابلیت وراثت می توان یک آرایه از نوع کلاس پایه تعریف کرد و اشیاء فرزند را داخل آن قرار داد:

Shape[] shapes = new Shape[5];
shapes[0] = new Circle();
shapes[1] = new Triangle();
shapes[2] = new Circle();
shapes[3] = new Rectangle();
shapes[4] = new Triangle();

foreach (var shape in shapes)
    shape.Draw();

Console.ReadKey();

با اجرای کد بالا خروجی زیر به نمایش داده می شود:

Drawing circle!
Drawing triangle!
Drawing circle!
Drawing rectangle!
Drawing triangle!

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

تعریف مجدد یا override کردن تنها محدود به رفتارها یا همان متدها نمی باشد. شما خصوصیات یا Property ها را نیز می توانید از نوع virtual تعریف کنید. برای مثال کلاسی را با نام Human فرض کنید که دارای سه خصوصیت به نام های FirstName و LastName و FullSpecification می باشد:

public class Human
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string FullSpecification
    {
        get { return FirstName + " " + LastName; }
    }
}

در کلاس بالا، خاصیت FullSpecification نام کامل را بر میگرداند. حالا کلاس فرزندی تعریف می کنیم با نام Employee یا کارمند که از کلاس Human مشتق شده است و خاصیت جدید با نام JobPosition یا موقعیت شغلی به آن اضافه می کنیم:

public class Employee : Human
{
    public string JobPosition { get; set; }
}

در صورتی که شی ای از روی Employee بسازیم و خاصیت FullSpecification آن را چاپ کنیم، نام و نام خانوداگی او در خروجی چاپ می شود. اما می خواهیم این خاصیت علاوه بر نام کامل، موقعیت شغلی او را نیز در خروجی چاپ کند. برای اینکار کافیست که در کلاس Human خاصیت FullSpecification را به صورت virtual تعریف کرده و در کلاس فرزند مجدد بخش get آن را تعریف کنیم:

public class Human
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public virtual string FullSpecification
    {
        get { return FirstName + " " + LastName; }
    }
}

public class Employee : Human
{
    public string JobPosition { get; set; }

    public override string FullSpecification
    {
        get { return base.FullSpecification + " with job position: " + JobPosition; }
    }
}

در کلاس Employee به بخش get توجه کنید:

get { return base.FullSpecification + " with job position: " + JobPosition; }

در این قسمت، از کلمه کلیدی base برای گرفتن مقدار FullSpecification استفاده شده است. این خاصیت نام کامل را برای ما بر میگرداند و ما به انتهای آن موقعیت شغلی شخص را اضافه می کنیم و بر میگردانیم.قابلیت Polymorphism در برنامه نویسی شی گرا نقش بسیار پر رنگی دارد و خیلی جاها از نوشتن کدهای تکراری جلوگیری کرده و حجم کد برنامه شما را به صورت محسوسی کاهش می دهد. در بخش بعدی آموزش زبان سی شارپ، با کلاس ها و متدهای abstract و همچنین کلاس های sealed آشنا می شویم

abstract و sealed

در قسمت قبلی آموزش با مفهوم polymorphism آشنا شدیم. در ادامه قصد داریم با کلاس های abstract و sealed آشنا شویم. زمانی که شروع به نوشتن برنامه ای می کنیم، بعد از مشخص کردن موجودیت های برنامه و طراحی کلاس های مربوطه، باید یکسری محدودیت ها برای استفاده از کلاس ها وضع کرد.

برای مثال، کلاس پایه ای داریم که این کلاس نباید مورد استفاده قرار بگیرد و تنها باید از کلاس های فرزند قابلیت ایجاد شی وجود داشته باشد یا کلاسی نوشته ایم و نباید اجازه ایجاد کلاس فرزند از روی آن کلاس داده شود. این قابلیت ها بخصوص در مواقعی که در تیم شما، افرادی از کدهای نوشته شده توسط شما استفاده می کنند یا کدی را برای استفاده از سایر برنامه نویس ها بر روی اینترنت منتشر می کنید کاربرد دارند. راه حل برای این شرایط استفاده از کلاس های abstract و کلاس های sealed می باشد.

کلاس ها و اعضاء abstract

به بخش قبل و کلاس Shape بر میگردیم که سه کلاس فرزند از روی آن ها ساخته بودیم:

public class Shape
{
    public void Draw()
    {
        Console.WriteLine("Drawing the shape!");
    }
}

public class Rectangle : Shape
{
         
}
 
public class Triangle : Shape
{
         
}
 
public class Circle : Shape
{
         
}

کلاس Shape به تنهایی برای ما کاربردی نداشته و تنها داخل کد باید از کلاس های فرزند استفاده کرد، یعنی نباید از روی کلاس Shape شی ایجاد شود. برای اینکار کافیست کلاس Shape را از نوع abstract تعریف کنیم:

public abstract class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Drawing the shape!");
    }
}

حال اگر بخواهیم از روی کلاس Shape یک شی ایجاد کنیم با پیغام خطا مواجه خواهیم شد:

i1

اما کاربرد کلاس های abstract به همین جا ختم نمی شود، در قسمت قبل متد Draw را در کلاس Shape به صورت virtual تعریف کرده و در کلاس های فرزند آن را override کردیم. یکی از قابلیت های زبان سی شارپ، تعریف متدها به صورت abstract می باشد.

متدهای abstract تنها شامل signature که در قسمت های اولیه با آن آشنا شدیم می باشد و بدنه ندارد، علاوه بر آن تمامی کلاس هایی که از یک کلاس abstract مشتق می شوند، در صورتی که کلاس abstract رفتار یا خاصیتی از نوع abstract داشته باشد، باید حتماً آن رفتار یا خاصیت را override کنند. برای مثال کلاس Shape را به صورت زیر تغییر می دهیم:

public abstract class Shape
{
    public abstract void Draw();
}

همانطور که مشاهده می کنید، متد Draw تنها تعریف شده و بدنه ای ندارد. حال اگر کلاس فرزندی از کلاس Shape مشتق شود باید متد Draw داخل آن Override شود. در غیر اینصورت با پیغام خطا مواجه خواهیم شد:

i2

می توانیم به صورت دستی متد Draw را تعریف کرده یا از قابلیت Resharper برای پیاده سازی اعضای abstract به صورت خودکار استفاده کنیم. برای انکار مکان نما را بر روی نام کلاس قرار داده و کلید های Alt+Enter را فشار می دهیم. با این کار منویی با نام Action Context نمایش داده می شود:

i3

از منوی ظاهر شده، گزینه Implement missing members را انتخاب می کنیم تا اعضای abstract پیاده سازی شوند. بعد از اینکار کد کلاس Rectangle به صورت زیر خواهد بود:

public class Rectangle : Shape
{
    public override void Draw()
    {
        throw new NotImplementedException();
    }
}

کدی که به صورت خودکار داخل بدنه متد Draw قرار گرفته مربوط به یکی از ویژگی های زبان سی شارپ با نام استثناها یا Exceptions می باشد که در بخش های بعدی با آن آشنا می شویم. حال کد مورد نظر را داخل متد Draw می نویسیم:

public class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing rectangle!");
    }
}

در صورتی که Resharper را نصب ندارید، برای پیاده سازی خودکار اعضاء abstract کلاس می توانید از قابلیت خود Visual Studio استفاده کنید. برای اینکار، مکان نما را به انتهای اول تعریف کلاس برده و کلید های Alt+. را فشار دهید. منویی به صورت زیر نمایش داده می شود:

i4

با انتخاب گزینه Implement abstract class Shape اعضاء ای که از نوع abstract تعریف شده اند در کلاس پیاده سازی می شوند.این قابلیت دقیقاً کار مشابهی با متدهای virtual انجام می دهد، با این تفاوت که متدهای abstract بدنه ای ندارند، اما اعضاء virtual می توانند بدنه ای داشته باشند که بوسیله کلمه کلیدی base می توان به آنها در کلاس فرزند دسترسی داشت. همچنین توجه کنید که اعضاء abstract تنها داخل کلاس های abstract قابل تعریف هستند.

کلاس ها و اعضاء sealed

در قسمت وراثت گفتیم که می توان زنجیره وراثت داشت. یعنی کلاس B از کلاس A و کلاس C از کلاس B مشتق شوند. اما فرض کنید بخواهیم زنجیره وراثت را در یک کلاس قطع کنیم. یعنی کلاس دیگری نتواند از کلاس ما ارث بری کند. برای اینکار کافیست کلاس مورد نظر را از نوع sealed تعریف کرد:

public class A
{
        
}

public sealed class B : A
{
        
}

در کد بالا کلاس B از نوع sealed تعریف شده، بدین معنا که هیچ کلاس دیگری نمی تواند از این کلاس مشتق شود. در تصویر زیر، کلاس C از کلاس B مشتق شده و پیغام خطا دریافت کردیم:

i5

یکی دیگر از کاربردهای کلمه کلیدی sealed جلوگیری از override کردن یک متد است. کلاس Shape و Rectangle را مثال میزنیم. میخواهیم اگر کلاسی از کلاس Rectangle مشتق شد قابلیت ovverride ردن متد Draw را نداشته باشد. کافیست متد Draw را از نوع sealed تعریف کنیم:

public abstract class Shape
{
    public virtual void Draw()
    {
        Console.WriteLine("Drawing the shape!");
    }
}

public class Rectangle : Shape
{
    public sealed override void Draw()
    {
        Console.WriteLine("Drawing rectangle!");
    }
}

همانطور که در کد بالا مشاهده می کنید، متد Draw در کلاس Rectangle از نوع sealed تعریف شده. حالا اگر کلاسی تعریف کنیم و آن را کلاس Rectangle مشتق کنیم، در کلاس فرزند قابلیت تعریف مجدد متد Draw را نخواهیم داشت.تا این قسمت از آموزش با کلیات برنامه نویسی شی گرا، مفاهیم وراثت و Polymorphism و همچنین کلاس های abstract و sealed آشنا شدیم. در بخش های بعدی با تکمیلی برنامه نویسی شی گرا آَشنا شده و مباحث Casting را با هم بررسی خواهیم کرد.

مباحث تکمیلی در وراثت

در قسمت قبل با کلمات کلیدی abstract و sealed آشنا شدیم. در این قسمت و قسمت بعد تصمیم دارم برخی نکات تکمیلی که از بخش برنامه نویسی شی گرا مانده را خدمت دوستان آموزش بدم. مواردی که در این بخش با آنها آشنا خواهید شد به شرح زیر است:

  1. سازنده ها در کلاس های فرزند و پدر
  2. سطح دسترسی protected
  3. مخفی کردن متدها با کلمه کلیدی new
  4. فیلدهای Readonly

سازنده هه در کلاس های فرزند و پدر

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

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

    public string FirstName { get; set; }
    public string LastName { get; set; }
}

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

public class Employee : Human
{
            
}

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


i1

برای رفع این مشکل دو راه وجود دارد، یکی تعریف سازنده پیش فرض در کلاس والد و روش دوم پیاده سازی سازنده ای برای کلاس فرزند که پارامتر های مورد نیاز برای کلاس والد را گرفته و به سازنده کلاس والد ارسال می کند. اگر از بخش های قبلی به خاطر داشته باشید، با مبحثی در سازنده آشنا شدیم به نام زنجیره سازنده ها یا Constructor Chaining. در مثال بالا، باید از قابلیت Constructor Chaining استفاده کرد، اما سازنده کلاس والد را صدا زد. کلاس Employee را به صورت زیر تغییر می دهیم:

public class Employee : Human
{
    public Employee(string firstName, string lastName) : base(firstName, lastName)
    {
    }
}

همانطور که مشاهده می کنید، سازنده ما دو پارامتر دریافت می کند و هر دوی این پارامترها را به عنوان ورودی به سازنده کلاس والد ارسال می کند. کلمه base در اینجا به سازنده تعریف شده در کلاس والد اشاره می کند. حال فرض کنید خصوصیت جدیدی به نام JobPosition به کلاس Employee اضافه کردیم و میخواهیم این خصوصیت از طریق سازنده مقدار دهی شود. کافیست کد کلاس Employee را به صورت زیر تغییر دهیم:

public class Employee : Human
{
    public string JobPosition { get; set; }

    public Employee(string firstName, string lastName, string jobPosition) : base(firstName, lastName)
    {
        JobPosition = jobPosition;
    }
}

همانطور که مشاهده می کنید، پارامتر سومی به نام jobPosition به سازنده اضافه کردیم و داخل سازنده کلاس فرزند خصوصیت JobPosition را مقدار دهی کردیم، اما مقادیر firstName و lastName به سازنده کلاس والد ارسال شد تا برای برای مقدار دهی خصوصیات FirstName و LastName از کد کلاس والد استفاده کنیم.

سطح دسترسی protected

در بخش های قبلی در مورد سطوح دسترسی مختلف صحبت کردیم و گفتیم که هر سطح دسترسی مشخص می کند یک کلاس یا اعضای کلاس تا چه سطحی از برنامه دسترسی داشته باشند. یک سطح دسترسی باقی ماند به نام protected که نیاز به بررسی مفهوم وراثت داشت. کلاس های زیر را در نظر بگیرید:

public class A
{
    private int id;
}

public class B : A
{
            
}

در کد بالا، فیلد id که در کلاس A تعریف شده، خارج از کلاس B قابل دسترس نخواهد بود، زیرا این فیلد به صورت private تعریف شده است:


i2

حتی در کلاس B که فرزند کلاس A می باشد، این فیلد در دسترس نمی باشد. یک راه حل تعریف فیلد id به صورت public یا internal است. البته دقت کنید در صورت تعریف کردن فیلد id به صورت internal در صورتی کلاس B به آن دسترسی خواهد داشت که محل تعریف کلاس A و B در یک پروژه باشد.

اما در صورتی که بخواهیم فیلد id تنها در کلاس های فرزند و خود کلاس A در دسترس باشد، باید سطح دسترسی فیلد id را protected تعریف کنیم. با این کار قابلیت دسترسی به فیلد id را از کلاس B خواهیم داشت. کلاس A را به صورت زیر تغییر می دهیم:

public class A
{
    protected int id;
}

یک حالت دیگر نیز وجود دارد و ترکیب استفاده از protected و internal است. در حالت بالا، در صورتی که کلاس A به صورت public تعریف شده باشد و ما خارج از پروژه ای که کلاس A تعریف شده کلاسی بسازیم و از کلاس A مشتق کنیم، به فیلد id دسترسی خواهیم داشت. برای اینکه فیلد id تنها از کلاس هایی که داخل همان پروژه ای که کلاس A وجود دارد تعریف شده اند و کلاس A مشتق شده اند قابل دسترسی باشد، دسترسی آن را از نوع protected internal تعریف می کنیم:

public class A
{
    protected internal int id;
}

دقت کنید که می توان protected internal را به صورت internal protected نیز نوشت و هیچ تفاوتی در عملکرد آنها وجود ندارد.*در توضیحات بالا، منظور از دسترسی خارج از پروژه، پروژه های است که به پروژه ما Reference داده می شوند، منظور از Reference دادن استفاده از کدهای موجود در یک پروژه می باشد. در بخش های بعدی روش تعریف Solution هایی که بیش از یک پروژه دارند و Reference دادن آنها به هم را بررسی خواهیم کرد.

مخفی سازی اعضاء بوسیله کلمه کلیدی new

در بخش polymorphism روش override کردن متدها و خصوصیات را گفتیم. حالتی وجود دارد که یک عضو کلاس از نوع virtual تعریف نشده، اما در کلاس فرزند عضوی همنام یکی از اعضای کلاس پدر تعریف شده است. به مثال زیر توجه کنید:

public class A
{
    public void PrintMessage()
    {
        Console.WriteLine("From class A");
    }
}

public class B : A
{
    public void PrintMessage()
    {
        Console.WriteLine("From class B");
    }
}

به تصویر زیر دقت کنید:


i3

در تصویر بالا مشاهده می کنید که کامپایلر به شما اخطار می دهد که برای مخفی سازی متد در کلاس فرزند بهتر است از کلمه کلیدی new استفاده کنید. متد PrintMessage در کلاس B را به صورت زیر تغییر می دهیم:

public class B : A
{
    public new void PrintMessage()
    {
        Console.WriteLine("From class B");
    }
}	

با کلمه کلیدی new، کامپایلر دیگر اخطاری به شما نمی دهد. دقت کنید که کلمه کلیدی new کاملاً با override کردن متدها تفاوت دارد. کد زیر پیغام From Class A را چاپ می کند، اما در صورت override کردن متد در کلاس B پیغام From class B چاپ می شد:

A obj = new B();
obj.PrintMessage();

نوشتن و ننوشتن کلمه کلیدی new تغییری در روند اجرا و عملیات کلاس ایجاد نمی کند، این اخطار تنها برای این است که شما اشتباهاً در کلاس های فرزند عضوی همنام با اعضای کلاس والد تعریف نکنید و این کار با آگاهی شما انجام شود.

فیلدهای readonly

فیلدهای readonly فیلدهایی هستند که تنها در سازنده می توان آنها را مقدار دهی کرد و خارج از سازنده شما امکان مقدار دهی آن ها را نخواهید داشت و تنها می توان از مقدار آنها استفاده کرد:

public class A
{
    private readonly int value;

    public A(int value)
    {
        this.value = value;
    }
}

در کلاس بالا فیلد value از نوع readonly تعریف شده است، در صورتی که جایی خارج از سازنده شما value را مقدار دهی کنید با پیغام خطا مواجه خواهید شد:


i4

در قسمت بعدی با کلاس ها و اعضای static، کلاس های partial و Extension Method ها آشنا خواهیم شد.

Exntesion Method ها

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

Console.WriteLine("Welcome to ITPro.ir");

دلیل این امر، تعریف کلاس Console و اعضای آن به صورت static است.

کلاس ها و اعضای static

اعضای static، در حقیقت اعضایی هستند که وابسته به شی نیستند و در بین کل شی های ساخته شده از یک کلاس مشترک می باشند. افرادی که قبلاً با زبان Visual Basic کار کرده باشند با این اعضاء با نام نام Shared آشنای دارند. در زبان سی شارپ اعضای static با کلمه کلیدی static تعریف می شوند. مثال زیر را در نظر بگیرید:

public class Messages
{
    public static void Welcome()
    {
        Console.WriteLine("Welcome to ITPro.ir");
    }
}

در کلاس بالا، متد Welcome به صورت static تعریف شده است. کافیست برای استفاده از این متد به صورت زیر عمل کنیم:

Messages.Welcome();

دقت کنید که هیچ شی ای از کلاس Messages ایجاد نشده است و تنها با نوشتن نام کلاس و تایپ کاراکتر . به اعضای static آن کلاس دسترسی داریم. در صورتی که شی ای از کلاس Messages بسازیم و لیست اعضای آن را مشاهده کنیم خبری از متد Welcome نخواهد بود.

ما می توانیم یک کلاس را به صورت static تعریف کنیم. در صورتی که کلاسی به صورت static تعریف شود، تمامی اعضای آن باید به صورت static تعریف شوند و همچنین دیگر امکان ساخت شی از روی آن کلاس وجود نخواهد داشت:

public static class Messages
{
    public static void Welcome()
    {
        Console.WriteLine("Welcome to ITPro.ir");
    }
}

در صورتی که داخل یک کلاس static عضو غیر static تعریف کنیم، با پیغام خطا مواجه خواهیم شد:


i1

در بخشی که متدها را توضیح می دادیم، هنگام تعریف متدها گفتیم متدهایی که داخل کلاس Program تعریف می شدند را باید به صورت static تعریف کنیم که از داخل متد Main به آنها دسترسی داشته باشیم. دلیل این کار این بود که متد Main خود به صورت static تعریف شده است و شما از داخل یک متد static می توانید تنها متدهای static داخل همان کلاس را صدا بزنید، در غیر اینصورت باید از روی آن کلاس شی ای بسازید و سپس متد مربوطه را اجرا کنید. به مثال زیر توجه کنید:

public class Messages
{
    public static void Welcome()
    {
        Console.WriteLine("Welcome to ITPro.ir");
    }

    public void Goodbye()
    {
            Console.WriteLine("Goodbye. Please comeback soon.");            
    }
}

کلاس Messages را از حالت static خارج کردیم، پس می توانیم اعضای غیر static داخل آن داشته باشیم. اما فرض کنید می خواهیم از متد Goodbye داخل متد Welcome استفاده کنیم. اگر متد Goodbye را بدون ساختن شی صدا کنیم پیغام خطا دریافت خواهیم کرد:


i2

برای رفع این مشکل متد Welcome را به صورت زیر باید بنویسیم:

public static void Welcome()
{
    Console.WriteLine("Welcome to ITPro.ir");
    var messages = new Messages();
    messages.Goodbye();
}

ابتدا از خود کلاس Messages یک شی ایجاد کرده و از روی شی اقدام به فراخوانی متد Goodbye کردیم. به این نکته دقت کنید که امکان استفاده از اعضای static در بخش های غیر static مشکلی ایجاد نمی کند و می توان به صورت مستقیم آن ها را فراخوانی کرد.

public class Messages
{
    public static void Welcome()
    {
        Console.WriteLine("Welcome to ITPro.ir");
    }

    public void Goodbye()
    {
        Console.WriteLine("Goodbye. Please comeback soon.");
        Welcome();
    }
}

در متد Goodbye ما به صورت مستقیم متد Welcome را صدا زدیم بدون اینکه پیغام خطایی دریافت کنیم. اما اگر برای فراخوانی متد Welcome در مثال بالا، از کلمه کلیدی this استفاده می کردیم، با پیغام خطا مواجه می شدیم. به این خاطر که کلمه کلیدی this همانطور که در قسمت های قبلی گفتیم، به شی ساخته شده از روی یک کلاس اشاره می کند.

در ابتدای همین بخش، گفتیم که اعضای static بین کل اشیاء ساخته شده از یک کلاس مشترک هستند. یعنی به ازای هر شی، مقدار متفاوتی ندارند، چون وابسته به شی نیستند. برای مثال، کد زیر یک خاصیت static از نوع int با نام Instances تعریف می کند که تعداد اشیاء ایجاد شده داخل یک کلاس را به ما نمایش می دهد. برای اینکه به ازای ساخته شدن هر شی مقدار Instances یک واحد اضافه شود، سازنده پیش فرض را برای کلاس به صورت زیر می نویسیم:

public class Sample
{
    public static int Instances { get; private set; }

    public Sample()
    {
        Instances++;
    }
}

دقت کنید، بخش set خاصیت Instances با سطح دسترسی private تعریف شده است. یعنی ما تنها می توانیم داخل کلاس آن را مقدار دهی کنیم و خارج از کلاس امکان مقدار دهی به آن وجود ندارد. سپس داخل سازنده پیش فرض یک واحد به Instances اضافه می کنیم. حال کد زیر را اجرا می کنیم:

Console.WriteLine(Sample.Instances);
var s1 = new Sample();
Console.WriteLine(Sample.Instances);
var s2 = new Sample();
var s3 = new Sample();
Console.WriteLine(Sample.Instances);
Console.ReadKey();

با اجرای قطعه کد بالا در متد Main، به ترتیب عددهای 0، سپس 1 و در انتها 3 که به ترتیب تعداد اشیاء ساخته شده از روی کلاس Sample هستند نمایش داده می شود. به همین دلیل گفته می شود که اعضای static بین کلیه اشیاء ساخته شده در یک کلاس مشترک هستند.

سازنده های static

در قسمت قبل گفتیم که سازنده ها به ما امکان اجرای کد مورد نظر در هنگام ساختن اشیاء را می دهند. اما سازنده ای وجود دارد که کاربردش برای اعضای static است. سازنده های static به صورت زیر تعریف می شوند:

static {classname}()
{
}

که به جای classname نام کلاسی که سازنده برای آن ایجاد می شود را می نویسیم. مثال:

public class StaticConstructor
{
    public static string FirstName { get; set; }
    public static string LastName { get; set; }

    static StaticConstructor()
    {
        FirstName = "None";
        LastName = "None";
    }
}

در کد بالا، هنگام اجرای سازنده مقادیر FirstName و LastName به None تغییر می کند. اما سازنده static چه زمانی اجرا می شود؟ اجرای سازنده های static درست زمانی که شما تصمیم دارید به یکی از فیلدهای static یک کلاس دسترسی پیدا کنید، تنها برای یکبار اجرا می شود. تنها برای یکبار، یعنی شما اگر 10 مرتبه فیلدهای static یک کلاس را استفاده کنید، سازنده static تنها برای دسترسی اول اجرا خواهد شد. مثال:

Console.WriteLine(StaticConstructor.FirstName);
StaticConstructor.FirstName = "Hossein";
Console.WriteLine(StaticConstructor.FirstName);

کد بالا به ترتیب مقادیر None و سپس Hossein را در خروجی چاپ می کند. به نکات زیر هنگام استفاده از سازنده های static توجه کنید:

  1. سازنده های static تنها یکبار در طول اجرای کد و آن هم زمان اولین دسترسی به اعضای static اجرا خواهند شد.
  2. سازنده های static نمی توانند سطح دسترسی غیر از public داشته باشند.
  3. سازنده های static نمی توانند پارامتری را به عنوان ورودی بگیرند.

استفاده از کلاس ها و اعضاء static باید با دقت زیاد انجام شود. زیرا این فیلدها خطرات زیادی را برای برنامه ایجاد می کنند، مخصوصاً برنامه های تحت وب که شما باید مباحث مربوط به همزمانی را در هنگام دسترسی به اعضای static رعایت کنید. در قسمت همزمانی و آشنایی با برنامه نویسی Aynchronous به صورت کامل با این مبحث آشنا می شوید.

Extension Method ها

بعد از آشنایی با کلاس ها و اعضای static به سراغ Exntesion Method ها می رویم. موقعیتی را در نظر بگیرید که می خواهید به یک کلاس متدی اضافه کنید. اما یا کد کلاس در اختیار شما نیست و یا نمی خواهید کد اصلی کلاس دستکاری شود. برای اینکار از Extension Method ها استفاده می شود.

این قابلیت به ما این اجازه را می دهد تا به نمونه های یک کلاس رفتاری را اضافه کنیم. این عملیات برای کلاس های تعریف شده توسط خود ما و همچنین نوع های داده اولیه و کلیه کلاس هایی که داخل دات نت تعریف شده اند قابل استفاده می باشد. شیوه کلی تعریف Extension Method ها به صورت زیر است:

1. ابتدا باید یک کلاس static برای تعریف Extension Method ها تعریف کنیم.

2. به ازای هر Extension Method، متدی با ساختار زیر داخل کلاس static تعریف شده ایجاد می کنیم:

public static {return-type} {name}(this {datatype} {instancename}, {parameters})
{
}
  1. return-type نوع داده بازگشتی متد را مشخص می کند.
  2. name نام متدی که قرار است به شی مورد نظر از یک کلاس اضافه شود. این نام کاملاً دلخواه است.
  3. datatype یا نوع داده ای که تصمیم داریم به شی های آن یک متد اضافه کنیم.
  4. instancename یا نام متغیری که بواسطه آن می خواهیم به شی ای که متد بر روی آن ایجاد می شود دسترسی داشته باشیم را مشخص می کنیم.این نام کاملاً دلخواه است.
  5. parameters یا لیست پارامترهای ورودی متدی که قصد تعریف آن را داریم مشخص می کند.

برای مثال، فرض کنید میخواهیم به نوع داده int یک متد اضافه کنیم که یک عدد را به عنوان ورودی گرفته و عدد داخل متغیر ایجاد شده را به توان ورودی رسانده و بر می گرداند. برای این کار کافیست ابتدا کلاسی برای Extension Method ها اضافه کنیم:

public static class IntExtensions
{
        
}

بهتر است برای Extension Method های هر نوع داده، یک کلاس جداگانه ایجاد کنیم مانند LongExtensions یا CustomExtensions که بخش اول اشاره به کلاسی می کند که ما تصمیم داریم برای آن Extension Method ایجاد کنیم. در ادامه متدی با نام Pow به نوع داده Int اضافه می کنیم:

public static class IntExtensions
{
    public static int Pow(this int number, int pow)
    {
        int result = 1;
        for (int counter = 0; counter < pow; counter++)
            result = result*number;
        return result;
    }
}

حال برای استفاده از این متد کافیست آن را برای متغیرهایی از نوع int به صورت زیر فراخوانی کنیم:

int myNum = 4;
var pow = myNum.Pow(3);
Console.WriteLine(pow);

به قسمت تعریف Extension Method بر گردیم، پارامتری با نام number داخل متد Pow تعریف کردیم، در حقیقت این متد به مقدار داخل متغیری اشاره می کند که ما Extension Method را بر روی آن اجرا می کنیم. در کد بالا، پارامتر number به مقدار 4 که داخل متغیر myNum ریخته شده اشاره می کند.

شما برای هر نوع داده و هر کلاسی می توانید Extension متد تعریف کنید. زمانی که Intellisense باز می شود، متد های عادی به صورت یک مکعب نمایش داده می شوند، اما Extension Method ها به صورت یک مکعب که یک فلش آبی رنگ به سمت پایین در بالای آن قرار دارد نمایش داده می شوند.


i3

کلاس های partial

کلاس های partial به شما این امکان را می دهند تا یک کلاس را به چند فایل بشکنید. برای مثال، یک فایل تعریف Property ها و یک فایل تعریف Method ها. در مثال زیر من کلاسی با نام Customer تعریف کردم که این کلاس به سه بخش تقسیم شده است، بخش اول خصوصیات، بخش دوم سازنده ها و بخش سوم متدها:

public partial class Customer
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public long Age { get; set; }
}

public partial class Customer
{
    public Customer()
    {
    }

    public Customer(string firstName, string lastName, long age)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
    }
}

public partial class Customer
{
    public string DisplayFullName()
    {
        return this.FirstName + " " + this.LastName;
    }
}

در حقیقت Customer یک کلاس است که به سه قسمت تقسیم شده. شما می توانید برای هر قسمت، یک فایل جداگانه ایجاد کنید. کلاس های partial زمان نوشتن برنامه ها کاربرد زیادی ندارند، به شخصه یاد ندارم داخل پروژه ای از این قابلیت استفاده کرده باشم.

بیشترین استفاده ای که از کلاس های partial شده است داخل برنامه هایی از نوع Windows Application که فایل های مربوط به فرم های برنامه به چند بخش شکسته شده اند. در قسمت های بعدی با این نوع از برنامه ها بیشتر آشنا خواهیم شد.

بعد از آشنایی با کلاس های static و کلاس های partial و همچنین Extension Method ها، در قسمت بعدی با مبحث Reference Type ها و Value Type ها و همچنین struct ها آشنا خواهیم شد. تا مبحث بعدی شما دوستان عزیز را به خدا می سپارم.

Value Type و Reference Type

همانطور که در قسمت های اولیه آموزش گفتیم، زبان سی شارپ یک زبان Strongly Typed است. یعنی تمامی نوع های داده در آن مشخص می باشند. اما کلیه نوع های داده در سی شارپ به دو دسته تقسیم می شوند:

  1. Reference Types
  2. Value Types

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

  1. حافظه stack
  2. حافظه heap

این دو حافظه از نظر میزان سرعت دسترسی و همچنین شیوه مدیریت آنها در زبان سی شارپ با یکدگیر تفاوت دارند. برای آشنایی با این دو حافظه بهتر است به بررسی Reference Types و Value Types بپردازیم و با نمونه کدهای عملی با شیوه عملکرد این دو نوع متغیر آشنا شویم.

Value Types

متغیرهایی که از نوع Value Type هستند، به صورت مستقیم داخل حافظه stack ذخیره می شوند. در زبان برنامه نویسی دات نت نوع های داده زیر از نوع Value Type هستند:

  1. byte
  2. sbyte
  3. short
  4. ushort
  5. int
  6. uint
  7. long
  8. ulong
  9. decimal
  10. double
  11. float
  12. bool
  13. char
  14. نوع های داده ای که با struct تعریف می شوند که در این بخش با آنها آشنا خواهیم شد.

زمانی که شما یک متغیر از نوع Value Type تعریف می کنید، به صورت مستقیم داخل حافظه stack محلی برای این متغیر در نظر گرفته شده و مقدار آن به صورت مستقیم داخل حافظه stack قرار میگیرد.اما ساختار حافظه stack چگونه است؟ به صورت خیلی مختصر توضیح می دم، فرض کنید شما تعداد 10 عدد بشقاب را روی میزی چیده اید.

فرض کنید بشقاب پنجم را می خواهید بردارید، برای اینکار باید از بالا چهار بشقاب را برداشته و سپس بشقاب پنجم را از بشفاب های چیده شده دریافت کنید. حافظه stack شبیه همین چیدمان است. آخرین ورودی حافظه stack، اولین خروجی از این حافظه می باشد.

به این روش اصطلاحاً First In Last Out یا FILO هم می گویند. اولین نفری که وارد شده، آخرین نفری است که خارج می شود. یکی از مهمترین کاربردهای حافظه stack مدیریت متدهای صدا زده شده داخل برنامه های سی شارپ است. در اوایل دوره آموزشی گفتیم نقطه شروع تمامی برنامه های زبان سی شارپ، متد Main است. مثال زیر را در نظر بگیرید:

public static void Main(string[] args)
{
    DisplayMessage();
}

public static void DisplayMessage()
{
    int value = 12;
    Console.WriteLine("Value: " + value);
}

زمانی که متد Main فراخوانی می شود داخل حافظه stack قرار می گیرد. حال شما متد دیگری با نام DisplayMessage را از داخل متد Main فراخوانی می کنید، این متد، پس از متد Main داخل حافظه stack قرار میگیرد. حال شما داخل متد DisplayMessage متغیری از نوع int با نام value تعریف می کنید.

این متغیر بعد از متد DisplayMessage داخل حافظه stack قرار گرفته، متد WriteLine فراخوانی می شود و این متد بعد از متغیر value داخل حافظه stack قرار می گیرد. زمانی که فراخوانی متد WriteLine به اتمام رسید، این متد از حافظه stack خارج شده و کنترل به متد قبلی که DisplayMessage است بازگردانده می شود.

زمانی که از scope متد DisplayMessage خارج می شویم، متغیر value بلااستفاده می شود و از حافظه stack خارج می شود. با بازگشتن روند اجرا به متد Main، متد DisplayMessage نیز ار حافظه stack خارج شده و با پایان متد Main این متد نیز از حافظه stack خارج می شود.

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

اگر قسمت متدهای Recursive را به خاطر داشته باشید، گفتیم برای جلوگیری از اجرای نامحدود متد، باید شرطی برای پایان دادن به اجرای متد در نظر بگیریم، دلیل این امر پر شدن حافظه stack و دریافت خطا می باشد. ساختاری از اجرای کد بالا در حافظه stack را در تصویر زیر مشاهده می کنید:


i1

پس از رسیدن به مرحله چهارم، به ترتیب موارد از روی حافظه stack برداشته می شوند تا با دستور Main برسیم.

تا اینجا متوجه شدیم که تمامی نوع های داده Value Type که در بالا آن ها را نام بردیم به صورت مستقیم در حافظه stack ذخیره می شوند.

Reference Types چیست؟

شیوه ذخیره متغیرهایی که نوع Reference Type هستندبا نوع های Value Type تفاوت دارد. در حقیقت Reference Type ها علاوه بر حافظه stack با حافظه دیگری با نام Heap سر و کار دارند. حافظه Heap حجم بیشتری از Stack در اختیار شما قرار می دهد و البته سرعت دسترسی به آن از حافظه stack کمتر است.

قبل از اینکه شروع به بررسی ساختار حافظه Heap کنیم بهتر است در مورد این که به چه نوع داده هایی Reference Type می گویند صحبت کنیم. شما هر کلاسی که تعریف می کنید از نوع Reference Type است. حتی شیوه استفاده از Reference Type ها با Value Type ها متفاوت است. زمانی که شما یک متغیر از نوع int تعریف می کنید به صورت مستقیم آن را مقدار دهی می کنید:

int number = 12;

اما زمانی که قصد استفاده از یک کلاس را داریم باید از روی آن یک شی بسازیم:

var customer = new Customer();

دقیقاً تفاوت در همینجاست. گفتیم ایجاد یک شی از دو بخش تشکیل شده:

  1. تعریف متغیری از نوع کلاسی که می خواهیم از روی آن یک شی بسازیم
  2. ایجاد شی و قرار دادن آن داخل متغیر

در مرحله اول، یعنی زمانی که شما متغیری برای یک شی تعریف می کنید، در حقیقت خانه ای داخل حافظه stack برای آن متغیر ذخیره کرده اید. برای مثال دستور زیر متغیری برای شی ای از نوع Customer داخل حافظه stack رزرو می کند:

Customer customer; // allocate stack memory for customer variable

در مرحله بعد، زمانی که شما شی ای را ایجاد کرده و داخل متغیر قرار می دهید، دات نت، شی ایجاد شده را داخل حافظه Heap قرار می دهد و سپس آدرس مربوط به شی در حافظه Heap را داخل متغیر ایجاد شده در حافظه stack قرار می دهد.

Customer customer; // allocate stack memory for customer variable
Customer customer = new Customer(); // create object on heap and assign it's address to customer variable on stack

در تصویر زیر می توانید ساختار متغیر customer و شی ایجاد شده برای آن را در حافظه stack و heap مشاهده کنید:


i2

پس از ایجاد متغیر و شی مربوطه، هر زمان که شما قصد دسترسی به یکی از اعضای شی Customer را داشته باشید، ابتدا به حافظه stack مراجعه شده و بر اساس آدرس شی در حافظه Heap، دسترسی به عضو آن شی برای شما فراهم می شود.حافظه Heap یک حافظه مدیریت شده می باشد.

منظور از مدیریت شده، عملیاتی است که CLR بر روی این حافظه انجام می دهد. گفتیم زمانی که از scope یک متغیر خارج می شویم، آن متغیر از حافظه stack حذف می شود. تا زمانی که این متغیر در حافظه stack وجود دارد، در حقیقت ارتباط بین آن و شی داخل Heap برقرار است و زمانی که متغیر از stack حذف شد این ارتباط نیز حذف می شود.

در حقیقت شی داخل حافظه Heap بلااستفاده است. در بخش معرفی دات نت در مورد سرویسی به نام GC به صورت مختصر صحبت کردیم. این سرویس وظیفه مدیریت حافظه Heap را بر عهده دارد.

سرویس GC، در بازه های زمانی مختلف حافظه Heap را بررسی کرده و شی هایی که از حافظه stack به آنها ارجاعی داده نشده را از Heap حذف می کند. عملیاتی که GC انجام می دهد کمی پیچیده است که در بخش های بعدی در مورد این سرویس به تفصیل صحبت خواهیم کرد، اما به اختصار GC عملیات های زیر را هنگام اجرا انجام می دهد:

  1. بررسی حافظه Heap و شناسایی شی هایی که دیگر مورد استفاده قرار نمی گیرند.
  2. حذف اشیاء بلااستفاده از حافظه Heap
  3. Defrag کردن یا مرتب سازی حافظه Heap

تفاوت Value Type و Reference Type هنگام استفاده

برای درک بهتر این موضوع مثالی می زنیم. ابتدا با Value Type ها شروع می کنیم. متغیری از نوع int با نام num1 تعریف می کنیم:

int num1 = 8;

سپس متغیر دیگری از نوع int و با نام num2 تعریف کرده و متغیر num1 را داخل آن قرار می دهیم و سپس مقدار num2 را تغییر می دهیم:

int num1 = 8;
int num2 = num1;
num2 = 17;

در مرحله بعد مقدار هر دو متغیر را در خروجی چاپ می کنیم:

int num1 = 8;
int num2 = num1;
num2 = 17;
Console.WriteLine(num1);
Console.WriteLine(num2);

خروجی کد بالا به ترتیب عددهای 8 و 17 می باشد. دلیل آن هم این است که وقتی شما متغیر num1 را داخل num2 قرار می دهید، در حقیقت برای num2 خانه ای در stack ایجاد شده و مقدار num1 داخل آن کپی می شود. اما برای Reference Type ها به این صورت نیست. کلاس زیر را در نظر بگیرید:

public class ValueHolder
{
    public int Value { get; set; }
}

حال داخل متد Main کد زیر را بنویسید:

var holder1 = new ValueHolder() {Value = 8};
var holder2 = holder1;
holder2.Value = 21;
Console.WriteLine(holder1.Value);
Console.WriteLine(holder2.Value);

با اجرای کد بالا، دو بار مقدار 21 در خروجی چاپ می شود. دلیل آن هم تفاوت ساختار متغیرهای Reference Type و Value Type است. زمانی که متغیر و شی holder1 ایجاد می شوند، بر اساس مکانیزم گفته شده در بالا، خانه ای در stack ایجاد شده که به شی ای داخل Heap اشاره می کند. زمانی که ما داخل holder2 متغیر holder1 را قرار می دهیم، در حقیقت آدرس holder1 را به آن منسب کردیم که در نتیجه holder1 و holder2 هر دو به یک خانه از حافظه heap اشاره می کنند:


i3

در نتیجه با تغییر خصوصیت Value در هر یک از متغیرهای holder1 و holder2، مقدار شی تغییر کرده و تغییر در هر دو متغیر منعکس می شود. زمانی که از Reference Type ها استفاده می کنید، باید به مسئله ذکر شده توجه زیادی داشته باشید، زیرا می تواند عملکرد کد شما را تحت تاثیر قرار دهد.

تعریف Value Type ها با کمک struct

همانطور که تا کنون گفته شد، می توانیم بوسیله کلاس نوع های داده مورد نظر خود را تعریف کنیم. نوع های داده ای که بوسیله کلمه کلیدی class تعریف می شوند از نوع Reference Type هستند. در زبان سی شارپ ما می توانیم بوسیله کلمه کلیدی struct نوع های داده ای از نوع Value Type تعریف کنیم.

ساختار struct ها دقیقاً مشابه کلاس ها می باشد، با این تفاوت که به جای کلمه کلیدی class از struct استفاده می کنیم. برای مثال، در کد زیر ما یک struct تعریف کردیم که طول و عرض که مستطیل را برای ما نگهداری می کند:

public struct Rectangle
{
    public float Width { get; set; }
    public float Height { get; set; }
}

حال کافیست از این ساختار در کد خود استفاده کنیم:

Rectangle rect1 = new Rectangle();
rect1.Width = 12;
rect1.Height = 13;

دقت کنید که ما از کلمه کلیدی new برای مقدار دهی اولیه rect1 استفاده کردیم، نباید عملیات new بر روی value type ها را با reference type ها اشتباه گرفت. زیرا struct ها یا داخل stack ذخیره می شوند، یا داخل نوع داده ای که در آن تعریف شده اند، همچنین زمانی که شما یک متغیر از نوع struct را داخل متغیر دیگری از همان نوع قرار می دهید، کل مقادیر داخل آن به متغیر جدید کپی می شود. به کد زیر توجه کنید:

Rectangle rect1 = new Rectangle();
rect1.Width = 12;
rect1.Height = 13;
Rectangle rect2 = rect1;
rect2.Height = 24;
Console.WriteLine(rect1.Height);
Console.WriteLine(rect2.Height);

کد زیر مقادیر 13 و 24 را در خروجی چاپ می کند، در حالی که اگر Rectangle را از نوع کلاس تعریف کردیم بودیم، برای هر دو متغیر مقدار 24 چاپ می شد، زیرا تمامی struct ها از نوع Value Type و کلاس ها از نوع Reference Type می باشند.زمان استفاده از struct ها پارامترهای زیر را مد نظر داشته باشید:

  1. تنها زمانی از struct استفاده کنید که سایز نوع داده شما کم است. برای نوع های داده ای که خصوصیات و منابع زیادی استفاده می کنند از کلاس استفاده کنید
  2. در struct ها نمی توانید از قابلیت وراثت یا inheritance استفاده کنید. تنها اسنفاده از interface ها مجاز می باشد که در بخش بعدی با آنها آشنا خواهید شد
  3. struct ها را نمی توانید از نوع static تعریف کنید و این کار تنها برای کلاس ها مجاز است.
  4. برای struct ها نمی توانید سازنده پیش فرض یا Default Constrcutor تعریف کنید
  5. برای struct ها اجباری به ایجاد نمونه با کلمه کلیدی new نیست، مانند نوع داده int

رشته ها Reference Type هستند یا Value Type

در حقیقت رشته ها Reference Type هستند، اما می توان با آنها مانند Value Type رفتار کرد. دلیل این امر هم ساختار پیاده سازی آن در زبان سی شارپ است. رشته ها در زبان سی شارپ Immutable هستند، یعنی هر زمان که شما تغییری در یک رشته ایجاد می کنید، خانه جدیدی در حافظه Heap ایجاد شده و آدرس آن داخل متغیر شما قرار می گیرد. به این دلیل می گویند که رشته ها Immutable هستند.

آشنایی با null و متغیرهای nullable

زمانی که شما متغیری از نوع reference Type تعریف می کنید، می توانید داخل آن مقدار null قرار دهید. مقدار null در حقیقت، مقدار نیست، null یعنی هیچ. بدین معنی که شما هنوز هیچ مقداری داخل متغیر قرار نداید:

Customer customer = null;

در صورتی که شما قصد استفاده از متغیری را داشته باشید که مقدار آن null است، با پیغام خطای NullReferenceException مواجه خواهید شد. پس باید قبل از اینکه از متغیری استفاده کنید که مشکوک به null بودن است با دستور if آن را چک کنید:

public void PrintCustomer(Customer customer)
{
    if(customer != null)
    {
        // write your code
    }
}

اما در زبان سی شارپ، تنها Reference Type ها می توانند مقدار null قبول کنند. راه حل استفاده از مقادیر null برای Value Type ها تعریف آنها به صورت nullable است. برای مثال، در کد زیر متغیر number از نوع int تعریف شده، اما nullable است:

int? number = null;

با قرار دادن علامت سوال بعد از نوع های داده Value Type، می توان مقدار null را به آنها منتسب کرد. متغیرهایی که از نوع nullable تعریف می شوند، دو خاصیت به نام های HasValue و Value دارند:

  1. HasValue: بررسی می کند که متغیر مقدار null دارد یا خیر، مقداری که این خصوصیت بر میگرداند از نوع bool است.
  2. Value: در صورتی که متغیر مقداری داشته باشد، می توان بوسیله این خصوصیت مقدار آن را گرفت.

البته می توان به صورت مستقیم مقادیر داخل متغیرهای nullable را گرفت یا آنها را با دستور if چک کرد:

public void PrintNumber(int? number)
{
    if(number != null)
    {
        Console.WriteLine(number);
    }
}

اما استفاده از خصوصیت های بالا نیز مجاز است:

public void PrintNumber(int? number)
{
    if(number.HasValue)
    {
        Console.WriteLine(number.Value);
    }
}

متغیر های از نوع string به دلیل اینکه Reference Type هستند، به صورت مستقیم می توان مقدار null را داخل آنها قرار داد و نمی توان string را از نوع nullable تعریف کرد.دو مفهوم مهم در مورد Reference Type ها و Value Type ها به نام های boxing و unboxing باقی می ماند که در بخش type casting در مورد این دو واژه به تفصیل صحبت خواهیم کرد. در بخش بعدی آموزش به بررسی مبحث interface ها خواهیم پرداخت.

Interface ها

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

تعریف interface ها

در حقیقت interface ها، به ما امکان تعریف مجموعه ای از خصوصیات و متدهای مرتبط را می دهند و قابلیت پیاده سازی در کلاس ها یا struct ها را دارند. با استفاده از interface ها، شما قابلیت پیاده سازی چندین ویژگی از چندین interface مختلف را در یک کلاس یا struct خواهید داشت. کلاس ها به صورت پیش فرض قابلیت ارث بری از چند کلاس را پشتیبانی نمی کنند و برای شبیه سازی این کار باید از interface ها استفاده کرد. در ابتدا با ساختار کلی تعریف interface آشنا می شویم:

{access-modifier} interface {name}
{
    {members}
}
  1. در قسمت access-modifier که سطح دسترسی به interface را مشخص می کنیم.
  2. در قسمت name، نام interface مشخص می شود. به این نکته توجه داشته باشید، استانداردی برای نام گذاری interface ها وجود دارد، به این صورت که در ابتدای نام interface بهتر است کاراکتر I قرار بگیرد تا کلاس ها و interface ها از یکدیگر مجزا شوند. برای مثال: IPerson یا IDatabaseManager.
  3. در قسمت member، باید اعضای interface را تعریف کنیم، دقت کنید که در این بخش برای اعضا نه می توان access-modifier مشخص کرد و نه بدنه ای برای متدها، تنها باید تعریف کلی از اعضاء نوشته شود و همچنین اعضای تعریف شده برای یک interface همگی به صورت پیش فرض سطح دسترسی public خواهند داشت.

با یک مثال ساده ادامه می دهیم. در نمونه کد زیر یک interface با نام INamed تعریف کردیم که یک خصوصیات به نام Name و یک متد با نام PrintName در آن تعریف شده است:

public interface INamed
{
    string Name { get; set; }

    void PrintName();
}

همانطور که در کد بالا مشاهده می کنید، سطح دسترسی نه برای خصوصیت مشخص شده و نه برای متد، همچنین متد ما بدنه نداشته و فقط حاوی signature می باشد. در ادامه قصد داریم تا از این interface استفاده کنیم. استفاده از interface دقیقاً مانند حالتی است که می خواهیم کلاس پدر را برای یک کلاس فرزند در وراثت مشخص کنیم:

public class Car : INamed
{
        
}

اما با نوشتن کد بالا پیغام خطا دریافت خواهید کرد. زیرا interface تنها حاوی تعریف کلی بوده و باید پیاده سازی در کلاسی که interface را به ارث برده انجام شود:

public class Car : INamed
{
    public string Name { get; set; }
    public void PrintName()
    {
        Console.WriteLine(Name);
    }
}

برای پیاده سازی خودکار interface، در صورتی که Resharper را نصب کرده باشید با بردن مکان نما بر روی نام کلاس، فشردن کلیدهای Alt+Enter و انتخاب گزینه Implement missing members عملیات پیاده سازی کلاس به صورت خودکار برای شما انجام می شود

و در صورتی که Resharper نصب نباشد، با رفتن بر روی نام interface در مقابل کلاس و فشردن کلیدهای Ctrl+. عملیات پیاده سازی را می توانید انجام دهید.تا اینجا عملیات پیاده سازی کلاس را انجام دادیم، اما این پیاده سازی چه ویژگی هایی برای ما دارد و چگونه باید از interface استفاده کنیم؟

اگر توضیحات قسمت وراثت را به خاطر داشته باشید، گفتیم زمانی که یک کلاس، از کلاس دیگری ارث بری می کنید، می توان برای ساختن یک نمونه از کلاس، از نوع داده پدر استفاده کرد. این مورد، در باره interface ها هم صدق می کند، در حقیقت ما می توانیم از interface برای ایجاد شی مورد نظر استفاده کنیم:

INamed namedInstance = new Car();

اما باید به یک نکته توجه داشته باشید، زمانی که از interface برای ساخت شی استفاده می کنید تنها می توانید به اعضایی از کلاس دسترسی داشته باشید که در interface تعریف شده اند.یکی دیگر از قابلیت های interface، همانطور که در ابتدای این بخش گفته شد، امکان Multiple-Inheritance می باشد.

به طور پیش فرض، شما تنها می توانید از یک کلاس در دات نت ارث بری کنید و امکان ارث بری از چند کلاس وجود ندارد. برای رفع این مشکل می توان از interface ها استفاده کرد. یعنی شما می توانید نام چند interface را در مقابل نام کلاس بنویسید. برای مثال، یک interface جدید با نام INotify تعریف می کنیم:

public interface INotify
{
    void Notify();
}

حال می توانیم در کلاس Car، علاوه بر INamed از INotify نیز استفاده کنیم:

public class Car : INamed, INotify
{
    public string Name { get; set; }
    public void PrintName()
    {
        Console.WriteLine(Name);
    }

    public void Notify()
    {
        Console.WriteLine("Notify me via Email!");
    }
}

پیاده سازی interface ها به صورت implicite و explicit

پیاده سازی interface ها در کلاس ها به دو صورت انجام می شود، implicite و explicit. تفاوت این دو روش در این است که در ابتدای نام عضو interface در کلاس، نام interface به همراه کاراکتر . قرار میگیرد. در قسمت های قبل، پیاده سازی ها بر اساس روش implicit انجام شد و در این قسمت با روش explicit آشنا می شویم. برای مثال فرض کنید می خواهیم INotify را به صورت explicit پیاده سازی کنیم، کد زیر پیاده سازی با این روش را نشان می دهد:

public class Car : INamed, INotify
{
    public string Name { get; set; }
    public void PrintName()
    {
        Console.WriteLine(Name);
    }

    void INotify.Notify()
    {
        Console.WriteLine("Notify me via Email!");
    }
}

اگر در کد بالا دقت کنید، برای متد Notify هیچ سطح دسترسی مشخص نشده است، زمانی که شما یک عضو را به صورت explicit پیاده سازی می کنید، هیچ سطح دسترسی نباید برای آن مشخص کنید. همچنین اعضایی که به صورت Explicit پیاده سازی می شوند تنها در صورتی قابل دسترس هستند که با نام interface از روی آنها شی ساخته شود. یعنی در کد زیر شما به متد Notify دسترسی نخواهید داشت:

Car carInstance = new Car();
carInstance.Notify();

کد بالا منجر به پیغام خطا خواهد شد. اما کد زیر بدون مشکل اجرا می شود:

INotify notifyInstance = new Car();
notifyInstance.Notify();

یکی از مهمترین کاربردهای پیاده سازی explicit، امکان پیاده سازی چند interface با اعضای هم نام در یک کلاس است! برای روشتر شدن موضوع فرض کنید ما Interface ای داریم با نام IEmailNotify که عملیات اطلاع رسانی را به بوسیله ایمیل و interface دیگری داریم با نام ISMSNotify که عملیات اطلاع رسانی را بوسیله پیامک انجام می دهد. هر دوی این interface ها متدی دارند با نام Notify:

public interface IEmailNotify
{
    void Notify();
}

public interface ISMSNotify
{
    void Notify();
}

حال کلاس Car را به صورت زیر تغییر می دهیم:

public class Car : INamed, IEmailNotify, ISMSNotify
{
    public string Name { get; set; }
    public void PrintName()
    {
        Console.WriteLine(Name);
    }

    void IEmailNotify.Notify()
    {
        Console.WriteLine("Notify via Email!");
    }

    void ISMSNotify.Notify()
    {
        Console.WriteLine("Notify via SMS!");
    }
}

کد بالا دو پیاده سازی برای متد Notify دارد. یکی برای IEmailNotify و یکی برای ISMSNotify که بر اساس نوع داده مورد استفاده برای شی، متد مربوطه فراخوانی خواهد شد. در ادامه کد زیر را در متد Main می نویسیم:

var car = new Car();

ISMSNotify smsNotify = car;
smsNotify.Notify();
IEmailNotify emailNotify = car;
emailNotify.Notify();

با اجرای کد بالا، ابتدا خروجی Notify via SMS و سپس Notify via Email در خروجی چاپ خواهد شد. ما در حقیقت یک شی از نوع Car ایجاد کردیم و یکبار آن را داخل متغیری از نوع ISMSNotify قرار دادیم و بار دوم در متغیری با نام IEmailNotify. عملیات فراخوانی متد Notify به صورت خودکار بر اساس نوع متغیر انجام خواهد شد.

مباحث مربوط به interface بسیار گسترده می باشد. یکی از مهمترین کاربردهای interface پیاده سازی IoC یا Inversion of Control و DI یا Dependency Injection در برنامه ها می باشد که در بخش بعدی با این تکنیک آشنا می شویم.

Dependency Injection

در قسمت قبلی سری آموزشی با مفهوم interface ها آشنا شدیم. interface ها نقش بسیار موثری در روند نوشتن یک برنامه بازی می کنند و در صورتی که یک برنامه نویس با نحوه استفاده صحیح از interface ها آشنا باشه، توانایی ایجاد کدهایی ساختاریافته و قابل گستری و نگهداری رو داره.

در این قسمت از سری آموزشی زبان برنامه نویسی سی شارپ و برنامه نویسی شی گرا، با دو مفهوم بسیار مهم در برنامه نویسی آشنا خواهیم شد، IoC که مخفف Inversion of Control و DI که مخفف Dependency Injection هست.همه ما در طول زندگی با وسایل زیادی سر و کار داریم

از وسایل اولیه زندگی مانند ماشین، یخچال، تلویزیون و ... تا وسایل که هر کس بر اساس نیاز کاری خودش با اونها سر و کار داره، مانند کامپیوتر یا لپ تاپ و گوشی. برای مثال، گوشی هوشمند خود را فرض کنید، این گوشی از قطعات زیادی تشکیل شده، صفحه نمایش، پردازنده، حافظه رم، باتری و کلی قطعات دیگه. حالا اتفاقی پیش میاد و خدای نکرده گوشی شما از دستتون میافته و صفحه نمایش گوشیتون آسیب میبینه.

کاری که می کنید باید گوشی رو به یک نمایندگی برده و صفحه نمایش رو تغییر بدید. حالا فرض کنید که گوشی شما جوری طراحی شده باشه که با آسیب دیدن صفحه نمایش نیاز باشه تا یک گوشی جدید تهیه کنید!!!! یا برای کارتون یک لپ تاپ تهیه کردید.

بعد از مدتی نیاز دارید تا حافظه رم لپ تاپ رو افزایش بدید. در این حالت شما لپ تاپ رو پیش نمایندگی یا یک کارشناس در این زمینه می برید و حافظه رم لپ تاپ شما افزایش داده میشه. حال فرض کنید که لپ تاپ شما همچین قابلیتی نداشته باشه و شما نیاز باشه برای تغییر یا ارتقا حافظه یک لپ تاپ جدید خریداری کنید!

برای حل این مشکل لوازم الکتریکی از قطعات مختلفی تشکیل شدند که قابلیت تغییر یا تعویض دارند. به این قابلیت طراحی ماژولار گفته میشه. در پیاده سازی سیستم های نرم افزاری نیز شما نیز به عنوان برنامه نویس باید با همچین دیدی نسبت به پیاده سازی نرم افزار اقدام کنید.

ما در اینجا در مورد تغییر یکی از قسمت های نرم افزار صحبت خواهیم کرد که ارتباط مستقیمی به interface ها و IoC و DI دارد.برای آشنایی بیشتر با این مفاهیم، با یک مثال جلو میرویم. فرض کنید سیستمی پیاده سازی کردید که اعضاء می توانند در این سامانه اقدام به ثبت نام کنند. برای عملیات های مرتبط با مدیریت اعضاء کلاسی با نام Members می نویسیم:

public class Members
{
    public void Register(string firstName, string lastName)
    {
        // add member to database            
    }
}


همانطور که مشاهده می کنید این کلاس یک متد با نام Register دارد که عملیات ثبت نام اعضاء را انجام می دهد (تنها متد تعریف شده و کدی برای ثبت نام نوشته نشده است). پس از مدتی، فردی که درخواست پیاده سازی نرم افزار را از شما داشته، می خواهد زمانی که یک عضو جدید به سامانه اضافه شد، یک ایمیل برای شخص ارسال شود. کد کلاس به صورت زیر تغییر می کند:

public class Members
{
    public void Register(string firstName, string lastName)
    {
        // add member to database
        // send email to name@host.com
    }
}


تا اینجا مشکلی نیست، بعد از مدتی مجدداً شخص ذکر شده از شما می خواهد به جای ارسال ایمیل، یک پیامک برای او ارسال شود. شما مجدداً باید کد داخل متد Register را تغییر داده و عملیات ارسال پیامک را اضافه کنید:

public class Members
{
    public void Register(string firstName, string lastName)
    {
        // add member to database
        // send sms to 0912*******
    }
}


با هر درخواست، ما دائماً در حال تغییر کدهای نوشته شده داخل کلاس Members هستیم. اگر کمی اصولی کار کنیم، کلاسی با نام SmsManager ایجاد می کنیم و از آن کلاس داخل کلاس Members استفاده می کنیم:

public class SmsManager
{
    public void Send(string message)
    {
        // send sms to 0912*******
    }
}

public class Members
{
    public void Register(string firstName, string lastName)
    {
        // add member to database
        var sms = new SmsManager();
        sms.Send("New user registered!");
    }
}


حالا فرض کنید که مجدد، شخص ذکر شده درخواست جایگزینی ارسال ایمیل به جای پیامک را به ما می دهد، ما کلاسی با نام EmailManager تعریف کرده و از آن استفاده می کنیم:

public class EmailManager
{
    public void Send(string message)
    {
        // send message to name@host.com
    }
}

public class SmsManager
{
    public void Send(string message)
    {
        // send sms to 0912*******
    }
}

public class Members
{
    public void Register(string firstName, string lastName)
    {
        // add member to database
        var email = new EmailManager();
        email.Send("New user registered!");
    }
}


با تعریف کلاس های EmailManager و SmsManager، حجم تغییرات کلاس Members خیلی کم شد. اما می توان این تغییرات را خیلی کمتر کرد. در اینجا می خواهیم یکی از کاربردهای بسیار مهم interface ها، یعنی IoC یا Inversion of Control را بررسی کنیم. در ابتدا ما یک interface با نام INotifySender به صورت زیر ایجاد می کنیم:

public interface INotifySender
{
    void Send(string message);
}


اگر دقت کنید، متد signature متد Send در کلاس های EmailManager و SmsManager مشترک است. پس این دو کلاس قابلیت پیاده سازی INotifySender را دارند. کلاس های ذکر شده را به صورت زیر تغییر می دهیم:

public class EmailManager : INotifySender
{
    public void Send(string message)
    {
        // send message to name@host.com
    }
}

public class SmsManager : INotifySender
{
    public void Send(string message)
    {
        // send sms to 0912*******
    }
}


در قدم بعدی باید کلاس Members را جوری تغییر دهیم تا وابستگی متد Register به یک کلاس خاص از بین برود، یعنی به کلاس EmailManager یا SmsManager وابسته نباشد. اینکار را می توان با استفاده از INotifySender انجام داد. کد کلاس Members را به صورت زیر تغییر می دهیم:

public class Members
{
    private readonly INotifySender sender;

    public Members()
    {
        sender = new EmailManager();
    }

    public void Register(string firstName, string lastName)
    {
        // add member to database
        sender.Send("New member registered!");
    }
}


تغییرات کد بالا را با هم بررسی می کنیم، فیلدی تعریف کردیم از نوع INotifySender که داخل متد Register از این فیلد برای ارسال پیام استفاده می کنیم. این فیلد از داخل سازنده کلاس Members مقدار دهی می شود. در کد بالا ما شی ای از نوع EmailManager که INotifySender را پیاده سازی کرده است ایجاد کرده و داخل فیلد sender میریزیم. حال فرض کنید که بخواهیم عملیات را از ایمیل به پیامک تغییر دهیم، برای اینکار تنها سازنده را به صورت زیر تغییر می دهیم:

public class Members
{
    private readonly INotifySender sender;

    public Members()
    {
        sender = new SmsManager();
    }

    public void Register(string firstName, string lastName)
    {
        // add member to database
        sender.Send("New member registered!");
    }
}


به عملیات بالا، Inversion of Control گفته می شود. ما در حقیقت وابستگی متد Register را به یک کلاس خاص از بین بردیم. به این نوع پیاده سازی tightly coupled گفته می شود. اما باز یک مشکل وجود دارد، ما هنوز هم در حال تغییر کلاس Members هستیم. در قدم بعد باید وابستگی کلاس Members را به کلاس های EmailManager و SmsManager به طور کامل از بین ببریم. برای اینکار، ما به جای ساختن شی داخل سازنده، آن را به عنوان یک پارامتر به سازنده کلاس Members ارسال می کنیم:

public class Members
{
    private readonly INotifySender sender;

    public Members(INotifySender sender)
    {
        this.sender = sender;
    }

    public void Register(string firstName, string lastName)
    {
        // add member to database
        sender.Send("New member registered!");
    }
}


نحوه استفاده از کلاس Members به صورت زیر است:

var members = new Members(new EmailManager());
members.Register("Hossein", "Ahmadi");


حال اگر بخواهیم نوع ارسال پیام را به پیامک تغییر دهیم تنها کافیست نوع شی ارسالی به سازنده کلاس Members را تغییر دهیم:

var members = new Members(new SmsManager());
members.Register("Hossein", "Ahmadi");


به این تکنیک، DI یا Dependency Injection گفته می شود. ترکیب IoC و DI کمک زیادی به شما در نوشتن کدهایی می کنند که به راحتی قابلیت تغییر و به روز رسانی دارند. در این مقاله با یکی از کاربردهای بسیار مهم interface ها آشنا شدید. کتابخانه های آماده ای برای IoC و DI وجود دارند که به آنها IoC Container نیز گفته می شوند که روند DI رو برای شما به عنوان یک برنامه نویس بسیار راحت تر می کنند. در یک فیلم آموزشی در مورد IoC Container ها به تفصیل صحبت خواهیم کرد. در قسمت بعدی با مفهوم Type Casting و انواع Cast ها در زبان سی شارپ آشنا خواهیم شد. 

تبدیل نوع یا Type Casting

در این قسمت با مبحث Type Casting و انواع Cast ها در زبان سی شارپ آشنا بشیم. ابتدا یک توضیح اولیه راجع موضوع Type Casting بدم و بعد بریم سراغ مثال ها و کدها. همانطور که در قسمت های قبلی گفته شد، در زبان سی شارپ انواع نوع داده برای ذخیره مقادیر وجود دارد.

برای مثال، نوع داده int برای ذخیره مقادیر عددی صحیح و نوع داده string برای نوع داده رشته استفاده می شود. اما بعضی اوقات هست که تصمیم داریم یک نوع داده را به یک نوع داده دیگر تبدیل کنیم. به تبدیل انواع داده به یکدیگر در زبان سی شارپ Type Casting می گویند. در قسمت های قبلی با یکی از این تبدیل ها آشنا شدیم: استفاده از متد Parse در نوع های داده اولیه. برای مثال زمانی که می خواستیم نوع داده رشته را به عددی تبدیل کنیم به صورت زیر عمل می کردیم:

int number = int.Parse("12");

کد بالا، رشته 12 را به نوع عددی int تبدیل کرده و داخل متغیر number ذخیره می کند. در این قسمت از آموزش، به تفصیل راجع به مبحث Type Casting صحبت کرده و انواع و اقسام آن را توضیح می دهیم.

انواع Type Casting در زبان سی شارپ

در زبان سی شارپ، دو نوع تبدیل نوع به یکدیگر با روش های مختلف وجود دارد:

  1. تبدیل نوع Implicit: در این نوع تبدیل بدون نیاز به نوشتن کد خاصی، می توان مقدار یک متغیر را داخل یک متغیر دیگر ریخت. برای مثال می توان نوع عددی int را بدون هیچ مشکلی به نوع داده long تبدیل کرد
  2. تبدیل نوع Explicit: در این نوع تبدیل، ما می بایست حتماً با نوشتن یکسری دستورات خاص، نوع ها را به یکدیگر تبدیل کنیم، برای مثال نمی توان نوع عددی long را به صورت مستقیم به نوع عددی int تبدیل کرد، زیرا نوع عددی long ظرفیت بیشتری از int دارد و ریختن مقدار long داخل int به صورت مستقیم یا implicit امکان پذیر نیست.

ابتدا یک مثال در مورد تبدیل Implicit بزنیم، در کد زیر یک متغیر از نوع int تعریف شده و سپس مقدار متغیر int را داخل یک متغیر از نوع long میریزیم:

int number1 = 12;
long number2 = number1;

کد بالا بدون هیچ مشکلی اجرا میشود و نوع داده int به نوع داده long تبدیل می شود. به این نوع تبدیل Implicit Type Casting گفته می شود. اما کد زیر را در نظر بگیرید:

long number1 = 12;
int number2 = number1;

کد بالا اجرا نخواهد شد، زیرا نوع داده long به صورت مستقیم قابل تبدیل به int نیست. برای این مواقع باید از تبدیل Explicit استفاده کرد. زمانی که قصد تبدیل نوعی به نوع دیگر از روش Explicit را داریم، باید قبل از نام متغیر یا مقداری که قرار بر تبدیل آن است، داخل پرانتز نام نوع داده مقصد نوشته شود. برای مثال، در کد زیر ما نوع داده long را به صورت Explicit به int تبدیل می کنیم:

long number1 = 12;
int number2 = (int)number1;

همانطور که مشاهده می کنید، قبل از نوشتن نام متغیر number1 در زمان تبدیل، داخل پرانتز نام long نوشته شده، یعنی ما تصمیم داریم مقدار متغیر number1 را به نوع داده long تبدیل کنیم. برای مثال کد زیر مقدار متغیری از نوع decimal را به int تبدیل می کنیم:

decimal number1 = 54.2255m;
int number2 = (int) number1;

در تبدیل بالا، دقت کنید که زمان تبدیل متغیر number1 به number2 که تبدیل از نوع decimal به int هست، قسمت اعشاری متغیر number1 از بین می رود.یک نکته در مورد تبدیل کلاس ها به یکدیگر، اگر به خاطر داشته باشید، در قسمت Inheritance گفتیم که تبدیل از کلاس فرزند به پدر به صورت مستقیم امکان پذیر است، برای مثال، کلاس های زیر را در نظر بگیرید:

    public class Base
    {
        public int Id { get; set; }
    }

    public class Derived : Base
    {
        public string Name { get; set; }
    }

کد زیر بدون مشکل اجرا میشود:

Derived derived = new Derived();
Base @base = derived;

در حقیقت ما متغیر derived که از نوع کلاس فرزند Dervied هست را بدون مشکل و به صورت Implicit به نوع Base تبدیل کردیم، اما برعکس این عملیات، یعنی تبدیل نوع پدر به فرزند به صورت مستقیم امکان پذیر نیست، یعنی در کد بالا اگر بخواهیم متغیر base@ رو به نوع Derived تبدیل کنیم، این کار باید به صورت Explicit انجام شود:

Derived derived = new Derived();
Base @base = derived;
Derived castedDervied = (Derived) @base;

کلمات کلیدی checked و unchecked

موقعیتی را فرض کنید که تصمیم داریم متغیری از نوع long را به نوع int تبدیل کنیم. همانطور که می دانید ظرفیت یا بازه عددی نوع long از نوع int بیشتر است، در صورتی که مقدار متغیر long در محدوده بازه عددی نوع int باشد، عملیات تبدیل بدون مشکل انجام می شود، اما فرض کنیم مقدار متغیر long بیشتر از محدوده نوع int باشد، در زمان تبدیل مقداری که داخل متغیر int ما ریخته می شود کاملاً متفاوت با عددی است که در متغیر نوع long است، برای مثال:

long number1 = 5555555555555555555;
var number2 = (int)number1;    

در کد بالا، پس از اجرای برنامه، مقدار متغیر number2 برابر 623494941- خواهد بود، زیر زمان تبدیل از bit های سر ریز شده مقدار long صرفنظر شده و تنها تعداد bit هایی که در نوع int قابل قرار گرفتن هستند در متغیر نوع int قرار میگیرند، به این مشکل، Overflow شدن گفته می شود. برای حل این مشکل می توان از کلمه کلیدی checked استفاده کرد، شیوه استفاده از این کلمه کلیدی به صورت زیر است:

long number1 = 5555555555555555555;
checked
{
    var number2 = (int)number1;    
}

همانطور که مشاهده می کنید، در کد بالا عملیات تبدیل از long به int در بدنه checked نوشته شده است، با این کار، در صورتی که سرریز اتفاق بیافتد، با پیغام خطای OverflowException برخورد می کنیم، کلمه unchecked دقیقاً شرایطی است که ما بدون نوشتن checked عملیات تبدیل را انجام می دهیم. یعنی سرریز bit ها در زمان تبدیل، در صورت استفاده از کلمه unchecked در نظر گرفته نمی شود.

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

ما با دو روش تبدیل انواع داده به یکدیگر آشنا شدیم، روش های Implicit و روش های Explicit، روش دیگری وجود دارد که این روش استفاده از کلاس ها و متدهای Helper می باشد. ما تا اینجا با یکی از این متدها، یعنی متد Parse آشنا شدیم. کلاسی در کتابخانه دات نت وجود دارد با نام Convert که به ما امکان تبدیل انواع داده به یکدیگر را می دهد. برای مثال در کد زیر بوسیله کلاس Convert، نوع رشته به نوع عددی int تبدیل می شود:

string str = "12345";
var number = Convert.ToInt32(str);

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

کلمات کلیدی is و as

زمانی که از کلاس ها استفاده می کنیم، نیاز داریم که یک کلاس را به نوع دیگر تبدیل کنیم، برای مثال کد زیر را در نظر بگیرید:

public class Base
{
}

public class Derived1 : Base
{
}

public class Derived2 : Base
{        
}

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

Base child = new Derived1();
Derived2 d2 = (Derived2) child;

در کد بالا، عملیات تبدیل به صورت Explicit انجام شده، اما به دلیل اینکه Derived1 قابل تبدیل به Derived2 نیست، پیغام خطا دریافت خواهیم کرد، برای رفع این مشکل می توان از کلمه کلیدی as استفاده کرد. تبدیل بالا را با استفاده از کلمه کلیدی as انجام می دهیم:

Base child = new Derived1();
Derived2 d2 = child as Derived2;

در کد بالا، در صورتی که child قابل تبدیل به نوع Dervied2 باشد، عملیات تبدیل انجام شده و در غیر اینصورت داخل متغیر d2، مقدار null ریخته می شود.مقدار null، به معنی هیچ است و می توان برای متغیرهایی که از نوع Reference Type هستند، null را استفاده کرد.

اما کلمه کلیدی is چه کاری انجام می دهد؟ کلمه کلیدی is، بررسی می کند که مقدار متغیر سمت چپ، قابل تبدیل به نوع نوشته سمت راست می باشد یا نه، اگر قابل تبدیل باشد، مقدار true و در غیر اینصورت مقدار false را بر میگرداند. مثال:

Base child = new Derived1();
bool isDerived2 = child is Derived2;

در کد بالا، بعد از اجرا متغیر isDerived2 مقدار false خواهد داشت، زیرا مقدار child که از نوع Derived1 است قابل تبدیل به نوع Derived2 نمی باشد.در این قسمت به بررسی انواع تبدیل ها به یکدیگر پرداختیم و شیوه های مختلف آن را بررسی کردیم، با کلمات کلیدی checked، unchecked، is، as آشنا شدیم و همچنین نحوه استفاده از کلاس Convert را توضیح دادیم. در قسمت بعدی آموزش با نحوه تعریف Cast های دلخواه و همچنین Operator Overloading آشنا می شویم.

Operator Overloading

در این قسمت از آموزش با مبحث Operator Overloading و شیوه تعریف کردن Cast های دلخواه آشنا می شویم. ابتدا بهتره با مفهوم Operator Overloading آشنا شده و بعد به سراغ مثال های عملی بریم. Operator Overloading به معنی تعریف کردن نحوه عملکرد یک Operator بر روی یک شی می باشد. برای مثال، عملگر های جمع، تفریق و ... را در نظر بگیرید، زمانی که ما عملگر جمع را بر روی دو متغیر از نوع int اعمال می کنیم، این عملگر باعث محاسبه حاصل جمع دو عدد می شود، یعنی حال جمع دو عدد را برای ما بر میگرداند:

int n1 = 12;
int n2 = 20;
int result = n1 + n2;

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

public class ValueHolder
{
    public ValueHolder(int value)
    {
        Value = value;
    }

    public int Value { get; set; }
}

کلاس بالا یک عدد داخل خودش نگهداری می کند:

var holder1 = new ValueHolder(12);
var holder2 = new ValueHolder(20);

حال، اگر بخواهیم حاصل جمع دو عدد داده شده را حساب کنیم، باید به صورت زیر عمل کنیم:

var result = holder1.Value + holder2.Value;

اما در صورتی که کد بالا را به صورت زیر بنویسیم با پیغام خطا مواجه می شویم:

var result = hodler1 + holder2;

دلیل وقوع خطا، عدم تعریف شدن عملگر + برای کلاس ValueHolder است. برای رفع این مشکل می بایست از قابلیت Operator Overloading استفاده کنیم. برای کلاس ValueHolder عملگر جمع را به صورت زیر می توانیم تعریف کنیم:

public class ValueHolder
{
    public ValueHolder(int value)
    {
        Value = value;
    }

    public int Value { get; set; }

    public static ValueHolder operator +(ValueHolder holder1, ValueHolder holder2)
    {
        return new ValueHolder(holder1.Value + holder2.Value);
    }
}

در کد بالا، متدی تعریف کردیم از نوع static که نوع بازگشتی آن از نوع کلاس ValueHolder می باشد، اما نکته اصلی یکی نوشتن کلمه کلیدی operator بعد از نوع بازگشتی و بعدی قسمت نام متد است، در مثال بالا، به جای نوشتن نام متد، از کلمه کلیدی operator و سپس عملگری که قصد تعریف آن را داریم استفاده شده است.

در قسمت پارامترهای متد، ما دو پارامتر به عنوان ورودی دریافت می کنیم که برای پارامتر اولی، مقدار سمت چپ عملگر و پارامتر دوم قسمت سمت راست عملگر قرار می گیرد. در بدنه متد نیز، شی جدیدی از نوع ValueHolder ایجاد شده و به عنوان مقدار پارامتر Constructor، حاصل جمع مقادیر Value برای دو پارامتر ورودی دریافت می شود.

با تغییر کلاس ValueHolder به صورت بالا، مشکلی در اجرای عملگر + به صورت مستقیم برای اشیاء تعریف شده از نوع ValueHolder وجود نخواهد داشت. در ادامه عملگر تفریق، ضرب و تقسیم را نیز تعریف می کنیم:

public class ValueHolder
{
    public ValueHolder(int value)
    {
        Value = value;
    }

    public int Value { get; set; }

    public static ValueHolder operator +(ValueHolder holder1, ValueHolder holder2)
    {
        return new ValueHolder(holder1.Value + holder2.Value);
    }

    public static ValueHolder operator -(ValueHolder holder1, ValueHolder holder2)
    {
        return new ValueHolder(holder1.Value - holder2.Value);
    }

    public static ValueHolder operator *(ValueHolder holder1, ValueHolder holder2)
    {
        return new ValueHolder(holder1.Value * holder2.Value);
    }

    public static ValueHolder operator /(ValueHolder holder1, ValueHolder holder2)
    {
        return new ValueHolder(holder1.Value / holder2.Value);
    }
}

عملگرهایی که تا اینجا تعریف کردیم، عملگرهای Binary بودند و به همین دلیل دو پارامتر برای ورودی دریافت می کردند، در ادامه دو عملگر ++ و -- رو هم تعریف میکنیم، اما تفاوتی که این دو عملگر دارند، این عملگرها Unary هستند و به همین دلیل یک پارامتر به عنوان ورودی میگیرند، در ادامه تنها کد مربوط به تعریف این عملگرها آمده است:

public static ValueHolder operator ++(ValueHolder holder)
{
    return new ValueHolder(holder.Value++);
}

public static ValueHolder operator --(ValueHolder holder)
{
    return new ValueHolder(holder.Value++);
}

در زبان سی شارپ می توانیم عملگر های مقایسه ای را نیز تعریف کنیم. برای مثال کد زیر را در نظر بگیرید:

var holder1 = new ValueHolder(10);
var holder2 = new ValueHolder(12);
if(holder1 > holder2)
{
}
else
{
}

با اجرای کد بالا، باز هم پیغام خطا دریافت می کنیم، زیرا هیچ عملگر مقایسه ای برای کلاس ValueHolder تعریف نشده، عملگر مربوطه را به صورت زیر تعریف می کنیم:

public static bool operator <(ValueHolder holder1, ValueHolder holder2)
{
    return holder1.Value < holder2.Value;
}

اما به یک نکته باید توجه کنید، زمانی که عملگر های مقایسه ای را تعریف می کنید، می بایست عملگر مخالف آن نیز تعریف شود، برای مثال، برای عملگر > باید عملگر < را نیز تعریف کنیم، در غیر اینصورت با پیغام خطا مواجه می شویم:

public static bool operator <(ValueHolder holder1, ValueHolder holder2)
{
    return holder1.Value < holder2.Value;
}

public static bool operator >(ValueHolder holder1, ValueHolder holder2)
{
    return holder1.Value > holder2.Value;
}

عملگر مساوی و مخالف نیز به صورت زیر تعریف می شوند:

public static bool operator ==(ValueHolder holder1, ValueHolder holder2)
{
    return holder1.Value == holder2.Value;
}

public static bool operator !=(ValueHolder holder1, ValueHolder holder2)
{
    return holder1.Value != holder2.Value;
}

تعریف Cast های دلخواه

یکی از قابلیت های زبان سی شارپ، قابلیت تعریف مجدد Cast ها یا Cast Overloading می باشد. در قسمت قبل در مورد انواع Cast ها گفتیم که بر دو نوع implicit و explicit می باشند. در زبان سی شارپ، می توانیم هر دو نوع این cast را تعریف کنیم. برای مثال، کد زیر را نظر بگیرید:

var holder = new ValueHolder(12);

int holderValue = holder;

در کد بالا، قصد داریم شی ای از نوع ValueHolder را به صورت مستقیم داخل متغیری از نوع int قرار دهیم که این کد نیز پیغام خطا تولید می کند، به دلیل اینکه در کد بالا، عملیات تبدیل به صورت implicit انجام می شود، می بایست عملیات تبدیل از نوع ValueHolder به نوع int را به صورت implicit داخل کلاس ValueHolder تعریف کنیم:

public static implicit operator int(ValueHolder holder)
{
    return holder.Value;
}

همانطور که در کد بالا مشاهده می کنید، ابتدا باید یک متد static تعریف شود، اما بعد از کلمه static باید نوع تبدیلی که قصد داریم تعریف کنیم را مشخص کنیم که در کد بالا، تبدیل تعریف شده از نوع implicit می باشد. در قسمت نام متد، نوعی که قصد داریم تبدیل به آن انجام شود را می نویسیم و به عنوان پارامتر ورودی، نوعی که قصد تبدیل از آن را داریم، یعنی در کد بالا عملیات Cast برای تبدیل implicit از نوع ValueHolder به نوع int را تعریف کردیم. در ادامه کد زیر را در نظر بگیرید:

ValueHolder holder = 12;

در کد بالا، عملیات تبدیل برعکس مثال قبلی است، یعنی عملیات تبدیل از نوع int به نوع ValueHolder انجام میشه که برای اینکار، به صورت زیر می توان عملیات تبدیل را تعریف کرد:

public static implicit operator ValueHolder(int value)
{
    return new ValueHolder(value);
}

علاوه بر تعریف cast به صورت implicit می توان، تبدیل ها را به صورت explicit نیز تعریف کرد. کلاس زیر را در نظر بگیرید:

public class ValueHolder2
{
    public ValueHolder2(int value)
    {
        Value = value;
    }

    public int Value { get; set; }
}

کلاس ValueHolder2 مانند کلاس ValueHolder تعریف شده و برای مثال می خواهیم از آن استفاده کنیم. در ادامه کد زیر عملیات تبدیل از ValueHolder2 به ValueHolder به صورت explicit انجام می شود:

ValueHolder2 holder2 = new ValueHolder2(12);
var holder = (ValueHolder) holder2;

برای اینکه در کد بالا، پیغام خطا دریافت نکنیم، باید عملیات تبدیل explicit از نوع ValueHolder2 به ValueHolder را در کلاس ValueHolder به صورت زیر تعریف کنیم:

public static explicit operator ValueHolder(ValueHolder2 holder2)
{
    return new ValueHolder(holder2.Value);
}

تنها تفاوتی که وجود دارد، به جای کلمه کلیدی implicit در تعریف متد، از کلمه کلیدی explicit استفاده شده است. در این بخش با مفاهیم operator overloading و تعریف cast ها به دو صورت implicit و explicit آشنا شدیم.در قسمت بعدی سری آموزشی، با مفهوم boxing و unboxing در زمان تبدیل کردن نوع ها آشنا خواهیم شد.

Boxing و Unboxing

 همانطور که در قسمت قبلی گفتیم، در این قسمت قصد داریم تا با مفاهیم Boxing و Unboxing آشنا شویم که مربوط به بحث تبدیل نوع ها به یکدیگر می شود.

Boxing چیست؟

همانطور که در قسمت های قبلی گفتیم، سی شارپ یک زبان سئ گرا است، یعنی ما می توانیم بوسیله کلاس ها شی های مورد نظر خود را ایجاد کنیم. بوسیله کلاس ها نوع های داده ارجاعی یا Reference Type ایجاد می شوند. همینطور با struct ها که وظیفه ایجاد Value Type ها را دارند آشنا شدیم.

اما کلیه این نوع های داده از نوع داده object ارث بری می کنند، یعنی فرزند نوع داده object هستند که به صورت پیش فرض در کتابخانه دات نت تعریف شده اند. اگر توضیحی بخواهیم برای عملیات Boxing ارائه دهیم، عملیات تبدیل یک Value Type به نوع داده object را boxing می گویند. مثال:

int number = 12;
object boxed = number;


در کد بالا، ابتدا یک متغیر از نوع int تعریف کردیم و سپس این متغیر را در متغیر دیگری با نام boxed و از نوع object قرار دادیم. به این عملیات boxing گفته می شود. اگر بخواهیم نگاه ریز تری به این پروسه داشته باشیم، عملیات boxing مقدار یک value type که در حافظه stack دخیره شده را داخل یک object و در حافظه heap نگهداری می کند. عملیات boxing را می توان به صورت implicit که در بالا مثال زدیم یا explicit انجام داد:

int number = 12;
object boxed = (object)number;

Unboxing چیست؟

عملیات Unboxing، دقیقاً عکس عملیات boxing است، یعنی ما یک متغیر از نوع object را به Value Type تبدیل کنیم:

object boxed = 12;
int unboxedNumber = (int)boxed;


در کد بالا، ابتدا عدد 12 داخل متغیر boxed از نوع object قرار گرفته و در خط بعدی عملیات unboxing انجام می شود، یعنی عدد 12 که از نوع int است و در متغیری از نوع object و در حافظه heap ذخیره شده، به صورت explicit تبدیل به نوع int شده و در متغیری از همین نوع قرار داده می شود. به این نکته دقت کنید که عملیات boxing و unboxing بر روی performance برنامه تاثیر می گذارد و بهتر است تا حد امکان از انجام این گونه تبدیل ها در برنامه خودداری کرد. در قسمت بعدی آموزش، با مبحث Generic آشنا خواهیم شد. 

جنریک ها (Generics)

در ادامه آموزش برنامه نویسی شی گرا در سی شارپ، با مبحث Generics آشنا خواهیم شد. همزمان با بزرگ تر شدن پروژه ای که در حال کار کردن بر روی آن هستید، باید تکنیک هایی را در کد نویسی استفاده کنید که به شما اجازه استفاده مجدد از کدهای نوشته شده را می دهند.

یکی از روش های استفاده مجدد از کدهای موجود در موقعیت های مختلف استفاده از قابلیت Generic ها می باشد. این قابلیت به شما اجازه می دهد تا نوع Data Type فیلد ها، خصوصیات و ... برای کلاس ها را زمان ساختن شی از روی کلاس مشخص کنید.

دوستانی که با زبان ++C آشنا هستند، قابلیت Generics در زبان سی شارپ، معادل قابلیت Template ها در زبان ++C است. قابلیت Generics از نسخه 2 به زبان سی شارپ اضافه شد.ابتدا بیایید ببینیم دنیای بدون Generic ها در زبان سی شارپ چگونه است؟ فرض کنید کلاسی تعریف می کنیم که یک مقدار از نوع int رو داخل خودش نگهداری می کنه:

public class ValueHolder
{
    public int Value { get; set; }
}

در قدم بعدی میخواهیم کلاس دیگری برای نگهداری مقادیر از نوع string ایجاد کنیم:

public class ValueHolder
{
    public string Value { get; set; }
}

اگر بخواهیم برای هر نوع داده یک کلاس جداگانه ایجاد کنیم، کار جالبی نیست، برای حل این مشکل دو راه حل وجود داره، یکی می توانیم کلاس بالا رو تغییر دهیم و نوع داده خاصیت Value رو از نوع object در نظر بگیریم تا هر مقداری داخلش قرار بگیره:

public class ValueHolder
{
    public object Value { get; set; }
}

اما کد بالا مشکلاتی دارد، یکی اینکه با قرار دادن مقادیر از نوع Value Type داخل خصوصیت Value، عملیات Boxing و زمان خواندن مقدار عملیات UnBoxing رخ میدهد که در قسمت قبل گفتیم این دو عملیات باعث کاهش کارآیی برنامه می شوند.

همچنین می توان هر مقداری را داخل Value قرار داد که این موضوع مخالف بحث Type Safety می باشد. برای حل این مشکلات، قابلیت Generics به زبان سی شارپ اضافه شد. مثال بالا رو با Generic ها پیاده سازی می کنیم و سپس توضیحات لازم رو خواهیم داد:

public class ValueHolder<T>
{
    public T Value { get; set; }
}

همانطوری که ملاحظه می کنید، در مقابل نام کلاس در میان <>، کاراکتر T نوشته شده، در حقیقت کاراکتر T در اینجا، بیانگر یک جایگاه برای یک متغیر می باشد که در داخل کلاس، به جای نوع داده بای خصوصیت کاراکتر T نوشته شده، در حقیقت ما در کد بالا گفتیم که کلاس ValueHolder یک جایگاه برای نوع داده در نظر می گیرد که ما نام T را برای آن انتخاب کردیم (انتخاب این نام کاملاً اختیاری است.

اما بهتر است هر نامی که انتخاب می شود ابتدای آن با کاراکتر T که مخفف Type است شروع شود)، سپس در داخل کلاس و قسمت هایی که قصد داریم نوع داده را زمان ساخت کلاس مشخص کنیم، به جای خود Data Type، کاراکتر T را می نویسیم که در کد بالا برای خصوصیت Value کاراکتر T نوشته شده. در قدم بعدی می بایست از روی کلاس ValueHolder یک شی بسازیم، اما ساختن شی در اینجا با حالت عادی تفاوت دارد، برای ساختن شی از روی کلاس های Generic به صورت زیر عمل می کنیم:

ValueHolder<int> value = new ValueHolder<int>();
value.Value = 12;

همانطور که مشاهده می کنید، در مقابل نام کلاس در زمان ساخت شی، داخل <> نام Data Type ای را که می خواهیم جایگزین کاراکتر T شود می نویسیم، در کد بالا، خصوصیت Value از نوع int تعریف می شود. علاوه بر نوع داده int، می توانیم هر نوع داده ای را زمان ساخت شی مشخص کنیم:

ValueHolder<string> stringHolder = new ValueHolder<string>();
stringHolder.Value = "ITPRO.IR";

دقت کنید، زمانی که نوع T را برای مثال، string در نظر گرفتید، دیگر نمی توانید داخل خصوصیت Value، مقادیری غیر از string قرار دهید، مانند اینکه کلاس ValueHolder نوع داده خصوصیت Value را از نوع string در نظر گرفته است.شما می توانید علاوه بر یک جایگاه برای نوع داده Generic، چندین جایگاه تعریف کنیم. مثال:

public class MultipleGeneric<T1, T2, T3>
{
    public T1 Value1 { get; set; }
    public T2 Value2 { get; set; }
    public T3 Value3 { get; set; }
}

و زمان ساختن شی از روی کلاس، به صورت زیر عمل می کنید:

MultipleGeneric<int, string, decimal> values = new MultipleGeneric<int, string, decimal>();
values.Value1 = 12;
values.Value2 = "ITPRO.IR";
values.Value3 = 2.5m;

شما علاوه بر اینکه می توانید کلاس ها را به صورت Generic تعریف کنید، امکان تعریف متدها را نیز به صورت Generic دارید. برای مثال، در کد زیر ما یک متد Generic تعریف کردیم که نوع پارامتر های ورودی در زمان صدا زدن متد مشخص می شوند:

public TOut DoSomething<TOut, TParam1, TParam2>(TParam1 param1, TParam2 param2)
{
    return default(TOut);
}

در کد بالا یه جایگاه TOut و TParam1 و TParam2 در نظر گرفتیم و در تعریف متد از آنها استفاده کردیم. حالا زمان فراخوانی متد به صورت زیر عمل می کنیم:

string result = DoSomething<string, int, int>(12, 20);

اما به یک نکته توجه کنید، در داخل بدنه متد، از کلمه کلیدی default مانند یک متد استفاده شده که داخل پرانتز، نام جایگاه TOut را نوشتیم، کلمه کلیدی TOut، مقدار پیش فرض را برای نوع داده مشخص شده برای جایگاه Generic بر می گرداند.

در کد بالا، نوع داده string برای TOut مشخص شده و عبارت default مقدار پیش فرض نوع داده string را بر میگرداند.برای استفاده از نوع داده generic برای متدها در کلاس ها، می توانید از جایگاه های تعریف شده برای کلاس نیز استفاده بکنید:

public class GenericType<T>
{
    public void DoSomething(T param)
    {
            
    }
}

در کد بالا، همانطور که مشاهده می کنید، برای پارامتر ورودی متد، از جایگاه T که در کلاس تعریف کردیم استفاده کردیم. در این قسمت با مباحث اولیه generic ها آشنا شدیم، در قسمت بعدی یکسری نکات کوچک راجع به generic ها را بررسی کرده، با کلاس List آشنا شده و به بررسی Constraint ها در Generics خواهیم پرداخت. ITPRO باشید و به دیگران هم توصیه کنید که ITPRO باشند.

List و Dictionary

در ادامه مباحث آموزشی زبان سی شارپ، در این قسمت به بررسی نکات تکمیلی Generic ها پرداخته و با یکسری از کلاس های Generic موجود در کتابخانه دات نت آشنا می شویم. در ابتدا به بررسی Constraint ها خواهیم پرداخت که به ما اجازه اعمال محدودیت در نوع داده انتخابی برای جایگاه های Generic را می دهند. برای مثال، مد زیر را در نظر بگیرید:

public class GenericType<T>
{
    public T Property { get; set; }
}

گاهی وقت ها، نیاز داریم که زمان ساخت شی، T در GenericType، تنها نوع داده ای که از نوع Value Type می باشد را قبول کند یا فقط Reference Type را قبول کند. برای اینکار، می توانیم از Constraint ها استفاده کنیم. برای استفاده از Constraint ها از کلمه کلیدی where استفاده می کنیم. انواع مختلفی از Constraint ها وجود دارد که در ادامه به بررسی آن ها خواهیم پرداخت.

1. نوع های داده مشتق شده از یک کلاس: بوسیله این قابلیت، می توان انتخاب کرد که یک پارامتر Generic، می بایست حتماً از یک کلاس مشخص مشتق شده باشد، به مثال زیر توجه کنید:

public class GenericType<T> where T : MyClass
{
    public T Property { get; set; }
}

با استفاده از کد بالا، می گوییم که تنها کلاس هایی را می توان به عنوان پارامتر جنریک T انتخاب کرد که از کلاس MyClass مشتق شده باشند. دقت کنید، ابتدا کلمه کلیدی where، سپس نام پارامتر جنریک و بعد از علامت کاراکتر :، نام کلاس نوشته شده است. دقت کنید که تنها می توان یک کلاس را برای این Constraint مشخص کرد.

2. نوع های داده ای که یک interface را پیاده سازی کرده اند: با کمک این Constraint، تنها نوع های داده ای را می توان برای یک Constraint انتخاب کرد که یک interface خاص را پیاده سازی کرده باشد:

public class GenericType<T> where T : IInterface1, IInterface2
{
    public T Property { get; set; }
}

شما می توانید یک یا چند interface را برای این Constraint انتخاب کنید. همچنین می توانید به صورت ترکیبی از نام یک کلاس و نام یک یا چند interface استفاده کنید:

public class GenericType<T> where T : MyClass, IInterface1, IInterface2
{
    public T Property { get; set; }
}

مزیت استفاده از این نوع Constraint ها چیست؟ زمانی که شما یک کلاس یا interface را به عنوان Constraint یک پارامتر Generic انتخاب می کنید، می توانید به اعضاء آن کلاس یا Interface ها در داخل آن کلاس دسترسی داشته باشید. برای مثال، نمونه کد زیر را در نظر بگیرید:

public interface IDiscount
{
    double CalculateTax();
}

public class Product : IDiscount
{
    public string Name { get; set; }
    public int Price { get; set; }

    public virtual double CalculateTax()
    {
        return 0;
    }
}

public class ProductManager<TProduct> where TProduct : Product
{
    public double CalculatePrice(TProduct product)
    {
        return product.Price - product.CalculateTax();
    }
}

به دلیل اینکه پارامتر TProduct از نوع IProduct و Product مشخص شده، می توان داخل کلاس ProductManager، به راحتی به خصوصیت Price و متد CalculateTax دسترسی داشت.

3. نوع های داده Value Type: در صورتی که از کلمه کلیدی struct برای Constraint یک پارامتر استفاده شود، تنها می توان از نوع های داده Value Type برای پارامتر ورودی جنریک استفاده کرد و در صورت استفاده از نوع های داده Reference Type، با پیغام خطا مواجه می شویم:

public class ValueHolder<T> where T : struct
{
    public T Value { get; set; }
}

4. نوع های داده Reference Type: در صورت نوشتن کلمه کلیدی class برای Constraint یک پارامتر، تنها می توان از نوع های داده Reference Type برای پارامتر ورودی جنریک استفاده کرد و بر عکس struct، در صورت استفاده از Value Type ها مانند int و float، پیغام خطا دریافت می کنیم:

public class ValueHolder<T> where T : class 
{
    public T Value { get; set; }
}

دقت کنید، امکان استفاده همزمان از Constraint های struct و class وجود ندارد، همچنین نمی توانید به صورت ترکیبی از struct یا class و مشخص کردن کلاس پایه برای پارامتر Generic استفاده کنید، اما می توانید به صورت ترکیبی از struct یا class و interface ها استفاده کنید. اما باید به اولویت ها دقت کنید، باید ابتدا کلمه کلیدی struct یا class را بنویسید و سپس نام interface ها را بنویسید:

public class ValueHolder<T> where T : struct, IInterface1, IInterface2
{
    public T Value { get; set; }
}

5. کلاس هایی که حتماً default constructor یا سازنده پیش فرض دارند:، در صورتی که به عنوان Constraint عبارت ()new را بنویسید، تنها از کلاس هایی می توانید استفاده کنید که سازنده پیش فرض داشته باشند. این Constraint می بایست، حتماً به عنوان آخرین Constraint نوشته شود:

public class ProductManager<TProduct> where TProduct : Product,new()
{
    public TProduct CreateProduct()
    {
        return new TProduct();
    }
}

در صورتی که در کد بالا، ()new را به عنوان Constraint ننویسیم، عبارت return new Product تولید خطا می کند، زیرا تنها در صورتی می توان اقدام به ساخت شی کرد که نوع داده مشخص شده برای پارامتر Generic، دارای سازنده پیش فرض باشد.

6. مشخص کردن Constraint ها برای چندین پارامتر Generic: فرض کنید کلاسی که نوشتیم چندین پارامتر Generic دارد، برای مشخص کردن Constraint برای چندین پارامتر به صورت زیر عمل می کنیم:

public class GenericType<T1, T2> where T1 : class,new() where T2 : IEnumerable,new()
{
        
}

برای خوانایی بهتر کد بهتر است Constraint هر پارامتر در یک خط مجزا نوشته شود:

public class GenericType<T1, T2> 
    where T1 : class,new() 
    where T2 : IEnumerable,new()
{
        
}

آشنایی با کلاس جنریک List

در کتابخانه DotNet، تعداد زیادی از کلاس های Generic وجود دارند که هر یک کاربرد های خاص خود را دارند، در این بخش، ابتدا می خواهیم با کلاس List که یک کلاس Generic است آشنا شویم، اما قبل از آن بهتر است که با کلاسی با نام ArrayList آشنا شویم، کلاس ArrayList، کلاسی است غیر جنریک که به ما اجازه اضافه کردن انواع Data Type های مختلف را میدهد، این کلاس در فضای نام System.Collections قرار دارد. شیوه استفاده از این کلاس به صورت زیر است:

System.Collections.ArrayList list = new ArrayList();
list.Add(12);
list.Add("Hossein");
list.Add(4.5m);

Console.WriteLine((int) list[0]);
Console.WriteLine((string)list[2]);

Console.ReadKey();

همانطور که می بینید، مانند آرایه ها، می توان داخل یک ArrayList با کمک متد Add مقادیر مختلف را اضافه کرد و با Index مربوطه مقداری را از خانه مورد نظر خواند. اما مشکلی که در اینجا وجود دارد، پارامتر ورودی متد Add، از نوع Object است و زمان استفاده از داده های Value Type، عملیات Boxing و UnBoxing رخ می دهد، همچنین مکانیزم Type Safety در این کلاس وجود ندارد

زیرا می توان هر مقداری را داخل آن اضافه کرد و همچنین زمان خواندن مقدار می بایست عملیات Casting را انجام دهیم. در این لحظه کلاس List که یک کلاس Generic است و در فضای نام System.Collections.Generic قرار دارد وارد صحنه می شود. مکانیزم استفاده از این کلاس به صورت زیر است:

System.Collections.Generic.List<int> numbers = new List<int>();
numbers.Add(12);
numbers.Add(21);

Console.WriteLine(numbers[0]);

Console.ReadKey();

همانطور که مشاهده می کنید، زمان ایجاد شی در کلاس List، ابتدا به عنوان پارامتر generic، نوع آیتم های لیست را مشخص می کنیم، حال متد Add تنها پارامترهای وردی از نوع int را قبول می کند و همچنین زمانی که مقداری را از یکی از خانه های List میخوانیم، مقدار برگشتی از نوع int خواهد بود. مشاهده می کنید که تمامی مشکلات موجود در کلاس ArrayList، بوسیله کلاس List برطرف شده اند. همچنین می توان بوسیله دستور foreach خانه های لیست را پیمایش کرد:

foreach (var number in numbers)
{
    Console.WriteLine(number);
}

برای حذف یک مقدار از لیست می توانید از متد Remove استفاده کنید:

numbers.Remove(12);

برای درج یک مقدار در لیست از متد Insert استفاده کنید که پارامتر اول ایندکس درج مقدار و پارامتر دوم مقدار مورد نظر برای درج می باشد:

numbers.Insert(1, 34);

بوسیله خاصیت Count می توانید تعداد آیتم های داخل لیست را بدست آورید:

var items = numbers.Count;

در صورتی که بخواهید مقداری را از یک ایندکس حذف کنید با دستور RemoveAt این کار امکان پذیر است، کافیست به عنوان پارامتر، ایندکس مورد نظر را مشخص کنید:

var items = numbers.RemoveAt(12);

دستورات دیگری نیز برای لیست ها وجود دارد که بررسی این دستورات را به عهده دوستان عزیز میگذارم.

کلاس Dictionary

این کلاس نیز یک کلاس Generic است و به شما این امکان را می دهد تا مقداری را به عمراه یک کلید در لیست ذخیره کنید و دسترسی به مقادیر بر اساس کلید خواهد بود. فرض کنید که لیستی از دانشجویان را می خواهیم ذخیره کنیم، ذخیره اطلاعات دانشجویان بر اساس شماره دانشجویی بوده و بعد می توانیم با کمک شماره دانشجویی به عنوان کلید، اطلاعات دانشجو را بازیابی کنیم، ابتدا کلاسی برای دانشجو تعریف می کنیم:

public class Student
{
    public Student(string studentId, string firstName, string lastName, byte age)
    {
        StudentId = studentId;
        FirstName = firstName;
        LastName = lastName;
        Age = age;
    }

    public string StudentId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public byte Age { get; set; }
}

در قدم بعدی، می توانیم با تعریف یک Dictionary اطلاعات دانشجو را به صورت زیر ذخیره کنیم:

Dictionary<string, Student> students = new Dictionary<string, Student>();
students.Add("10001", new Student("10001", "Hossein", "Ahmadi", 30));
students.Add("10002", new Student("10002", "Mohammad", "Nasiri", 30));

در خط اول یک شی با نام Student از روی کلاس Dictionary ایجاد کردیم، همانطور که مشاهده می کنید، کلاس Dictionary دو پارامتر جنریک دارد، اولی برای کلید و دومی برای مقدار که در کد بالا، نوع string را برای کلید و نوع Student را برای مقدار مشخص کردیم.

در قسمت های بعدی بوسیله دستور Add، با شناسه های 10001 و 10002 دو دانشجو به لیست اضافه شدند. پارامتر اول دستور Add، کلید و پارامتر دوم مقدار را دریافت می کند. در صورتی که به عنوان کلید، مقداری تکراری پاس داده شود، پیغام خطا صادر خواهد شد. برای دسترسی به یک مقدار، می توان از کلید برای دسترسی استفاده کرد، برای مثال، می خواهیم اطلاعات دانشجویی با کد 10002 را بگیریم، به صورت زیر عمل خواهیم کرد:

var student = students["10002"];

کد بالا، شی ای از نوع student که با کد 10002 بوسیله دستور Add اضافه شده است را برای ما بر میگرداند. در صورتی که کلید 10002 در دیکشنری وجود نداشته باشد، پیغام خطا صادر خواهد شد.دوستان می توانند به عنوان تمرین برای برای آشنایی بیشتر با مبحث Generic ها و دیکشنری ها، برنامه ای را بنویسند که اطلاعات مخاطبین را بر اساس شماره موبایل در لیستی ذخیره کند.

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

مدیریت خطاها

در قسمت پایانی آموزش برنامه نویسی به زبان سی شارپ، با مبحث Exception ها و مدیریت استثناها در زبان سی شارپ آشنا می شویم، بروز خطا در برنامه امری اجتناب نا پذیر است و یک برنامه نویس موظف است که خطاها را به درستی در برنامه ها مدیریت کرده و زمان بروز خطا، پیغامی مناسب به کاربر نمایش دهد. در زبان سی شارپ، به خطاها Exception یا استثنا می گویند. در برنامه های کامپیوتری خطاها بر دو دسته اند:

  1. خطاهای نحوی یا Syntax Errors: این خطاها به دلیل نوشتن اشتباه دستورات ایجاد شده و معمولاً زمان کامپایل برنامه قابل رفع هستند.
  2. خطاهای منطقی یا Logical Errors: این خطاها به دلیل انجام اشتباه یک عملیات یا ورود اشتباه یک دستور در زمان اجرا اتفاق می افتند.

بیشترین تمرکز ما برای مدیریت خطاها، روی دسته دوم خطاهاست، برای شروع کد زیر را در نظر بگیرید:

var firstNumber = int.Parse(Console.ReadLine());
var secondNumber = int.Parse(Console.ReadLine());

Console.WriteLine(firstNumber/secondNumber);

کد بالا، دو عدد را از ورودی خوانده و حاصل تقسیم این دو عدد را در خروجی چاپ می کند، اما فرض کنید مقدار عدد دوم صفر وارد شود، امکان تقسیم اعداد بر عدد صفر وجود ندارد و در صورت ورود عدد صفر به عنوان ورودی دوم، با پیغام خطای DivideByZero مواجه می شویم. برای رفع ایین مشکل، می بایست از مکانیزم کنترل Exception ها استفاده کنیم. در زبان سی شارپ این مکانیزم، با ساختار try..catch انجام می شود:

try
{
    // place your code here
}
catch([ExceptionType])
{
}
catch([ExceptionType])
{
}
finally
{
}

قسمتی از کد که احتمال وقوع خطا در آن وجود دارد را باید داخل بدنه try بنویسید، با این کار، در صورت وقوع خطا در کدی که داخل بدنه try نوشته شده، قسمت catch اجرا می شود. اما نحوه اجرای قسمت catch به چه صورت است؟ در کتابخانه، برای هر نوع خطا، یک کلاس تعریف شده، برای مثال، برای خطای تقسیم بر صفر کلاسی با نام DivideByZeroException وجود دارد.

کلاً تمامی کلاس های مرتبط با خطا های مختلف با کلمه Exception تمام می شوند، کلاس هایی مانند InvalidOperationException یا StackOverlowException، تمامی این کلاس از کلاس پایه ای با نام SystemException مشتق شده اند که خود کلاس SysteException از کلاس Exception مشتق شده است.

در حقیقت کلاس Exception کلاس پایه ای برای کلیه خطاهای سیستم می باشد. حال شما بر اساس نوع خطایی که قصد مدیریت آن را دارید، نام Data Type آن را در مقابل catch می نویسید، برای مثال، اگر تصمیم دارید خطای تقسیم بر صفر را مدیریت کنید، ساختار try..catch به صورت زیر نوشته می شود:

try
{

}
catch (DivideByZeroException)
{                
}

در صورتی که خطای تقسیم بر صفر در سیستم رخ دهد، بدنه catch اجرا خواهد شد، کد ابتدای آموزش را به صورت زیر تغییر می دهیم:

try
{
    var firstNumber = int.Parse(Console.ReadLine());
    var secondNumber = int.Parse(Console.ReadLine());

    Console.WriteLine(firstNumber / secondNumber);
}
catch (DivideByZeroException)
{
    Console.WriteLine("Second number must be greater than zero!");
}

Console.ReadKey();

با اجرای کد بالا، در صورتی که عدد دوم را صفر وارد کنیم، به جای متوقف شدن برنامه و بروز خطا، پیغام مناسب برای کاربر نمایش داده می شود. اما نکته ای که وجود دارد، شما می توانید بیشتر از یک بدنه catch داشته باشید. برای مثال، در کد بالا در صورتی که شما به جای عدد کاراکتر a را وارد کنید، با خطای FormatException مواجه می شوید، برای مدیریت این خطا کافیست کد بالا را به صورت زیر تغییر دهید:

try
{
    var firstNumber = int.Parse(Console.ReadLine());
    var secondNumber = int.Parse(Console.ReadLine());

    Console.WriteLine(firstNumber/secondNumber);
}
catch (DivideByZeroException)
{
    Console.WriteLine("Second number must be greater than zero!");
}
catch (FormatException)
{
    Console.WriteLine("Invalid input format!");
}

با کد بالا، در صورت اشتباه در ورودی، خطا مدیریت شده و پیغام مناسب نمایش داده می شود. قسمت دیگر ساختار try..catch، بدنه finally می باشد، این قسمت از ساختار، در هر صورت اجرا خواهد شد، چه خطا رخ بدهد، چه خطا رخ ندهد، کد بالا را به صورت زیر تغییر می دهیم:

try
{
    var firstNumber = int.Parse(Console.ReadLine());
    var secondNumber = int.Parse(Console.ReadLine());

    Console.WriteLine(firstNumber/secondNumber);
}
catch (DivideByZeroException)
{
    Console.WriteLine("Second number must be greater than zero!");
}
catch (FormatException)
{
    Console.WriteLine("Invalid input format!");
}
finally
{
    Console.WriteLine("Thank you for choosing ITPRO.IR!");
}

با کد بالا، پیغام داخل بدنه finally در هر صورت در خروجی چاپ خواهد شد، استفاده از بدنه finally بیشتر زمانی کاربرد دارد که شما می خواهید بعد از اتمام عملیات، اقدام به پاک سازی حافظه و آزاد سازی منابع کنید.امکان مدیریت خطاها به صورت عمومی نیز وجود دارد، گفتیم کلیه کلاس های مربوط به خطاها از کلاس Exception مشتق شده اند، برای مدیریت عمومی خطاها، کافیست در بدنه catch به جای یک نوع مشخص از خطا، نام Exception را بنویسیم:

try
{
    var firstNumber = int.Parse(Console.ReadLine());
    var secondNumber = int.Parse(Console.ReadLine());

    Console.WriteLine(firstNumber/secondNumber);
}
catch (Exception)
{
    Console.WriteLine("Oops! Your input stopped me!");
}

با کد بالا دیگر نوع خطا تفاوتی نمی کند، با وقوع هر خطایی، پیغام داخل بدنه catch در خروجی چاپ می شود.همانطور که گفتیم، کلاس Exception، کلاس پایه ای برای کلیه خطاهای سیستم می باشند، این کلاس حاوی یک سری خصوصیات است که اطلاعات دقیق تری به ما می دهند.

برای دسترسی به اطلاعات خطا، به جای نوشتن تنها نام Exception در مقابل بدنه catch، داخل پرانتز Exception را به صورت یک پارامتر تعریف می کنیم تا بتوانیم به اطلاعات آن داخل بدنه catch دسترسی داشته باشیم:

try
{
    var firstNumber = int.Parse(Console.ReadLine());
    var secondNumber = int.Parse(Console.ReadLine());

    Console.WriteLine(firstNumber/secondNumber);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
    Console.WriteLine(ex.StackTrace);
    Console.WriteLine(ex.InnerException.ToString());
}

همانطور که مشاهده می کنید کلاس Exception شامل یکسری خصوصیات است، در زیر به بررسی مهمترین خصوصیات کلاس Exception می پردازیم:

  1. خصوصیت Message: این خصوصیت حاوی پیغام خطای تولید شده توسط برنامه است
  2. خصوصیت StackTrace: این خصوصیت شامل جزئیاتی از خطای رخ داده شده است، ممکن است خطاها در هر قسمت از برنامه رخ دهند، بوسیله stack trace قابلیت ردیابی خطا و اینکه وقوع این خطا بعد از فراخوانی کدام متدها و در کدام فایل و خط از برنامه اتفاق افتاده را خواهیم داشت.
  3. خصوصیت InnerException: گاهی اوقات، یک خطا می تواند شامل یک خطای درونی باشد، برای مثال، شما زمانی که با بانک SQL Server کار می کنید، ممکن از زمان کار با بانک، خطایی دریافت کنید، خصوصیت Inner Exception اطلاعات جزئی تری از خطاهای اتفاق افتاده به شما می دهد. این خصوصیت از نوع Exception بوده و اطلاعات خطاهای درونی یک خطا را به ما می دهد.

خطاهای دلخواه و دستور throw

در زبان سی شارپ امکان تعریف خطاهای دلخواه وجود دارد، همانطور که در قسمت قبلی گفتیم، هر خطا از کلاس SystemException مشتق شده که خود SystemException از کلاس Exception مشتق می شود، در دات نت کلاس دیگری وجود دارد به نام ApplicationException که از کلاس Exception مشتق شده و ما می توانیم با ایجاد کلاس هایی که از ApplicationException مشتق شده اند، خطاهای دلخواه خود را تعریف کنیم. برای مثال، کد زیر را در نظر بگیرید:

public class StudentManager
{
    public void RegisterStudent(string firstName, string lastName, byte age)
    {
        // add student to database
    }
}

فرض کنید، می خواهیم از ثبت نام افرادی که سنشان کمتر از 18 سال است جلوگیری کنیم. برای اینکار می توانیم به این صورت عمل کنیم، ابتدا یک کلاس برای خطای سن کمتر از 18 سال تعریف می کنیم:

public class InvalidAgeException : ApplicationException
{
    public InvalidAgeException(string message) : base(message)
    {
    }
}

دقت کنید به عنوان پارامتر ورودی سازنده، پیغام خطا را دریافت و به سازنده کلاس پدر ارسال می کنیم. حال باید از این خطا در کلاس StudentManager استفاده کنیم، یعنی با ورود سن کمتر از 18 سال، خطای InvalidAgeException صادر شود، در زبان سی شارپ، برای صدور خطا از دستور throw استفاده می کنیم:

public class StudentManager

{

public void RegisterStudent(string firstName, string lastName, byte age)

{

if (age < 18)

throw new InvalidAgeException("Age must be greater that 18!");

// add student to database

}

}

در مرحله بعد، با اجرای دستور زیر خطا دریافت خواهیم کرد:

new StudentManager().RegisterStudent("Hossein", "Ahmadi", 17);

حال می توانیم با ساختار try..catch این خطا را مدیریت کنیم:

try
{

}
catch (InvalidAgeException ex)
{
    Console.WriteLine(ex.ToString());
    throw;
}

همانطور که مشاهده می کنید در ساختار بالا، خطای InvalidAgeException مدیریت شده و پیغام خطا در خروجی چاپ می شود.در این قسمت از آموزش ما با مفهوم Exception ها و نحوه مدیریت خطاها در زبان سی شارپ آشنا شدیم، همچنین با تعریف و تولید خطاهای دلخواه (دستور throw) آشنا شدیم. امیدوارم که این مجموعه بتواند سهم کوچکی در یادگیری دوستان داشته باشد.


حسین احمدی
حسین احمدی

بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

حسین احمدی ، بنیانگذار TOSINSO ، توسعه دهنده وب و برنامه نویس ، بیش از 12 سال سابقه فعالیت حرفه ای در سطح کلان ، مشاور ، مدیر پروژه و مدرس نهادهای مالی و اعتباری ، تخصص در پلتفرم دات نت و زبان سی شارپ ، طراحی و توسعه وب ، امنیت نرم افزار ، تحلیل سیستم های اطلاعاتی و داده کاوی ...

22 خرداد 1394 این مطلب را ارسال کرده

نظرات