v.tajari
v.tajari
خواندن ۱۶ دقیقه·۴ سال پیش

بررسی انواع مقداری Value Types و انواع ارجاعی Reference Types در #C

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

انواع مقداری Value Types

همانطور که از نام آن­ها مشخص است، به طور مستقیم حاوی مقدار هستند. در تصویر زیر، مقدار ۱۰۰ در فضای حافظه‌ی اختصاص یافته به متغیر i، ذخیره شده است.

نحوه ذخیره‌سازی انواع مقداری
نحوه ذخیره‌سازی انواع مقداری


زمانی که یک متغیر از این نوع را به دیگری تخصیص می‌­دهیم، مقدار آن در متغیر جدید کپی می­‌شود. نوع Structure(Struct) از انواع مقداری است. این نوع از System.ValueType ارث­‌بری می­کند، این کلاس تضمین می­‌کند که کلاس‌­های مشتق شده به جای heap از stack تخصیص داده شوند. به بیان ساده، داده­‌هایی اختصاص داده شده در Stack به سرعت ایجاد شده و از بین می­روند، زیرا طول عمر آن­ها توسط Scope ای که در آن تعریف شده‌­اند، تعیین می‌­شود. از طرف دیگر داده‌­هایی که در Heap تخصیص داده می‌­شوند، توسط garbage collector مانیتور می‌­شوند و طول عمر آن­ها توسط فاکتورهای مختلف تعیین می­شود. Struct هایی که به طور توکار در سی شارپ هستند شامل:

  • انواع عددی مثل long، int، short، byte و ....
  • انوع اعشاری مثل float، double، decimal
  • نوع bool
  • نوع char

و همچین می‌­توانیم Struct های دلخواه خود را ایجاد کنیم:

struct Point { public int X; public int Y; public Point(int xPos, int yPos) { X = xPos; Y = yPos; } public void Display() { Console.WriteLine(&quotX = {0}, Y = {1}&quot, 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(&quotAssigning value types\n&quot); 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(&quot\n=> Changed p1.X\n&quot); 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(&quotX = {0}, Y = {1}&quot, X, Y); } public static void ReferenceTypeAssignment() { Console.WriteLine(&quotAssigning reference types\n&quot); 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(&quot\n=> Changed p1.X\n&quot); 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(&quotString = {0}, Top = {1}, Bottom = {2}, &quot + &quotLeft = {3}, Right = {4}&quot, RectInfo.InfoString, RectTop, RectBottom, RectLeft, RectRight); } }

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

static void ValueTypeContainingRefType() { // ایجاد متغیر اول Console.WriteLine(&quot-> Creating r1&quot); Rectangle r1 = new Rectangle(&quotFirst Rect&quot, 10, 10, 50, 50); // تخصیص به متغیر جدید Console.WriteLine(&quot-> Assigning r2 to r1&quot); Rectangle r2 = r1; // تغییر در متغیر دوم Console.WriteLine(&quot-> Changing values of r2&quot); r2.RectInfo.InfoString = &quotThis is new info!&quot 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(&quotBefore call: X: {0}, Y: {1}&quot, x, y); Console.WriteLine(&quotAnswer is: {0}&quot, Add(x, y)); Console.WriteLine(&quotAfter call: X: {0}, Y: {1}&quot, 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 :

  • پارامترهای 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(&quotName: {0}, Age: {1}&quot, personName, personAge); } } static void SendAPersonByValue(Person p) { // Change the age of &quotp&quot? p.personAge = 99; // Will the caller see this reassignment? p = new Person(&quotNikki&quot, 99); } static void Main(string[] args) { // Passing ref-types by value. Console.WriteLine(&quot***** Passing Person object by value *****&quot); Person fred = new Person(&quotFred&quot, 12); Console.WriteLine(&quot\nBefore by value call, Person is:&quot); fred.Display(); SendAPersonByValue(fred); Console.WriteLine(&quot\nAfter by value call, Person is:&quot); 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 &quotp&quot. p.personAge = 555; // &quotp&quot is now pointing to a new object on the heap! p = new Person(&quotNikki&quot, 999); } static void Main(string[] args) { // Passing ref-types by ref. Console.WriteLine(&quot***** Passing Person object by reference *****&quot); Person mel = new Person(&quotMel&quot, 23); Console.WriteLine(&quotBefore by ref call, Person is:&quot); mel.Display(); SendAPersonByReference(ref mel); Console.WriteLine(&quotAfter by ref call, Person is:&quot); mel.Display(); Console.ReadLine(); }

همانطور که می­بینید، هر دو فیلد شی تغییر کرده­‌اند، به دلیل استفاده از ref ، این متد قادر است آدرسی را که متغیر mel به آن اشاره دارد را تغییر دهد.

ارسال انواع ارجاعی با ارجاع
ارسال انواع ارجاعی با ارجاع


cValue TypesReference Typesسی شارپ
شاید از این پست‌ها خوشتان بیاید