مقاله: آموزش برنامه نویسی شئ گرا در زبان سی شارپ - قسمت بیست و یکم :: Reference Types و Value Types
جمعه, ۱۵ ارديبهشت ۱۳۹۶، ۰۳:۳۵ ب.ظ
همانطور که در قسمت های اولیه آموزش گفتیم، زبان سی شارپ یک زبان Strongly
Typed است. یعنی تمامی نوع های داده در آن مشخص می باشند. اما کلیه نوع های
داده در سی شارپ به دو دسته تقسیم می شوند:
- Reference Types
- Value Types
تفاوت این دو نوع داده، در شیوه برخورد زبان سی شارپ و شیوه تخصیص حافظه و مدیریت آنها می باشد. در این قسمت به تفصیل در مورد این دو نوع داده صحبت کرده و در انتها با struct ها در زبان سی شارپ آشنا خواهیم شد.
در بستر دات نت، متغیرها هنگام ایجاد در دو حافظه مختلف ایجاد می شوند:
- حافظه stack
- حافظه heap
این دو حافظه از نظر میزان سرعت دسترسی و همچنین شیوه مدیریت آنها در زبان سی شارپ با یکدگیر تفاوت دارند. برای آشنایی با این دو حافظه بهتر است به بررسی Reference Types و Value Types بپردازیم و با نمونه کدهای عملی با شیوه عملکرد این دو نوع متغیر آشنا شویم.
Value Types
متغیرهایی که از نوع Value Type هستند، به صورت مستقیم داخل حافظه stack ذخیره می شوند. در زبان برنامه نویسی دات نت نوع های داده زیر از نوع Value Type هستند:
- byte
- sbyte
- short
- ushort
- int
- uint
- long
- ulong
- decimal
- double
- float
- bool
- char
- نوع های داده ای که با 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 را در تصویر زیر مشاهده می کنید:
پس از رسیدن به مرحله چهارم، به ترتیب موارد از روی حافظه 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();
دقیقاً تفاوت در همینجاست. گفتیم ایجاد یک شئ از دو بخش تشکیل شده:
- تعریف متغیری از نوع کلاسی که می خواهیم از روی آن یک شئ بسازیم
- ایجاد شئ و قرار دادن آن داخل متغیر
در مرحله اول، یعنی زمانی که شما متغیری برای یک شئ تعریف می کنید، در حقیقت خانه ای داخل حافظه 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 مشاهده کنید:
پس از ایجاد متغیر و شئ مربوطه، هر زمان که شما قصد دسترسی به یکی از اعضای شئ Customer را داشته باشید، ابتدا به حافظه stack مراجعه شده و بر اساس آدرس شئ در حافظه Heap، دسترسی به عضو آن شئ برای شما فراهم می شود.
حافظه Heap یک حافظه مدیریت شده می باشد. منظور از مدیریت شده، عملیاتی است که CLR بر روی این حافظه انجام می دهد. گفتیم زمانی که از scope یک متغیر خارج می شویم، آن متغیر از حافظه stack حذف می شود. تا زمانی که این متغیر در حافظه stack وجود دارد، در حقیقت ارتباط بین آن و شئ داخل Heap برقرار است و زمانی که متغیر از stack حذف شد این ارتباط نیز حذف می شود. در حقیقت شئ داخل حافظه Heap بلااستفاده است. در بخش معرفی دات نت در مورد سرویسی به نام GC به صورت مختصر صحبت کردیم. این سرویس وظیفه مدیریت حافظه Heap را بر عهده دارد. سرویس GC، در بازه های زمانی مختلف حافظه Heap را بررسی کرده و شئ هایی که از حافظه stack به آنها ارجاعی داده نشده را از Heap حذف می کند. عملیاتی که GC انجام می دهد کمی پیچیده است که در بخش های بعدی در مورد این سرویس به تفصیل صحبت خواهیم کرد، اما به اختصار GC عملیات های زیر را هنگام اجرا انجام می دهد:
- بررسی حافظه Heap و شناسایی شئ هایی که دیگر مورد استفاده قرار نمی گیرند.
- حذف اشیاء بلااستفاده از حافظه Heap
- 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 اشاره می کنند:
در نتیجه با تغییر خصوصیت 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 ها پارامترهای زیر را مد نظر داشته باشید:
- تنها زمانی از struct استفاده کنید که سایز نوع داده شما کم است. برای نوع های داده ای که خصوصیات و منابع زیادی استفاده می کنند از کلاس استفاده کنید
- در struct ها نمی توانید از قابلیت وراثت یا inheritance استفاده کنید. تنها اسنفاده از interface ها مجاز می باشد که در بخش بعدی با آنها آشنا خواهید شد
- struct ها را نمی توانید از نوع static تعریف کنید و این کار تنها برای کلاس ها مجاز است.
- برای struct ها نمی توانید سازنده پیش فرض یا Default Constrcutor تعریف کنید
- برای 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 دارند:
- HasValue: بررسی می کند که متغیر مقدار null دارد یا خیر، مقداری که این خصوصیت بر میگرداند از نوع bool است.
- 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 ها خواهیم پرداخت.
منبع:programming.itpro.ir
- ۹۶/۰۲/۱۵