مقدمه

یادآوری می‌کنم که نوع‌های تعریف‌شده در CTS می‌توانند به فرم کلاس (class)، ساختار (struct)، شمارشی (enum)، اینترفیس (interface) و نماینده (delegate) باشند. در CTS ساز و کار تعریف نوع‌های سفارشی هم مستند شده و می‌توانیم نوع‌هایی به فرم هر یک از موارد پنجگانه‌ی بالا تعریف کنیم. اما تعدادی نوع پایه یا Primitive وجود دارد که در اکثر برنامه‌ها به آنها نیاز داریم. این نوع‌ها برای کار با مقادیر عددی، رشته‌های متنی و مقادیر بولین تدارک دیده شده‌اند و از طریق فضای نام System در دسترس ما قرار دارند. در این درس، این نوع‌های Primitive را معرفی می‌کنیم.

نوع‌های فضای نام System

همانطور که در مقدمه اشاره شد، فضای نام System شامل چند نوع داده‌ی پایه‌ای و پرکاربرد است که حتی در برنامه‌های خیلی کوچک و ساده هم به آنها نیاز داریم. این نوع‌ها را می‌توانید در جدول زیر ببینید.

کلمه کلیدی C# نوع متناظر در System مقادیر سازگاری با CLS
sbyte System.SByte اعداد صحیح از -27 تا 27 - 1 خیر
byte System.Byte اعداد صحیح از صفر تا 28 - 1 بله
short System.Int16 اعداد صحیح از -215 تا 215 - 1 بله
ushort System.UInt16 اعداد صحیح از صفر تا 216 - 1 خیر
int System.Int32 اعداد صحیح از -231 تا 231 - 1 بله
uint System.UInt32 اعداد صحیح از صفر تا 232 - 1 خیر
long System.Int64 اعداد صحیح از -263 تا 263 - 1 بله
ulong System.UInt64 اعداد صحیح از صفر تا 264 - 1 خیر
float System.Single اعداد اعشاری 32 بیتی بله
double System.Double اعداد اعشاری 64 بیتی بله
decimal System.Decimal اعداد اعشاری 128 بیتی بله
bool System.Boolean مقادیر true و false بله
char System.Char کاراکترهای یونیکد بله
string System.String رشته‌های کاراکتری بله
object System.Object مقادیر از هر نوع بله

همانطور که در جدول بالا قید شده، نوع‌هایی که برای اعداد صحیحِ فاقد علامت (اعداد مثبت) در نظر گرفته شده‌اند، بخشی از استاندارد CLS نیستند. بنابراین، کدی که از این نوع‌ها استفاده کرده باشد، بین زبان‌های .NET پرتابل نیست و تضمینی برای امکان توسعه‌ی آن با زبان‌هایی غیر از C# وجود ندارد.

نوع‌های جدول بالا را می‌توان در ۵ گروه قرار داد. هشت نوع اول برای کار با اعداد صحیح، سه نوع بعد برای اعداد اعشاری، نوع Boolean برای مقادیر بولین و دو نوع Char و String برای کار با متن کاربرد دارند. نوع Object هم یک نوع خاص است که در ریشه‌ی درخت سلسله‌مراتبی نوع‌های .NET قرار دارد. در ادامه، این گروه‌ها را با جزئیات بیشتر مورد بررسی قرار می‌دهیم. اما قبل از آن، لازم است یک شفاف‌سازی در مورد نام نوع‌های جدول بالا داشته باشیم.

تأکید و تکرار می‌کنم که نوع‌های بالا در فضای نام System تعریف شده‌اند و نام اصلی آنها چیزی است که در ستون دوم جدول بالا می‌بینید. اما عبارات موجود در ستون اول، نام‌های مستعاری هستند که برای دسترسیِ راحت‌تر به این نوع‌ها تدارک دیده شده‌اند. طبیعتاً ما در کدها از این نام‌های مستعار استفاده می‌کنیم نه نام کامل نوع‌ها. اما باید متوجه این تفاوت باشید. برای مثال، باید بدانید که String یک نوع است اما string یک کلمه کلیدی برای ارجاع به این نوع. با این حال، برای احتراز از ایجاد ابهام، با هم توافق می‌کنیم که از این به بعد در توضیحاتمان هم از نام مستعار نوع‌ها استفاده کنیم. بنابراین، به‌جای عبارت نوع String از نوع string و به جای عبارت نوع Single از نوع float استفاده می‌کنیم.

اعداد صحیح در C#

می‌دانیم که یک عدد صحیح یا Integer عددی است که فاقد قسمت اعشار است. در C# چندین نوع برای کار با اعداد صحیح در نظر گرفته شده که هر نوع، بازه‌ای از اعداد صحیح را شامل است. نوع‌هایی که با u شروع می‌شوند (و نوع byte)، نوع‌های unsigned یا فاقد علامت هستند و شامل اعداد صحیح نامنفی هستند. جدول زیر نوع‌های مربوط به اعداد صحیح را به همراه سایز و بازه‌ی مقادیر هر نوع نشان می‌دهد.

نوع سایز بازه مقادیر
byte 8-bit 0 to 255
sbyte 8-bit -128 to 127
short 16-bit -32,768 to 32,767
ushort 16-bit 0 to 65,535
int 32-bit −2,147,483,648 to 2,147,483,647
uint 32-bit 0 to 4,294,967,295
long 64-bit −9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
ulong 64-bit 0 to 18,446,744,073,709,551,615

به طور کلی، یک نوعِ صحیحِ n بیتیِ فاقد علامت، شامل اعداد بین صفر تا 2n - 1 است و یک نوع صحیحِ n بیتیِ دارای علامت، شامل اعداد بین -2n-1 و 2n-1 - 1 است. پس، عدد 20 رقمی که در آخرین سطر جدول دیده می‌شود، برابر با 264 - 1 است.

مقادیری که مستقیماً در برنامه وارد می‌شوند، لیترال (literal) نامیده می‌شوند. لیترال‌های صحیح از نوع پیش‌فرض int در نظر گرفته می‌شوند.

روش‌های نمایش اعداد صحیح

در C# لیترال‌های صحیح در مبنای 10 در نظر گرفته می‌شوند و این کاملاً مطابق انتظار است. اما امکان نمایش اعداد در مبناهای 2 و 16 هم وجود دارد. اعداد در مبنای 2 که باینری هم نامیده می‌شوند، فقط از ارقام 0 و 1 تشکیل می‌شوند و اعداد در مبنای 16 که هگزادسیمال هم گفته می‌شوند، از 16 رقم شامل ارقام صفر تا 9 و کاراکترهای a تا f تشکیل می‌شوند.

برای نمایش اعداد باینری، باید پیشوند 0b یا 0B و برای اعداد هگزادسیمال، پیشوند 0x یا 0X را قبل از عدد قرار داد. به علاوه، ارقام یک عدد را می‌توانیم با استفاده از کاراکتر _ از هم جدا کنیم. از C# 7 به بعد، کامپایلر این زبان، کاراکتر _ را در بین اعداد نادیده می‌گیرد.

Copy Icon Program.cs
int billion = 1_000_000_000;

int x = 255;
int y = 0b1111_1111;
int z = 0xff;
            
Console.WriteLine(billion);
Console.WriteLine(x);
Console.WriteLine(y);
Console.WriteLine(z);

اگر این برنامه را با استفاده از کامند dotnet run اجرا کنید، نتیجه‌ی زیر را دریافت خواهید کرد.

1000000000
255
255
255
          

کلمات کلیدی checked و unchecked

وقتی مقداری خارج از بازه‌ی مقادیر یک نوع را به متغیری از آن نوع اختصاص دهیم، چه اتفاقی می‌افتد؟
البته که چنین تخصیصی به طور مستقیم ممکن نیست و بدیهی است که هر دو گزاره‌ی زیر با خطا همراه است.

Copy Icon Program.cs
byte b = 300;
int i = 3_000_000_000;

اما کامپایلر همیشه هم اینقدر قاطع نیست. با وجودی که ظاهراً مقدار متغیر z در کد زیر با مقدار متغیر i در کد بالا یکی است، اما این بار کامپایلر خطایی تولید نمی‌کند.

Copy Icon Program.cs
int x = 2_000_000_000;
int y = 1_000_000_000;
int z = x + y;

قاطعیت کامپایلر در کد قبل و انفعالش در این کد، کاملاً منطقی است. در کد قبل، کامپایلر به این دلیل قاطعانه در سوتش می‌دمد که با هیچ ابهامی روبرو نیست. ما سعی کرده‌ایم صراحتاً و مستقیماً یک مقدار خارج از بازه‌ی مقادیر نوع int را به یک متغیر int بدهیم. اما در مورد کد بعدی، کامپایلر با ابهام مواجه است؛ چون نمی‌تواند متغیرهای x و y را ارزیابی کند و آخرین مقدار آنها را بداند. چیزی که کامپایلر در گزاره‌ی آخر می‌بیند این است که مجموع دو مقدار int در یک مقدار int دیگر ذخیره شده که هیچ مشکلی ندارد. برای اینکه ببینیم چه مقداری در متغیر z قرار گرفته، مقدار این متغیر را در کنسول چاپ می‌کنیم.

Copy Icon Program.cs
int x = 2_000_000_000;
int y = 1_000_000_000;
int z = x + y;
            
Console.WriteLine(z);

عددی که در خروجی خواهید دید -1294967296 است که حاصل سرریز عدد 3_000_000_000 از بازه‌ی مقادیر int است. در واقع، این عدد از کم کردن طول بازه‌ی مقادیر int از عدد 3_000_000_000 بدست می‌آید. بنابراین، اتفاقی که هنگام تخصیص مقادیر خارج از بازه‌ی مقادیر یک نوع به یک متغیر از آن نوع رخ می‌دهد، سرریز یا Overflow است.

اما این فقط رفتار پیش‌فرض است و در صورت تمایل، می‌توانیم این رفتار را تغییر دهیم و ترتیبی دهیم که در اینگونه موارد، به‌جای سرریز، یک خطای زمان اجرا رخ دهد تا ما را از این موضوع باخبر کند. کلمات کلیدی checked و unchecked به همین منظور معرفی شده‌اند. کد بالا را به صورت زیر ویرایش کنید.

Copy Icon Program.cs
int x = 2_000_000_000;
int y = 1_000_000_000;
int z = checked(x + y);

Console.WriteLine(z);

با اجرای برنامه، خواهید دید که یک خطای زمان اجرا رخ داده و پیغام زیر نمایش داده می‌شود.

Arithmetic operation resulted in an overflow.

اگر بخواهیم کلمه کلیدی checked را روی یک مجموعه از دستورات اعمال کنیم، می‌توانیم یک بلاک برای checked ایجاد کنیم.

Copy Icon Program.cs
checked
{
  int x = 2_000_000_000;
  int y = 1_000_000_000;
  int z = x + y;
  Console.WriteLine(z);
}

در صورت تمایل، می‌توانیم ترتیبی دهیم که در کل پروژه و بدون نیاز به استفاده از کلمه کلیدی checked، همه‌ی محاسبات چک شوند و در صورت بروز سرریز، خطای زمان اجرا گزارش شود. برای این کار، باید یک پراپرتی با نام CheckForOverflowUnderflow مانند زیر به فایل پروژه اضافه کنیم.

Copy Icon SimpleCsharpApp.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <CheckForOverflowUnderflow>true</CheckForOverflowUnderflow>
  </PropertyGroup>
            
</Project>

حالا اگر برنامه‌ی بالا را بدون استفاده از کلمه کلیدی checked اجرا کنیم، باز هم خطای زمان اجرای ناشی از سرریز را دریافت خواهیم کرد.

در این وضعیت، اگر بخواهیم بخشی از کدها را از بررسی برای سرریز معاف کنیم، باید از کلمه کلیدی unchecked استفاده کنیم. روش استفاده از unchecked کاملاً مشابه checked است.

اعداد اعشاری در C#

در C# سه نوع float، double و decimal برای کار با اعداد اعشاری در نظر گرفته شده است. تفاوت این نوع‌ها در دقت آنهاست. نوع float دارای 32 بیت است، نوع double دارای 64 بیت سایز است و از دقت مضاعف برخوردار است و نوع decimal که 128 بیت فضا دارد، دقت بالاتری دارد و برای کنترل خطای تقریب در اعداد اعشاری، طراحی شده است.

لیترال‌های اعشاری از نوع double در نظر گرفته می‌شوند و اگر بخواهیم یک لیترال اعشاری را در یک متغیر از نوع float یا decimal ذخیره کنیم، باید یک پسوند f یا m به عدد اضافه کنیم. به گزاره‌‌های زیر نگاه کنید.

Copy Icon Program.cs
double x = 1.25;
float y = 1.25f;
decimal z = 1.25m;

در گزاره‌ی اول، یک لیترال اعشاری که همانطور که گفتیم، از نوع double در نظر گرفته می‌شود، در متغیری از همین نوع ذخیره شده است. اما در گزاره‌ی دوم، اگر پسوند f را از عدد بگیریم، با خطا مواجه می‌شویم و برنامه کامپایل نمی‌شود. علت بروز خطا این است که در طرفین عملگر تخصیص ( = ) باید مقادیر هم‌نوع وجود داشته باشد اما ما در سمت چپ یک متغیر از نوع float داریم و در سمت راست یک لیترال که از نوع double است. با اضافه‌کردن پسوند f به عدد، اتفاقی که رخ می‌دهد این است که عدد مورد نظر به یک float تبدیل می‌شود. پسوند m نیز کار تبدیل عدد double به decimal را انجام می‌دهد.

اینجا یکی از معدود جاهایی است که کامپایلر C# ماهیت case sensitive خود را فراموش می‌کند و اجازه می‌دهد که برای پسوند لیترال‌های float و decimal به‌جای f و m از F و M هم استفاده کنیم.

حالا شاید این سؤال برایتان پیش آمده باشد که: چرا این مسئله‌ی یکسان نبودن نوع مقادیر طرفین عملگر تخصیص، در مورد اعداد صحیح وجود نداشت؟ مثلاً چرا گزاره‌ای مثل byte b = 200; منجر به بروز خطا نمی‌شود؟ پاسخ این سؤال را در درس بعدی دریافت خواهید کرد.

خطای تقریب برای اعداد اعشاری

همه‌ی زبان‌های برنامه‌نویسی و از جمله C# با مسئله‌ای به نام خطای تقریب اعداد اعشاری مواجه هستند. این مسئله به پیاده‌سازی درونی اعداد در حافظه‌ی کامپیوتر برمی‌گردد که امکان نمایش دقیق اعدادی مثل 0.1 و 0.2 را غیرممکن می‌کند. در حقیقت، چیزی که به جای مقدار دقیق این عددها ذخیره می‌شود، یک تقریب از آنهاست. به مثال زیر توجه کنید.

Copy Icon Program.cs
double d = 0.1 + 0.2;
Console.WriteLine(d); // 0.30000000000000004

اگر این برنامه را اجرا کنید، خواهید دید که مقدار 0.3000000000000004 در متغیر d ذخیره شده که با عدد مورد انتظار ما یعنی 0.3 متفاوت است؛ اگرچه خیلی به آن نزدیک است. این مسئله در مورد نوع float هم وجود دارد و عدم توجه به آن، به ویژه در مقایسه‌ها، می‌تواند مشکل‌ساز شود.

اما نوع decimal طوری طراحی شده که این مشکل را نداشته باشد و نتیجتاً می‌توانیم در مواردی مثل محاسبات مالی که به دقت زیادی نیاز است، از این نوع استفاده کنیم. اگر کد زیر را اجرا کنید، خواهید دید که این بار مقدار مورد انتظارِ 0.3 نمایش داده می‌شود.

Copy Icon Program.cs
decimal d = 0.1m + 0.2m;
Console.WriteLine(d);

مقادیر decimal نسبت به float و double فضای بیشتری اشغال می‌کنند و اعمال روی آنها کندتر انجام می‌شود. بنابراین، در استفاده از آن نباید افراط کرد. بعداً خواهیم دید که با استفاده از متدهایی مثل double.Round() هم می‌توان مشکل خطای تقریب را بدون توسل به نوع decimal کنترل کرد.

مقادیر بولین در C#

نوع bool در C# شامل دو مقدار true و false است و بنابراین، یک متغیر از نوع bool فقط می‌تواند یکی از این دو مقدار را دریافت کند.

Copy Icon Program.cs
bool isRaining = false; 
bool isSunny = true;

هنگام بررسی عملگرهای مقایسه‌ای و گزاره‌های شرطی در درس‌های آینده، کاربردهای مقادیر بولین را خواهیم دید.

رشته‌های متنی در C#

در C# دو نوع char و string برای کار با مقادیر متنی در نظر گرفته شده است. نوع char شامل کاراکترهای یونیکد است و نوع string برای نمایش رشته‌های کاراکتری کاربرد دارد. از آپسترف یا کوتیشن تکی برای نمایش کاراکترها و از کوتیشن جفتی برای نمایش رشته‌ها استفاده می‌شود.

Copy Icon Program.cs
char ch = 'Z';
string str = "Hello";
            
Console.WriteLine(ch);
Console.WriteLine(str);

یک رشته دنباله‌ای از کاراکترهاست و دسترسی به هر کاراکتر رشته از طریق اندیس آن کاراکتر ممکن است. اندیس اولین کاراکتر یک رشته برابر با صفر و اندیس دومین کاراکتر برابر با 1 و به طور کلی، اندیس کاراکتر n-ام برابر با n-1 است.

Copy Icon Program.cs
string message = "Hello";
char second = message[1];  
            
Console.WriteLine(second);  // e 

کاراکترهای گریز و رشته‌های تحت‌الفظی

چند کاراکتر خاص داریم که از آنها با نام کاراکترهای گریز (escape characters) یاد می‌شود. برای درج این کاراکترها باید آنها را بعد از یک کاراکتر بک‌اسلش وارد کرد. مهمترین کاراکترهای گریز عبارتند از:

  • کاراکتر \n برای ایجاد یک خط جدید
  • کاراکتر \t برای درج Tab
  • کاراکتر \" برای درج کوتیشن
  • کاراکتر \' برای درج آپسترف
  • کاراکتر \\ برای درج بک‌اسلش
  • کاراکتر \u برای درج کاراکترها با استفاده از کد آنها در یونیکد

کاراکتر \u به ما امکان می‌دهد که هر کاراکتر یونیکدی را در رشته درج کنیم. کافیست کد عددی (codepoint) متناظر با کاراکتر را بعد از \u در قالب چهار کاراکتر وارد کنیم.

Copy Icon Program.cs
string languages = "Programming Languages:\n\tPython\n\tJava\n\tJavaScript";
Console.WriteLine(languages);
            
string quote = "George Orwell says: \"Words can be like X-rays.\"";
Console.WriteLine(quote);
            
string path = "Location is C:\\Program Files\\Adobe\\Photoshop";
Console.WriteLine(path);
            
string fact = "\u03C0 \u2248 3.14";
Console.WriteLine(fact);

با قرار دادن یک کاراکتر @ قبل از رشته، می‌توانیم آن را به یک رشته‌ی تحت‌الفظی (verbatim string) تبدیل کنیم. در این رشته‌ها، کاراکترهای گریز عمل نمی‌کنند و رشته به همان صورتی که دیده می‌شود، در خروجی چاپ می‌شود.

Copy Icon Program.cs
string languages = @"
Programming Languages: 
  Python
  Java 
  JavaScript
";
Console.WriteLine(languages);
            
string path = @"Location is C:\Program Files\Adobe\Photoshop";
Console.WriteLine(path);

درج مقادیر و عبارات در رشته‌ها

برای درج مقادیر و عبارت‌ها در رشته‌ها می‌توانیم از سینتکسی به نام String Interpolation استفاده کنیم. در این سینتکس، یک کاراکتر $ قبل از رشته قرار داده می‌شود و متغیر یا عبارت مورد نظر در رشته درون یک جفت آکلاد قرار داده می‌شود. به این ترتیب، می‌توانیم رشته‌ها و عبارات را با هم ترکیب کنیم.

Copy Icon Program.cs
string name = "Alice";
int age = 30;
string message = $"Hello, my name is {name} and I am {age} years old.";

Console.WriteLine(message);

نتیجه‌ی اجرای این کد به صورت زیر خواهد بود.

Hello, my name is Alice and I am 30 years old.

درون آکلادها می‌توان هر عبارت معتبر C# مانند متغیرها، فراخوانی متدها و یا عبارات محاسباتی را قرار داد.

Copy Icon Program.cs
int x = 5;
int y = 10;
string result = $"The sum of {x} and {y} is {x + y}.";

Console.WriteLine(result);  // Output: The sum of 5 and 10 is 15.