در قسمت قبل با return کردن object و overloading آشنا شديد. در اين قسمت از زنگ سيشارپ قصد داريم به مباحث مهم stack ،heap ،value types ،reference types، boxing و unboxing بپردازيم و همچنين optional arguments، named arguments، garbageCollection و object initializers را مورد بحث و بررسي قرار دهيم.
هنگاميکه يک متغير تعريف ميکنيد، دقيقاً چه اتفاقي ميافتد؟
هنگاميکه شما در اپليکيشنهاي NET. يک متغير تعريف ميکنيد، قسمتي از حافظهي RAM براي اين منظور اختصاص داده ميشود. اين قسمت از حافظه، شامل سه چيز است: نام متغير، data type متغير و مقدار متغير.
با توجه به data type، متغير شما در قسمتهاي متفاوتي طراحی وب سایت ذخيره ميشود. دو نوع تخصيص حافظه وجود دارد که يکي stack memory و ديگري heap memory است. براي اينکه بهتر با stack و heap آشنا شويد به کد زير و شرح آن توجه کنيد:
public void Method1()
{
// line 1
int x = 2;
// line 2
int y = 5;
// line 3
MyClass ob = new MyClass();
}
هنگاميکه line 1 اجرا ميشود، کامپايلر مقدار کمي از حافظه را در stack براي اين منظور اختصاص ميدهد. stack مسئول پيگيري حافظهي مورد نياز (در حال اجرا) در اپليکيشن شما است. همانطور که پيش از اين با نحوهي ذخيرهسازي اطلاعات در stack آشنا شديد، stack عمليات Last In First Out را اجرا ميکند و هنگامي که line 2 اجرا ميشود، متغير y در بالاي stack ذخيره خواهد شد. در line 3 ما يک شيء بهوجود آوردهايم و در اينجا اندکي داستان متفاوت ميشود. پس از اينکه line 3 اجرا شد، متغير ob در stack ذخيره ميشود و شيءاي که ساخته شده در heap قرار ميگيرد. نکته دقيقاً همينجاست که reference ها در stack ذخيره ميشوند و عبارت MyClass ob حافظه را براي يک شيء از اين کلاس اشغال نميکند. اين عبارت تنها متغير ob را در stack قرار ميدهد (و به آن مقدار null ميدهد) و هنگاميکه کلمهي کليدي new اجرا ميشود، شيء اين کلاس در heap ذخيره خواهد شد. در نهايت هنگاميکه برنامه به انتهاي متد ميرسد، متغيرهايي که در stack بودند همهگي پاک ميشوند. توجه کنيد که پس از به پايان رسيدن متد چيزي از heap پاک نميشود بلکه اشياي درون heap بعداً توسط garbage collector پاک خواهند شد. در مورد garbage collector در انتهاي اين مقاله صحبت خواهيم کرد.
ممکن است برايتان سوال باشد که چرا stack و heap ؟ نميشود همه در يکجا ذخيره شوند؟ اگر با دقت نگاه کنيد ميبينيد که data type هاي اصلي (value types)، پيچيده و سنگين نيستند. آنها مقادير تکي مثل int i = 5 را نگه ميدارند در حاليکه object data types يا reference types پيچيدهتر و سنگينتر هستند، آنها به اشياي ديگري رجوع ميکنند. به عبارت ديگر، آنها به چندين مقدار رجوع ميکنند (زيرا اشياء ميتوانند شامل مقادير زيادي از فيلد و متد و… باشند) که هرکدام از آنها بايد در حافظه ذخيره شده باشد. اشياء به dynamic memory و data type هاي اصلي (value types) به static memory نياز دارند. اگر اطلاعات شما نيازمند dynamic memory باشد، در heap ذخيره ميشود، اگر نيازمند static memory باشد، در stack ذخيره خواهد شد.
Value types و Reference types
اکنون که با مفاهيم stack و heap آشنا شديد بهتر ميتوانيد مفهوم value types و reference types را درک کنيد. Value type ها تمام و کمال در stack ذخيره ميشوند، يعني هم مقدار و هم متغير همهگي يکجا هستند اما در reference type متغير در stack است درحاليکه object در heap قرار ميگيرد و متغير و شيء به هم متصل ميشوند (متغير به شيء اشاره ميکند).
در زير، data type اي از جنس int داريم با اسم i که مقدارش به متغيري از نوع int با اسم j اختصاص داده ميشود. اين دو متغير در stack ذخيره ميشوند. هنگاميکه مقدار i را به j اختصاص ميدهيم، يک کپي (کاملاً جدا و مجزا) از مقدار i به j داده ميشود و به عبارت ديگر هنگامي که يکي از آنها را تغيير دهيد، ديگري تغيير نمييابد:
هنگاميکه يک شيء ميسازيد و reference آن را با يک reference ديگر مساوي قرار ميدهيد، آنگاه هر دوي اين reference ها به يک شيء رجوع ميکنند و تغيير هر کدام از آنها باعث تغيير شيء ميشود زيرا هردو reference به يک شيء اشاره ميکنند.
به مثال زير توجه کنيد:
ing System;
class Person
{
public string Name;
public string Family;
public void Show()
{
Console.WriteLine(Name + " " + Family);
}
}
class Myclass
{
static void Main()
{
Person ob1 = new Person();
Person ob2 = ob1;
ob1.Name = "Nicolas";
ob1.Family = "Cage";
Console.Write("ob1: ");
ob1.Show();
Console.Write("ob2: ");
ob2.Show();
Console.WriteLine();
ob2.Name = "Ian";
ob2.Family = "Somerhalder";
Console.Write("ob1: ");
ob1.Show();
Console.Write("ob2: ");
ob2.Show();
}
}
همانطور که ميبينيد، ابتدا يک شيء ساخته و سپس reference ديگري تعريف کردهايم و نهايتاً آنها را مساوي هم قرار دادهايم. توجه کنيد که براي ob2 شيء جديد تعريف نکردهايم بلکه ob2 به همان شيءاي رجوع ميکند که ob1 به آن رجوع ميکند. بنابراين تغيير هرکدام بر روي شيء تاثير ميگذارد. همانطور که ميبينيد، ob1.Name و ob2.Family در ابتدا برابر با Nicolas Cage است سپس با تغيير ob2.Name و ob2.Family به Ian Somerhalder مقادير فيلدهاي ob1 نيز تغيير خواهند کرد. به شکل زير توجه کنيد:
Boxing and Unboxing
بهطور خلاصه، وقتيکه يک مقدار value type را تبديل به reference type ميکنيد، در واقع اطلاعات را از stack به heap ميبريد و هنگاميکه يک مقدار reference type را تبديل به value type ميکنيد، اطلاعات را از heap به stack ميبريد. اين رفت و برگشت اطلاعات از stack به heap روي performance (کارايي، سرعت اجرا) برنامه تاثير ميگذارد. فرستادن اطلاعات از stack به heap در اصطلاح boxing و فرستادن اطلاعات از heap به stack در اصطلاح unboxing ناميده ميشود.
استفاده از boxing و unboxing باعث افت performance ميشود بنابراين تا آنجا که ميتوانيد از انجام اينکار پرهيز کنيد و فقط در مواردي که واقعاً نيازمند اينکار هستيد و راه ديگري نيست، از آن استفاده کنيد.
Garbage Collection
Garbage Collection نوعي مديريت حافظهي خودکار محسوب ميشود. هربار که يک شيء ميسازيد، object شما در heap ذخيره ميشود. تا زمانيکه فضاي کافي براي ذخيرهي اين اشياء داشته باشيد ميتوانيد شيء جديد بسازيد اما همانطور که ميدانيد حافظه نامحدود نيست و ممکن است پر شود. بنابراين بايد object هاي بياستفاده، از حافظه پاک شوند تا بتوان مجدداً اشياي ديگري را در حافظه ذخيره کرد. در بسياري از زبانهاي برنامهنويسي براي آزاد کردن حافظه از چيزهايي که در آن ذخيره شده، بهصورت دستي و کدنويسي بايد اينکار انجام شود. مثلاً در ++C براي اين منظور از delete operator استفاده ميشود اما سيشارپ براي اين منظور از راه حلي بهتر و سادهتر به اسم Garbage Collection استفاده ميکند. Garbage Collection بدون اينکه برنامهنويس نياز باشد کار خاصي انجام دهد بهصورت خودکار، اشيايي که در heap قرار دارند و به هيچ reference اي وصل نيستند را پاک ميکنند. اينکه دقيقاً چه زماني اينکار انجام ميشود، مشخص نيست اما اگر ميخواهيد قبل از پاک شدن يک شيء توسط garbage collector کار خاصي را انجام دهيد يا فقط از پاک شدن آن مطلع شويد از destructors استفاده ميکنيد. از destructor در سطوح حرفهاي برنامهنويسي استفاده ميشود و دانستن آن چندان براي شما که اول راه هستيد ضروري نيست اما اگر در اين مورد کنجکاويد ميتوانيد شخصاً در مورد آن تحقيق کنيد.
Object Initializers
Object Initializers روشي ديگر براي ساخت شيء و مقدار دهي به field ها و property هاي (در مورد property بعداً بحث خواهيم کرد) کلاس است. با استفاده از object initializers، ديگر constructor کلاس را به روش معمول صدا نميزنيد بلکه اسم field ها و property ها را مينويسيد و مستقيماً به آنها مقدار ميدهيد. استفادهي اصلي object initializers براي anonymous type هاي ساخته شده توسط LINQ است (در مورد LINQ و anonymous types بعداً صحبت خواهيم کرد) اما در حالت معمول نيز ميتوانند مورد استفاده قرار گيرند.
به مثال زير توجه کنيد:
using System;
class Human
{
public string Name;
public int Age;
public void Show()
{
Console.WriteLine(Name + " " + Age);
}
}
class ObjInitializersDemo
{
static void Main()
{
Human Man = new Human { Name = "Paul", Age = 28 };
Man.Show();
}
}
همانطور که ميبينيد، Man.Name برابر با Paul و Man.Age را برابر با ?? قرار دادهايم. نکته اينجاست که از هيچ constructor اي استفاده نکردهايم بلکه شيء Man توسط خط کد زير توليد شده است:
1
Human Man = new Human { Name = "Paul", Age = 28 };
Optional Arguments
C# 4.0 ويژگي جديدي بهنام Optional Arguments دارد که باعث ميشود براي فرستادن argument ها و دريافت پارامترها، روش ديگري نيز در دستتان باشد. همانطور که اسم اين ويژگي جديد (argument هاي دلخواه) بيانکنندهي ماهيت آن است، با استفاده از optional arguments ميتوانيد متدهايي تعريف کنيد که از بين چندين پارامترش، بعضي از آنها قابليت اين را داشته باشند که براي دريافت argument، اجباري نداشته باشند و اگر صلاح دانستيد به آنها argument دهيد. استفاده از اين ويژگي بسيار راحت است، کافي است هنگام تعريف پارامترها به آنها يک مقدار پيشفرض بدهيد.
به نمونهي زير توجه کنيد:
1
2
3
4
public void OptArg(int a, int b = 2, int c = 3)
{
Console.WriteLine("This is a, b, c: {0} {1} {2}", a, b, c);
}
در متد بالا، پارامتر b و c اختياري هستند و به اين طريق شما ويژگي optional argument را فعال کرديد. توجه کنيد که پارامتر a همان حالت معمول را دارد و اختياري نيست و حتماً بايد مقدار دهي شود.
به مثال زير توجه کنيد:
using System;
class OptionalArgs
{
public void OptArg(int a, int b = 2, int c = 3)
{
Console.WriteLine("This is a, b, c: {0} {1} {2}", a, b, c);
}
}
class OptionalArgsDemo
{
static void Main()
{
OptionalArgs ob = new OptionalArgs();
ob.OptArg(5);
ob.OptArg(3, 9);
ob.OptArg(4, 6, 8);
}
}
در اين مثال، متد ()OptArg به سه طريق صدا زده شده است. ابتدا يک، سپس دو و در نهايت سه argument دريافت کرده است. اين امکان وجود ندارد که اين متد را بدون هيچ argument اي اجرا کنيد چراکه پارامتر a اختياري نيست و مقداردهي به آن اجباري است. آيا استفاده از اين روش شبيه به method overloading نيست؟ بله، شما با اين کار به يک متد به سه طريق مقدار دادهايد که به method overloading شباهت دارد اما اين روشها جايگزيني براي هم نيستند بلکه در بعضي موارد براي راحتي برنامهنويس استفاده ميشود و در برخي موارد براي خط کد کمتر ممکن است از اين روش هم بتوانيد بهرهمند شويد. توجه کنيد که اگر به پارامترهاي دلخواه هيچ مقداري ندهيد، مقدار پيشفرض آنها در نظر گرفته ميشود. همچنين پارامترهاي که اجباري هستند بايد پيش از پارامترهاي اختياري قرار بگيرند. براي نمونه، خط کد زير نادرست است:
1
2
3
public void OptArg(int b = 2, int c = 3, int a) // Error!
// Or
public void OptArg(int b = 2, int a, int c = 3) // Error!
بهدليل اينکه پارامتر a اجباري است بايد پيش از پارمترهاي اختياري قرار بگيرد. از optional arguments نيز ميتوانيد در constructor، indexer و delegate نيز استفاده کنيد (indexer و delegate در مقالات آينده مورد بحث قرار ميگيرند).
Named Arguments
يکي ديگر از ويژگيهاي جديدي که به C# 4.0 افزوده شده، named argument است. همانطور که ميدانيد، هنگاميکه argument هايي را به متد ميفرستيد، ترتيب اين argument ها بايد مطابق با ترتيب پارامترهايي باشد که در متد تعريف شدهاند. با استفاده از named arguments ميتوانيد اين محدوديت و اجبار را برداريد. استفاده از اين ويژگي نيز بسيار ساده است، کافيست طراحی وب سایت نام پارامتري که argument قرار است به آن داده شود را در هنگام ارسال argument مشخص کنيد و بعد از اينکار، ديگر ترتيب argument ها اهميتي ندارد.
به مثال زير توجه کنيد:
static int Div(int firstParam, int secondParam)
{
return firstParam / secondParam;
}
static void Main()
{
int result;
// Call by use of normal way (positional arguments).
result = Div(10, 5);
Console.WriteLine(result);
// Call by use of named arguments.
result = Div(firstParam: 10, secondParam: 5);
Console.WriteLine(result);
// Order dosn't matter with a named argument.
result = Div(secondParam:5, firstParam: 10);
Console.WriteLine(result);
}
}
همانطور که ميبينيد متد ()Div در هر سه باري که فراخواني شده، نتيجهي يکساني را توليد کرده است. ابتدا از اين متد بهصورت معمول استفاده کرديم و سپس در فراخواني بعدي، نام پارامترها را نيز مشخص کردهايم (در اينجا از ويژگي named arguments استفاده شد) و در نهايت همانطور که ميبينيد، ترتيب را بههم زديم و جاي argument ها را عوض کرديم اما نتيجه تغيير نکرده است.