در سی شارپ انواع دادهای با توجه به نحوه ذخیرهسازی در حافظه به دو دستهی انواع مقداری و انواع ارجاعی تقسیم میشوند. در این پست مفاهیم و ارسال آنها به عنوان پارامتر را بررسی میکنیم.
انواع مقداری Value Types
همانطور که از نام آنها مشخص است، به طور مستقیم حاوی مقدار هستند. در تصویر زیر، مقدار ۱۰۰ در فضای حافظهی اختصاص یافته به متغیر i، ذخیره شده است.
زمانی که یک متغیر از این نوع را به دیگری تخصیص میدهیم، مقدار آن در متغیر جدید کپی میشود. نوع Structure(Struct) از انواع مقداری است. این نوع از System.ValueType ارثبری میکند، این کلاس تضمین میکند که کلاسهای مشتق شده به جای heap از stack تخصیص داده شوند. به بیان ساده، دادههایی اختصاص داده شده در Stack به سرعت ایجاد شده و از بین میروند، زیرا طول عمر آنها توسط Scope ای که در آن تعریف شدهاند، تعیین میشود. از طرف دیگر دادههایی که در Heap تخصیص داده میشوند، توسط garbage collector مانیتور میشوند و طول عمر آنها توسط فاکتورهای مختلف تعیین میشود. Struct هایی که به طور توکار در سی شارپ هستند شامل:
و همچین میتوانیم Struct های دلخواه خود را ایجاد کنیم:
struct Point { public int X; public int Y; public Point(int xPos, int yPos) { X = xPos; Y = yPos; } public void Display() { Console.WriteLine("X = {0}, Y = {1}", X, Y); } }
static void LocalValueTypes() { int i = 0; Point p = new Point(); }// اینجا متغیرها از پشته حذف می شوند
انواع ارجاعی Reference Types
انواع ارجاعی به طور مستقیم حاوی دیتا نیستند و در واقع آدرسی از حافظه، که شامل دیتا است، را ذخیره میکنند. در تصویر زیر فضای حافظه 0x803200 برای متغیر s در نظر گرفته شده است. مقدار متغیر s آدرس 0x600000 است که در واقع، آدرس حافظه مربوط به دیتای واقعی است.
دو متغیر از نوع ارجاعی میتوانند به شی یکسانی اشاره کنند، بنابراین تغییر در هر یک در دیگری تاثیر دارد. انواع ارجاعی که به صورت توکار در سی شارپ هستند، شامل: dynamic، Object و string هستند. با کلمات کلید class، interface، delegate و record میتوان نوع ارجاعی تعریف کرد. در انواع ارجاعی، متغیر در Stack و شیای که متغیر به آن اشاره میکند در heap تخصیص داده میشود.
void CreateNewTextBox() { TextBox myTextBox = new TextBox(); }
در مثال بالا زمانی که اجرای متد CreateNewTextBox پایان مییابد، متغیر myTextBox از stack حذف میشود. اما شی TextBox که اکنون دیگر متغیری به آن اشاره نمیکند،تا زمانی که garbage collector به طور خودکار آن را حذف نکند، در فضای heap باقی میماند.
انواع مقداری، انواع ارجاعی و عمگر انتساب (assignment operator)
زمانی که یک نوع مقداری را به دیگری تخصیص (assign) میدهید، در واقع یک کپی عضو به عضو از داده ها را انجام میدهید. برای یک نوع دادهای ساده مثل System.Int32، تنها عضوی که کپی میشود، مقدار عددی است. اما در مورد ساختار Point در مثال قبل، مقادیر X و Y در متغیر جدید کپی میشوند.
static void ValueTypeAssignment() { Console.WriteLine("Assigning value types\n"); Point p1 = new Point(10, 10); Point p2 = p1; // Print both points. p1.Display(); p2.Display(); //Change p1.X and print again. p2.X is not changed. p1.X = 100; Console.WriteLine("\n=> Changed p1.X\n"); p1.Display(); p2.Display(); }
در کد بالا، به دلیل اینکه Point یک نوع مقداری است، دو کپی از آن روی Stack داریم و هر کدام از آنها بصورت مستقل تغییر میکنند. بنابراین زمانی که مقدار p1.X را تغییر میدهیم، مقدار p2.X تغییر نخواهد کرد. برخلاف انواع مقداری ، زمانی که شما یک نوع ارجاعی را به دیگری تخصیص می دهید، هر دو متغیر به شی یکسانی اشاره میکنند. برای مثال کلاس PointRef را در نظر بگیرید:
class PointRef { public int X; int Y; public PointRef(int xPos, int yPos) { X = xPos; Y = yPos; } public void Display()=>Console.WriteLine("X = {0}, Y = {1}", X, Y); } public static void ReferenceTypeAssignment() { Console.WriteLine("Assigning reference types\n"); PointRef p1 = new PointRef(10, 10); PointRef p2 = p1; // Print both point refs. p1.Display(); p2.Display(); // Change p1.X and print again. p1.X = 100; Console.WriteLine("\n=> Changed p1.X\n"); p1.Display(); p2.Display(); }
در اینجا دو متغیر ارجاعی داریم که هر دو به شی یکسانی در Heap اشاره میکنند. بنابراین زمانی که مقدار X را از طریق متغیر p1 تغییر میدهید، p2.X هم مقداری یکسانی را برمیگرداند.
نکته: String یک نوع ارجاعی است، اما تغییرناپذیر immutable است. ینی زمانی که مقداری را به متغییری از نوع String تخصیص میدهیم، دیگر قابل تغییر نیست. هر وقت مقدار متغیر را تغییر دهیم، کامپایلر یک شی جدید از نوع string ایجاد میکند و متغیر را به آن ارجاع میدهد. بنابراین ارسال String بعنوان پارامتر، یک متغیر جدید در حافظه ایجاد خواهد کرد و تغییر در آن تاثیری در مقدار اولیه نخواهد داشت.
انواع مقداری که دارای فیلد از نوع ارجاعی هستند
فرض کنید که یک نوع مقداری به نام Rectangle داریم که یک فیلد از نوع ارجاعی (کلاس) به نام ShapeInfo دارد.
class ShapeInfo { public string InfoString; public ShapeInfo(string info) { InfoString = info; } } struct Rectangle { // فیلد از نوع ارجاعی public ShapeInfo RectInfo; public int RectTop, RectLeft, RectBottom, RectRight; public Rectangle(string info, int top, int left, int bottom, int right) { RectInfo = new ShapeInfo(info); RectTop = top; RectBottom = bottom; RectLeft = left; RectRight = right; } public void Display() { Console.WriteLine("String = {0}, Top = {1}, Bottom = {2}, " + "Left = {3}, Right = {4}", RectInfo.InfoString, RectTop, RectBottom, RectLeft, RectRight); } }
حال سوال اینجاست که اگر متغیر از نوع Rectangle را به دیگری تخصیص دهیم، چه اتفاقی میافتد؟ با توجه به مباحث قبل، مقادیر عددی به ازای هر متغیر Rectangle باید مستقل باشند. اما در مورد نوع ارجاعی که داخل ساختار وجود دارد، چطور؟ برای درک بهتر کدهای زیر را در نظر بگیرید:
static void ValueTypeContainingRefType() { // ایجاد متغیر اول Console.WriteLine("-> Creating r1"); Rectangle r1 = new Rectangle("First Rect", 10, 10, 50, 50); // تخصیص به متغیر جدید Console.WriteLine("-> Assigning r2 to r1"); Rectangle r2 = r1; // تغییر در متغیر دوم Console.WriteLine("-> Changing values of r2"); r2.RectInfo.InfoString = "This is new info!" r2.RectBottom = 4444; // نمایش مقادیر r1.Display(); r2.Display(); }
زمانی که یک متغیر از نوع مقداری که دارای فیلدی از نوع ارجاعی باشد را به دیگری تخصیص دهیم فیلد ارجاعی را به صورت مشترک استفاده میکنند. در این حالت، دو ساختار مستقل از هم داریم که فیلد ارجاعی آنها به یک شی یکسان در حافظه اشاره میکنند.
ارسال انواع مقداری بعنوان پارامتر
زمانی که یک متغیر از نوع مقداری را به متدی ارسال میکنید، به طور پیشفرض یک کپی از مقدار آن ارسال میشود . در مثال زیر چون متد Add کپی مستقل خود از متغیر x را دارد، هر تغییری که در آن بدهد، در متد Main تاثیری ندارد.
static int Add(int x, int y) { int ans = x + y; // متد اصلی تغییرات را نمیبیند //زیرا شما یک کپی از مقدار اولیه را تغییر میدهید x = 10000; y = 88888; return ans; } static void Main(string[] args) { int x = 9, y = 10; Console.WriteLine("Before call: X: {0}, Y: {1}", x, y); Console.WriteLine("Answer is: {0}", Add(x, y)); Console.WriteLine("After call: X: {0}, Y: {1}", x, y); Console.ReadLine(); }
همانطور که میبینید، مقادیر x و y قبل و بعد از فراخوانی متد Add() یکسان هستند.
ارسال انواع مقداری با out Modifier
با استفاده از پارامترهای خروجی، میتوان چندین مقدار را از متد برگرداند. متدهایی که پارامتر از نوع خروجی دریافت میکنند، قبل از خروج از حوزه متد، باید آن را مقداردهی کنند و متد اصلی میتواند تغییرات را دریافت کند.
static void Add(int x, int y, out int ans) { ans = x + y; }
فراخوانی متدها با پارامتر خروجی نیز، نیاز به استفاده از کلمه کلیدی out دارد.
int ans; Add(90, 90, out ans);
با معرفی C# 7.0 ، نیازی به تعریف پارامترهای خروجی قبل از استفاده آنها نیست.
Add(90, 90, out int ans);
اگر پارامترهای خروجی برای شما اهمیتی ندارند، میتوانید آنها را نادیده بگیرید.
Add(90, 90, out _);
ارسال انواع مقداری با ref Modifier
کلمه کلیدی ref نشاندهنده این است که پارامتر باید بصورت ارجاعی ارسال شود. بنابراین هر تغییر که در متد روی آن انجام شود، در متد اصلی تاثیر دارد. در مثال زیر هر دو متغیر number و refArgument به شی یکسانی اشاره دارند.
void Method(ref int refArgument) { refArgument = refArgument + 44; } int number = 1; Method(ref number); Console.WriteLine(number); // Output: 45
تفاوت پارامتر out و ref :
ارسال با in Modifier
کلمه کلیدی in پارامتر را بصورت ارجاعی و فقط خواندنی میفرستد، بنابراین متدی که فراخوانی میشود، نمیتواند آن را تغییر دهد. استفاده از این مورد میتواند مصرف حافظه را کاهش دهد. زمانی که انواع مقداری به عنوان پارامتر ارسال میشوند، بوسیله متد ، کپی میشوند. اگر این اشیا بزرگ باشند (مثل یک ساختار بزرگ) سربار کپی این اشیا میتواند چشمگیر باشد. اگر بصورت ارجاعی ارسال شوند متد میتواند، آنها را تغییر دهد. هر دوی این مشکلات با کلمه کلیدی In حل میشوند.
static int AddReadOnly(in int x, in int y) { //خطا پارامترها فقط خواندنی هستند //x = 10000; //y = 88888; int ans = x + y; return ans; }
ارسال انواع ارجاعی با مقدار
ارسال نوع ارجاعی بوسیله ارجاع کاملا متفاوت با ارسال آن با مقدار است. به مثال زیر توجه کنید:
class Person { public string personName; public int personAge; // Constructors. public Person(string name, int age) { personName = name; personAge = age; } public Person() { } public void Display() { Console.WriteLine("Name: {0}, Age: {1}", personName, personAge); } } static void SendAPersonByValue(Person p) { // Change the age of "p"? p.personAge = 99; // Will the caller see this reassignment? p = new Person("Nikki", 99); } static void Main(string[] args) { // Passing ref-types by value. Console.WriteLine("***** Passing Person object by value *****"); Person fred = new Person("Fred", 12); Console.WriteLine("\nBefore by value call, Person is:"); fred.Display(); SendAPersonByValue(fred); Console.WriteLine("\nAfter by value call, Person is:"); fred.Display(); Console.ReadLine(); }
همانطور که میبینید فیلد personAge تغییر کرده است، اما personName تغییر نکرده است. دلیل این است که یک کپی از متغیر fred که به شی person اشاره دارد، به متد SendAPersonByValue ارسال شده است و این متد دقیقا به شی یکسانی اشاره میکند، بنابراین میتواند دیتای مربوط به شی را تغییر دهد. با دستور new ، متغیر p به یک شی دیگر اشاره میکند و تغییر در آن، تاثیری در متغیر fred ندارد. در واقع متد قادر نیست آدرسی که fred به آن اشاره دارد را تغییر دهد.
ارسال انواع ارجاعی با ارجاع
حال فرض کنید متد SendAPersonByReference را داریم که پارامتر از نوع person را با ارجاع دریافت میکند:
static void SendAPersonByReference(ref Person p) { // Change some data of "p". p.personAge = 555; // "p" is now pointing to a new object on the heap! p = new Person("Nikki", 999); } static void Main(string[] args) { // Passing ref-types by ref. Console.WriteLine("***** Passing Person object by reference *****"); Person mel = new Person("Mel", 23); Console.WriteLine("Before by ref call, Person is:"); mel.Display(); SendAPersonByReference(ref mel); Console.WriteLine("After by ref call, Person is:"); mel.Display(); Console.ReadLine(); }
همانطور که میبینید، هر دو فیلد شی تغییر کردهاند، به دلیل استفاده از ref ، این متد قادر است آدرسی را که متغیر mel به آن اشاره دارد را تغییر دهد.