فصل ۲: مبانی زبان C# — نخستین برنامه، Syntax و مبانی Type
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
۲. مبانی زبان C#
در این فصل، مبانی زبان C# را معرفی میکنیم.
اولین برنامهٔ C#
در ادامه برنامهای میبینید که 12 را در 30 ضرب میکند و نتیجه، یعنی 360، را روی صفحه چاپ میکند. دو اسلش روبهجلو نشان میدهند که ادامهٔ خط یک comment است:
int x = 12 * 30; // Statement 1
System.Console.WriteLine (x); // Statement 2
برنامهٔ ما از دو statement تشکیل شده است. statementها در C# بهترتیب اجرا میشوند و با semicolon پایان مییابند. statement نخست عبارت 12 * 30 را محاسبه میکند و نتیجه را در متغیری به نام x ذخیره میکند که نوع آن عدد صحیح 32 بیتی (int) است.
statement دوم متد WriteLine را روی کلاسی به نام Console فراخوانی میکند که در namespaceای به نام System تعریف شده است. این کار متغیر x را در یک پنجرهٔ متنی روی صفحه چاپ میکند.
یک method کاری را انجام میدهد؛ یک class اعضای تابعی و اعضای داده را گروهبندی میکند تا یک واحد سازندهٔ شیءگرا شکل گیرد. کلاس Console اعضایی را که قابلیتهای ورودی/خروجی (I/O) خط فرمان را مدیریت میکنند، مانند متد WriteLine، گروهبندی میکند. class نوعی type است که در «Type Basics» در صفحهٔ 36 آن را بررسی میکنیم.
در بیرونیترین سطح، typeها در namespaceها سازماندهی میشوند. بسیاری از typeهای پرکاربرد — از جمله کلاس Console — در namespace System قرار دارند. کتابخانههای .NET در namespaceهای تودرتو سازماندهی شدهاند. برای مثال، namespace System.Text شامل typeهایی برای کار با متن است و System.IO شامل typeهایی برای ورودی/خروجی است.
qualify کردن کلاس Console با namespace System در هر بار استفاده، کد را شلوغ میکند. دستور using با import کردن یک namespace اجازه میدهد از این شلوغی جلوگیری کنید:
using System; // Import the System namespace
int x = 12 * 30;
Console.WriteLine (x); // No need to specify System.
یک شکل پایه از استفادهٔ مجدد کد این است که توابع سطحبالاتری بنویسید که توابع سطحپایینتر را فراخوانی میکنند. میتوانیم برنامهٔ خود را با متد قابلاستفادهٔ مجددی به نام FeetToInches refactor کنیم که یک عدد صحیح را در 12 ضرب میکند:
using System;
Console.WriteLine (FeetToInches (30)); // 360
Console.WriteLine (FeetToInches (100)); // 1200
int FeetToInches (int feet)
{
int inches = feet * 12;
return inches;
}
متد ما شامل مجموعهای از statementهاست که با یک جفت brace احاطه شدهاند. به این ساختار statement block گفته میشود.
یک method میتواند با مشخصکردن parameterها دادهٔ ورودی را از فراخواننده دریافت کند و با مشخصکردن return type دادهٔ خروجی را به فراخواننده بازگرداند. متد FeetToInches ما پارامتری برای دریافت feet و return typeای برای خروجی inches دارد:
int FeetToInches (int feet)
...
literalهای 30 و 100 آرگومانهایی هستند که به متد FeetToInches فرستاده شدهاند.
اگر متدی ورودی دریافت نمیکند، از پرانتز خالی استفاده کنید. اگر چیزی بازنمیگرداند، از کلیدواژهٔ void استفاده کنید:
using System;
SayHello();
void SayHello()
{
Console.WriteLine ("Hello, world");
}
methodها یکی از چند نوع function در C# هستند. نوع دیگری از function که در برنامهٔ نمونهٔ خود استفاده کردیم عملگر * است که ضرب را انجام میدهد. constructorها، propertyها، eventها، indexerها و finalizerها نیز وجود دارند.
کامپایل (Compilation)
کامپایلر C# کد منبع (مجموعهای از فایلها با پسوند .cs) را به یک assembly کامپایل میکند. assembly واحد بستهبندی و deployment در .NET است. یک assembly میتواند application یا library باشد. یک برنامهٔ عادی console یا Windows نقطهٔ ورود دارد، در حالی که library نقطهٔ ورود ندارد. هدف library این است که توسط یک application یا libraryهای دیگر فراخوانی (reference) شود. خود .NET مجموعهای از libraryهاست (بهعلاوهٔ یک محیط زمان اجرا).
هر یک از برنامههای بخش پیشین مستقیماً با مجموعهای از statementها آغاز میشدند که top-level statement نام دارند. وجود top-level statementها بهصورت ضمنی یک entry point برای یک برنامهٔ console یا Windows میسازد. (بدون top-level statementها، متد Main entry point برنامه را مشخص میکند — بخش «Custom Types» در صفحهٔ 37 را ببینید.)
ابزار dotnet (dotnet.exe در Windows) به شما کمک میکند کد منبع و binaryهای .NET را از خط فرمان مدیریت کنید. میتوانید از آن برای build و اجرای برنامه استفاده کنید؛ بهعنوان جایگزین یک محیط توسعهٔ یکپارچه (IDE) مانند Visual Studio یا Visual Studio Code.
میتوانید ابزار dotnet را با نصب .NET 8 SDK یا با نصب Visual Studio به دست آورید. محل پیشفرض آن در Windows برابر %ProgramFiles%\dotnet و در Ubuntu Linux برابر /usr/bin/dotnet است.
برای کامپایل یک application، ابزار dotnet به یک project file و یک یا چند فایل C# نیاز دارد. دستور زیر یک console project جدید را scaffold میکند (ساختار پایهٔ آن را میسازد):
dotnet new Console -n MyFirstProgram
این دستور زیرپوشهای به نام MyFirstProgram میسازد که شامل project fileای به نام MyFirstProgram.csproj و فایل C#ای به نام Program.cs است که «Hello world» را چاپ میکند.
برای build و اجرای برنامه، دستور زیر را از پوشهٔ MyFirstProgram اجرا کنید:
dotnet run MyFirstProgram
یا اگر فقط میخواهید build کنید و اجرا نکنید:
dotnet build MyFirstProgram.csproj
assembly خروجی در یک زیرپوشه درون bin\debug نوشته میشود.
assemblyها را با جزئیات در فصل 17 توضیح میدهیم.
نحو (Syntax)
نحو C# از نحو C و C++ الهام گرفته است. در این بخش عناصر نحوی C# را با استفاده از برنامهٔ زیر توضیح میدهیم:
using System;
int x = 12 * 30;
Console.WriteLine (x);
شناسهها و کلیدواژهها (Identifiers and Keywords)
identifierها نامهایی هستند که برنامهنویسان برای classها، methodها، variableها و موارد دیگر انتخاب میکنند. identifierهای برنامهٔ نمونهٔ ما، بهترتیبی که ظاهر میشوند، عبارتاند از:
System x Console WriteLine
یک identifier باید یک واژهٔ کامل باشد و اساساً از نویسههای Unicode ساخته شود که با یک حرف یا زیرخط آغاز میشوند. identifierهای C# نسبت به بزرگی و کوچکی حروف حساساند. طبق قرارداد، parameterها، local variableها و private fieldها باید به سبک camel case باشند (برای مثال myVariable) و همهٔ identifierهای دیگر به سبک Pascal case باشند (برای مثال MyMethod).
keywordها نامهایی هستند که برای کامپایلر معنای ویژه دارند. در برنامهٔ نمونهٔ ما دو keyword وجود دارد: using و int.
بیشتر keywordها reserved هستند؛ یعنی نمیتوانید از آنها بهعنوان identifier استفاده کنید. فهرست کامل keywordهای reserved در C# چنین است:
abstract as base bool break byte case catch char checked
class const continue decimal default delegate do double else
enum event explicit extern false finally fixed float for
foreach goto if implicit in int interface internal is lock
long namespace new null object operator out override params
private protected public readonly record ref return sbyte
sealed short sizeof stackalloc static string struct switch
this throw true try typeof uint ulong unchecked unsafe
ushort using virtual void volatile while
اگر واقعاً بخواهید identifierای استفاده کنید که با یک keyword reserved برخورد دارد، میتوانید آن را با پیشوند @ qualify کنید. برای نمونه:
int using = 123; // Illegal
int @using = 123; // Legal
نماد @ بخشی از خود identifier نیست. بنابراین @myVariable همان myVariable است.
keywordهای contextual
برخی keywordها contextual هستند؛ یعنی میتوانید آنها را بدون نماد @ نیز بهعنوان identifier به کار ببرید:
add alias and ascending async await by descending dynamic
equals file from get global group init into join let
managed nameof nint not notnull nuint on or orderby partial
remove required select set unmanaged value var with when
where yield
در keywordهای contextual، در contextای که استفاده میشوند ابهامی ایجاد نمیشود.
literalها، punctuatorها و operatorها
literalها قطعههای ابتدایی دادهاند که از نظر واژگانی مستقیماً در برنامه قرار گرفتهاند. literalهایی که در برنامهٔ نمونه استفاده کردیم 12 و 30 هستند.
punctuatorها به مرزبندی ساختار برنامه کمک میکنند. نمونهٔ آن semicolon است که یک statement را پایان میدهد. statementها میتوانند در چند خط ادامه پیدا کنند:
Console.WriteLine
(1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10);
یک operator عبارتها را تبدیل و ترکیب میکند. بیشتر operatorهای C# با یک symbol نمایش داده میشوند؛ مانند عملگر ضرب *. operatorها را بعدتر در این فصل با جزئیات بیشتری بررسی میکنیم. operatorهایی که در برنامهٔ نمونه استفاده کردیم عبارتاند از:
نقطه یک عضو از چیزی را نشان میدهد (یا در literalهای عددی، نقطهٔ اعشار را). پرانتز هنگام اعلان یا فراخوانی method استفاده میشود؛ پرانتز خالی زمانی استفاده میشود که method آرگومانی نمیپذیرد. (پرانتز کاربردهای دیگری نیز دارد که بعدتر در این فصل خواهید دید.) علامت مساوی assignment انجام میدهد. (دو علامت مساوی، ==، مقایسهٔ برابری انجام میدهد؛ همانطور که بعدتر خواهید دید.)
Commentها
C# دو سبک متفاوت برای مستندسازی کد منبع ارائه میکند: commentهای تکخطی و commentهای چندخطی. یک comment تکخطی با دو اسلش روبهجلو آغاز میشود و تا انتهای خط ادامه مییابد؛ برای مثال:
int x = 3; // Comment about assigning 3 to x
یک comment چندخطی با /* آغاز و با */ پایان مییابد؛ برای مثال:
int x = 3; /* This is a comment that
spans two lines */
commentها میتوانند tagهای مستندسازی XML را در خود داشته باشند که آنها را در «XML Documentation» در صفحهٔ 272 توضیح میدهیم.
مبانی Type
یک type نقشهٔ ساخت یک مقدار را تعریف میکند. در این مثال، از دو literal از نوع int با مقدارهای 12 و 30 استفاده میکنیم. همچنین متغیری از نوع int با نام x اعلان میکنیم:
int x = 12 * 30;
Console.WriteLine (x);
یک variable محل ذخیرهای را مشخص میکند که میتواند در طول زمان مقدارهای متفاوتی داشته باشد. در مقابل، یک constant همیشه همان مقدار را نمایش میدهد (بعداً بیشتر دربارهٔ آن صحبت میکنیم):
همهٔ مقدارها در C# نمونهای از یک type هستند. معنای یک مقدار و مجموعهٔ مقدارهای ممکنی که یک variable میتواند داشته باشد توسط type آن تعیین میشود.
نمونههایی از typeهای از پیش تعریفشده
typeهای از پیش تعریفشده (predefined types) typeهایی هستند که کامپایلر بهطور ویژه از آنها پشتیبانی میکند. type int یک type از پیش تعریفشده برای نمایش مجموعهٔ اعداد صحیحی است که در 32 بیت حافظه جا میگیرند، از −231 تا 231−1؛ و type پیشفرض literalهای عددی در این بازه است. میتوانید با نمونههای type int کارهایی مانند محاسبات حسابی انجام دهید:
یک type از پیش تعریفشدهٔ دیگر C#، string است. type string دنبالهای از نویسهها مانند «.NET» یا oreilly.com را نمایش میدهد. میتوانید با فراخوانی functionها روی رشتهها با آنها کار کنید:
string message = "Hello world";
string upperMessage = message.ToUpper();
Console.WriteLine (upperMessage); // HELLO WORLD
int x = 2022;
message = message + x.ToString();
Console.WriteLine (message); // Hello world2022
در این مثال، x.ToString() را فراخوانی کردیم تا نمایش رشتهای عدد صحیح x را به دست آوریم. میتوانید ToString() را روی متغیری از تقریباً هر type فراخوانی کنید.
type از پیش تعریفشدهٔ bool دقیقاً دو مقدار ممکن دارد: true و false. type bool معمولاً همراه statement if استفاده میشود تا جریان اجرا بهصورت شرطی شاخهبندی شود:
bool simpleVar = false;
if (simpleVar)
Console.WriteLine ("This will not print");
int x = 5000;
bool lessThanAMile = x < 5280;
if (lessThanAMile)
Console.WriteLine ("This will print");
Typeهای سفارشی
همانطور که میتوانیم methodهای خودمان را بنویسیم، میتوانیم typeهای خودمان را نیز بنویسیم. در مثال بعد، type سفارشیای به نام UnitConverter تعریف میکنیم؛ کلاسی که بهعنوان نقشهٔ ساخت تبدیل واحدها عمل میکند:
UnitConverter feetToInchesConverter = new UnitConverter (12);
UnitConverter milesToFeetConverter = new UnitConverter (5280);
Console.WriteLine (feetToInchesConverter.Convert(30)); // 360
Console.WriteLine (feetToInchesConverter.Convert(100)); // 1200
Console.WriteLine (feetToInchesConverter.Convert(
milesToFeetConverter.Convert(1))); // 63360
public class UnitConverter
{
int ratio; // Field
public UnitConverter (int unitRatio) // Constructor
{
ratio = unitRatio;
}
public int Convert (int unit) // Method
{
return unit * ratio;
}
}
اعضای یک type
یک type شامل data memberها و function memberهاست. data member مربوط به UnitConverter fieldی به نام ratio است. function memberهای UnitConverter متد Convert و constructor مربوط به UnitConverter هستند.
تقارن typeهای predefined و typeهای custom
یکی از جنبههای زیبای C# این است که میان typeهای predefined و custom تفاوتهای اندکی وجود دارد. type از پیش تعریفشدهٔ int بهعنوان نقشهٔ ساخت اعداد صحیح عمل میکند. داده — 32 بیت — را نگه میدارد و function memberهایی مانند ToString فراهم میکند که از آن داده استفاده میکنند.
به همین ترتیب، type سفارشی UnitConverter ما بهعنوان نقشهٔ ساخت تبدیلهای واحد عمل میکند. داده — یعنی ratio — را نگه میدارد و function memberهایی برای استفاده از آن داده فراهم میکند.
Constructorها و instanceسازی
داده با instanceسازی یک type ایجاد میشود. typeهای predefined را میتوان صرفاً با استفاده از literalی مانند 12 یا "Hello world" instanceسازی کرد. operator new نمونههای یک type سفارشی را میسازد. با statement زیر نمونهای از type UnitConverter را ساختیم و اعلان کردیم:
UnitConverter feetToInchesConverter = new UnitConverter (12);
بلافاصله پس از اینکه operator new یک object را instanceسازی کرد، constructor آن object برای انجام initialization فراخوانی میشود. constructor مانند یک method تعریف میشود، با این تفاوت که نام method و return type به نام type حاوی آن تقلیل مییابد:
public UnitConverter (int unitRatio) { ratio = unitRatio; }
اعضای instance در برابر static
data memberها و function memberهایی که روی نمونهٔ type کار میکنند، instance member نام دارند. متد Convert در UnitConverter و متد ToString در int نمونههایی از instance member هستند. بهطور پیشفرض، memberها instance member هستند.
data memberها و function memberهایی که روی نمونهٔ type کار نمیکنند را میتوان static علامتگذاری کرد. برای ارجاع به یک static member از بیرون type آن، بهجای یک instance نام type را مشخص میکنید. نمونهٔ آن متد WriteLine است
از کلاس Console. چون این متد static است، Console.WriteLine() را فراخوانی میکنیم، نه new Console().WriteLine().
(کلاس Console در واقع بهصورت static class اعلان شده است؛ یعنی همهٔ memberهای آن static هستند و هرگز نمیتوانید نمونهای از Console بسازید.)
در کد زیر، instance field به نام Name به یک نمونهٔ خاص از Panda مربوط است، در حالی که Population به مجموعهٔ همهٔ نمونههای Panda مربوط میشود. دو نمونه از Panda میسازیم، نامهای آنها را چاپ میکنیم و سپس کل جمعیت را چاپ میکنیم:
Panda p1 = new Panda ("Pan Dee");
Panda p2 = new Panda ("Pan Dah");
Console.WriteLine (p1.Name); // Pan Dee
Console.WriteLine (p2.Name); // Pan Dah
Console.WriteLine (Panda.Population); // 2
public class Panda
{
public string Name; // Instance field
public static int Population; // Static field
public Panda (string n) // Constructor
{
Name = n; // Assign the instance field
Population = Population + 1; // Increment the static Population field
}
}
تلاش برای ارزیابی p1.Population یا Panda.Name یک خطای زمان کامپایل ایجاد میکند.
کلیدواژهٔ public
کلیدواژهٔ public memberها را در معرض classهای دیگر قرار میدهد. در این مثال، اگر field Name در Panda با public علامتگذاری نمیشد، private بود و از بیرون class قابل دسترسی نبود. public کردن یک member، شیوهٔ ارتباط یک type است: «این همان چیزی است که میخواهم typeهای دیگر ببینند — هر چیز دیگری جزئیات پیادهسازی خصوصی خود من است.» در اصطلاح شیءگرایی میگوییم memberهای public، memberهای private کلاس را کپسوله میکنند.
تعریف namespaceها
بهویژه در برنامههای بزرگتر، منطقی است typeها را در namespaceها سازماندهی کنیم. در ادامه روش تعریف کلاس Panda درون namespaceای به نام Animals آمده است:
using System;
using Animals;
Panda p = new Panda ("Pan Dee");
Console.WriteLine (p.Name);
namespace Animals
{
public class Panda
{
...
}
}
در این مثال، namespace Animals را نیز import کردیم تا top-level statementهای ما بتوانند بدون qualification به typeهای آن دسترسی داشته باشند. بدون این import، باید چنین مینوشتیم:
Animals.Panda p = new Animals.Panda ("Pan Dee");
namespaceها را در پایان این فصل با جزئیات پوشش میدهیم (بخش «Namespaces» در صفحهٔ 95 را ببینید).
تعریف متد Main
همهٔ مثالهای ما تا اینجا از top-level statementها استفاده کردهاند (قابلیتی که در C# 9 معرفی شد).
بدون top-level statementها، یک برنامهٔ سادهٔ console یا Windows به شکل زیر است:
using System;
class Program
{
static void Main() // Program entry point
{
int x = 12 * 30;
Console.WriteLine (x);
}
}
در نبود top-level statementها، C# بهدنبال متدی static به نام Main میگردد که entry point میشود. متد Main میتواند درون هر classی تعریف شود (و فقط یک متد Main میتواند وجود داشته باشد).
متد Main میتواند بهصورت اختیاری یک عدد صحیح (بهجای void) بازگرداند تا مقداری را به محیط اجرا برگرداند (که معمولاً مقدار غیرصفر نشاندهندهٔ خطاست). متد Main همچنین میتواند بهصورت اختیاری آرایهای از رشتهها را بهعنوان parameter بپذیرد (که با هر آرگومانی که به executable ارسال شده است پر میشود). برای مثال:
static int Main (string[] args) {...}
(متد Main همچنین میتواند برای پشتیبانی از برنامهنویسی ناهمگام، که آن را در فصل 14 پوشش میدهیم، بهصورت async اعلان شود و Task یا Task<int> بازگرداند.)
Top-Level Statementها
top-level statementها (معرفیشده در C# 9) اجازه میدهند از سربار یک متد static به نام Main و class حاوی آن پرهیز کنید. فایلی با top-level statementها از سه بخش، به این ترتیب، تشکیل میشود:
- (اختیاری) دستورهای
using - مجموعهای از statementها که میتوانند بهصورت اختیاری با اعلان methodها ترکیب شوند
- (اختیاری) اعلانهای type و namespace
برای مثال:
using System; // Part 1
Console.WriteLine ("Hello, world"); // Part 2
void SomeMethod1() { ... } // Part 2
Console.WriteLine ("Hello again!"); // Part 2
void SomeMethod2() { ... } // Part 2
class SomeClass { ... } // Part 3
namespace SomeNamespace { ... } // Part 3
چون CLR صریحاً از top-level statementها پشتیبانی نمیکند، کامپایلر کد شما را تقریباً به شکل زیر ترجمه میکند:
using System; // Part 1
static class Program$ // Special compiler-generated name
{
static void Main$ (string[] args) // Compiler-generated name
{
Console.WriteLine ("Hello, world"); // Part 2
void SomeMethod1() { ... } // Part 2
Console.WriteLine ("Hello again!"); // Part 2
void SomeMethod2() { ... } // Part 2
}
}
class SomeClass { ... } // Part 3
namespace SomeNamespace { ... } // Part 3
توجه کنید که همهچیز در بخش 2 داخل متد اصلی wrap شده است. این یعنی SomeMethod1 و SomeMethod2 مانند متدهای local عمل میکنند. پیامدهای کامل را در «Local methods» در صفحهٔ 106 بررسی میکنیم؛ مهمترین آنها این است که متدهای local (مگر اینکه static اعلان شوند) میتوانند به متغیرهایی که در متد حاوی آنها اعلان شدهاند دسترسی داشته باشند:
int x = 3;
LocalMethod();
void LocalMethod() { Console.WriteLine (x); } // We can access x
پیامد دیگر این است که متدهای top-level از classها یا typeهای دیگر قابل دسترسی نیستند.
top-level statementها میتوانند بهصورت اختیاری یک مقدار integer به فراخواننده بازگردانند و به متغیر «جادویی» از نوع string[] با نام args دسترسی داشته باشند که متناظر با آرگومانهای خط فرمان فرستادهشده توسط فراخواننده است.
چون یک برنامه فقط میتواند یک entry point داشته باشد، در یک project C# حداکثر یک فایل میتواند top-level statement داشته باشد.
Typeها و تبدیلها (Types and Conversions)
C# میتواند میان نمونههای typeهای سازگار تبدیل انجام دهد. یک conversion همیشه از یک مقدار موجود، مقدار جدیدی ایجاد میکند. conversionها میتوانند implicit یا explicit باشند: conversionهای implicit بهطور خودکار رخ میدهند و conversionهای explicit به cast نیاز دارند.
در مثال زیر، یک int را بهصورت implicit به type long تبدیل میکنیم (که ظرفیت بیتی آن دو برابر int است) و یک int را بهصورت explicit به type short cast میکنیم (که ظرفیت بیتی آن نصف int است):
int x = 12345; // int is a 32-bit integer
long y = x; // Implicit conversion to 64-bit integer
short z = (short)x; // Explicit conversion to 16-bit integer
conversionهای implicit هنگامی مجازند که هر دو شرط زیر برقرار باشند:
- کامپایلر بتواند تضمین کند که همیشه موفق خواهند شد.
- در conversion هیچ اطلاعاتی از دست نرود.1
در مقابل، conversionهای explicit هنگامی لازماند که یکی از شرایط زیر برقرار باشد:
- کامپایلر نتواند تضمین کند که همیشه موفق خواهند شد.
- ممکن باشد در conversion اطلاعاتی از دست برود.
(اگر کامپایلر بتواند تعیین کند که یک conversion همیشه شکست خواهد خورد، هر دو نوع conversion ممنوعاند. conversionهایی که genericها را درگیر میکنند نیز میتوانند در شرایطی شکست بخورند — بخش «Type Parameters and Conversions» در صفحهٔ 166 را ببینید.)
Value Typeها در برابر Reference Typeها
همهٔ typeهای C# در یکی از دستههای زیر قرار میگیرند:
- Value typeها
- Reference typeها
- Generic type parameterها
- Pointer typeها
value typeها بیشتر typeهای built-in (بهطور مشخص همهٔ typeهای عددی، type char و type bool) و همچنین typeهای سفارشی struct و enum را دربر میگیرند.
reference typeها همهٔ typeهای class، array، delegate و interface را دربر میگیرند. (این شامل type از پیش تعریفشدهٔ string نیز میشود.)
تفاوت بنیادی value typeها و reference typeها در نحوهٔ مدیریت آنها در حافظه است.
Value typeها
محتوای یک variable یا constant از نوع value type صرفاً یک value است. برای مثال، محتوای value type داخلی int برابر 32 بیت داده است.
میتوانید با کلیدواژهٔ struct یک value type سفارشی تعریف کنید (شکل 2-1):
public struct Point { public int X; public int Y; }
یا کوتاهتر:
public struct Point { public int X, Y; }
شکل 2-1. یک نمونهٔ value type در حافظه
assignment یک نمونهٔ value type همیشه خود نمونه را کپی میکند؛ برای مثال:
Point p1 = new Point();
p1.X = 7;
Point p2 = p1; // Assignment causes copy
Console.WriteLine (p1.X); // 7
Console.WriteLine (p2.X); // 7
p1.X = 9; // Change p1.X
Console.WriteLine (p1.X); // 9
Console.WriteLine (p2.X); // 7
شکل 2-2 نشان میدهد که p1 و p2 فضای ذخیرهسازی مستقل دارند.
شکل 2-2. assignment یک نمونهٔ value type را کپی میکند
Reference typeها
یک reference type از value type پیچیدهتر است و دو بخش دارد: یک object و reference به آن object. محتوای variable یا constant از نوع reference type، reference به objectای است که value را در خود دارد. در ادامه type Point از مثال قبلی را بهجای struct به شکل class بازنویسی کردهایم (شکل 2-3):
public class Point { public int X, Y; }
شکل 2-3. یک نمونهٔ reference type در حافظه
assignment یک variable از نوع reference type، reference را کپی میکند نه نمونهٔ object را. این کار به چند variable اجازه میدهد به همان object اشاره کنند — چیزی که بهطور عادی با value typeها ممکن نیست. اگر مثال قبلی را تکرار کنیم، اما این بار Point یک class باشد، عملی روی p1 بر p2 نیز اثر میگذارد:
Point p1 = new Point();
p1.X = 7;
Point p2 = p1; // Copies p1 reference
Console.WriteLine (p1.X); // 7
Console.WriteLine (p2.X); // 7
p1.X = 9; // Change p1.X
Console.WriteLine (p1.X); // 9
Console.WriteLine (p2.X); // 9
شکل 2-4 نشان میدهد که p1 و p2 دو reference هستند که به همان object اشاره میکنند.
شکل 2-4. assignment یک reference را کپی میکند
Null
میتوان literal null را به یک reference نسبت داد؛ یعنی reference به هیچ objectی اشاره نمیکند:
Point p = null;
Console.WriteLine (p == null); // True
// The following line generates a runtime error
// (a NullReferenceException is thrown):
Console.WriteLine (p.X);
class Point {...}
در مقابل، یک value type در حالت عادی نمیتواند مقدار null داشته باشد:
Point p = null; // Compile-time error
int x = null; // Compile-time error
struct Point {...}
سربار ذخیرهسازی
نمونههای value type دقیقاً به اندازهٔ حافظهای که برای ذخیرهٔ fieldهایشان لازم است فضا اشغال میکنند. در این مثال، Point هشت byte حافظه میگیرد:
struct Point
{
int x; // 4 bytes
int y; // 4 bytes
}
reference typeها به تخصیصهای حافظهٔ جداگانه برای reference و object نیاز دارند. object به اندازهٔ fieldهایش، بهعلاوهٔ سربار مدیریتی اضافی، byte مصرف میکند. مقدار دقیق این سربار ذاتاً جزئیات خصوصی پیادهسازی runtime .NET است، اما حداقل 8 byte است که برای ذخیرهٔ یک کلید به type object و نیز اطلاعات موقتی مانند وضعیت lock آن برای multithreading و یک flag برای نشاندادن اینکه garbage collector آن را از جابهجایی ثابت کرده است استفاده میشود. هر reference به یک object نیز بسته به اینکه runtime .NET روی پلتفرم 32 یا 64 بیتی اجرا میشود، 4 یا 8 byte اضافی نیاز دارد.
ردهبندی typeهای از پیش تعریفشده
typeهای predefined در C# به شکل زیرند:
Value typeها
- عددی
- عدد صحیح signed:
sbyte، short، int، long - عدد صحیح unsigned:
byte، ushort، uint، ulong - عدد حقیقی:
float، double، decimal
- منطقی:
bool - نویسه:
char
Reference typeها
typeهای predefined در C# نام مستعار typeهای .NET در namespace System هستند. میان دو statement زیر فقط تفاوت نحوی وجود دارد:
int i = 5;
System.Int32 i = 5;
مجموعهٔ value typeهای از پیش تعریفشده، بهجز decimal، در CLR با نام primitive type شناخته میشوند. دلیل این نام آن است که مستقیماً از طریق instructionهای کد کامپایلشده پشتیبانی میشوند و این معمولاً به پشتیبانی مستقیم پردازندهٔ زیربنایی منتهی میشود؛ برای مثال:
// Underlying hexadecimal representation
int i = 7; // 0x7
bool b = true; // 0x1
char c = 'A'; // 0x41
float f = 0.5f; // uses IEEE floating-point encoding
typeهای System.IntPtr و System.UIntPtr نیز primitive هستند (فصل 24 را ببینید).
Typeهای عددی (Numeric Types)
C# typeهای عددی از پیش تعریفشدهٔ نشاندادهشده در جدول 2-1 را دارد.
جدول 2-1. typeهای عددی از پیش تعریفشده در C#| دسته | type در C# | type در System | پسوند | اندازه | بازه |
| صحیح — signed | sbyte | SByte | | 8 بیت | −27 تا 27−1 |
short | Int16 | | 16 بیت | −215 تا 215−1 |
int | Int32 | | 32 بیت | −231 تا 231−1 |
long | Int64 | L | 64 بیت | −263 تا 263−1 |
nint | IntPtr | | 32/64 بیت | |
| صحیح — unsigned | byte | Byte | | 8 بیت | 0 تا 28−1 |
ushort | UInt16 | | 16 بیت | 0 تا 216−1 |
uint | UInt32 | U | 32 بیت | 0 تا 232−1 |
ulong | UInt64 | UL | 64 بیت | 0 تا 264−1 |
nuint | UIntPtr | | 32/64 بیت | |
| حقیقی | float | Single | F | 32 بیت | ± (~10−45 تا 1038) |
double | Double | D | 64 بیت | ± (~10−324 تا 10308) |
decimal | Decimal | M | 128 بیت | ± (~10−28 تا 1028) |
در میان typeهای صحیح، int و long شهروندان درجهیکاند و هم C# و هم runtime آنها را ترجیح میدهند. typeهای صحیح دیگر معمولاً برای interoperability یا زمانی استفاده میشوند که بهرهوری فضا اهمیت حیاتی دارد. typeهای integer با اندازهٔ بومی nint و nuint هنگام کار با pointerها بیشترین کاربرد را دارند، بنابراین آنها را در فصلی بعد توضیح خواهیم داد (بخش «Native-Sized Integers» در صفحهٔ 266 را ببینید).