فصل ۲: Typeهای عددی، Boolean، Stringها و Arrayها در C#
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
در میان typeهای عدد حقیقی، float و double typeهای ممیز شناور (floating-point types)2 نامیده میشوند و معمولاً برای محاسبات علمی و گرافیکی به کار میروند. type decimal معمولاً برای محاسبات مالی استفاده میشود؛ جایی که محاسبات دقیق در مبنای 10 و دقت بالا لازم است.
Literalهای عددی
literalهای typeهای صحیح میتوانند از نمایش دهدهی یا hexadecimal استفاده کنند؛ hexadecimal با پیشوند 0x مشخص میشود. برای مثال:
int x = 127;
long y = 0x7F;
میتوانید برای خواناترشدن یک literal عددی، در هر جای آن زیرخط قرار دهید:
میتوانید اعداد را با پیشوند 0b بهصورت binary مشخص کنید:
var b = 0b1010_1011_1100_1101_1110_1111;
literalهای حقیقی میتوانند از نمایش decimal و/یا exponential استفاده کنند:
double d = 1.5;
double million = 1E06;
استنباط type در literalهای عددی
بهطور پیشفرض، کامپایلر type یک literal عددی را یا double یا یک type صحیح استنباط میکند:
- اگر literal شامل نقطهٔ اعشار یا نماد نمایی (
E) باشد، type آن double است. - در غیر این صورت، type literal نخستین type از فهرست زیر است که مقدار literal در آن جا میگیرد:
int، uint، long و ulong.
برای مثال:
Console.WriteLine ( 1.0.GetType()); // Double (double)
Console.WriteLine ( 1E06.GetType()); // Double (double)
Console.WriteLine ( 1.GetType()); // Int32 (int)
Console.WriteLine ( 0xF0000000.GetType()); // UInt32 (uint)
Console.WriteLine (0x100000000.GetType()); // Int64 (long)
پسوندهای عددی
پسوندهای عددی type یک literal را صریحاً تعریف میکنند. پسوندها میتوانند با حروف کوچک یا بزرگ نوشته شوند و به شکل زیرند:
| پسوند/دسته | type در C# | مثال |
| F | float | float f = 1.0F; |
| D | double | double d = 1D; |
| M | decimal | decimal d = 1.0M; |
| U | uint | uint i = 1U; |
| L | long | long i = 1L; |
| UL | ulong | ulong i = 1UL; |
پسوندهای U و L بهندرت لازم میشوند، زیرا typeهای uint، long و ulong تقریباً همیشه یا قابل استنباطاند یا میتوانند بهصورت implicit از int تبدیل شوند:
long i = 5; // Implicit lossless conversion from int literal to long
پسوند D از نظر فنی زائد است، زیرا همهٔ literalهایی که نقطهٔ اعشار دارند بهصورت double استنباط میشوند. همچنین همیشه میتوانید به یک literal عددی نقطهٔ اعشار اضافه کنید:
پسوندهای F و M مفیدترین پسوندها هستند و هنگام مشخصکردن literalهای float یا decimal باید همیشه اعمال شوند. بدون پسوند F، خط زیر کامپایل نمیشود، زیرا 4.5 از نوع double استنباط میشود و تبدیل implicit به float ندارد:
همین اصل برای یک literal از نوع decimal نیز برقرار است:
decimal d = -1.23M; // Will not compile without the M suffix.
معنای conversionهای عددی را در بخش بعد با جزئیات توضیح میدهیم.
Conversionهای عددی
تبدیل میان typeهای صحیح
conversion میان typeهای صحیح زمانی implicit است که type مقصد بتواند هر مقدار ممکن type مبدأ را نمایش دهد. در غیر این صورت conversion صریح لازم است؛ برای مثال:
int x = 12345; // int is a 32-bit integer
long y = x; // Implicit conversion to 64-bit integral type
short z = (short)x; // Explicit conversion to 16-bit integral type
تبدیل میان typeهای ممیز شناور
یک float میتواند بهصورت implicit به double تبدیل شود، زیرا double میتواند هر مقدار ممکن float را نمایش دهد. تبدیل معکوس باید explicit باشد.
تبدیل میان typeهای ممیز شناور و صحیح
همهٔ typeهای صحیح میتوانند بهصورت implicit به همهٔ typeهای ممیز شناور تبدیل شوند:
تبدیل معکوس باید explicit باشد:
تبدیل implicit یک type صحیح بزرگ به یک type ممیز شناور، بزرگی مقدار را حفظ میکند اما گاهی دقت را از دست میدهد. دلیل آن این است که typeهای ممیز شناور همیشه دامنهٔ بزرگتری از typeهای صحیح دارند، اما ممکن است دقت کمتری داشته باشند. بازنویسی مثال با عددی بزرگتر این موضوع را نشان میدهد:
int i1 = 100000001;
float f = i1; // Magnitude preserved, precision lost
int i2 = (int)f; // 100000000
Conversionهای decimal
همهٔ typeهای صحیح میتوانند بهصورت implicit به type decimal تبدیل شوند، زیرا decimal میتواند هر مقدار ممکن از typeهای صحیح C# را نمایش دهد. همهٔ conversionهای عددی دیگر به decimal و از decimal باید explicit باشند، زیرا احتمال خارجشدن مقدار از بازه یا از دست رفتن دقت را ایجاد میکنند.
Operatorهای حسابی
operatorهای حسابی (+، -، *، /، %) برای همهٔ typeهای عددی، بهجز typeهای صحیح 8 و 16 بیتی، تعریف شدهاند:
+ | جمع |
- | تفریق |
* | ضرب |
/ | تقسیم |
% | باقیماندهٔ تقسیم |
Operatorهای افزایش و کاهش
operatorهای افزایش و کاهش (بهترتیب ++ و --) typeهای عددی را 1 واحد افزایش یا کاهش میدهند. بسته به اینکه مقدار variable را پیش یا پس از increment/decrement بخواهید، operator میتواند بعد یا قبل از variable قرار گیرد؛ برای مثال:
int x = 0, y = 0;
Console.WriteLine (x++); // Outputs 0; x is now 1
Console.WriteLine (++y); // Outputs 1; y is now 1
عملیات تخصصی روی typeهای صحیح
typeهای صحیح عبارتاند از int، uint، long، ulong، short، ushort، byte و sbyte.
تقسیم
عملیات تقسیم روی typeهای صحیح همیشه باقیمانده را حذف میکند (به سمت صفر گرد میکند). تقسیم بر variableای که مقدار آن صفر است در زمان اجرا خطا ایجاد میکند (DivideByZeroException):
int a = 2 / 3; // 0
int b = 0;
int c = 5 / b; // throws DivideByZeroException
تقسیم بر literal یا constant صفر یک خطای زمان کامپایل ایجاد میکند.
Overflow
در زمان اجرا، عملیات حسابی روی typeهای صحیح میتوانند overflow کنند. بهطور پیشفرض این اتفاق بیصدا رخ میدهد — هیچ exceptionای پرتاب نمیشود و نتیجه رفتاری «wraparound» نشان میدهد؛ گویی محاسبه روی type صحیح بزرگتری انجام شده و بیتهای پرارزش اضافی دور ریخته شدهاند. برای مثال، کمکردن یک واحد از کوچکترین مقدار ممکن int به بزرگترین مقدار ممکن int میرسد:
int a = int.MinValue;
a--;
Console.WriteLine (a == int.MaxValue); // True
Operatorهای بررسی overflow
operator checked به runtime دستور میدهد وقتی یک expression یا statement از نوع صحیح از حدود حسابی آن type فراتر رفت، بهجای overflow بیصدا یک OverflowException ایجاد کند. operator checked روی expressionهایی که شامل ++، --، +، - (دودویی و unary)، *، / و operatorهای conversion صریح میان typeهای صحیح هستند اثر میگذارد. بررسی overflow هزینهٔ کارایی اندکی دارد.
میتوانید checked را پیرامون یک expression یا یک statement block به کار ببرید:
int a = 1000000;
int b = 1000000;
int c = checked (a * b); // Checks just the expression.
checked // Checks all expressions
{ // in statement block.
...
c = a * b;
...
}
میتوانید بررسی overflow حسابی را برای همهٔ expressionهای یک برنامه به حالت پیشفرض تبدیل کنید؛ با انتخاب گزینهٔ «checked» در سطح project (در Visual Studio به Advanced Build Settings بروید). اگر سپس لازم باشد بررسی overflow را فقط برای expressionها یا statementهای خاصی غیرفعال کنید، میتوانید این کار را با operator unchecked انجام دهید. برای مثال، کد زیر حتی اگر گزینهٔ «checked» پروژه انتخاب شده باشد exception پرتاب نمیکند:
int x = int.MaxValue;
int y = unchecked (x + 1);
unchecked { int z = x + 1; }
بررسی overflow برای expressionهای constant
صرفنظر از تنظیم «checked» پروژه، expressionهایی که در زمان کامپایل ارزیابی میشوند همیشه از نظر overflow بررسی میشوند — مگر اینکه operator unchecked را اعمال کنید:
int x = int.MaxValue + 1; // Compile-time error
int y = unchecked (int.MaxValue + 1); // No errors
Operatorهای bitwise
C# از operatorهای bitwise زیر پشتیبانی میکند:
| Operator | معنا | expression نمونه | نتیجه |
|---|
~ | Complement | ~0xfU | 0xfffffff0U |
& | And | 0xf0 & 0x33 | 0x30 |
| | Or | 0xf0 | 0x33 | 0xf3 |
^ | Exclusive Or | 0xff00 ^ 0x0ff0 | 0xf0f0 |
<< | Shift left | 0x20 << 2 | 0x80 |
>> | Shift right | 0x20 >> 1 | 0x10 |
>>> | Unsigned shift right | int.MinValue >>> 1 | 0x40000000 |
operator شیفت به راست (>>) هنگام کار روی اعداد صحیح signed، بیت با مرتبهٔ بالا را تکرار میکند؛ در حالی که operator شیفت به راست unsigned (>>>) این کار را نمیکند.
Typeهای صحیح 8 و 16 بیتی
typeهای صحیح 8 و 16 بیتی عبارتاند از byte، sbyte، short و ushort. این typeها operatorهای حسابی اختصاصی خود را ندارند؛ بنابراین C# در صورت نیاز آنها را بهصورت implicit به typeهای بزرگتر تبدیل میکند. این موضوع هنگام تلاش برای assignment نتیجه به یک type صحیح کوچک میتواند خطای زمان کامپایل ایجاد کند:
short x = 1, y = 1;
short z = x + y; // Compile-time error
در این حالت، x و y بهصورت implicit به int تبدیل میشوند تا جمع انجام شود. این یعنی نتیجه نیز int است که نمیتواند بهصورت implicit دوباره به short cast شود (زیرا ممکن است باعث از دست رفتن داده شود). برای کامپایلشدن باید cast صریح اضافه کنید:
short z = (short) (x + y); // OK
مقادیر ویژهٔ Float و Double
برخلاف typeهای صحیح، typeهای ممیز شناور مقدارهایی دارند که برخی عملیات با آنها بهطور ویژه رفتار میکنند. این مقدارهای ویژه عبارتاند از NaN (Not a Number)، +∞، −∞ و −0. کلاسهای float و double برای NaN، +∞ و −∞ و نیز مقدارهای دیگری مانند MaxValue، MinValue و Epsilon constant دارند؛ برای مثال:
Console.WriteLine (double.NegativeInfinity); // -Infinity
constantهایی که مقدارهای ویژهٔ double و float را نمایش میدهند به شکل زیرند:
| مقدار ویژه | constant در double | constant در float |
|---|
| NaN | double.NaN | float.NaN |
| +∞ | double.PositiveInfinity | float.PositiveInfinity |
| −∞ | double.NegativeInfinity | float.NegativeInfinity |
| −0 | −0.0 | −0.0f |
تقسیم یک عدد غیرصفر بر صفر، مقدار بینهایت تولید میکند:
Console.WriteLine ( 1.0 / 0.0); // Infinity
Console.WriteLine (−1.0 / 0.0); // -Infinity
Console.WriteLine ( 1.0 / −0.0); // -Infinity
Console.WriteLine (−1.0 / −0.0); // Infinity
تقسیم صفر بر صفر یا کمکردن بینهایت از بینهایت، NaN تولید میکند:
Console.WriteLine ( 0.0 / 0.0); // NaN
Console.WriteLine ((1.0 / 0.0) − (1.0 / 0.0)); // NaN
هنگام استفاده از ==، یک مقدار NaN هرگز با مقدار دیگری برابر نیست، حتی با NaN دیگر:
Console.WriteLine (0.0 / 0.0 == double.NaN); // False
برای آزمودن اینکه یک مقدار NaN است، باید از متد float.IsNaN یا double.IsNaN استفاده کنید:
Console.WriteLine (double.IsNaN (0.0 / 0.0)); // True
اما هنگام استفاده از object.Equals، دو مقدار NaN برابرند:
Console.WriteLine (object.Equals (0.0 / 0.0, double.NaN)); // True
float و double از مشخصات formatهای IEEE 754 پیروی میکنند که تقریباً همهٔ پردازندهها بهطور بومی از آن پشتیبانی میکنند. اطلاعات دقیق دربارهٔ رفتار این typeها در منبع www.ieee.org موجود است.
double در برابر decimal
double برای محاسبات علمی (مانند محاسبهٔ مختصات فضایی) مفید است. decimal برای محاسبات مالی و مقدارهایی که ساخته میشوند، نه نتیجهٔ اندازهگیریهای دنیای واقعی، مناسب است. خلاصهٔ تفاوتها در ادامه آمده است.
| دسته | double | decimal |
|---|
| نمایش داخلی | مبنای 2 | مبنای 10 |
| دقت دهدهی | 15–16 رقم معنادار | 28–29 رقم معنادار |
| بازه | ±(~10−324 تا ~10308) | ±(~10−28 تا ~1028) |
| مقدارهای ویژه | +0، −0، +∞، −∞ و NaN | ندارد |
| سرعت | بومی پردازنده | غیربومی پردازنده (حدود 10 برابر کندتر از double) |
خطاهای rounding در اعداد حقیقی
float و double اعداد را درون خود با مبنای 2 نمایش میدهند. به همین دلیل فقط اعدادی که در مبنای 2 قابل بیاناند دقیق نمایش داده میشوند. در عمل، این یعنی بیشتر literalهایی که جزء کسری دارند (و در مبنای 10 هستند) دقیق نمایش داده نمیشوند؛ برای مثال:
float x = 0.1f; // Not quite 0.1
Console.WriteLine (x + x + x + x + x + x + x + x + x + x); // 1.0000001
به همین دلیل float و double برای محاسبات مالی بد هستند. در مقابل، decimal در مبنای 10 کار میکند و بنابراین میتواند اعدادی را که در مبنای 10 قابل بیاناند (و همچنین عاملهای آن، یعنی مبنای 2 و مبنای 5) دقیق نمایش دهد. چون literalهای حقیقی در مبنای 10 هستند، decimal میتواند اعدادی مانند 0.1 را دقیق نمایش دهد. با این حال، نه double و نه decimal نمیتوانند عدد کسریای را که نمایش دهدهی آن تکرارشونده است دقیق نمایش دهند:
decimal m = 1M / 6M; // 0.1666666666666666666666666667M
double d = 1.0 / 6.0; // 0.16666666666666666
این موضوع به خطاهای rounding انباشته منجر میشود:
decimal notQuiteWholeM = m+m+m+m+m+m; // 1.0000000000000000000000000002M
double notQuiteWholeD = d+d+d+d+d+d; // 0.99999999999999989
که عملیات برابری و مقایسه را برهم میزند:
Console.WriteLine (notQuiteWholeM == 1M); // False
Console.WriteLine (notQuiteWholeD < 1.0); // True
Type بولی و Operatorها
type bool در C# (نام مستعار type System.Boolean) یک مقدار منطقی است که میتوان literal true یا false را به آن نسبت داد.
با اینکه یک مقدار Boolean فقط به یک bit فضای ذخیرهسازی نیاز دارد، runtime یک byte حافظه استفاده میکند، زیرا این کوچکترین قطعهای است که runtime و پردازنده میتوانند بهطور کارآمد با آن کار کنند. برای جلوگیری از ناکارآمدی فضایی در آرایهها، .NET کلاسی به نام BitArray در namespace System.Collections فراهم میکند که برای استفاده از فقط یک bit به ازای هر مقدار Boolean طراحی شده است.
Conversionهای bool
هیچ conversion از طریق casting میان type bool و typeهای عددی، در هیچیک از دو جهت، امکانپذیر نیست.
Operatorهای برابری و مقایسه
== و != برابری و نابرابری هر type را میآزمایند، اما همیشه یک مقدار bool بازمیگردانند.3 value typeها معمولاً مفهوم بسیار سادهای از برابری دارند:
int x = 1;
int y = 2;
int z = 1;
Console.WriteLine (x == y); // False
Console.WriteLine (x == z); // True
برای reference typeها، برابری بهطور پیشفرض بر اساس reference است، نه value واقعی object زیربنایی (در فصل 6 بیشتر دربارهٔ این موضوع صحبت میکنیم):
Dude d1 = new Dude ("John");
Dude d2 = new Dude ("John");
Console.WriteLine (d1 == d2); // False
Dude d3 = d1;
Console.WriteLine (d1 == d3); // True
public class Dude
{
public string Name;
public Dude (string n) { Name = n; }
}
operatorهای برابری و مقایسه، یعنی ==، !=، <، >، >= و <=، برای همهٔ typeهای عددی کار میکنند، اما باید در مورد اعداد حقیقی با احتیاط از آنها استفاده کنید (همانطور که در «Real Number Rounding Errors» در صفحهٔ 54 دیدیم). operatorهای مقایسه روی memberهای type enum نیز با مقایسهٔ valueهای type صحیح زیربنایی آنها کار میکنند. این موضوع را در «Enums» در صفحهٔ 154 توضیح میدهیم.
operatorهای برابری و مقایسه را با جزئیات بیشتر در «Operator Overloading» در صفحهٔ 256، «Equality Comparison» در صفحهٔ 344 و «Order Comparison» در صفحهٔ 355 توضیح میدهیم.
Operatorهای شرطی
operatorهای && و || شرطهای and و or را میآزمایند. آنها اغلب همراه operator ! که not را بیان میکند استفاده میشوند. در مثال زیر، متد UseUmbrella اگر هوا بارانی یا آفتابی باشد true بازمیگرداند (برای محافظت در برابر باران یا آفتاب)، به شرط آنکه همزمان باد نوزد (چتر در باد بیفایده است):
static bool UseUmbrella (bool rainy, bool sunny, bool windy)
{
return !windy && (rainy || sunny);
}
operatorهای && و || در صورت امکان ارزیابی را short-circuit میکنند. در مثال پیشین، اگر هوا بادی باشد، expression (rainy || sunny) حتی ارزیابی نمیشود.
short-circuiting برای اینکه expressionهایی مانند نمونهٔ زیر بدون پرتاب NullReferenceException اجرا شوند ضروری است:
if (sb != null && sb.Length > 0) ...
operatorهای & و | نیز شرطهای and و or را میآزمایند:
return !windy & (rainy | sunny);
تفاوت این است که آنها short-circuit نمیکنند. به همین دلیل، بهندرت بهجای operatorهای شرطی استفاده میشوند.
Operator شرطی (operator سهتایی)
operator شرطی (که معمولاً operator سهتایی یا ternary operator نامیده میشود، چون تنها operatorی است که سه operand میگیرد) شکل q ? a : b دارد؛ بنابراین، اگر شرط q درست باشد، a ارزیابی میشود؛ در غیر این صورت b ارزیابی میشود:
static int Max (int a, int b)
{
return (a > b) ? a : b;
}
operator شرطی بهویژه در expressionهای Language-Integrated Query (LINQ) (فصل 8) سودمند است.
رشتهها و کاراکترها
type char در C# (نام مستعار type System.Char) یک کاراکتر Unicode را نمایش میدهد و 2 byte (UTF-16) فضا اشغال میکند. literal از نوع char داخل single quote مشخص میشود:
char c = 'A'; // Simple character
escape sequenceها کاراکترهایی را بیان میکنند که نمیتوان آنها را بهصورت literal نوشت یا تفسیر کرد. یک escape sequence از یک backslash و سپس کاراکتری با معنای ویژه تشکیل میشود؛ برای مثال:
char newLine = '\n';
char backSlash = '\\';
جدول 2-2 کاراکترهای escape sequence را نشان میدهد.
جدول 2-2. کاراکترهای escape sequence| Char | معنا | مقدار |
\' | Single quote | 0x0027 |
\" | Double quote | 0x0022 |
\\ | Backslash | 0x005C |
\0 | Null | 0x0000 |
\a | Alert | 0x0007 |
\b | Backspace | 0x0008 |
\f | Form feed | 0x000C |
\n | New line | 0x000A |
| Char | معنا | مقدار |
\r | Carriage return | 0x000D |
\t | Horizontal tab | 0x0009 |
\v | Vertical tab | 0x000B |
escape sequenceِ \u (یا \x) به شما اجازه میدهد هر کاراکتر Unicode را از طریق کد hexadecimal چهاررقمی آن مشخص کنید:
char copyrightSymbol = '\u00A9';
char omegaSymbol = '\u03A9';
char newLine = '\u000A';
Conversionهای char
یک conversion ضمنی از char به یک type عددی برای typeهای عددیای کار میکند که میتوانند یک unsigned short را در خود جای دهند. برای دیگر typeهای عددی، conversion صریح لازم است.
Type رشته
type string در C# (نام مستعار type System.String که در فصل 6 بهطور عمیق بررسی میشود) یک دنبالهٔ immutable (غیرقابلتغییر) از کاراکترهای Unicode را نمایش میدهد. literal رشته داخل double quote مشخص میشود:
escape sequenceهایی که برای literalهای char معتبرند، داخل رشتهها نیز کار میکنند:
string a = "Here's a tab:\t";
هزینهٔ این کار آن است که هرگاه به یک backslash واقعی نیاز داشته باشید، باید آن را دو بار بنویسید:
string a1 = "\\\\server\\fileshare\\helloworld.cs";
برای اجتناب از این مشکل، C# literalهای رشتهای verbatim را مجاز میکند. یک verbatim string literal با @ آغاز میشود و از escape sequenceها پشتیبانی نمیکند. رشتهٔ verbatim زیر با رشتهٔ قبلی یکسان است:
string a2 = @"\\server\fileshare\helloworld.cs";
یک verbatim string literal همچنین میتواند چند خط را در بر بگیرد:
string escaped = "First Line\r\nSecond Line";
string verbatim = @"First Line
Second Line";
// True if your text editor uses CR-LF line separators:
Console.WriteLine (escaped == verbatim);
میتوانید کاراکتر double quote را با دو بار نوشتن آن در یک literal verbatim قرار دهید:
string xml = @"<customer id=""123""></customer>";
Raw string literalها (C# 11)
قرار دادن یک رشته در سه یا چند کاراکتر quote (""") یک raw string literal ایجاد میکند. raw string literalها میتوانند تقریباً هر دنبالهای از کاراکترها را بدون escaping یا دوبرابرکردن در خود داشته باشند:
string raw = """<file path="c:\temp\test.txt"></file>""";
raw string literalها نمایش literalهای JSON، XML و HTML و نیز regular expressionها و source code را آسان میکنند. اگر لازم باشد سه (یا بیشتر) کاراکتر quote را داخل خود رشته قرار دهید، میتوانید رشته را در چهار (یا بیشتر) کاراکتر quote قرار دهید:
string raw = """"The """ sequence denotes raw string literals."""";
raw string literalهای چندخطی تابع قواعد ویژهای هستند. میتوانیم رشتهٔ "Line 1\r\nLine 2" را به شکل زیر نمایش دهیم:
string multiLineRaw = """
Line 1
Line 2
""";
توجه کنید که quoteهای آغاز و پایان باید روی خطهایی جدا از محتوای رشته باشند. افزون بر این:
- Whitespace پس از
""" آغازین (روی همان خط) نادیده گرفته میشود.
- Whitespace پیش از
""" پایانی (روی همان خط) بهعنوان indentation مشترک در نظر گرفته میشود و از همهٔ خطهای رشته حذف میگردد. این کار اجازه میدهد برای خوانایی source code از indentation استفاده کنید، بدون اینکه آن indentation بخشی از رشته شود.
نمونهٔ دیگری برای نشان دادن قواعد raw string literal چندخطی:
if (true)
Console.WriteLine ("""
{
"Name" : "Joe"
}
""");
خروجی به شکل زیر است:
اگر هر خط در یک raw string literal چندخطی با indentation مشترکی که quoteهای پایانی مشخص کردهاند آغاز نشده باشد، compiler خطا تولید میکند.
raw string literalها را میتوان interpolate کرد؛ البته با قواعد ویژهای که در «String interpolation» در صفحهٔ 60 شرح داده میشوند.
الحاق رشتهها
operator + دو رشته را به هم متصل میکند:
ممکن است یکی از operandها مقداری غیررشتهای باشد که در این صورت ToString روی آن مقدار فراخوانی میشود:
string s = "a" + 5; // a5
استفادهٔ تکراری از operator + برای ساختن تدریجی یک رشته ناکارآمد است: راهحل بهتر استفاده از type System.Text.StringBuilder است (که در فصل 6 توضیح داده میشود).
String interpolation
رشتهای که قبل از آن کاراکتر $ قرار گرفته باشد، interpolated string نامیده میشود. interpolated stringها میتوانند expressionهایی داخل brace داشته باشند:
int x = 4;
Console.Write ($"A square has {x} sides"); // Prints: A square has 4 sides
هر expression معتبر C# با هر typeای میتواند داخل braceها ظاهر شود و C# با فراخوانی متد ToString یا معادل آن، expression را به رشته تبدیل میکند. میتوانید قالببندی را با افزودن colon و یک format string به expression تغییر دهید (format stringها در «String.Format and composite format strings» در صفحهٔ 296 توضیح داده میشوند):
string s = $"255 in hex is {byte.MaxValue:X2}"; // X2 = 2-digit hexadecimal
// Evaluates to "255 in hex is FF"
اگر لازم باشد از colon برای هدف دیگری استفاده کنید (برای مثال یک ternary conditional operator که بعداً بررسی میکنیم)، باید کل expression را داخل پرانتز قرار دهید:
bool b = true;
Console.WriteLine ($"The answer in binary is {(b ? 1 : 0)}");
از C# 10، interpolated stringها میتوانند constant باشند، به شرط آنکه valueهای interpolateشده constant باشند:
const string greeting = "Hello";
const string message = $"{greeting}, world";
از C# 11، interpolated stringها اجازه دارند چند خط را در بر بگیرند (چه standard باشند و چه verbatim):
string s = $"this interpolation spans {1 +
1} lines";
raw string literalها (از C# 11) را نیز میتوان interpolate کرد:
string s = $"""The date and time is {DateTime.Now}""";
برای قرار دادن یک brace واقعی در interpolated string:
- در standard و verbatim string literalها، کاراکتر brace موردنظر را تکرار کنید.
- در raw string literalها، با تکرار prefixِ
$ دنبالهٔ interpolation را تغییر دهید.
استفاده از دو (یا بیشتر) کاراکتر $ در prefix یک raw string literal، دنبالهٔ interpolation را از یک brace به دو (یا بیشتر) brace تغییر میدهد:
Console.WriteLine ($$"""{ "TimeStamp": "{{DateTime.Now}}" }""");
// Output: { "TimeStamp": "01/01/2024 12:13:25 PM" }
این کار توانایی copy-and-paste کردن متن داخل یک raw string literal را بدون نیاز به تغییر خود رشته حفظ میکند.
مقایسهٔ رشتهها
برای انجام مقایسهٔ برابری رشتهها میتوانید از operator == (یا یکی از متدهای Equals رشته) استفاده کنید. برای مقایسهٔ ترتیب باید از متد CompareTo رشته استفاده کنید؛ operatorهای < و > پشتیبانی نمیشوند. مقایسهٔ برابری و ترتیب را با جزئیات در «Comparing Strings» در صفحهٔ 297 شرح میدهیم.
رشتههای UTF-8
از C# 11 میتوانید از suffixِ u8 برای ساخت literalهای رشتهای با encodingِ UTF-8 بهجای UTF-16 استفاده کنید. این قابلیت برای سناریوهای پیشرفتهای مانند کار سطح پایین با متن JSON در نقاط حساس از نظر performance در نظر گرفته شده است:
ReadOnlySpan<byte> utf8 = "ab→cd"u8; // Arrow symbol consumes 3 bytes
Console.WriteLine (utf8.Length); // 7
type زیربنایی ReadOnlySpan<byte> است که در فصل 23 بررسی میکنیم. میتوانید با فراخوانی متد ToArray() آن را به array تبدیل کنید.
آرایهها
یک array تعداد ثابتی variable (که element نامیده میشوند) از یک type مشخص را نمایش میدهد. elementهای یک array همیشه در یک بلوک پیوسته از حافظه ذخیره میشوند و دسترسی بسیار کارآمدی فراهم میکنند.
array با square bracket پس از type عنصر مشخص میشود:
char[] vowels = new char[5]; // Declare an array of 5 characters
square bracketها همچنین array را index میکنند و به یک element مشخص بر اساس موقعیت دسترسی میدهند:
vowels[0] = 'a';
vowels[1] = 'e';
vowels[2] = 'i';
vowels[3] = 'o';
vowels[4] = 'u';
Console.WriteLine (vowels[1]); // e
این کد «e» را چاپ میکند، زیرا indexهای array از 0 آغاز میشوند. میتوانید برای پیمایش تکتک elementهای array از دستور for استفاده کنید. حلقهٔ for در این مثال عدد صحیح i را از 0 تا 4 گردش میدهد:
for (int i = 0; i < vowels.Length; i++)
Console.Write (vowels[i]); // aeiou
propertyِ Length یک array تعداد elementهای آن را بازمیگرداند.
پس از ایجاد یک array، نمیتوانید طول آن را تغییر دهید. namespaceِ System.Collection و subnamespaceهای آن data structureهای سطح بالاتری مانند arrayهای با اندازهٔ پویا و dictionaryها فراهم میکنند.
یک array initialization expression به شما اجازه میدهد array را در یک مرحله declare و populate کنید:
char[] vowels = new char[] {'a','e','i','o','u'};
یا بهسادگی:
char[] vowels = {'a','e','i','o','u'};
همهٔ arrayها از کلاس System.Array ارثبری میکنند و serviceهای مشترکی برای همهٔ arrayها فراهم میکنند. این memberها شامل methodهایی برای گرفتن و تنظیم elementها مستقل از type array هستند. آنها را در «The Array Class» در صفحهٔ 377 توضیح میدهیم.
مقداردهی اولیهٔ پیشفرض elementها
ایجاد یک array همیشه elementها را از پیش با مقدارهای پیشفرض مقداردهی میکند. مقدار پیشفرض برای یک type حاصل bitwise zeroing حافظه است. برای مثال، ایجاد یک array از integerها را در نظر بگیرید. چون int یک value type است، این کار 1,000 integer را در یک بلوک پیوسته از حافظه تخصیص میدهد. مقدار پیشفرض هر element برابر 0 خواهد بود:
int[] a = new int[1000];
Console.Write (a[123]); // 0
Value typeها در برابر reference typeها
اینکه typeِ element یک array از نوع value type باشد یا reference type، پیامدهای مهمی برای performance دارد. وقتی type عنصر value type باشد، مقدار هر element بهعنوان بخشی از خود array تخصیص داده میشود، همانطور که در اینجا نشان داده شده است:
Point[] a = new Point[1000];
int x = a[500].X; // 0
public struct Point { public int X, Y; }
اگر Point یک class بود، ایجاد array صرفاً 1,000 reference تهی تخصیص میداد:
Point[] a = new Point[1000];
int x = a[500].X; // Runtime error, NullReferenceException
public class Point { public int X, Y; }
برای اجتناب از این خطا، باید پس از ساخت array، 1,000 نمونهٔ Point را بهطور صریح instantiate کنیم:
Point[] a = new Point[1000];
for (int i = 0; i < a.Length; i++) // Iterate i from 0 to 999
a[i] = new Point(); // Set array element i with new point
خود array، صرفنظر از type عنصر، همیشه یک object از نوع reference type است. برای نمونه، کد زیر قانونی است:
Indexها و Rangeها
indexها و rangeها (که در C# 8 معرفی شدند) کار با elementها یا بخشهایی از یک array را ساده میکنند.
Indexها
indexها اجازه میدهند با operatorِ ^ به elementها نسبت به انتهای array اشاره کنید. ^1 به آخرین element، ^2 به element ماقبل آخر و به همین ترتیب اشاره میکند:
char[] vowels = new char[] {'a','e','i','o','u'};
char lastElement = vowels [^1]; // 'u'
char secondToLast = vowels [^2]; // 'o'
(^0 برابر طول array است، بنابراین vowels[^0] خطا ایجاد میکند.)
C# indexها را با کمک type Index پیادهسازی میکند، بنابراین میتوانید کار زیر را نیز انجام دهید:
Index first = 0;
Index last = ^1;
char firstElement = vowels [first]; // 'a'
char lastElement = vowels [last]; // 'u'
Rangeها
rangeها با استفاده از operatorِ .. اجازه میدهند یک array را «slice» کنید:
char[] firstTwo = vowels [..2]; // 'a', 'e'
char[] lastThree = vowels [2..]; // 'i', 'o', 'u'
char[] middleOne = vowels [2..3]; // 'i'
عدد دوم در range انحصاری است؛ بنابراین ..2 elementهای پیش از vowels[2] را بازمیگرداند.
میتوانید از نماد ^ در rangeها نیز استفاده کنید. کد زیر دو کاراکتر آخر را بازمیگرداند:
char[] lastTwo = vowels [^2..]; // 'o', 'u'
C# rangeها را با کمک type Range پیادهسازی میکند، بنابراین میتوانید کار زیر را نیز انجام دهید:
Range firstTwoRange = 0..2;
char[] firstTwo = vowels [firstTwoRange]; // 'a', 'e'
آرایههای چندبعدی
arrayهای چندبعدی دو نوع دارند: rectangular و jagged. arrayهای rectangular یک بلوک nبعدی از حافظه را نمایش میدهند و arrayهای jagged، arrayهایی از arrayها هستند.
Arrayهای rectangular
arrayهای rectangular با استفاده از comma برای جدا کردن هر بُعد declare میشوند. کد زیر یک array rectangular دوبعدی با ابعاد 3 در 3 declare میکند:
int[,] matrix = new int[3,3];
متد GetLength یک array طول یک بُعد مشخص را بازمیگرداند (از 0 شروع میشود):
for (int i = 0; i < matrix.GetLength(0); i++)
for (int j = 0; j < matrix.GetLength(1); j++)
matrix[i,j] = i * 3 + j;
میتوانید یک array rectangular را با مقدارهای صریح مقداردهی اولیه کنید. کد زیر arrayای مشابه مثال قبلی ایجاد میکند:
int[,] matrix = new int[,]
{
{0,1,2},
{3,4,5},
{6,7,8}
};
Arrayهای jagged
arrayهای jagged با استفاده از square bracketهای متوالی برای نمایش هر بُعد declare میشوند. در اینجا نمونهای از declare کردن یک array jagged دوبعدی آمده است که outermost dimension آن 3 است:
int[][] matrix = new int[3][];
ابعاد داخلی در declaration مشخص نمیشوند، زیرا برخلاف یک array rectangular، هر array داخلی میتواند طول دلخواهی داشته باشد. هر array داخلی بهطور ضمنی به null مقداردهی اولیه میشود، نه به یک array خالی. باید هر array داخلی را بهصورت دستی ایجاد کنید:
for (int i = 0; i < matrix.Length; i++)
{
matrix[i] = new int[3]; // Create inner array
for (int j = 0; j < matrix[i].Length; j++)
matrix[i][j] = i * 3 + j;
}
میتوانید یک array jagged را با مقدارهای صریح مقداردهی اولیه کنید. کد زیر arrayای مشابه مثال قبلی، همراه با یک element اضافی در انتها، ایجاد میکند:
int[][] matrix = new int[][]
{
new int[] {0,1,2},
new int[] {3,4,5},
new int[] {6,7,8,9}
};
Expressionهای سادهشدهٔ مقداردهی اولیهٔ array
دو راه برای کوتاهکردن expressionهای مقداردهی اولیهٔ array وجود دارد. راه نخست حذف operatorِ new و type qualificationها است:
char[] vowels = {'a','e','i','o','u'};
int[,] rectangularMatrix =
{
{0,1,2},
{3,4,5},
{6,7,8}
};
int[][] jaggedMatrix =
{
new int[] {0,1,2},
new int[] {3,4,5},
new int[] {6,7,8,9}
};
(از C# 12 میتوانید در arrayهای تکبعدی بهجای brace از square bracket استفاده کنید.)
راه دوم استفاده از keywordِ var است که به compiler دستور میدهد یک local variable را بهطور ضمنی typeدهی کند. چند مثال ساده:
var i = 3; // i is implicitly of type int
var s = "sausage"; // s is implicitly of type string
همین اصل را میتوان بر arrayها اعمال کرد، با این تفاوت که میتوان یک مرحله جلوتر رفت. با حذف type qualifier پس از keywordِ new، compiler type array را استنتاج میکند:
var vowels = new[] {'a','e','i','o','u'}; // Compiler infers char[]
در اینجا نحوهٔ اعمال آن به arrayهای چندبعدی آمده است:
var rectMatrix = new[,] // rectMatrix is implicitly of type int[,]
{
{0,1,2},
{3,4,5},
{6,7,8}
};
var jaggedMat = new int[][] // jaggedMat is implicitly of type int[][]
{
new[] {0,1,2},
new[] {3,4,5},
new[] {6,7,8,9}
};
برای اینکه این کار عمل کند، همهٔ elementها باید بهطور ضمنی قابل conversion به یک type واحد باشند (و دستکم یکی از elementها باید از همان type باشد، و دقیقاً یک بهترین type وجود داشته باشد)، مانند مثال زیر:
var x = new[] {1,10000000000}; // all convertible to long
بررسی محدوده
تمام indexگذاریهای array در runtime از نظر محدوده بررسی میشوند. اگر از index نامعتبر استفاده کنید، IndexOutOfRangeException پرتاب میشود:
int[] arr = new int[3];
arr[3] = 1; // IndexOutOfRangeException thrown
بررسی محدودهٔ array برای type safety ضروری است و debugging را سادهتر میکند.