فصل ششم: مبانی .NET

بسیاری از امکانات اصلی که هنگام برنامه‌نویسی به آن‌ها نیاز دارید، توسط خود زبان C# فراهم نمی‌شوند، بلکه توسط انواع (Types) موجود در BCL یا همان .NET Base Class Library ارائه می‌شوند.

در این فصل، با انواعی آشنا می‌شوید که در انجام کارهای پایه‌ای برنامه‌نویسی به شما کمک می‌کنند، مانند:

همچنین با انواع پایه‌ای .NET مانند String، DateTime و Enum نیز آشنا خواهید شد.

انواعی که در این بخش پوشش داده می‌شوند، بیشتر در فضای نام (Namespace) System قرار دارند، به جز موارد زیر:


مدیریت رشته و متن 📝

Char

یک char در زبان C# نشان‌دهنده یک کاراکتر یونیکد (Unicode Character) واحد است و در واقع معادل ساختار (struct) System.Char است.

در فصل ۲ توضیح دادیم که چگونه لیترال‌های char را بیان کنید:

char c = 'A';
char newLine = '\n';

ساختار System.Char مجموعه‌ای از متدهای static برای کار با کاراکترها ارائه می‌دهد، مانند ToUpper، ToLower و IsWhiteSpace. شما می‌توانید این متدها را هم از طریق System.Char و هم از طریق char فراخوانی کنید:

Console.WriteLine (System.Char.ToUpper ('c'));    // C
Console.WriteLine (char.IsWhiteSpace ('\t'));     // True

متدهای ToUpper و ToLower به محلی‌سازی (Locale) کاربر نهایی احترام می‌گذارند و این می‌تواند گاهی منجر به بروز خطاهای ظریف شود. به عنوان مثال، عبارت زیر در کشور ترکیه مقدار false را برمی‌گرداند:

char.ToUpper ('i') == 'I'

دلیل این موضوع این است که در ترکیه، نتیجه‌ی char.ToUpper ('i') برابر با 'İ' است (به نقطه روی حرف توجه کنید!).

برای جلوگیری از این مشکل، System.Char (و همچنین System.String) نسخه‌های culture-invariant از متدهای ToUpper و ToLower را ارائه می‌دهند که با پسوند Invariant مشخص می‌شوند. این متدها همیشه از قوانین فرهنگ انگلیسی استفاده می‌کنند:

Console.WriteLine (char.ToUpperInvariant ('i'));    // I

این در واقع یک میانبر برای کد زیر است:

Console.WriteLine (char.ToUpper ('i', CultureInfo.InvariantCulture))

برای اطلاعات بیشتر درباره locale‌ها و فرهنگ‌ها، به بخش Formatting and Parsing در صفحه 317 مراجعه کنید.

بیشتر متدهای static دیگر در char مربوط به دسته‌بندی کاراکترها هستند. جدول 6-1 این متدها را نشان می‌دهد.

Conventions-UsedThis-Book

دسته‌بندی دقیق‌تر کاراکترها 🔠

برای دسته‌بندی دقیق‌تر، نوع char متدی static به نام GetUnicodeCategory ارائه می‌دهد؛ این متد یک مقدار از نوع شمارش (Enumeration) UnicodeCategory برمی‌گرداند که اعضای آن در ستون سمت راست جدول 6-1 نشان داده شده‌اند.

با Casting صریح از یک عدد صحیح (int) می‌توان یک char تولید کرد که خارج از مجموعه تخصیص‌داده‌شده‌ی یونیکد باشد. برای آزمایش اعتبار یک کاراکتر، متد char.GetUnicodeCategory را فراخوانی کنید؛ اگر نتیجه برابر با UnicodeCategory.OtherNotAssigned باشد، آن کاراکتر نامعتبر است.

یک char دارای پهنای 16 بیت است—به اندازه‌ای که بتواند هر کاراکتر یونیکد را در Basic Multilingual Plane (BMP) نمایش دهد. برای فراتر رفتن از این محدوده، باید از Surrogate Pairs استفاده کنید؛ روش‌های انجام این کار در بخش “Text Encodings and Unicode” در صفحه 301 توضیح داده شده‌اند.


String

یک string در زبان C# (یا همان System.String) دنباله‌ای از کاراکترهاست که تغییرناپذیر (Immutable) است؛ یعنی پس از ایجاد، قابل تغییر نیست. در فصل ۲ نحوه‌ی نوشتن لیترال‌های رشته‌ای (String Literals)، انجام مقایسه برابری (Equality Comparisons) و اتصال دو رشته (Concatenation) را توضیح دادیم.

در این بخش، به سایر قابلیت‌های کار با رشته‌ها می‌پردازیم که از طریق اعضای static و instance کلاس System.String ارائه شده‌اند.


ساخت رشته‌ها 🏗️

ساده‌ترین روش ساخت یک رشته، انتساب یک لیترال است (همان‌طور که در فصل ۲ دیدیم):

string s1 = "Hello";
string s2 = "First Line\r\nSecond Line";
string s3 = @"\\server\fileshare\helloworld.cs";

برای ساخت یک توالی تکراری از کاراکترها، می‌توانید از سازنده (Constructor) رشته استفاده کنید:

Console.Write (new string ('*', 10));      // **********

همچنین می‌توانید یک رشته را از یک آرایه‌ی char بسازید. متد ToCharArray برعکس این کار را انجام می‌دهد:

char[] ca = "Hello".ToCharArray();
string s = new string (ca);              // s = "Hello"

سازنده‌های رشته همچنین Overload شده‌اند تا انواع مختلفی از Pointer‌های ناامن (unsafe) مانند char* را نیز بپذیرند و رشته‌هایی از این انواع بسازند.


رشته‌های تهی و خالی ⚠️

یک رشته‌ی خالی (Empty String) طولی برابر با صفر دارد. برای ساخت رشته‌ی خالی می‌توانید از یک لیترال یا فیلد static به نام string.Empty استفاده کنید. برای بررسی خالی بودن یک رشته می‌توانید از مقایسه‌ی برابری یا بررسی ویژگی Length استفاده کنید:

string empty = "";
Console.WriteLine (empty == "");              // True
Console.WriteLine (empty == string.Empty);    // True
Console.WriteLine (empty.Length == 0);        // True

از آن‌جایی که رشته‌ها Reference Type هستند، می‌توانند مقدار null نیز داشته باشند:

string nullString = null;
Console.WriteLine (nullString == null);        // True
Console.WriteLine (nullString == "");          // False
Console.WriteLine (nullString.Length == 0);    // NullReferenceException

متد static string.IsNullOrEmpty میانبری مفید برای بررسی این است که آیا یک رشته null یا خالی است یا خیر.


دسترسی به کاراکترهای داخل یک رشته 🔍

Indexer رشته یک کاراکتر را در شاخص مشخص‌شده برمی‌گرداند. همانند سایر عملیات روی رشته‌ها، شاخص‌گذاری از صفر شروع می‌شود:

string str  = "abcde";
char letter = str[1];        // letter == 'b'

نوع string رابط IEnumerable را پیاده‌سازی می‌کند، بنابراین می‌توانید با دستور foreach روی کاراکترهای آن حلقه بزنید:

foreach (char c in "123") Console.Write (c + ",");    // 1,2,3,

جستجو در رشته‌ها 🔎

ساده‌ترین متدهای جستجو در رشته‌ها عبارتند از StartsWith، EndsWith و Contains. هر سه مقدار true یا false برمی‌گردانند:

Console.WriteLine ("quick brown fox".EndsWith ("fox"));      // True
Console.WriteLine ("quick brown fox".Contains ("brown"));    // True

این متدها Overload شده‌اند تا بتوانید یک StringComparison enum مشخص کنید و حساسیت نسبت به حروف کوچک و بزرگ و فرهنگ (Culture) را کنترل کنید (به بخش “Ordinal versus culture comparison” در صفحه 297 مراجعه کنید). حالت پیش‌فرض، یک مقایسه‌ی حساس به حروف بزرگ و کوچک با استفاده از قوانین فرهنگ جاری (محلی‌سازی شده) است. کد زیر یک جستجوی غیرحساس به حروف بزرگ و کوچک را با استفاده از قوانین فرهنگ Invariant انجام می‌دهد:

"abcdef".StartsWith ("aBc", StringComparison.InvariantCultureIgnoreCase)

متد IndexOf موقعیت اولین وقوع یک کاراکتر یا زیررشته را برمی‌گرداند (یا −1 اگر زیررشته یافت نشود):

Console.WriteLine ("abcde".IndexOf ("cd"));   // 2

IndexOf همچنین Overload شده تا یک startPosition (شاخص شروع جستجو) و همچنین یک StringComparison enum را بپذیرد:

Console.WriteLine ("abcde abcde".IndexOf ("CD", 6,
                  StringComparison.CurrentCultureIgnoreCase));    // 8

متد LastIndexOf مشابه IndexOf است، با این تفاوت که از انتهای رشته به ابتدا جستجو می‌کند.

متد IndexOfAny اولین موقعیت یک یا چند کاراکتر مشخص را برمی‌گرداند:

Console.Write ("ab,cd ef".IndexOfAny (new char[] {' ', ','} ));       // 2
Console.Write ("pas5w0rd".IndexOfAny ("0123456789".ToCharArray() ));  // 3

LastIndexOfAny نیز همین کار را انجام می‌دهد اما در جهت معکوس.


دستکاری رشته‌ها ✂️

از آن‌جا که String تغییرناپذیر (Immutable) است، تمام متدهایی که رشته را «دستکاری» می‌کنند، در واقع یک رشته‌ی جدید برمی‌گردانند و رشته‌ی اصلی را بدون تغییر باقی می‌گذارند (این موضوع زمانی هم که یک متغیر رشته را دوباره مقداردهی می‌کنید صادق است).


متد Substring – جدا کردن بخشی از رشته

string left3 = "12345".Substring (0, 3);     // left3 = "123";
string mid3  = "12345".Substring (1, 3);     // mid3 = "234";

اگر طول (length) را مشخص نکنید، باقی‌مانده‌ی رشته بازگردانده می‌شود:

string end3  = "12345".Substring (2);        // end3 = "345";

متدهای Insert و Remove – درج یا حذف کاراکتر

string s1 = "helloworld".Insert (5, ", ");    // s1 = "hello, world"
string s2 = s1.Remove (5, 2);                 // s2 = "helloworld";

PadLeft و PadRight – پر کردن رشته

این متدها رشته را تا طول مشخصی با کاراکتر دلخواه (یا اگر مشخص نشود، با فاصله) پر می‌کنند:

Console.WriteLine ("12345".PadLeft (9, '*'));  // ****12345
Console.WriteLine ("12345".PadLeft (9));       //     12345

اگر طول مشخص‌شده کوتاه‌تر از طول رشته باشد، رشته‌ی اصلی بدون تغییر برگردانده می‌شود.


TrimStart، TrimEnd و Trim – حذف کاراکترهای اضافی

این متدها کاراکترهای مشخص‌شده را از ابتدای رشته (TrimStart) یا انتهای رشته (TrimEnd) حذف می‌کنند؛ Trim هر دو را انجام می‌دهد. به‌طور پیش‌فرض، این متدها فاصله‌ها و کاراکترهای خالی (Whitespace) مانند فاصله، تب، خطوط جدید و نسخه‌های یونیکد این‌ها را حذف می‌کنند:

Console.WriteLine ("  abc \t\r\n ".Trim().Length);   // 3

Replace – جایگزینی کاراکتر یا زیررشته

این متد تمام (غیرهم‌پوشان) رخدادهای یک کاراکتر یا زیررشته را جایگزین می‌کند:

Console.WriteLine ("to be done".Replace (" ", " | ") );  // to | be | done
Console.WriteLine ("to be done".Replace (" ", "")    );  // tobedone

ToUpper و ToLower – تبدیل به حروف بزرگ یا کوچک

این متدها نسخه‌ی حروف بزرگ و کوچک رشته را برمی‌گردانند. به‌طور پیش‌فرض، به تنظیمات زبان جاری کاربر احترام می‌گذارند؛ در حالی که ToUpperInvariant و ToLowerInvariant همیشه از قوانین حروف الفبای انگلیسی استفاده می‌کنند.


تقسیم و اتصال رشته‌ها 🔗

Split – تقسیم رشته

متد Split یک رشته را به بخش‌هایی تقسیم می‌کند:

string[] words = "The quick brown fox".Split();
foreach (string word in words)
    Console.Write (word + "|");    // The|quick|brown|fox|

به‌طور پیش‌فرض، Split از کاراکترهای فاصله به‌عنوان جداکننده استفاده می‌کند. این متد همچنین Overload شده تا یک آرایه‌ی params از char یا string‌های جداکننده بپذیرد. همچنین می‌تواند یک StringSplitOptions enum دریافت کند که گزینه‌ای برای حذف ورودی‌های خالی دارد؛ این قابلیت زمانی مفید است که کلمات با چندین جداکننده‌ی پشت‌سرهم از هم جدا شده باشند.


Join و Concat – اتصال رشته‌ها

متد Join عکس Split عمل می‌کند. این متد به یک جداکننده و یک آرایه‌ی رشته نیاز دارد:

string[] words = "The quick brown fox".Split();
string together = string.Join (" ", words);      // The quick brown fox

متد Concat مشابه Join است اما تنها یک آرایه‌ی رشته به‌صورت params می‌پذیرد و جداکننده‌ای اعمال نمی‌کند. Concat دقیقاً معادل عملگر + است (در واقع کامپایلر + را به Concat تبدیل می‌کند):

string sentence     = string.Concat ("The", " quick", " brown", " fox");
string sameSentence = "The" + " quick" + " brown" + " fox";

String.Format و رشته‌های ترکیبی

متد static Format راهی راحت برای ساخت رشته‌هایی است که در آن‌ها مقادیر متغیرها گنجانده می‌شوند. این مقادیر می‌توانند از هر نوعی باشند؛ Format تنها متد ToString را روی آن‌ها صدا می‌زند.

رشته‌ای که شامل متغیرهای جاسازی‌شده است، رشته‌ی قالب ترکیبی (Composite Format String) نام دارد. هنگام فراخوانی String.Format، شما رشته‌ی قالب ترکیبی را به همراه هر یک از متغیرها ارسال می‌کنید:

string composite = "It's {0} degrees in {1} on this {2} morning";
string s = string.Format (composite, 35, "Perth", DateTime.Now.DayOfWeek);
// s == "It's 35 degrees in Perth on this Friday morning"

(و این دما سلسیوس است!)

ما می‌توانیم از رشته‌های درون‌یابی‌شده (Interpolated String Literals) هم برای همین کار استفاده کنیم (به بخش “String Type” در صفحه 58 مراجعه کنید). کافی است رشته را با علامت $ شروع کرده و عبارات را داخل آکولاد قرار دهید:

string s = $"It's hot this {DateTime.Now.DayOfWeek} morning";

هر عدد داخل آکولاد آیتم قالب (Format Item) نامیده می‌شود. این عدد به موقعیت آرگومان مربوط است و می‌تواند به‌صورت اختیاری موارد زیر را دنبال کند:

حداقل عرض برای تراز کردن ستون‌ها مفید است. اگر مقدار منفی باشد، داده به‌صورت چپ‌چین قرار می‌گیرد؛ در غیر این صورت راست‌چین خواهد شد:

string composite = "Name={0,-20} Credit Limit={1,15:C}";
Console.WriteLine (string.Format (composite, "Mary", 500));
Console.WriteLine (string.Format (composite, "Elizabeth", 20000));

نتیجه:

Name=Mary                 Credit Limit=        $500.00
Name=Elizabeth            Credit Limit=     $20,000.00

معادل همین بدون استفاده از string.Format:

string s = "Name=" + "Mary".PadRight (20) +
           " Credit Limit=" + 500.ToString ("C").PadLeft (15);

محدودیت اعتبار (Credit Limit) به‌واسطه‌ی رشته‌ی قالب "C" به‌صورت واحد پولی فرمت شده است. جزئیات رشته‌های قالب را در بخش “Formatting and Parsing” در صفحه 317 شرح داده‌ایم.


مقایسه‌ی رشته‌ها 🔠

هنگام مقایسه‌ی دو مقدار، ‎.NET بین دو مفهوم مقایسه‌ی برابری (Equality Comparison) و مقایسه‌ی ترتیب (Order Comparison) تفاوت قائل می‌شود.

نکته‌ی مهم این است که مقایسه‌ی برابری زیرمجموعه‌ای از مقایسه‌ی ترتیب نیست؛ هر کدام هدف متفاوتی دارند. برای مثال، امکان دارد دو مقدار نابرابر در یک موقعیت ترتیب‌دهی مشابه قرار بگیرند. در بخش “Equality Comparison” در صفحه‌ی 226 دوباره به این موضوع بازمی‌گردیم.

برای مقایسه‌ی برابری رشته‌ها می‌توانید از عملگر == یا یکی از متدهای Equals در نوع رشته استفاده کنید. متدهای Equals انعطاف‌پذیرتر هستند زیرا اجازه می‌دهند گزینه‌هایی مانند بی‌تفاوتی به حروف کوچک و بزرگ (Case Insensitivity) را مشخص کنید.

یک تفاوت دیگر این است که عملگر == روی رشته‌ها زمانی که متغیرها به نوع object تبدیل (Cast) شوند به‌طور قابل‌اعتماد عمل نمی‌کند. دلیل این موضوع را در “Equality Comparison” صفحه‌ی 226 توضیح خواهیم داد.

برای مقایسه‌ی ترتیب رشته‌ها می‌توانید از متد CompareTo (نمونه‌ای یا Instance) یا متدهای استاتیک Compare و CompareOrdinal استفاده کنید. این متدها یک عدد مثبت، منفی یا صفر برمی‌گردانند که نشان می‌دهد مقدار اول بعد از، قبل از یا هم‌تراز مقدار دوم قرار می‌گیرد.

قبل از بررسی جزئیات هرکدام، لازم است با الگوریتم‌های مقایسه‌ی رشته‌ای ‎.NET آشنا شویم.


مقایسه‌ی ترتیبی (Ordinal) در برابر مقایسه‌ی مبتنی بر فرهنگ (Culture) 🌍

دو الگوریتم اصلی برای مقایسه‌ی رشته‌ها وجود دارد:

  1. Ordinal (ترتیبی): کاراکترها را صرفاً به‌عنوان عدد (بر اساس مقدار عددی یونیکد آن‌ها) تفسیر می‌کند.
  2. Culture-Sensitive (حساس به فرهنگ): کاراکترها را با توجه به الفبای یک فرهنگ خاص تفسیر می‌کند.

دو فرهنگ خاص در ‎.NET وجود دارد:

برای مقایسه‌ی برابری، هر دو الگوریتم ترتیبی و حساس به فرهنگ کاربرد دارند.
اما برای مرتب‌سازی تقریباً همیشه مقایسه‌ی حساس به فرهنگ ترجیح داده می‌شود، چون برای مرتب‌سازی الفبایی به یک الفبا نیاز است. الگوریتم ترتیبی صرفاً به مقادیر یونیکد تکیه دارد، که حروف انگلیسی را به ترتیب الفبایی قرار می‌دهد، ولی دقیقاً همان‌طور که انتظار دارید نیست.

برای مثال، فرض کنید حساسیت به بزرگی و کوچکی حروف فعال است و رشته‌های زیر را داریم:

فرهنگ ثابت (Invariant) آن‌ها را به این ترتیب مرتب می‌کند:

"atom", "Atom", "Zamia"

اما الگوریتم ترتیبی (Ordinal) نتیجه‌ی زیر را تولید می‌کند:

"Atom", "Zamia", "atom"

دلیل این تفاوت این است که فرهنگ ثابت حروف کوچک و بزرگ را در کنار هم قرار می‌دهد (aA bB cC ...)، ولی الگوریتم ترتیبی همه‌ی حروف بزرگ را ابتدا و سپس همه‌ی حروف کوچک را قرار می‌دهد (A...Z, a...z). این رفتار در واقع برگرفته از مجموعه‌کاراکتر ASCII است که در دهه‌ی 1960 ساخته شده بود.


مقایسه‌ی برابری رشته‌ها ✍️

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

این الگوریتم برای == و Equals انتخاب شده زیرا:

برای مقایسه‌های آگاه به فرهنگ یا بی‌تفاوت به بزرگی حروف، متدهای زیر وجود دارند:

public bool Equals (string value, StringComparison comparisonType);
public static bool Equals (string a, string b, StringComparison comparisonType);

نسخه‌ی استاتیک حتی زمانی که یکی یا هر دو رشته null باشند نیز به درستی کار می‌کند.
StringComparison یک Enum است که به‌صورت زیر تعریف شده است:

public enum StringComparison
{
    CurrentCulture,               // حساس به بزرگی حروف
    CurrentCultureIgnoreCase,
    InvariantCulture,             // حساس به بزرگی حروف
    InvariantCultureIgnoreCase,
    Ordinal,                      // حساس به بزرگی حروف
    OrdinalIgnoreCase
}

مثال‌ها:

Console.WriteLine (string.Equals ("foo", "FOO",
                   StringComparison.OrdinalIgnoreCase));   // True

Console.WriteLine ("ṻ" == "ǖ");                            // False

Console.WriteLine (string.Equals ("ṻ", "ǖ",
                   StringComparison.CurrentCulture));      // ?

(نتیجه‌ی مثال سوم بستگی به تنظیمات زبان فعلی رایانه دارد.)


مقایسه‌ی ترتیب رشته‌ها 📊

متد CompareTo (نمونه‌ای) روی رشته‌ها یک مقایسه‌ی حساس به فرهنگ و حساس به بزرگی حروف انجام می‌دهد. برخلاف عملگر ==، این متد از مقایسه‌ی ترتیبی استفاده نمی‌کند، چون برای مرتب‌سازی، مقایسه‌ی حساس به فرهنگ مفیدتر است.

تعریف متد:

public int CompareTo (string strB);

این متد رابط IComparable را پیاده‌سازی می‌کند که یک پروتکل استاندارد مقایسه در کتابخانه‌های ‎.NET است. بنابراین متد CompareTo رفتار پیش‌فرض مرتب‌سازی رشته‌ها را در کاربردهایی مثل مجموعه‌های مرتب‌شده تعریف می‌کند.
برای اطلاعات بیشتر درباره‌ی IComparable به بخش “Order Comparison” در صفحه‌ی 355 مراجعه کنید.

برای سایر انواع مقایسه، متدهای استاتیک زیر نیز وجود دارند:

public static int Compare (string strA, string strB,
                           StringComparison comparisonType);

public static int Compare (string strA, string strB, bool ignoreCase,
                           CultureInfo culture);

public static int Compare (string strA, string strB, bool ignoreCase);

public static int CompareOrdinal (string strA, string strB);

دو متد آخر در واقع میان‌بری برای فراخوانی دو متد اول هستند.

تمام متدهای مقایسه‌ی ترتیب، عددی مثبت، منفی یا صفر برمی‌گردانند که نشان‌دهنده‌ی ترتیب دو مقدار است:

Console.WriteLine ("Boston".CompareTo ("Austin"));    // 1
Console.WriteLine ("Boston".CompareTo ("Boston"));    // 0
Console.WriteLine ("Boston".CompareTo ("Chicago"));   // -1
Console.WriteLine ("ṻ".CompareTo ("ǖ"));              // 1
Console.WriteLine ("foo".CompareTo ("FOO"));          // -1

برای انجام یک مقایسه‌ی بی‌تفاوت به بزرگی حروف با فرهنگ فعلی:

Console.WriteLine (string.Compare ("foo", "FOO", true));   // 0

با ارائه‌ی یک شیء CultureInfo، می‌توانید هر الفبای دلخواهی را وارد کنید:

// CultureInfo در فضای نام System.Globalization تعریف شده است
CultureInfo german = CultureInfo.GetCultureInfo ("de-DE");
int i = string.Compare ("Müller", "Muller", false, german);

🔠 StringBuilder (رشته‌ساز)

کلاس StringBuilder در فضای نام System.Text نشان‌دهنده‌ی یک رشته‌ی قابل‌تغییر (Mutable) است. یعنی می‌توان آن را ویرایش کرد. با استفاده از StringBuilder می‌توانید بخش‌هایی از متن را اضافه (Append)، درج (Insert)، حذف (Remove) یا جایگزین (Replace) کنید، بدون این‌که نیاز باشد کل رشته را دوباره بسازید.

سازنده‌ی StringBuilder (Constructor) می‌تواند به‌صورت اختیاری یک مقدار اولیه‌ی رشته و همچنین اندازه‌ی شروع ظرفیت داخلی را دریافت کند (ظرفیت پیش‌فرض برابر با ۱۶ کاراکتر است). اگر اندازه‌ی رشته از این ظرفیت فراتر رود، StringBuilder به‌صورت خودکار ساختار داخلی خود را برای جا دادن داده‌های جدید تغییر اندازه می‌دهد (با اندکی کاهش کارایی) تا زمانی که به حداکثر ظرفیت خود برسد (پیش‌فرض برابر با int.MaxValue است).

یکی از استفاده‌های رایج StringBuilder، ساخت یک رشته‌ی طولانی با اضافه‌کردن مداوم بخش‌های جدید است. این روش بسیار سریع‌تر و بهینه‌تر از چسباندن (Concatenate) رشته‌های معمولی است:

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 50; i++) sb.Append(i).Append(",");
Console.WriteLine(sb.ToString());

خروجی:

0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,
27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,

برای گرفتن نتیجه‌ی نهایی، از متد ToString() استفاده می‌کنیم.

علاوه بر متدهای Insert، Remove و Replace (که Replace مشابه Replace در کلاس string عمل می‌کند)، کلاس StringBuilder ویژگی‌های زیر را دارد:

برای پاک‌کردن محتوای یک StringBuilder، می‌توانید یکی از دو روش زیر را استفاده کنید:

  1. ایجاد یک نمونه‌ی جدید.
  2. قرار دادن مقدار Length برابر با صفر.

⚠️ توجه: تنظیم Length = 0 ظرفیت داخلی را کاهش نمی‌دهد. یعنی اگر رشته‌ی شما قبلاً یک میلیون کاراکتر داشته باشد، پس از صفر کردن طول، همچنان حدود ۲ مگابایت حافظه اشغال خواهد کرد. برای آزادسازی حافظه باید یک StringBuilder جدید ایجاد کنید و نمونه‌ی قبلی را از محدوده‌ی استفاده خارج کنید (تا توسط Garbage Collector حذف شود).


🌐 کدگذاری متن و یونیکد (Text Encodings and Unicode)

مجموعه‌کاراکتر (Character Set) مجموعه‌ای از کاراکترهاست که هر کدام یک کد عددی (Code Point) دارند. دو مجموعه‌کاراکتر رایج عبارت‌اند از:

سیستم تایپ در .NET برای کار با یونیکد طراحی شده است. با این حال، چون ASCII زیرمجموعه‌ای از یونیکد است، به‌صورت ضمنی پشتیبانی می‌شود.

کدگذاری متن (Text Encoding) وظیفه دارد کاراکترها را از کد عددی (Code Point) به نمایش دودویی (Binary) و بالعکس تبدیل کند. در .NET، کدگذاری‌ها بیشتر هنگام کار با فایل‌های متنی یا جریان‌های داده (Streams) استفاده می‌شوند. برای مثال، وقتی یک فایل متنی را به رشته تبدیل می‌کنید، Encoder داده‌های باینری را به نمایش داخلی یونیکد که انواع char و string نیاز دارند، تبدیل می‌کند.

کدگذاری‌ها می‌توانند تعیین کنند:

در .NET دو دسته اصلی از کدگذاری‌ها وجود دارد:

  1. کدگذاری‌هایی که کاراکترهای یونیکد را به یک مجموعه‌کاراکتر دیگر نگاشت می‌کنند

    • شامل کدگذاری‌های قدیمی مثل EBCDIC شرکت IBM و مجموعه‌های ۸ بیتی که قبل از یونیکد رایج بودند (شناخته‌شده به‌وسیله‌ی Code Page).
    • ASCII نیز در این دسته قرار می‌گیرد: تنها ۱۲۸ کاراکتر اول را پشتیبانی می‌کند و باقی را حذف می‌کند.
    • GB18030 نیز در این دسته است و از سال ۲۰۰۰ به‌عنوان استاندارد اجباری برای نرم‌افزارهای مورد استفاده یا فروخته‌شده در چین معرفی شد.
  2. کدگذاری‌هایی که از طرح‌های استاندارد یونیکد استفاده می‌کنند

    • شامل UTF-8، UTF-16 و UTF-32UTF-7 قدیمی).

    • هر کدام از نظر فضای ذخیره‌سازی متفاوت‌اند:

      • UTF-8: بین ۱ تا ۴ بایت برای هر کاراکتر. ۱۲۸ کاراکتر اول فقط ۱ بایت می‌گیرند (سازگار با ASCII). پرکاربردترین کدگذاری، مخصوصاً در اینترنت، و پیش‌فرض بسیاری از عملیات I/O در .NET است.
      • UTF-16: از یک یا دو واژه‌ی ۱۶ بیتی برای هر کاراکتر استفاده می‌کند. این همان قالب داخلی .NET برای نمایش رشته‌ها و کاراکترهاست.
      • UTF-32: ناکارآمدترین از نظر فضا؛ هر کاراکتر دقیقاً ۴ بایت می‌گیرد. به‌ندرت استفاده می‌شود اما برای دسترسی تصادفی به کاراکترها بسیار ساده است.

🔑 دریافت یک شیء Encoding

کلاس Encoding در فضای نام System.Text، پایه‌ی مشترک تمام کلاس‌های مرتبط با کدگذاری است. این کلاس چند زیرکلاس دارد که خانواده‌های مختلف کدگذاری را پیاده‌سازی می‌کنند. رایج‌ترین کدگذاری‌ها از طریق ویژگی‌های استاتیک (Static Properties) کلاس Encoding در دسترس هستند.

Conventions-UsedThis-Book

به‌دست آوردن سایر Encodingها 🎛️

می‌توانید با استفاده از متد Encoding.GetEncoding و وارد کردن یک نام استاندارد کاراکترست از Internet Assigned Numbers Authority (IANA) سایر Encodingها را به‌دست آورید:

// در .NET 5+ و .NET Core، ابتدا باید RegisterProvider را فراخوانی کنید:
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Encoding chinese = Encoding.GetEncoding("GB18030");

متد ایستای GetEncodings فهرستی از تمام Encodingهای پشتیبانی‌شده را همراه با نام‌های استاندارد IANA برمی‌گرداند:

foreach (EncodingInfo info in Encoding.GetEncodings())
    Console.WriteLine(info.Name);

روش دیگر برای به‌دست آوردن یک Encoding این است که مستقیماً یک کلاس Encoding را نمونه‌سازی کنید. این کار به شما اجازه می‌دهد گزینه‌های مختلفی را از طریق آرگومان‌های سازنده مشخص کنید، از جمله:


Encoding برای فایل‌ها و جریان‌ها (File و Stream I/O) 📂

رایج‌ترین کاربرد یک شیء Encoding کنترل نحوه خواندن و نوشتن متن در فایل یا جریان است. برای مثال، کد زیر رشته "Testing..." را در فایلی به نام data.txt با Encoding UTF-16 می‌نویسد:

System.IO.File.WriteAllText("data.txt", "Testing...", Encoding.Unicode);

اگر آرگومان نهایی را حذف کنید، WriteAllText از UTF-8 استفاده می‌کند که رایج‌ترین و پیش‌فرض Encoding برای تمام عملیات فایل و جریان است.

ما در فصل ۱۵، در بخش “Stream Adapters” در صفحه 709 دوباره به این موضوع بازخواهیم گشت.


Encoding برای آرایه‌های بایتی 💾

می‌توانید از یک شیء Encoding برای تبدیل بین string و byte[] استفاده کنید.

byte[] utf8Bytes  = System.Text.Encoding.UTF8.GetBytes("0123456789");
byte[] utf16Bytes = System.Text.Encoding.Unicode.GetBytes("0123456789");
byte[] utf32Bytes = System.Text.Encoding.UTF32.GetBytes("0123456789");

Console.WriteLine(utf8Bytes.Length);   // 10
Console.WriteLine(utf16Bytes.Length);  // 20
Console.WriteLine(utf32Bytes.Length);  // 40

string original1 = System.Text.Encoding.UTF8.GetString(utf8Bytes);
string original2 = System.Text.Encoding.Unicode.GetString(utf16Bytes);
string original3 = System.Text.Encoding.UTF32.GetString(utf32Bytes);

Console.WriteLine(original1);   // 0123456789
Console.WriteLine(original2);   // 0123456789
Console.WriteLine(original3);   // 0123456789

UTF-16 و جفت‌های جایگزین (Surrogate Pairs) 🧩

یادتان باشد که .NET کاراکترها و رشته‌ها را در UTF-16 ذخیره می‌کند. چون UTF-16 برای هر کاراکتر به یک یا دو کلمه ۱۶ بیتی نیاز دارد و نوع char فقط ۱۶ بیت طول دارد، برخی از کاراکترهای Unicode نیاز به دو char دارند. پیامدهای این موضوع:

اکثر برنامه‌ها این موضوع را نادیده می‌گیرند زیرا تقریباً همه کاراکترهای رایج در Basic Multilingual Plane (BMP) جا می‌گیرند که تنها به یک کلمه ۱۶ بیتی نیاز دارد. BMP شامل چندین زبان جهانی و بیش از ۳۰,۰۰۰ کاراکتر چینی است.

اما کاراکترهای خارج از BMP شامل زبان‌های باستانی، نمادهای نت‌نویسی موسیقی، برخی کاراکترهای نادر چینی و بیشتر ایموجی‌ها هستند.

برای پشتیبانی از کاراکترهای دوکلمه‌ای می‌توانید از متدهای ایستای char استفاده کنید:

string ConvertFromUtf32(int utf32)
int    ConvertToUtf32(char highSurrogate, char lowSurrogate)

کاراکترهای دوکلمه‌ای Surrogate نامیده می‌شوند و به‌راحتی قابل تشخیص‌اند چون هر کلمه در بازه 0xD800 تا 0xDFFF قرار دارد. متدهای مفید:

bool IsSurrogate(char c)
bool IsHighSurrogate(char c)
bool IsLowSurrogate(char c)
bool IsSurrogatePair(char highSurrogate, char lowSurrogate)

کلاس StringInfo در فضای نام System.Globalization نیز متدها و ویژگی‌هایی برای کار با کاراکترهای دوکلمه‌ای فراهم می‌کند.

کاراکترهای خارج از BMP معمولاً به فونت‌های خاص نیاز دارند و پشتیبانی محدودی در سیستم‌عامل دارند.


تاریخ‌ها و زمان‌ها ⏰

ساختارهای (Struct) تغییرناپذیر (Immutable) زیر در فضای نام System برای نمایش تاریخ و زمان به کار می‌روند:

DateTime, DateTimeOffset, TimeSpan, DateOnly, TimeOnly

زبان C# هیچ کلیدواژه خاصی برای این انواع تعریف نکرده است.


TimeSpan

TimeSpan یک بازه زمانی یا زمان روز را نمایش می‌دهد. در نقش دوم، این زمان همان زمان ساعت (بدون تاریخ) است که معادل مدت‌زمان سپری‌شده از نیمه‌شب است، فرض بر این‌که تغییر ساعت تابستانی وجود ندارد.

سه روش برای ساخت یک TimeSpan وجود دارد:

نمونه سازنده‌ها:

public TimeSpan(int hours, int minutes, int seconds);
public TimeSpan(int days, int hours, int minutes, int seconds);
public TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds);
public TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds, int microseconds);
public TimeSpan(long ticks);   // هر tick = 100ns

نمونه متدهای From…:

public static TimeSpan FromDays(double value);
public static TimeSpan FromHours(double value);
public static TimeSpan FromMinutes(double value);
public static TimeSpan FromSeconds(double value);
public static TimeSpan FromMilliseconds(double value);
public static TimeSpan FromMicroseconds(double value);

نمونه استفاده:

Console.WriteLine(new TimeSpan(2, 30, 0));     // 02:30:00
Console.WriteLine(TimeSpan.FromHours(2.5));    // 02:30:00
Console.WriteLine(TimeSpan.FromHours(-2.5));   // -02:30:00

عملگرهای < و > و همچنین + و - برای TimeSpan سربارگذاری شده‌اند:

TimeSpan.FromHours(2) + TimeSpan.FromMinutes(30); // 2.5 ساعت
TimeSpan.FromDays(10) - TimeSpan.FromSeconds(1);  // 9.23:59:59

ویژگی‌های عددی:

TimeSpan nearlyTenDays = TimeSpan.FromDays(10) - TimeSpan.FromSeconds(1);

Console.WriteLine(nearlyTenDays.Days);         // 9
Console.WriteLine(nearlyTenDays.Hours);        // 23
Console.WriteLine(nearlyTenDays.Minutes);      // 59
Console.WriteLine(nearlyTenDays.Seconds);      // 59
Console.WriteLine(nearlyTenDays.Milliseconds); // 0

ویژگی‌های Total… مقادیر اعشاری (double) را برای کل بازه برمی‌گردانند:

Console.WriteLine(nearlyTenDays.TotalDays);         // 9.99998842592593
Console.WriteLine(nearlyTenDays.TotalHours);        // 239.999722222222
Console.WriteLine(nearlyTenDays.TotalMinutes);      // 14399.9833333333
Console.WriteLine(nearlyTenDays.TotalSeconds);      // 863999
Console.WriteLine(nearlyTenDays.TotalMilliseconds); // 863999000

متد ایستای Parse رشته را به TimeSpan تبدیل می‌کند (برعکس ToString). متد TryParse مشابه است ولی به‌جای پرتاب استثنا در صورت خطا، false برمی‌گرداند.

کلاس XmlConvert نیز متدهایی برای تبدیل TimeSpan/رشته طبق پروتکل‌های استاندارد XML دارد.

مقدار پیش‌فرض TimeSpan برابر با TimeSpan.Zero است.

برای نمایش زمان روز (مدت‌زمان سپری‌شده از نیمه‌شب)، می‌توانید از ویژگی DateTime.Now.TimeOfDay استفاده کنید.


DateTime و DateTimeOffset ⏰

DateTime و DateTimeOffset دو struct تغییرناپذیر (immutable) برای نمایش یک تاریخ و در صورت نیاز، زمان هستند. این دو دارای دقتی معادل 100 نانوثانیه بوده و محدوده‌ای از سال 0001 تا 9999 را پوشش می‌دهند.

DateTimeOffset از نظر کارکرد شبیه DateTime است. تفاوت اصلی آن در این است که یک اختلاف زمانی (offset) نسبت به زمان هماهنگ جهانی (UTC) را هم ذخیره می‌کند؛ این موضوع باعث می‌شود مقایسه مقادیر در مناطق زمانی مختلف دقیق‌تر انجام شود.


انتخاب بین DateTime و DateTimeOffset 🤔

تفاوت اصلی این دو در نحوه مدیریت مناطق زمانی (Time Zone) است:

این موضوع در مقایسه‌ی برابری (Equality) اهمیت دارد و معیار انتخاب یکی از این دو است:

مثال مهم: ساعت تابستانی (Daylight Saving Time) می‌تواند این تفاوت را حتی اگر برنامه شما نیازی به پشتیبانی از مناطق زمانی مختلف نداشته باشد، مهم کند.

نمونه:
DateTime مقادیر زیر را متفاوت می‌داند، در حالی که DateTimeOffset آنها را برابر می‌داند:


مزیت DateTimeOffset در بیشتر موارد 🌍

در اکثر سناریوها، منطق برابری DateTimeOffset بهتر است.
مثلاً برای مقایسه زمان دو رویداد بین‌المللی، DateTimeOffset به‌صورت پیش‌فرض پاسخ درست را ارائه می‌دهد.
حتی یک هکر که قصد اجرای یک حمله‌ی Distributed Denial of Service داشته باشد نیز از DateTimeOffset استفاده می‌کند!

اما در DateTime برای دستیابی به همین دقت باید همه‌چیز را در یک منطقه زمانی واحد (معمولاً UTC) استاندارد کنید که این دو مشکل دارد:


مزیت DateTime در زمان‌های محلی 🖥️

با این حال، DateTime در مشخص کردن زمان نسبت به ساعت محلی سیستم اجرا مناسب‌تر است.
مثلاً اگر بخواهید برای هر دفتر بین‌المللی خود یک آرشیو هفتگی در ساعت ۳ صبح به وقت محلی انجام دهید، DateTime بهتر است چون به زمان محلی هر سایت احترام می‌گذارد.

نکته:
DateTimeOffset برای ذخیره‌ی اختلاف زمانی از یک عدد صحیح کوتاه (short) استفاده می‌کند و این اختلاف را برحسب دقیقه ذخیره می‌کند.
اما اطلاعات منطقه زمانی (Region) را ذخیره نمی‌کند. یعنی نمی‌دانیم اختلاف +08:00 به زمان سنگاپور اشاره دارد یا پرت.

ما در بخش “Dates and Time Zones” صفحه 312 دوباره به موضوع مناطق زمانی و مقایسه‌ی برابری بازمی‌گردیم.

همچنین SQL Server 2008 پشتیبانی مستقیم از DateTimeOffset را با معرفی یک نوع داده‌ی جدید با همین نام اضافه کرده است.


ساخت یک DateTime 🛠️

DateTime چندین سازنده (Constructor) دارد که مقادیر سال، ماه و روز را به‌صورت اعداد صحیح دریافت می‌کنند و در صورت نیاز می‌توان ساعت، دقیقه، ثانیه، میلی‌ثانیه (و از .NET 7 میکروثانیه) را هم مشخص کرد:

public DateTime (int year, int month, int day);
public DateTime (int year, int month, int day,
                 int hour, int minute, int second, int millisecond);

مشخص کردن DateTimeKind 🧭

سازنده‌های DateTime به شما اجازه می‌دهند یک مقدار از نوع enum به نام DateTimeKind هم مشخص کنید که شامل این مقادیر است:

این مقدار همان پرچم سه‌حالته‌ای است که در بخش قبلی توضیح داده شد.

توجه: DateTime برخلاف DateTimeOffset اطلاعات دقیق منطقه زمانی یا اختلاف عددی از UTC را ذخیره نمی‌کند.

مقدار Kind در یک شیء DateTime نشان‌دهنده‌ی همین حالت است.


استفاده از تقویم‌های مختلف 📅

سازنده‌های DateTime قابلیت دریافت یک شیء Calendar را هم دارند.
این امکان به شما اجازه می‌دهد از هر زیرکلاسی از Calendar در System.Globalization برای ایجاد تاریخ استفاده کنید:

DateTime d = new DateTime (5767, 1, 1,
                          new System.Globalization.HebrewCalendar());
Console.WriteLine (d);    // 12/12/2006 12:00:00 AM

نمایش تاریخ به تنظیمات کنترل پنل سیستم شما بستگی دارد.
در نهایت، DateTime همیشه از تقویم میلادی (Gregorian) برای ذخیره استفاده می‌کند و این تبدیل فقط در زمان ساخت شیء انجام می‌شود.

اگر بخواهید محاسباتی با یک تقویم خاص انجام دهید، باید مستقیماً متدهای همان زیرکلاس Calendar را به‌کار ببرید.


ساخت DateTime با Ticks یا زمان‌های خاص ⏳

همچنین برای سازگاری با سیستم‌های دیگر، متدهای استاتیک زیر وجود دارند:


ساخت DateTime از رشته‌های متنی 🔤

برای ساخت یک DateTime از رشته‌ها، می‌توانید از متدهای استاتیک زیر استفاده کنید:

هر دو متد پارامترهای اختیاری برای مشخص کردن پرچم‌ها (flags) و فرمت‌دهنده‌ها (format providers) دارند؛ همچنین ParseExact یک رشته‌ی فرمت هم دریافت می‌کند.

توضیحات کامل‌تر در بخش “Formatting and Parsing” صفحه 317 آمده است.


ساخت DateTimeOffset و کار با آن ⏰

DateTimeOffset مشابه DateTime عمل می‌کند، اما اضافه بر تاریخ و زمان، یک اختلاف زمانی نسبت به UTC هم دریافت می‌کند.

سازنده‌ها (Constructors)

public DateTimeOffset (int year, int month, int day,
                       int hour, int minute, int second,
                       TimeSpan offset);

public DateTimeOffset (int year, int month, int day,
                       int hour, int minute, int second, int millisecond,
                       TimeSpan offset);

ساخت از یک DateTime موجود

می‌توانید یک DateTimeOffset را از یک شیء DateTime بسازید:

public DateTimeOffset (DateTime dateTime);
public DateTimeOffset (DateTime dateTime, TimeSpan offset);

همچنین امکان تبدیل ضمنی (implicit cast) از DateTime به DateTimeOffset وجود دارد:

DateTimeOffset dt = new DateTime(2000, 2, 3);

این کار مفید است چون اکثر کتابخانه‌های BCL .NET هنوز DateTime را پشتیبانی می‌کنند، نه DateTimeOffset.

قوانین پیش‌فرض برای offset


تبدیل DateTimeOffset به DateTime

سه ویژگی (property) برای تبدیل وجود دارد:


زمان فعلی (Now, Today, UtcNow)

نوع ویژگی توضیح
DateTime Now تاریخ و زمان محلی
DateTimeOffset Now تاریخ و زمان محلی + offset
DateTime Today فقط تاریخ، زمان صفر (نیمه‌شب)
DateTime/DateTimeOffset UtcNow تاریخ و زمان در UTC
Console.WriteLine(DateTime.Now);         // 11/11/2019 1:23:45 PM
Console.WriteLine(DateTimeOffset.Now);   // 11/11/2019 1:23:45 PM -06:00
Console.WriteLine(DateTime.Today);       // 11/11/2019 12:00:00 AM
Console.WriteLine(DateTime.UtcNow);      // 11/11/2019 7:23:45 AM
Console.WriteLine(DateTimeOffset.UtcNow);// 11/11/2019 7:23:45 AM +00:00

دقت این مقادیر معمولاً بین ۱۰ تا ۲۰ میلی‌ثانیه است و به سیستم عامل بستگی دارد.


دسترسی به اجزای تاریخ و زمان

DateTime dt = new DateTime(2000, 2, 3, 10, 20, 30);

Console.WriteLine(dt.Year);         // 2000
Console.WriteLine(dt.Month);        // 2
Console.WriteLine(dt.Day);          // 3
Console.WriteLine(dt.DayOfWeek);    // Thursday
Console.WriteLine(dt.DayOfYear);    // 34
Console.WriteLine(dt.Hour);         // 10
Console.WriteLine(dt.Minute);       // 20
Console.WriteLine(dt.Second);       // 30
Console.WriteLine(dt.Millisecond);  // 0
Console.WriteLine(dt.Ticks);        // 630851700300000000
Console.WriteLine(dt.TimeOfDay);    // 10:20:30  (TimeSpan)

محاسبات روی تاریخ و زمان

متدهای نمونه برای تغییر مقادیر تاریخ/زمان (برمی‌گردانند یک شیء جدید):

AddYears, AddMonths, AddDays,
AddHours, AddMinutes, AddSeconds,
AddMilliseconds, AddTicks
TimeSpan ts = TimeSpan.FromMinutes(90);
Console.WriteLine(dt.Add(ts));
Console.WriteLine(dt + ts); // مشابه بالا
DateTime thisYear = new DateTime(2015, 1, 1);
DateTime nextYear = thisYear.AddYears(1);
TimeSpan oneYear = nextYear - thisYear;

قالب‌بندی و پارس کردن (Formatting & Parsing)

این متدها در واقع میانبرهایی برای قالب‌های استاندارد هستند. ToString می‌تواند فرمت سفارشی و provider نیز دریافت کند.

جلوگیری از پارس اشتباه

اگر تنظیمات Culture هنگام parse با زمان فرمت شده متفاوت باشد، ممکن است parse اشتباه شود.
راه حل: از فرمت culture-agnostic استفاده کنید، مثلاً "o":

DateTime dt1 = DateTime.Now;
string cannotBeMisparsed = dt1.ToString("o");
DateTime dt2 = DateTime.Parse(cannotBeMisparsed);

TimeZoneInfo ⏰🌍

کلاس TimeZoneInfo اطلاعاتی درباره نام مناطق زمانی، افست‌های UTC و قوانین Daylight Saving Time ارائه می‌دهد.


TimeZone

متد ایستا TimeZone.CurrentTimeZone یک شیء TimeZone برمی‌گرداند:

TimeZone zone = TimeZone.CurrentTimeZone;
Console.WriteLine (zone.StandardName);      // Pacific Standard Time
Console.WriteLine (zone.DaylightName);      // Pacific Daylight Time

متد GetDaylightChanges اطلاعات خاص مربوط به Daylight Saving Time برای یک سال مشخص را برمی‌گرداند:

DaylightTime day = zone.GetDaylightChanges (2019);
Console.WriteLine (day.Start.ToString ("M"));       // 10 March
Console.WriteLine (day.End.ToString ("M"));         // 03 November
Console.WriteLine (day.Delta);                      // 01:00:00

TimeZoneInfo

متد ایستا TimeZoneInfo.Local یک شیء TimeZoneInfo بر اساس تنظیمات محلی جاری برمی‌گرداند. مثال زیر نتیجه اجرای آن در کالیفرنیا را نشان می‌دهد:

TimeZoneInfo zone = TimeZoneInfo.Local;
Console.WriteLine (zone.StandardName);      // Pacific Standard Time
Console.WriteLine (zone.DaylightName);      // Pacific Daylight Time

متدهای IsDaylightSavingTime و GetUtcOffset به شکل زیر کار می‌کنند:

DateTime dt1 = new DateTime (2019, 1, 1);   // DateTimeOffset هم کار می‌کند
DateTime dt2 = new DateTime (2019, 6, 1);
Console.WriteLine (zone.IsDaylightSavingTime (dt1));     // True
Console.WriteLine (zone.IsDaylightSavingTime (dt2));     // False
Console.WriteLine (zone.GetUtcOffset (dt1));             // -08:00:00
Console.WriteLine (zone.GetUtcOffset (dt2));             // -07:00:00

برای دریافت TimeZoneInfo هر منطقه زمانی جهان، از متد FindSystemTimeZoneById استفاده می‌کنیم. در اینجا به منطقه غرب استرالیا می‌رویم:

TimeZoneInfo wa = TimeZoneInfo.FindSystemTimeZoneById
                  ("W. Australia Standard Time");
Console.WriteLine (wa.Id);                   // W. Australia Standard Time
Console.WriteLine (wa.DisplayName);          // (GMT+08:00) Perth
Console.WriteLine (wa.BaseUtcOffset);        // 08:00:00
Console.WriteLine (wa.SupportsDaylightSavingTime);     // True

ویژگی Id همان مقداری است که به FindSystemTimeZoneById پاس داده شده است.
متد ایستا GetSystemTimeZones تمام مناطق زمانی جهان را برمی‌گرداند و می‌توان تمام شناسه‌های معتبر را لیست کرد:

foreach (TimeZoneInfo z in TimeZoneInfo.GetSystemTimeZones())
    Console.WriteLine (z.Id);

می‌توان یک منطقه زمانی سفارشی ایجاد کرد با TimeZoneInfo.CreateCustomTimeZone.
چون TimeZoneInfo غیرقابل تغییر است، همه اطلاعات لازم باید هنگام ایجاد به متد داده شوند.

همچنین می‌توان یک منطقه زمانی از پیش تعریف‌شده یا سفارشی را با ToSerializedString به رشته‌ای نیمه‌قابل‌خواندن برای انسان تبدیل کرد و با TimeZoneInfo.FromSerializedString دوباره بازیابی نمود.


متد ایستا ConvertTime یک DateTime یا DateTimeOffset را از یک منطقه زمانی به منطقه‌ای دیگر تبدیل می‌کند. می‌توان تنها منطقه مقصد را مشخص کرد یا هر دو منطقه مبدأ و مقصد را وارد نمود. تبدیل مستقیم از/به UTC نیز با متدهای ConvertTimeFromUtc و ConvertTimeToUtc ممکن است.


برای کار با Daylight Saving Time، TimeZoneInfo متدهای اضافی زیر را فراهم می‌کند:


برای گرفتن تاریخ‌های شروع و پایان Daylight Saving Time از یک TimeZoneInfo، باید GetAdjustmentRules را صدا زد که خلاصه قوانین DST را برای همه سال‌ها برمی‌گرداند. هر قانون دارای DateStart و DateEnd است:

foreach (TimeZoneInfo.AdjustmentRule rule in wa.GetAdjustmentRules())
    Console.WriteLine ("Rule: applies from " + rule.DateStart +
                                        " to " + rule.DateEnd);

مثال: غرب استرالیا اولین بار Daylight Saving Time را در ۲۰۰۶ معرفی کرد و سپس در ۲۰۰۹ لغو نمود. سال اول نیاز به قانون ویژه داشت، بنابراین دو قانون داریم:

Rule: applies from 1/01/2006 12:00:00 AM to 31/12/2006 12:00:00 AM
Rule: applies from 1/01/2007 12:00:00 AM to 31/12/2009 12:00:00 AM

هر AdjustmentRule دارای DaylightDelta از نوع TimeSpan (معمولاً یک ساعت) و ویژگی‌های DaylightTransitionStart و DaylightTransitionEnd است که از نوع TimeZoneInfo.TransitionTime بوده و ویژگی‌های زیر را دارند:

public bool IsFixedDateRule { get; }
public DayOfWeek DayOfWeek { get; }
public int Week { get; }
public int Day { get; }
public int Month { get; }
public DateTime TimeOfDay { get; }

زمان انتقال (TransitionTime) می‌تواند هم تاریخ ثابت و هم شناور را نشان دهد. مثال تاریخ شناور: «آخرین یک‌شنبه ماه مارس».
قوانین تفسیر TransitionTime:

1️⃣ اگر برای End Transition، IsFixedDateRule=true، Day=1، Month=1 و TimeOfDay=DateTime.MinValue، آن سال پایان DST وجود ندارد (معمولاً در نیم‌کره جنوبی).
2️⃣ اگر IsFixedDateRule=true، Month، Day و TimeOfDay تعیین‌کننده شروع یا پایان قانون هستند.
3️⃣ اگر IsFixedDateRule=false، Month، DayOfWeek، Week و TimeOfDay تعیین‌کننده شروع یا پایان قانون هستند.

در حالت آخر، Week شماره هفته ماه است و «۵» یعنی آخرین هفته.


مثال قوانین انتقال wa:

foreach (TimeZoneInfo.AdjustmentRule rule in wa.GetAdjustmentRules())
{
    Console.WriteLine ("Rule: applies from " + rule.DateStart +
                                        " to " + rule.DateEnd);
    Console.WriteLine ("   Delta: " + rule.DaylightDelta);
    Console.WriteLine ("   Start: " + FormatTransitionTime
                                   (rule.DaylightTransitionStart, false));
    Console.WriteLine ("   End:   " + FormatTransitionTime
                                   (rule.DaylightTransitionEnd, true));
    Console.WriteLine();
}

در FormatTransitionTime قوانین فوق رعایت می‌شوند:

static string FormatTransitionTime (TimeZoneInfo.TransitionTime tt,
                                    bool endTime)
{
    if (endTime && tt.IsFixedDateRule
                && tt.Day == 1 && tt.Month == 1
                && tt.TimeOfDay == DateTime.MinValue)
        return "-";
    string s;
    if (tt.IsFixedDateRule)
        s = tt.Day.ToString();
    else
        s = "The " +
            "first second third fourth last".Split()[tt.Week - 1] +
            " " + tt.DayOfWeek + " in";
    return s + " " + DateTimeFormatInfo.CurrentInfo.MonthNames [tt.Month-1]
             + " at " + tt.TimeOfDay.TimeOfDay;
}

Daylight Saving Time و DateTime 🌞🕒

اگر از DateTimeOffset یا یک UTC DateTime استفاده کنید، مقایسه برابری تحت تأثیر Daylight Saving Time قرار نمی‌گیرد. اما با DateTimeهای محلی، DST می‌تواند مشکل‌ساز باشد.

می‌توان قوانین را به‌طور خلاصه به این صورت بیان کرد:

متد IsDaylightSavingTime مشخص می‌کند که یک DateTime محلی تحت DST قرار دارد یا نه. زمان‌های UTC همیشه false برمی‌گردانند:

Console.Write (DateTime.Now.IsDaylightSavingTime());     // True یا False
Console.Write (DateTime.UtcNow.IsDaylightSavingTime());  // همیشه False

اگر dto یک DateTimeOffset باشد، عبارت زیر همان عملکرد را دارد:

dto.LocalDateTime.IsDaylightSavingTime

پایان Daylight Saving Time برای الگوریتم‌هایی که از زمان محلی استفاده می‌کنند، پیچیدگی ایجاد می‌کند؛ زیرا وقتی ساعت‌ها عقب کشیده می‌شوند، همان ساعت (یا دقیق‌تر، Delta) دوباره تکرار می‌شود.

مقایسه مطمئن بین هر دو DateTime با صدا زدن ToUniversalTime روی هرکدام انجام می‌شود. این روش تنها در صورتی شکست می‌خورد که یکی از آن‌ها DateTimeKind.Unspecified باشد.
این احتمال خطا دلیل دیگری برای ترجیح دادن DateTimeOffset است.


Formatting و Parsing 📝

Formatting یعنی تبدیل به رشته؛ Parsing یعنی تبدیل از رشته.
نیاز به format یا parse در برنامه‌نویسی بسیار رایج است، و .NET مکانیزم‌های متعددی برای آن ارائه می‌دهد:

1️⃣ ToString و Parse
این متدها عملکرد پیش‌فرض را برای بسیاری از انواع فراهم می‌کنند.

2️⃣ Format Providers
این‌ها به شکل متدهای اضافی ToString و Parse ظاهر می‌شوند و رشته فرمت و/یا یک format provider می‌گیرند. Format Providers بسیار منعطف و آگاه به فرهنگ (culture-aware) هستند. .NET شامل format provider برای انواع عددی و DateTime/DateTimeOffset است.

3️⃣ XmlConvert
یک کلاس ایستا که فرمت و پارس کردن را با رعایت استانداردهای XML انجام می‌دهد. همچنین برای تبدیل مستقل از فرهنگ یا جلوگیری از اشتباه در پارس کردن مفید است. از انواع عددی، bool، DateTime, DateTimeOffset, TimeSpan و Guid پشتیبانی می‌کند.

4️⃣ Type Converters
این‌ها مخصوص طراحان و XAML parsers هستند.

در این بخش، ما روی دو مکانیزم اول تمرکز می‌کنیم، مخصوصاً Format Providers، سپس XmlConvert و Type Converters را بررسی خواهیم کرد.


ToString و Parse 🔄

ساده‌ترین مکانیزم فرمتینگ متد ToString است. این متد خروجی معنادار برای انواع ساده (bool, DateTime, DateTimeOffset, TimeSpan, Guid و انواع عددی) ارائه می‌دهد.
برای عملیات معکوس، هرکدام از این انواع متد Parse ایستا دارند:

string s = true.ToString();     // s = "True"
bool b = bool.Parse(s);         // b = true

اگر پارس کردن شکست بخورد، FormatException پرتاب می‌شود.
بسیاری از انواع همچنین متد TryParse دارند که اگر تبدیل شکست بخورد، false برمی‌گرداند به‌جای پرتاب استثنا:

bool failure = int.TryParse("qwerty", out int i1);
bool success = int.TryParse("123", out int i2);

اگر به خروجی نیاز ندارید و فقط می‌خواهید بررسی کنید که پارس موفق می‌شود یا نه، می‌توانید از discard استفاده کنید:

bool success = int.TryParse("123", out int _);

اگر انتظار خطا دارید، TryParse سریع‌تر و شیک‌تر از استفاده از Parse در بلوک try/catch است.


فرهنگ و Parse/ToString 🌐

متدهای Parse و TryParse برای DateTime(Offset) و انواع عددی به تنظیمات فرهنگ محلی احترام می‌گذارند.
می‌توان با مشخص کردن یک شیء CultureInfo این رفتار را تغییر داد. استفاده از InvariantCulture معمولاً ایده خوبی است:

Console.WriteLine(double.Parse("1.234"));  // 1234 در آلمان
double x = double.Parse("1.234", CultureInfo.InvariantCulture);
string y = 1.234.ToString(CultureInfo.InvariantCulture);

از .NET 8 به بعد، انواع عددی و تاریخ/زمان امکان فرمتینگ و پارس مستقیم UTF-8 را دارند با متدهای TryFormat و Parse/TryParse که روی byte array یا Span کار می‌کنند. این در سناریوهای با عملکرد بالا می‌تواند کارآمدتر از کار با رشته‌های UTF-16 و تبدیل جداگانه باشد.


Format Providers و IFormattable 🛠️

گاهی نیاز دارید کنترل بیشتری روی فرمتینگ و پارس داشته باشید.
در .NET، همه انواع عددی و DateTime(Offset) اینترفیس IFormattable را پیاده‌سازی می‌کنند:

public interface IFormattable
{
    string ToString(string format, IFormatProvider formatProvider);
}

مثال:

NumberFormatInfo f = new NumberFormatInfo();
f.CurrencySymbol = "$$";
Console.WriteLine(3.ToString("C", f));  // $$ 3.00

اینجا "C" رشته فرمت برای Currency است و NumberFormatInfo مشخص می‌کند که پول و سایر نمایش‌های عددی چگونه رندر شوند.


اگر رشته فرمت یا provider برابر null باشد، یک مقدار پیش‌فرض استفاده می‌شود: CultureInfo.CurrentCulture، که تنظیمات کنترل پنل سیستم را منعکس می‌کند:

Console.WriteLine(10.3.ToString("C"));  // $10.30
Console.WriteLine(10.3.ToString("F4")); // 10.3000

سه Format Provider اصلی

تمام Enumها نیز قابل فرمت هستند، گرچه کلاس ویژه IFormatProvider ندارند.


CultureInfo و Format Providers 🌎

CultureInfo میانجی برای NumberFormatInfo و DateTimeFormatInfo است، که تنظیمات منطقه‌ای فرهنگ را اعمال می‌کنند:

CultureInfo uk = CultureInfo.GetCultureInfo("en-GB");
Console.WriteLine(3.ToString("C", uk));  // £3.00

فرمت تاریخ با InvariantCulture همیشه یکسان است، صرف‌نظر از تنظیمات سیستم:

DateTime dt = new DateTime(2000, 1, 2);
CultureInfo iv = CultureInfo.InvariantCulture;
Console.WriteLine(dt.ToString(iv));      // 01/02/2000 00:00:00
Console.WriteLine(dt.ToString("d", iv)); // 01/02/2000

ویژگی‌های InvariantCulture:

استفاده از NumberFormatInfo یا DateTimeFormatInfo 🔢📅

در مثال بعد، یک NumberFormatInfo ایجاد می‌کنیم و جداکننده گروه (Group Separator) را از ویرگول به فاصله تغییر می‌دهیم. سپس از آن برای فرمت کردن یک عدد با سه رقم اعشار استفاده می‌کنیم:

NumberFormatInfo f = new NumberFormatInfo();
f.NumberGroupSeparator = " ";
Console.WriteLine(12345.6789.ToString("N3", f));  // 12 345.679

تنظیمات اولیه برای NumberFormatInfo یا DateTimeFormatInfo بر اساس InvariantCulture هستند.
گاهی اوقات مفیدتر است که نقطه شروع متفاوتی انتخاب کنید. برای این کار می‌توانید یک Format Provider موجود را Clone کنید:

NumberFormatInfo f = (NumberFormatInfo)
                     CultureInfo.CurrentCulture.NumberFormat.Clone();

یک Format Provider کلون‌شده همیشه قابل نوشتن است، حتی اگر نمونه اصلی فقط خواندنی باشد.


فرمت ترکیبی (Composite Formatting) 🧩

رشته‌های فرمت ترکیبی اجازه می‌دهند جایگزینی متغیرها با رشته فرمت را ترکیب کنید.
متد استاتیک string.Format یک رشته فرمت ترکیبی می‌گیرد:

string composite = "Credit={0:C}";
Console.WriteLine(string.Format(composite, 500));  // Credit=$500.00

خود کلاس Console متدهای Write و WriteLine را برای پشتیبانی از فرمت ترکیبی اورلود کرده است:

Console.WriteLine("Credit={0:C}", 500);  // Credit=$500.00

می‌توانید رشته فرمت ترکیبی را به StringBuilder (با AppendFormat) یا به یک TextWriter برای I/O اضافه کنید.

string.Format می‌تواند یک Format Provider اختیاری هم بگیرد. مثال:

string s = string.Format(CultureInfo.InvariantCulture, "{0}", someObject);

این معادل است با:

string s;
if (someObject is IFormattable)
    s = ((IFormattable)someObject).ToString(null, CultureInfo.InvariantCulture);
else if (someObject == null)
    s = "";
else
    s = someObject.ToString();

پارس کردن با Format Providers 🔧

هیچ اینترفیس استانداردی برای پارس کردن از طریق Format Provider وجود ندارد.
در عوض، هر نوع شرکت‌کننده، متد استاتیک Parse و TryParse خود را اورلود کرده تا Format Provider را بپذیرد و به صورت اختیاری NumberStyles یا DateTimeStyles را هم دریافت کند.

NumberStyles و DateTimeStyles کنترل می‌کنند که پارس کردن چگونه انجام شود، مانند اینکه آیا پرانتز یا نماد ارز می‌تواند در ورودی باشد یا نه. (به‌طور پیش‌فرض، هر دو گزینه پاسخ نه هستند.)

مثال:

int error = int.Parse("(2)");  // Exception پرتاب می‌شود
int minusTwo = int.Parse("(2)", NumberStyles.Integer | NumberStyles.AllowParentheses);  // OK
decimal fivePointTwo = decimal.Parse("£5.20", NumberStyles.Currency,
                                      CultureInfo.GetCultureInfo("en-GB"));

بخش بعدی تمام اعضای NumberStyles و DateTimeStyles و قوانین پیش‌فرض پارس هر نوع را فهرست می‌کند.


IFormatProvider و ICustomFormatter 🛠️

تمام Format Providers اینترفیس IFormatProvider را پیاده‌سازی می‌کنند:

public interface IFormatProvider 
{ 
    object GetFormat(Type formatType); 
}

هدف این متد ایجاد indirection است؛ این همان چیزی است که اجازه می‌دهد CultureInfo به NumberFormatInfo یا DateTimeFormatInfo مناسب ارجاع دهد.

با پیاده‌سازی IFormatProvider و ICustomFormatter، می‌توانید Format Provider سفارشی خود را بسازید که با انواع موجود کار کند.

ICustomFormatter یک متد دارد:

string Format(string format, object arg, IFormatProvider formatProvider);

مثال یک Format Provider سفارشی که اعداد را به کلمات تبدیل می‌کند:

public class WordyFormatProvider : IFormatProvider, ICustomFormatter
{
    static readonly string[] _numberWords = "zero one two three four five six seven eight nine minus point".Split();
    IFormatProvider _parent;   // امکان زنجیره سازی Format Providerها

    public WordyFormatProvider() : this(CultureInfo.CurrentCulture) { }
    public WordyFormatProvider(IFormatProvider parent) => _parent = parent;

    public object GetFormat(Type formatType)
    {
        if (formatType == typeof(ICustomFormatter)) return this;
        return null;
    }

    public string Format(string format, object arg, IFormatProvider prov)
    {
        // اگر رشته فرمت ما نبود، به parent واگذار کن:
        if (arg == null || format != "W")
            return string.Format(_parent, "{0:" + format + "}", arg);

        StringBuilder result = new StringBuilder();
        string digitList = string.Format(CultureInfo.InvariantCulture, "{0}", arg);

        foreach (char digit in digitList)
        {
            int i = "0123456789-.".IndexOf(digit, StringComparison.InvariantCulture);
            if (i == -1) continue;
            if (result.Length > 0) result.Append(' ');
            result.Append(_numberWords[i]);
        }

        return result.ToString();
    }
}

نکته: در متد Format از string.Format با InvariantCulture برای تبدیل عدد به رشته استفاده شد. این اطمینان می‌دهد که فقط کاراکترهای 0123456789-. استفاده شوند و نه نسخه‌های بین‌المللی.

مثال استفاده از WordyFormatProvider:

double n = -123.45;
IFormatProvider fp = new WordyFormatProvider();
Console.WriteLine(string.Format(fp, "{0:C} in words is {0:W}", n));
// -$123.45 in words is minus one two three point four five

توجه: Custom Format Providers تنها در Composite Format Strings قابل استفاده هستند.

رشته‌های فرمت استاندارد و پرچم‌های پارس 🔢📏

رشته‌های فرمت استاندارد تعیین می‌کنند که یک نوع عددی یا DateTime/DateTimeOffset چگونه به رشته تبدیل شود. دو نوع رشته فرمت وجود دارد:


رشته‌های فرمت استاندارد ✅

با این رشته‌ها، راهنمایی کلی ارائه می‌دهید.
یک رشته فرمت استاندارد از یک حرف تشکیل شده و به‌طور اختیاری می‌تواند یک رقم بعد از آن داشته باشد (که معنی آن بستگی به حرف دارد).
مثال‌ها: "C" یا "F2"


رشته‌های فرمت سفارشی 🎨

با این رشته‌ها، می‌توانید هر کاراکتر را با قالب مشخص مدیریت کنید.
مثال: "0:#.000E+00"

توجه: رشته‌های فرمت سفارشی با Format Providerهای سفارشی ارتباطی ندارند.


رشته‌های فرمت عددی 🧮

جدول 6-2 تمام رشته‌های فرمت استاندارد عددی را فهرست می‌کند.

Conventions-UsedThis-Book

ارائه نکردن رشته فرمت عددی 🔢❌

اگر هیچ رشته فرمت عددی ارائه ندهید (یا رشته null یا خالی باشد)، معادل استفاده از رشته فرمت استاندارد "G" بدون رقم بعدی است. این رفتار به شرح زیر است:

گرد کردن خودکار توضیح داده‌شده معمولاً مفید است و معمولاً متوجه آن نمی‌شویم. با این حال، ممکن است مشکلاتی ایجاد کند اگر بخواهید یک عدد را round-trip کنید؛ یعنی آن را به رشته تبدیل کرده و دوباره بازگردانید (شاید چند بار) بدون آن‌که برابری مقدار از بین برود.

برای همین منظور، رشته‌های فرمت R، G17، و G9 وجود دارند تا از گرد کردن ضمنی جلوگیری کنند.

جدول 6-3، رشته‌های فرمت عددی سفارشی را فهرست می‌کند.

Conventions-UsedThis-Book

NumberStyles 🔢

هر نوع عددی (numeric type) یک متد Parse ایستا (static) تعریف می‌کند که یک آرگومان NumberStyles می‌پذیرد. NumberStyles یک enum با قابلیت flags است که به شما امکان می‌دهد مشخص کنید رشته چگونه به عدد تبدیل شود. اعضای قابل ترکیب آن عبارت‌اند از:

علاوه بر این، NumberStyles اعضای مرکب زیر را نیز تعریف می‌کند:

به جز None، همه مقادیر مرکب شامل AllowLeadingWhite و AllowTrailingWhite هستند. شکل 6-1 ترکیب باقی‌مانده آن‌ها را نشان می‌دهد، که سه مورد مفیدتر، برجسته شده‌اند.

Conventions-UsedThis-Book

وقتی که متد Parse را بدون مشخص کردن هیچ پرچمی (flag) فراخوانی می‌کنید، مقادیر پیش‌فرض نشان داده‌شده در شکل ۶-۲ اعمال می‌شوند. 🔹

Conventions-UsedThis-Book

اگر نمی‌خواهید از مقادیر پیش‌فرض نشان داده‌شده در شکل ۶-۲ استفاده کنید، باید صریحاً NumberStyles را مشخص کنید:

int thousand = int.Parse("3E8", NumberStyles.HexNumber);
int minusTwo = int.Parse("(2)", NumberStyles.Integer | NumberStyles.AllowParentheses);
double aMillion = double.Parse("1,000,000", NumberStyles.Any);
decimal threeMillion = decimal.Parse("3e6", NumberStyles.Any);
decimal fivePointTwo = decimal.Parse("$5.20", NumberStyles.Currency);

چون ما format provider مشخص نکردیم، این مثال با نماد پول محلی، جداکننده گروهی (group separator)، نقطه اعشار و غیره در سیستم شما کار می‌کند.

مثال بعدی به صورت سخت‌کد شده (hardcoded) است تا با علامت یورو و یک فاصله به عنوان جداکننده گروهی برای ارزها کار کند:

NumberFormatInfo ni = new NumberFormatInfo();
ni.CurrencySymbol = "€";
ni.CurrencyGroupSeparator = " ";
double million = double.Parse("€1 000 000", NumberStyles.Currency, ni);

رشته‌های قالب‌بندی تاریخ/زمان 🕒

رشته‌های قالب‌بندی برای DateTime/DateTimeOffset را می‌توان بر اساس اینکه تنظیمات فرهنگ (culture) و format provider را رعایت می‌کنند یا خیر به دو گروه تقسیم کرد. جدول ۶-۴ آن‌هایی را که رعایت می‌کنند نشان می‌دهد و جدول ۶-۵ آن‌هایی را که رعایت نمی‌کنند. خروجی نمونه از قالب‌بندی DateTime زیر به دست آمده است (با فرهنگ ثابت InvariantCulture در مورد جدول ۶-۴):

new DateTime(2000, 1, 2, 17, 18, 19);

Conventions-UsedThis-Book

Conventions-UsedThis-Book

رشته‌های قالب‌بندی "r", "R" و "u" پسوندی تولید می‌کنند که UTC را نشان می‌دهد؛ اما به‌طور خودکار یک DateTime محلی را به UTC تبدیل نمی‌کنند، بنابراین شما خودتان باید تبدیل را انجام دهید. جالب اینجاست که "U" به‌طور خودکار به UTC تبدیل می‌شود، اما پسوند منطقه زمانی را نمی‌نویسد! در واقع، "o" تنها مشخصه قالب در این گروه است که می‌تواند یک DateTime بدون ابهام را بدون دخالت کاربر نمایش دهد.

DateTimeFormatInfo همچنین از رشته‌های قالب‌بندی سفارشی پشتیبانی می‌کند: این‌ها مشابه رشته‌های قالب‌بندی سفارشی عددی هستند. فهرست کامل آن‌ها گسترده است و در مستندات مایکروسافت آنلاین موجود است. یک مثال از رشته قالب‌بندی سفارشی:

yyyy-MM-dd HH:mm:ss

تجزیه و اشتباه در تجزیه DateTime 🕒

رشته‌هایی که ماه یا روز را ابتدا می‌گذارند، ابهام‌آمیز هستند و می‌توانند به‌راحتی اشتباه تجزیه شوند—به‌ویژه اگر مشتریان جهانی داشته باشید. این مشکل در کنترل‌های رابط کاربری وجود ندارد، زیرا همان تنظیمات هنگام تجزیه و قالب‌بندی اعمال می‌شوند.

اما هنگام نوشتن در یک فایل، مثلاً، اشتباه در تشخیص روز/ماه می‌تواند مشکل‌ساز شود. دو راه‌حل وجود دارد:

روش دوم مقاوم‌تر است—به‌ویژه اگر قالبی انتخاب کنید که سال چهاررقمی را ابتدا قرار دهد؛ چنین رشته‌هایی برای تجزیه اشتباه توسط طرف دیگر سخت‌تر هستند. علاوه بر این، رشته‌هایی که با قالب سال‌اول استاندارد (مانند "o") قالب‌بندی شده‌اند، می‌توانند به‌درستی در کنار رشته‌های قالب‌بندی‌شده محلی تجزیه شوند—مانند یک «دهنده جهانی». (تاریخ‌هایی که با "s" یا "u" قالب‌بندی شده‌اند، مزیت اضافه‌ای هم دارند: قابلیت مرتب‌سازی.)

برای مثال، فرض کنید یک رشته DateTime مستقل از فرهنگ تولید می‌کنیم:

string s = DateTime.Now.ToString("o");

رشته قالب "o" میلی‌ثانیه‌ها را در خروجی شامل می‌کند.

رشته قالب سفارشی زیر همان خروجی "o" را بدون میلی‌ثانیه می‌دهد:

yyyy-MM-ddTHH:mm:ss K

ما می‌توانیم این رشته را به دو روش تجزیه کنیم:

DateTime dt1 = DateTime.ParseExact(s, "o", null);

(می‌توانید نتیجه مشابهی با متدهای XmlConvert.ToString و XmlConvert.ToDateTime به دست آورید.)

DateTime dt2 = DateTime.Parse(s);

این روش هم برای DateTime و هم DateTimeOffset کار می‌کند.

استفاده از ParseExact معمولاً ترجیح داده می‌شود اگر قالب رشته‌ای که تجزیه می‌کنید را می‌دانید، زیرا اگر رشته به‌درستی قالب‌بندی نشده باشد، یک استثناء پرتاب می‌شود—که معمولاً بهتر از ریسک تجزیه اشتباه تاریخ است.


DateTimeStyles ⚙️

DateTimeStyles یک flags enum است که دستورالعمل‌های اضافی هنگام فراخوانی Parse روی DateTime(Offset) ارائه می‌دهد. اعضای آن عبارتند از:

None, AllowLeadingWhite, AllowTrailingWhite, AllowInnerWhite,
AssumeLocal, AssumeUniversal, AdjustToUniversal,
NoCurrentDateDefault, RoundTripKind

همچنین یک عضو ترکیبی به نام AllowWhiteSpaces وجود دارد:

AllowWhiteSpaces = AllowLeadingWhite | AllowTrailingWhite | AllowInnerWhite

مقدار پیش‌فرض None است، به این معنی که فضای خالی اضافی معمولاً مجاز نیست (فضای خالی که بخشی از الگوی استاندارد DateTime است، مستثنی است).

اگر رشته‌ای شامل زمان اما بدون تاریخ باشد، به‌طور پیش‌فرض تاریخ امروز اعمال می‌شود. اگر پرچم NoCurrentDateDefault را اعمال کنید، به جای آن از 1 ژانویه 0001 استفاده می‌شود.


رشته‌های قالب‌بندی Enum 🔢

در بخش «Enums» در صفحه ۱۵۴، قالب‌بندی و تجزیه مقادیر enum را توضیح دادیم.
جدول ۶-۶ هر رشته قالب‌بندی و نتیجه اعمال آن روی عبارت زیر را نشان می‌دهد:

Console.WriteLine(System.ConsoleColor.Red.ToString(formatString));

Conventions-UsedThis-Book

مکانیزم‌های دیگر تبدیل 🔄

در دو بخش قبلی، فرمت پروایدرها را بررسی کردیم—مکانیزم اصلی .NET برای قالب‌بندی و تجزیه. سایر مکانیزم‌های مهم تبدیل در انواع و فضای نام‌های مختلف پراکنده‌اند. برخی تبدیل به/از رشته انجام می‌دهند و برخی انواع دیگری از تبدیل‌ها را ارائه می‌کنند. در این بخش، موضوعات زیر را بررسی می‌کنیم:


Convert ⚡

.NET انواع زیر را base types می‌نامد:

کلاس static Convert متدهایی برای تبدیل هر base type به هر base type دیگر ارائه می‌دهد. متأسفانه بیشتر این متدها کاربردی نیستند: یا استثناء پرتاب می‌کنند یا در کنار کست‌های ضمنی اضافی هستند. با این حال، در بین این متدها، برخی متدهای مفید نیز وجود دارند که در بخش‌های بعدی آورده شده‌اند.

تمام base types به‌طور صریح IConvertible را پیاده‌سازی می‌کنند، که متدهایی برای تبدیل به تمام base typeهای دیگر تعریف می‌کند. در بیشتر موارد، پیاده‌سازی این متدها صرفاً یک متد در Convert را فراخوانی می‌کند. در موارد نادر، نوشتن متدی که یک آرگومان از نوع IConvertible بپذیرد، مفید است.


تبدیل اعداد حقیقی به صحیح با گرد کردن 🔢

در فصل ۲ دیدیم که کست‌های ضمنی و صریح اجازه می‌دهند بین انواع عددی تبدیل انجام دهید:

کست‌ها برای کارایی بهینه شده‌اند؛ بنابراین داده‌هایی که جا نمی‌شوند، قطع می‌شوند. این مشکل هنگام تبدیل از عدد حقیقی به عدد صحیح پیش می‌آید، زیرا معمولاً می‌خواهید گرد کنید نه قطع کنید. متدهای تبدیل عددی Convert این مشکل را حل می‌کنند—همیشه گرد می‌کنند:

double d = 3.9;
int i = Convert.ToInt32(d);    // i == 4

Convert از Banker’s Rounding استفاده می‌کند، که مقادیر وسط را به عدد صحیح زوج نزدیک می‌کند (این از ایجاد سوگیری مثبت یا منفی جلوگیری می‌کند). اگر Banker’s Rounding مشکل‌ساز است، ابتدا می‌توانید Math.Round را روی عدد حقیقی فراخوانی کنید که آرگومانی برای کنترل گرد کردن مقادیر وسط دارد.


تجزیه اعداد در مبناهای ۲، ۸ و ۱۶ 🔢

در بین متدهای To(integral-type)، overloadهایی وجود دارند که اعداد را در مبنای دیگر تجزیه می‌کنند:

int thirty = Convert.ToInt32("1E", 16);    // تجزیه هگزادسیمال
uint five  = Convert.ToUInt32("101", 2);   // تجزیه باینری

آرگومان دوم مبنا را مشخص می‌کند. می‌تواند هر مبنایی باشد—تا زمانی که ۲، ۸، ۱۰ یا ۱۶ باشد!


تبدیل‌های داینامیک ⚡

گاهی نیاز دارید از یک نوع به نوع دیگر تبدیل کنید، اما تا زمان اجرا نوع‌ها مشخص نیستند. برای این منظور، کلاس Convert متد ChangeType را ارائه می‌دهد:

public static object ChangeType(object value, Type conversionType);

نوع منبع و مقصد باید یکی از base typeها باشد. ChangeType همچنین آرگومان اختیاری IFormatProvider را می‌پذیرد. مثال:

Type targetType = typeof(int);
object source = "42";
object result = Convert.ChangeType(source, targetType);

Console.WriteLine(result);             // 42
Console.WriteLine(result.GetType());   // System.Int32

یک کاربرد مفید آن، نوشتن یک deserializer است که بتواند با چند نوع مختلف کار کند. همچنین می‌تواند هر enum را به نوع عددی متناظر خود تبدیل کند. محدودیت ChangeType این است که نمی‌توانید رشته قالب یا پرچم تجزیه مشخص کنید.


تبدیل Base-64 📄

گاهی نیاز است داده باینری مانند یک bitmap را در یک سند متنی مانند XML یا ایمیل قرار دهید. Base-64 روش متداولی است که داده باینری را با استفاده از ۶۴ کاراکتر از مجموعه ASCII به صورت قابل خواندن رمزگذاری می‌کند.

XmlConvert 🗂️

اگر با داده‌هایی سروکار دارید که منبع آن‌ها XML است یا قرار است در یک فایل XML ذخیره شوند، کلاس XmlConvert (در فضای نام System.Xml) مناسب‌ترین متدها را برای قالب‌بندی و تجزیه فراهم می‌کند. متدهای XmlConvert بدون نیاز به رشته قالب‌بندی خاص، جزئیات فرمت XML را مدیریت می‌کنند. برای مثال، مقدار true در XML به صورت "true" است و نه "True". کتابخانه BCL در .NET به طور گسترده از XmlConvert استفاده می‌کند.

XmlConvert همچنین برای سریال‌سازی مستقل از فرهنگ نیز مناسب است. متدهای قالب‌بندی در XmlConvert به صورت overloaded ToString ارائه می‌شوند و متدهای تجزیه به شکل ToBoolean, ToDateTime و غیره هستند:

string s = XmlConvert.ToString(true);         // s = "true"
bool isTrue = XmlConvert.ToBoolean(s);

متدهایی که به/از DateTime تبدیل می‌کنند، آرگومانی به نام XmlDateTimeSerializationMode می‌پذیرند که یک enum با مقادیر زیر است:

2010-02-22T14:08:30.9375           // Unspecified
2010-02-22T14:07:30.9375+09:00     // Local
2010-02-22T05:08:30.9375Z          // Utc

Type Converters 🎨

Type converters برای قالب‌بندی و تجزیه در محیط‌های design-time طراحی شده‌اند. آن‌ها همچنین مقادیر را در اسناد XAML تجزیه می‌کنند—مثلاً در WPF.

در .NET بیش از ۱۰۰ نوع مبدل وجود دارد که مواردی مانند رنگ‌ها، تصاویر و URIs را پوشش می‌دهند. در مقابل، فرمت پروایدرها تنها برای چند نوع ساده ارزشمند هستند.

Type converters معمولاً رشته‌ها را به روش‌های مختلف بدون نیاز به راهنما تجزیه می‌کنند. برای مثال، در یک برنامه WPF در Visual Studio، اگر رنگ پس‌زمینه یک کنترل را با نوشتن "Beige" در پنجره ویژگی‌ها تعیین کنید، TypeConverter نوع Color تشخیص می‌دهد که شما به نام رنگ اشاره کرده‌اید نه رشته RGB یا رنگ سیستم. این انعطاف‌پذیری گاهی کاربرد Type converters را فراتر از طراحان و اسناد XAML مفید می‌کند.

تمام Type converters از کلاس TypeConverter در System.ComponentModel مشتق می‌شوند. برای دریافت یک TypeConverter، از متد TypeDescriptor.GetConverter استفاده کنید. مثال برای نوع Color:

TypeConverter cc = TypeDescriptor.GetConverter(typeof(Color));

TypeConverter متدهایی مانند ConvertToString و ConvertFromString دارد:

Color beige  = (Color) cc.ConvertFromString("Beige");
Color purple = (Color) cc.ConvertFromString("#800080");
Color window = (Color) cc.ConvertFromString("Window");

به طور قراردادی، نام Type converters با Converter پایان می‌یابد و معمولاً در همان فضای نام نوع مورد نظر قرار دارند. هر نوع با TypeConverterAttribute به مبدل خود متصل است تا طراحان به‌طور خودکار مبدل‌ها را شناسایی کنند.

Type converters همچنین می‌توانند خدمات design-time ارائه دهند، مانند ایجاد فهرست مقادیر استاندارد برای منوهای کشویی یا کمک به سریال‌سازی کد.


BitConverter 🔢

بیشتر base types را می‌توان به آرایه بایت تبدیل کرد با فراخوانی:

foreach (byte b in BitConverter.GetBytes(3.5))
    Console.Write(b + " ");   // 0 0 0 0 0 0 12 64

BitConverter همچنین متدهایی مانند ToDouble برای تبدیل برعکس ارائه می‌دهد.

توجه: انواع decimal و DateTime(Offset) توسط BitConverter پشتیبانی نمی‌شوند، اما می‌توانید:


جهانی‌سازی 🌍

برای بین‌المللی کردن یک برنامه، دو جنبه وجود دارد: Globalization و Localization.

.NET برای کار دوم کمک می‌کند و به طور پیش‌فرض قوانین فرهنگ محلی را اعمال می‌کند. همانطور که دیدیم، فراخوانی ToString روی DateTime یا عدد، قوانین قالب‌بندی محلی را رعایت می‌کند. اما این می‌تواند باعث شکست برنامه شود اگر انتظار داشته باشید تاریخ یا اعداد طبق یک فرهنگ فرضی قالب‌بندی شوند. راه‌حل:

چک‌لیست جهانی‌سازی:

تست کردن 🧪

می‌توانید برنامه خود را با فرهنگ‌های مختلف آزمایش کنید با تعیین مجدد ویژگی CurrentCulture در کلاس Thread (System.Threading). مثال زیر فرهنگ جاری را به ترکیه تغییر می‌دهد:

Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("tr-TR");

ترکیه یک مورد آزمایشی ویژه است زیرا:

همچنین می‌توانید با تغییر تنظیمات قالب‌بندی اعداد و تاریخ در کنترل پنل ویندوز آزمایش کنید؛ این تغییرات در فرهنگ پیش‌فرض (CultureInfo.CurrentCulture) منعکس می‌شوند.

متد CultureInfo.GetCultures() آرایه‌ای از تمام فرهنگ‌های موجود را بازمی‌گرداند.

Thread و CultureInfo همچنین از ویژگی CurrentUICulture پشتیبانی می‌کنند که بیشتر با Localization مرتبط است و در فصل ۱۷ بررسی می‌شود.


کار با اعداد 🔢

تبدیل‌ها

ما تبدیل‌های عددی را در فصل‌ها و بخش‌های قبلی پوشش دادیم؛ جدول ۶-۷ تمام گزینه‌ها را خلاصه می‌کند.

Conventions-UsedThis-Book

کلاس Math ➗

جدول ۶-۸ اعضای کلیدی کلاس static Math را فهرست می‌کند.

Conventions-UsedThis-Book

روش‌های گرد کردن و کلاس‌های عددی 🔢


Max و Min


BigInteger 🧮

BigInteger twentyFive = 25;  // تبدیل ضمنی از integer
BigInteger googol = BigInteger.Pow(10, 100);
BigInteger googol = BigInteger.Parse("1".PadRight(101, '0'));
Console.WriteLine(googol.ToString());
double g2 = (double)googol;        // تبدیل صریح
BigInteger g3 = (BigInteger)g2;    // تبدیل صریح
RandomNumberGenerator rand = RandomNumberGenerator.Create();
byte[] bytes = new byte[32];
rand.GetBytes(bytes);
var bigRandomNumber = new BigInteger(bytes);

Half 🟰

Half h = (Half)123.456;
Console.WriteLine(h); // 123.44  (توجه به از دست رفتن دقت)

Complex 🔷

var c1 = new Complex(2, 3.5);
var c2 = new Complex(3, 0);
Complex c3 = Complex.FromPolarCoordinates(1.3, 5);

Random 🎲

Random r1 = new Random(1);
Random r2 = new Random(1);
var rand = System.Security.Cryptography.RandomNumberGenerator.Create();
byte[] bytes = new byte[32];
rand.GetBytes(bytes);

این توضیحات پایه‌های کار با اعداد و انواع تخصصی در C# را پوشش می‌دهند.

BitOperations 🔧

کلاس System.Numerics.BitOperations (از .NET 6) متدهایی را برای عملیات پایه‌ای بر مبنای ۲ ارائه می‌دهد:


Enumها ⚙️

در فصل ۳، نوع enum در C# را معرفی کردیم و نحوه ترکیب اعضا، تست برابری، استفاده از عملگرهای منطقی و انجام تبدیل‌ها را توضیح دادیم. .NET پشتیبانی از enumها را با نوع System.Enum گسترش می‌دهد که دو نقش دارد:

یکپارچه‌سازی نوعی یعنی می‌توانید هر عضو enum را به صورت implicit به یک instance از System.Enum تبدیل کنید:

Display(Nut.Macadamia);     // Nut.Macadamia
Display(Size.Large);        // Size.Large

void Display(Enum value)
{
    Console.WriteLine(value.GetType().Name + "." + value.ToString());
}

enum Nut  { Walnut, Hazelnut, Macadamia }
enum Size { Small, Medium, Large }

متدهای utility استاتیک روی System.Enum بیشتر برای انجام تبدیل‌ها و گرفتن لیست اعضا استفاده می‌شوند.


تبدیل Enumها 🔄

سه روش برای نمایش مقدار یک enum وجود دارد:

  1. به عنوان عضو enum
  2. به عنوان مقدار صحیح underlying
  3. به عنوان رشته (string)

Enum به عدد صحیح

تبدیل صریح بین عضو enum و مقدار عددی آن امکان‌پذیر است:

[Flags] 
public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 }

int i = (int) BorderSides.Top;       // i == 4
BorderSides side = (BorderSides)i;   // side == BorderSides.Top

برای یک System.Enum، ابتدا به object تبدیل کنید و سپس به نوع عددی:

static int GetIntegralValue(Enum anyEnum)
{
    return (int)(object)anyEnum;
}

روش دیگر استفاده از Convert.ToDecimal است:

static decimal GetAnyIntegralValue(Enum anyEnum)
{
    return Convert.ToDecimal(anyEnum);
}

روش سوم، استفاده از Enum.GetUnderlyingType و Convert.ChangeType است تا نوع اصلی حفظ شود:

static object GetBoxedIntegralValue(Enum anyEnum)
{
    Type integralType = Enum.GetUnderlyingType(anyEnum.GetType());
    return Convert.ChangeType(anyEnum, integralType);
}

object result = GetBoxedIntegralValue(BorderSides.Top);
Console.WriteLine(result);           // 4
Console.WriteLine(result.GetType()); // System.Int32

همچنین می‌توانید از ToString("D") برای گرفتن مقدار عددی به صورت رشته استفاده کنید:

static string GetIntegralValueAsString(Enum anyEnum)
{
    return anyEnum.ToString("D");  // خروجی چیزی شبیه "4"
}

عدد صحیح به Enum

برای تبدیل عدد صحیح به enum، از Enum.ToObject استفاده می‌کنیم:

object bs = Enum.ToObject(typeof(BorderSides), 3);
Console.WriteLine(bs);  // Left, Right

این نسخه داینامیک معادل:

BorderSides bs = (BorderSides)3;

است و برای همه نوع‌های عددی و object قابل استفاده است.

تبدیل به رشته

تبدیل رشته به Enum

BorderSides leftRight = (BorderSides) Enum.Parse(
    typeof(BorderSides),
    "Left, Right"
);

پیمایش مقادیر Enum 🔍

متد Enum.GetValues آرایه‌ای شامل تمام اعضای یک نوع enum مشخص را برمی‌گرداند:

foreach (Enum value in Enum.GetValues(typeof(BorderSides)))
    Console.WriteLine(value);

نحوه کار Enum ⚙️

نکته منفی: Enumها نوع ایمنی قوی ارائه نمی‌دهند. مثال:

[Flags] public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 }
BorderSides b = BorderSides.Left;
b += 1234;  // خطا ندارد!

با وجود اینکه یک instance از enum و مقدار عددی آن در runtime تفاوتی ندارند، اما کد زیر به شکل متفاوتی عمل می‌کند:

Console.WriteLine(BorderSides.Right.ToString());    // Right
Console.WriteLine(BorderSides.Right.GetType().Name); // BorderSides

Struct Guid 🆔

ساخت یک Guid جدید:

Guid g = Guid.NewGuid();
Console.WriteLine(g.ToString());  // 0d57629c-7d6e-4847-97cb-9e2fc25083fe

ساخت Guid از مقادیر موجود:

public Guid(byte[] b);  // آرایه 16 بایتی
public Guid(string g);   // رشته قالب‌بندی شده
Guid g1 = new Guid("{0d57629c-7d6e-4847-97cb-9e2fc25083fe}");
Guid g2 = new Guid("0d57629c7d6e484797cb9e2fc25083fe");
Console.WriteLine(g1 == g2);  // True

مقایسه برابری ⚖️

  1. Value equality

    • دو مقدار از نظر محتوا برابر هستند.
  2. Referential equality

    • دو reference به همان شیء اشاره می‌کنند.

مثال برابری مقداری:

int x = 5, y = 5;
Console.WriteLine(x == y);  // True

مثال برابری مقداری پیچیده‌تر با DateTimeOffset:

var dt1 = new DateTimeOffset(2010,1,1,1,1,1, TimeSpan.FromHours(8));
var dt2 = new DateTimeOffset(2010,1,1,2,1,1, TimeSpan.FromHours(9));
Console.WriteLine(dt1 == dt2);  // True

مثال برابری مرجع:

class Foo { public int X; }
Foo f1 = new Foo { X = 5 };
Foo f2 = new Foo { X = 5 };
Console.WriteLine(f1 == f2); // False

Foo f3 = f1;
Console.WriteLine(f1 == f3); // True
Uri uri1 = new Uri("http://www.linqpad.net");
Uri uri2 = new Uri("http://www.linqpad.net");
Console.WriteLine(uri1 == uri2);  // True
var s1 = "http://www.linqpad.net";
var s2 = "http://" + "www.linqpad.net";
Console.WriteLine(s1 == s2);  // True

پروتکل‌های استاندارد برابری ⚖️

سه پروتکل استاندارد برای مقایسه برابری در C# وجود دارد:

  1. عملگرهای == و !=
  2. متد virtual Equals در object
  3. اینترفیس IEquatable

همچنین، پروتکل‌های قابل اتصال (pluggable) و اینترفیس IStructuralEquatable نیز وجود دارند که در فصل ۷ توضیح داده می‌شوند.


عملگرهای == و !=

int x = 5;
int y = 5;
Console.WriteLine(x == y);  // True
object x = 5;
object y = 5;
Console.WriteLine(x == y);  // False

متد virtual Object.Equals

object x = 5;
object y = 5;
Console.WriteLine(x.Equals(y));  // True

چرا پیچیدگی وجود دارد؟

چرا == virtual نیست و دقیقاً مثل Equals عمل نمی‌کند؟ دلایل:

  1. اگر اولین آرگومان null باشد، Equals با NullReferenceException مواجه می‌شود؛ عملگر static چنین خطایی ندارد.
  2. گاهی مفید است که == و Equals تعاریف متفاوتی از برابری داشته باشند.
  3. چون == static و compile-time است، بسیار سریع اجرا می‌شود؛ این برای کدهای محاسباتی سنگین اهمیت دارد.

مثال: مقایسه ایمن دو شیء

روش نامطمئن (ممکن است خطای null بدهد):

public static bool AreEqual(object obj1, object obj2) 
    => obj1.Equals(obj2);

روش ایمن با بررسی null:

public static bool AreEqual(object obj1, object obj2)
{
    if (obj1 == null) return obj2 == null;
    return obj1.Equals(obj2);
}

نسخه کوتاه‌تر:

public static bool AreEqual(object obj1, object obj2)
    => obj1 == null ? obj2 == null : obj1.Equals(obj2);

متد ایستا object.Equals

کلاس object یک متد ایستا Equals هم دارد که همان کار AreEqual را انجام می‌دهد، اما با دو پارامتر:

public static bool Equals(object objA, object objB)
object x = 3, y = 3;
Console.WriteLine(object.Equals(x, y)); // True

x = null;
Console.WriteLine(object.Equals(x, y)); // False

y = null;
Console.WriteLine(object.Equals(x, y)); // True
class Test<T>
{
    T _value;
    public void SetValue(T newValue)
    {
        if (!object.Equals(newValue, _value))
        {
            _value = newValue;
            OnValueChanged();
        }
    }
    protected virtual void OnValueChanged() { ... }
}
if (!EqualityComparer<T>.Default.Equals(newValue, _value))

متد ایستا object.ReferenceEquals

Widget w1 = new Widget();
Widget w2 = new Widget();
Console.WriteLine(object.ReferenceEquals(w1, w2)); // False

اینترفیس IEquatable<T>

public interface IEquatable<T>
{
    bool Equals(T other);
}
class Test<T> where T : IEquatable<T>
{
    public bool IsEqual(T a, T b)
    {
        return a.Equals(b); // بدون boxing
    }
}

زمانی که Equals و == برابر نیستند

double x = double.NaN;
Console.WriteLine(x == x);       // False
Console.WriteLine(x.Equals(x));  // True
var sb1 = new StringBuilder("foo");
var sb2 = new StringBuilder("foo");

Console.WriteLine(sb1 == sb2);       // False (برابری مرجع)
Console.WriteLine(sb1.Equals(sb2));  // True  (برابری مقدار)

برابری و نوع‌های سفارشی

رفتار پیش‌فرض برابری

گاهی لازم است این رفتار را تغییر دهید:

  1. تغییر معنای برابری

    • وقتی رفتار پیش‌فرض == یا Equals برای نوع شما طبیعی نیست یا مصرف‌کننده انتظار دیگری دارد.
    • مثال: DateTimeOffset که می‌خواهیم فقط UTC DateTime مقایسه شود و نه offset.
    • مثال دیگر: انواع عددی با NaN مثل float و double.
    • در کلاس‌ها، گاهی value equality طبیعی‌تر است تا referential equality (مثلاً System.Uri یا System.String).
    • در records، کامپایلر به‌طور خودکار structural equality ایجاد می‌کند، اما گاهی نیاز به اصلاح مقایسه برخی فیلدها داریم.
  2. افزایش سرعت برابری برای structs

    • الگوریتم پیش‌فرض مقایسه struct کند است.
    • با override کردن Equals و پیاده‌سازی IEquatable<T> و overload کردن == می‌توان سرعت را چند برابر کرد.
    • برای reference types، override برابری تأثیر چندانی روی سرعت ندارد چون برابری مرجع سریع است.

مراحل override برابری

  1. Override کردن GetHashCode() و Equals()
  2. (اختیاری) overload کردن != و ==
  3. (اختیاری) پیاده‌سازی IEquatable
record Test(int X, int Y)
{
    public virtual bool Equals(Test t) => t != null && t.X == X && t.Y == Y;
}

Override کردن GetHashCode


Override کردن Equals


Overload کردن == و !=

پیاده‌سازی IEquatable<T>

پیاده‌سازی IEquatable<T> به شما اجازه می‌دهد که متد Equals سریع‌تر و بدون boxing برای value types اجرا شود. نتایج آن باید با object.Equals مطابقت داشته باشد.

مثال: struct Area

فرض کنید struct‌ای داریم که مساحت را نشان می‌دهد و طول و عرض قابل تبادل هستند (مثلاً 5 × 10 با 10 × 5 برابر است):

public struct Area : IEquatable<Area>
{
    public readonly int Measure1;
    public readonly int Measure2;

    public Area(int m1, int m2)
    {
        Measure1 = Math.Min(m1, m2);
        Measure2 = Math.Max(m1, m2);
    }

    public override bool Equals(object other)
        => other is Area a && Equals(a);  // فراخوانی Equals نوع خاص

    public bool Equals(Area other)          // پیاده‌سازی IEquatable<Area>
        => Measure1 == other.Measure1 && Measure2 == other.Measure2;

    public override int GetHashCode()
        => HashCode.Combine(Measure1, Measure2);

    public static bool operator ==(Area a1, Area a2) => Equals(a1, a2);
    public static bool operator !=(Area a1, Area a2) => !(a1 == a2);
}

استفاده از struct Area

Area a1 = new Area(5, 10);
Area a2 = new Area(10, 5);

Console.WriteLine(a1.Equals(a2));  // True
Console.WriteLine(a1 == a2);       // True

مقایسه قابل پیکربندی (Pluggable Equality Comparers)

اگر بخواهید نوعی رفتار برابری مخصوص یک سناریو داشته باشد، می‌توانید از IEqualityComparer استفاده کنید. این برای کلاس‌های collection استاندارد بسیار مفید است و در فصل بعد (“Plugging in Equality and Order”) توضیح داده می‌شود.


مقایسه ترتیب (Order Comparison)

C# و .NET دو پروتکل استاندارد برای ترتیب‌دهی اشیا دارند:

  1. رابط‌های IComparable (IComparable و IComparable<T>)
  2. اپراتورهای < و >

IComparable و IComparable

public interface IComparable       { int CompareTo(object other); }
public interface IComparable<in T> { int CompareTo(T other); }

مثال:

Console.WriteLine("Beck".CompareTo("Anne"));   // 1
Console.WriteLine("Beck".CompareTo("Beck"));   // 0
Console.WriteLine("Beck".CompareTo("Chris"));  // -1

IComparable در مقابل Equals

وقتی یک نوع هم Equals را override کرده و هم رابط‌های IComparable را پیاده‌سازی می‌کند، انتظار داریم که:

به عبارت دیگر، برابری می‌تواند سخت‌گیرتر از ترتیب باشد، اما ترتیب نمی‌تواند سخت‌گیرتر از برابری باشد.

مثال System.String

توجه: اگر بخواهید نظم مرتب‌سازی متفاوتی داشته باشید، می‌توانید از IComparer قابل پیکربندی استفاده کنید (مثلاً مقایسه بدون حساسیت به حروف کوچک/بزرگ).


پیاده‌سازی IComparable و اپراتورهای < و >

if (Equals(other)) return 0;

شرایطی که < و > overload می‌شوند

  1. نوع مفهومی قوی از "بزرگتر از" و "کوچکتر از" دارد.
  2. تنها یک روش یا زمینه برای مقایسه وجود دارد.
  3. نتیجه مقایسه مستقل از فرهنگ است.

مثال: System.String فرهنگ‌محور است و بنابراین < و > را overload نمی‌کند.


مثال struct Note

public struct Note : IComparable<Note>, IEquatable<Note>, IComparable
{
    int _semitonesFromA;
    public int SemitonesFromA => _semitonesFromA;

    public Note(int semitonesFromA)
    {
        _semitonesFromA = semitonesFromA;
    }

    // IComparable<Note>
    public int CompareTo(Note other)
    {
        if (Equals(other)) return 0;  // Fail-safe
        return _semitonesFromA.CompareTo(other._semitonesFromA);
    }

    // IComparable
    int IComparable.CompareTo(object other)
    {
        if (!(other is Note)) throw new InvalidOperationException("CompareTo: Not a note");
        return CompareTo((Note)other);
    }

    // اپراتورهای < و >
    public static bool operator <(Note n1, Note n2) => n1.CompareTo(n2) < 0;
    public static bool operator >(Note n1, Note n2) => n1.CompareTo(n2) > 0;

    // IEquatable<Note>
    public bool Equals(Note other) => _semitonesFromA == other._semitonesFromA;

    public override bool Equals(object other)
    {
        if (!(other is Note)) return false;
        return Equals((Note)other);
    }

    public override int GetHashCode() => _semitonesFromA.GetHashCode();

    public static bool operator ==(Note n1, Note n2) => Equals(n1, n2);
    public static bool operator !=(Note n1, Note n2) => !(n1 == n2);
}

✅ نکات مهم:

کلاس‌های کمکی (Utility Classes) در .NET

Console

کلاس استاتیک Console برای مدیریت ورودی/خروجی در برنامه‌های کنسولی استفاده می‌شود:

ویژگی‌های مفید:

مثال:

Console.WindowWidth = Console.LargestWindowWidth;
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("test... 50%");
Console.CursorLeft -= 3;
Console.Write("90%"); // test... 90%

Environment

کلاس استاتیک System.Environment اطلاعات مفیدی درباره سیستم فراهم می‌کند:

امکان دسترسی به متغیرهای محیطی:
GetEnvironmentVariable, GetEnvironmentVariables, SetEnvironmentVariable

در Windows Store apps، تعداد اعضای Environment محدود است.


Process

کلاس System.Diagnostics.Process برای اجرای فرآیندها و تعامل با آنها:

Process.Start("notepad.exe");
Process.Start("notepad.exe", "e:\\file.txt");
ProcessStartInfo psi = new ProcessStartInfo
{
    FileName = "cmd.exe",
    Arguments = "/c ipconfig /all",
    RedirectStandardOutput = true,
    UseShellExecute = false
};
Process p = Process.Start(psi);
string result = p.StandardOutput.ReadToEnd();
Console.WriteLine(result);
(string output, string errors) Run(string exePath, string args = "")
{
    using var p = Process.Start(new ProcessStartInfo(exePath, args)
    {
        RedirectStandardOutput = true,
        RedirectStandardError = true,
        UseShellExecute = false
    });

    var errors = new StringBuilder();
    p.ErrorDataReceived += (sender, errorArgs) =>
    {
        if (errorArgs.Data != null) errors.AppendLine(errorArgs.Data);
    };
    p.BeginErrorReadLine();

    string output = p.StandardOutput.ReadToEnd();
    p.WaitForExit();
    return (output, errors.ToString());
}

UseShellExecute

UseShellExecute مشخص می‌کند که چگونه CLR یک فرآیند را شروع کند.

این تفاوت یک تغییر ناسازگار (breaking change) است و هنگام مهاجرت کد از .NET Framework باید بررسی شود.


با UseShellExecute = true:

محدودیت: نمی‌توان جریان‌های ورودی/خروجی را redirect کرد.

اگر نیاز به redirect باشد، می‌توانید UseShellExecute = false و cmd.exe با /c استفاده کنید (مانند مثال ipconfig).

عملکرد داخلی:


AppContext

کلاس استاتیک System.AppContext دو ویژگی اصلی و چند قابلیت مدیریت وضعیت ارائه می‌دهد:


مدیریت سوئیچ‌های ویژگی‌ها

AppContext.SetSwitch("MyLibrary.SomeBreakingChange", true);
bool isDefined, switchValue;
isDefined = AppContext.TryGetSwitch("MyLibrary.SomeBreakingChange", out switchValue);

نکته طراحی: پارامتر out در این API ضروری نیست و بهتر بود nullable bool برگرداند تا مقدار true, false یا null برای undefined داشته باشیم.
مثال پیشنهادی:

bool switchValue = AppContext.GetSwitch("...") ?? false;