آموزش کار با رشته‌ها در C#: ترفندها و نکات

فهرست مطالب

آموزش کار با رشته‌ها در C#: ترفندها و نکات

رشته‌ها (Strings) در برنامه‌نویسی، از اساسی‌ترین و پرکاربردترین انواع داده محسوب می‌شوند. تقریباً در هر برنامه‌ای، از نمایش نام کاربر و پیام‌های خطا گرفته تا ذخیره و پردازش داده‌ها، با رشته‌ها سروکار داریم. در زبان C#، رشته‌ها دارای ویژگی‌های منحصر به فردی هستند که درک عمیق آن‌ها برای هر برنامه‌نویس حرفه‌ای ضروری است. این راهنمای جامع، شما را با تمام جنبه‌های کار با رشته‌ها در C# آشنا می‌کند؛ از مفاهیم پایه‌ای و عملیات رایج گرفته تا ترفندهای بهینه‌سازی، مدیریت حافظه، و استفاده از ابزارهای پیشرفته مانند عبارات باقاعده. هدف ما ارائه دانشی عملی و کاربردی است تا بتوانید با اطمینان خاطر و کارایی بالا، در پروژه‌های خود از رشته‌ها استفاده کنید.

در این مقاله، ابتدا به معرفی ماهیت رشته‌ها و مفهوم کلیدی «تغییرناپذیری» (Immutability) در C# می‌پردازیم. سپس رایج‌ترین متدها و عملیات مربوط به رشته‌ها را تشریح می‌کنیم و در ادامه، به مباحث پیشرفته‌تری مانند بهینه‌سازی الحاق رشته‌ها با StringBuilder، قالب‌بندی پیشرفته رشته‌ها با String Interpolation، و نکات دقیق در مقایسه رشته‌ها ورود خواهیم کرد. همچنین، نحوه کار با Encoding‌ها و پردازش قدرتمند رشته‌ها با عبارات باقاعده را بررسی خواهیم نمود. در نهایت، بهترین رویه‌ها و نکاتی را برای اجتناب از خطاهای رایج و بهبود کارایی در برنامه‌هایتان ارائه خواهیم داد.

با ما همراه باشید تا به دنیای پرکاربرد و پیچیده رشته‌ها در C# سفری جامع داشته باشیم و مهارت‌های خود را در این زمینه ارتقا دهیم.

مبانی رشته‌ها در C# و مفهوم Immutable

در C#، رشته‌ها با استفاده از نوع داده string که یک نام مستعار (alias) برای کلاس System.String در BCL دات‌نت است، نمایش داده می‌شوند. برخلاف بسیاری از انواع داده اولیه مانند int یا bool که انواع ارزشی (Value Types) هستند، string یک نوع ارجاعی (Reference Type) است. این بدان معناست که متغیرهای رشته‌ای، به جای نگهداری مستقیم مقدار رشته، به محلی در حافظه که مقدار رشته در آن ذخیره شده است، اشاره می‌کنند.

مفهوم تغییرناپذیری (Immutability)

یکی از مهم‌ترین و گاهی اوقات گیج‌کننده‌ترین ویژگی‌های رشته‌ها در C#، مفهوم تغییرناپذیری (Immutability) است. به این معنا که پس از اینکه یک شیء رشته‌ای ایجاد شد، محتوای آن هرگز نمی‌تواند تغییر کند. هر عملیاتی که به نظر می‌رسد محتوای یک رشته را تغییر می‌دهد (مانند الحاق، جایگزینی، یا برش)، در واقع یک شیء رشته‌ای جدید در حافظه ایجاد می‌کند و متغیر اصلی را به آن شیء جدید ارجاع می‌دهد، در حالی که شیء اصلی دست‌نخورده باقی می‌ماند.

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


string myString = "Hello";
Console.WriteLine($"Original String: {myString}"); // Output: Original String: Hello

myString = myString + " World";
Console.WriteLine($"Modified String: {myString}"); // Output: Modified String: Hello World

در مثال بالا، ممکن است به نظر برسد که مقدار myString تغییر کرده است. اما آنچه واقعاً اتفاق افتاده، به شرح زیر است:

  1. ابتدا، یک شیء رشته‌ای با مقدار “Hello” در حافظه ایجاد می‌شود و متغیر myString به آن اشاره می‌کند.
  2. هنگامی که myString = myString + " World"; اجرا می‌شود، runtime یک شیء رشته‌ای کاملاً جدید با مقدار “Hello World” در حافظه ایجاد می‌کند.
  3. سپس، متغیر myString به‌روزرسانی می‌شود تا به این شیء جدید اشاره کند.
  4. شیء رشته‌ای اولیه (“Hello”) همچنان در حافظه وجود دارد، اما هیچ متغیری به آن اشاره نمی‌کند (و در نهایت توسط Garbage Collector پاک خواهد شد).

این رفتار تغییرناپذیری، پیامدهای مهمی دارد که در طول این مقاله به آن‌ها اشاره خواهیم کرد، از جمله تأثیر بر کارایی در عملیات مکرر الحاق رشته‌ها و همچنین مزایایی مانند Thread Safety (ایمنی ریسمان) و ساده‌سازی ذخیره‌سازی و اشتراک‌گذاری رشته‌ها در حافظه.

چرا رشته‌ها Immutable هستند؟

دلایل متعددی برای طراحی رشته‌ها به صورت تغییرناپذیر در C# (و بسیاری از زبان‌های دیگر) وجود دارد:

  • ایمنی ریسمان (Thread Safety): از آنجا که محتوای یک رشته هرگز تغییر نمی‌کند، نیازی به قفل‌گذاری (locking) یا همگام‌سازی (synchronization) برای دسترسی از چندین ریسمان وجود ندارد. این امر کدنویسی چندریسه‌ای را بسیار ساده‌تر و امن‌تر می‌کند.
  • پایداری هش‌کد (Hash Code Stability): رشته‌ها اغلب به عنوان کلید در ساختارهای داده‌ای مانند Dictionary و HashTable استفاده می‌شوند. هش‌کد یک رشته بر اساس محتوای آن محاسبه می‌شود. اگر رشته‌ها قابل تغییر بودند، هش‌کد آن‌ها ممکن بود پس از تغییر محتوا، نامعتبر شود و به مشکلات جدی در این ساختارها منجر شود.
  • بهینه‌سازی حافظه (Memory Optimization – String Pooling/Interning): دات‌نت قابلیتی به نام String Interning (یا String Pooling) دارد. این قابلیت به runtime اجازه می‌دهد که تنها یک نمونه از رشته‌های یکسان را در حافظه نگهداری کند. هنگامی که شما دو رشته با محتوای یکسان ایجاد می‌کنید، دات‌نت می‌تواند آن‌ها را به یک شیء واحد در حافظه اشاره دهد. این بهینه‌سازی تنها زمانی امکان‌پذیر است که رشته‌ها تغییرناپذیر باشند، زیرا تضمین می‌کند که اگر چندین متغیر به یک شیء رشته‌ای اشاره کنند، محتوای آن شیء هرگز به طور ناخواسته توسط یکی از متغیرها تغییر نخواهد کرد.

عملیات رایج روی رشته‌ها: متدها و کاربردها

کلاس System.String مجموعه‌ای غنی از متدها را برای انجام عملیات مختلف روی رشته‌ها فراهم می‌کند. در این بخش، به بررسی مهم‌ترین و پرکاربردترین این متدها می‌پردازیم.

1. Length: طول رشته

ویژگی Length تعداد کاراکترهای یک رشته را برمی‌گرداند.


string text = "C# Programming";
int length = text.Length; // length خواهد بود 14
Console.WriteLine($"طول رشته: {length}");

2. Contains: بررسی وجود زیررشته

متد Contains() بررسی می‌کند که آیا یک زیررشته مشخص در رشته وجود دارد یا خیر و یک مقدار bool برمی‌گرداند.


string sentence = "Hello World C# Developers";
bool containsCsharp = sentence.Contains("C#"); // true
bool containsJava = sentence.Contains("Java"); // false
Console.WriteLine($"شامل 'C#' است؟ {containsCsharp}");
Console.WriteLine($"شامل 'Java' است؟ {containsJava}");

3. StartsWith و EndsWith: بررسی شروع و پایان رشته

متدهای StartsWith() و EndsWith() بررسی می‌کنند که آیا رشته با یک زیررشته خاص شروع یا به آن ختم می‌شود.


string fileName = "document.pdf";
bool startsWithDoc = fileName.StartsWith("doc"); // true
bool endsWithPdf = fileName.EndsWith(".pdf");   // true
Console.WriteLine($"با 'doc' شروع می‌شود؟ {startsWithDoc}");
Console.WriteLine($"با '.pdf' پایان می‌یابد؟ {endsWithPdf}");

4. IndexOf و LastIndexOf: یافتن موقعیت کاراکتر/زیررشته

متد IndexOf() موقعیت (ایندکس) اولین رخداد یک کاراکتر یا زیررشته را برمی‌گرداند. اگر یافت نشود، -1 برمی‌گرداند. LastIndexOf() آخرین رخداد را برمی‌گرداند.


string data = "programming is fun in C# programming";
int firstIndex = data.IndexOf("programming"); // 0
int lastIndex = data.LastIndexOf("programming"); // 25
int charIndex = data.IndexOf('i'); // 9
int notFound = data.IndexOf("Python"); // -1
Console.WriteLine($"اولین 'programming' در: {firstIndex}");
Console.WriteLine($"آخرین 'programming' در: {lastIndex}");
Console.WriteLine($"اولین 'i' در: {charIndex}");
Console.WriteLine($"'Python' یافت نشد: {notFound}");

5. Substring: برش قسمتی از رشته

متد Substring() برای استخراج قسمتی از یک رشته (زیررشته) استفاده می‌شود. این متد دو نسخه دارد: یکی که فقط ایندکس شروع را می‌گیرد و تا انتهای رشته ادامه می‌یابد، و دیگری که ایندکس شروع و طول زیررشته را می‌گیرد.


string message = "Welcome to C# course";
string part1 = message.Substring(11); // "C# course"
string part2 = message.Substring(0, 7); // "Welcome" (از ایندکس 0، به طول 7 کاراکتر)
Console.WriteLine($"قسمت اول: {part1}");
Console.WriteLine($"قسمت دوم: {part2}");

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

6. Replace: جایگزینی کاراکترها/زیررشته‌ها

متد Replace() تمام رخدادهای یک کاراکتر یا زیررشته را با کاراکتر یا زیررشته دیگری جایگزین می‌کند و یک رشته جدید برمی‌گرداند.


string original = "Hello World";
string modified = original.Replace("World", "C#"); // "Hello C#"
string noSpace = original.Replace(" ", ""); // "HelloWorld"
Console.WriteLine($"رشته اصلی: {original}");
Console.WriteLine($"رشته جایگزین شده: {modified}");
Console.WriteLine($"بدون فاصله: {noSpace}");

7. ToUpper و ToLower: تغییر حروف کوچک و بزرگ

این متدها برای تبدیل تمام کاراکترهای یک رشته به حروف بزرگ یا کوچک استفاده می‌شوند.


string mixedCase = "ProGraMMing";
string upper = mixedCase.ToUpper(); // "PROGRAMMING"
string lower = mixedCase.ToLower(); // "programming"
Console.WriteLine($"حروف بزرگ: {upper}");
Console.WriteLine($"حروف کوچک: {lower}");

نکته: این متدها ممکن است تحت تأثیر تنظیمات فرهنگی (Culture) قرار گیرند، به خصوص برای زبان‌هایی که دارای کاراکترهای ویژه هستند. برای عملیات مستقل از فرهنگ، می‌توانید از متدهایی مانند ToUpperInvariant() و ToLowerInvariant() استفاده کنید.

8. Trim, TrimStart, TrimEnd: حذف فواصل اضافی

این متدها برای حذف فواصل (Whitespace) از ابتدا، انتها یا هر دو طرف رشته استفاده می‌شوند. این فواصل شامل Space, Tab, Newline و غیره هستند.


string paddedText = "   Hello C#   ";
string trimmed = paddedText.Trim();       // "Hello C#"
string trimmedStart = paddedText.TrimStart(); // "Hello C#   "
string trimmedEnd = paddedText.TrimEnd();   // "   Hello C#"
Console.WriteLine($"Trimmed: '{trimmed}'");
Console.WriteLine($"Trimmed Start: '{trimmedStart}'");
Console.WriteLine($"Trimmed End: '{trimmedEnd}'");

می‌توانید کاراکترهای خاصی را نیز برای حذف به این متدها بدهید.


string data = "***Important Message***";
string cleaned = data.Trim('*'); // "Important Message"
Console.WriteLine($"Cleaned: '{cleaned}'");

9. Split: تقسیم رشته

متد Split() یک رشته را بر اساس یک یا چند جداکننده به آرایه‌ای از زیررشته‌ها تقسیم می‌کند.


string csvData = "apple,banana,orange,grape";
string[] fruits = csvData.Split(','); // {"apple", "banana", "orange", "grape"}
Console.WriteLine("میوه‌ها:");
foreach (string fruit in fruits)
{
    Console.WriteLine($"- {fruit.Trim()}"); // Trim برای حذف فواصل احتمالی پس از کاما
}

string sentence = "This is a sample sentence.";
string[] words = sentence.Split(' '); // {"This", "is", "a", "sample", "sentence."}
Console.WriteLine("کلمات:");
foreach (string word in words)
{
    Console.WriteLine($"- {word}");
}

// Split با گزینه‌ها
string messyData = "item1;;item2; item3 ;";
string[] items = messyData.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
// Result: {"item1", "item2", "item3"}
Console.WriteLine("Items from messy data:");
foreach (string item in items)
{
    Console.WriteLine($"- {item}");
}

StringSplitOptions.RemoveEmptyEntries باعث می‌شود که زیررشته‌های خالی (که ممکن است از جداکننده‌های پشت سر هم ایجاد شوند) حذف شوند، و StringSplitOptions.TrimEntries فواصل اضافی اطراف هر زیررشته را حذف می‌کند.

الحاق رشته‌ها: بهینه‌سازی و پرهیز از مشکلات کارایی

الحاق رشته‌ها (String Concatenation) یکی از رایج‌ترین عملیات در برنامه‌نویسی است، اما اگر به درستی مدیریت نشود، می‌تواند منجر به مشکلات جدی کارایی، به ویژه در حلقه‌های بزرگ یا پردازش حجم زیادی از داده شود. این مشکلات عمدتاً به دلیل تغییرناپذیری (Immutability) رشته‌ها ایجاد می‌شوند.

عملگر + و string.Concat

هنگامی که از عملگر + برای الحاق رشته‌ها استفاده می‌کنید (مانند str1 + str2)، یا متد string.Concat() را فراخوانی می‌کنید، در پشت صحنه چه اتفاقی می‌افتد؟ هر بار که این عملیات انجام می‌شود، یک شیء رشته‌ای جدید در حافظه ایجاد شده و محتوای دو رشته اصلی (یا بیشتر) به آن کپی می‌شود. این فرآیند به طور مکرر در حلقه‌ها، منجر به ایجاد تعداد زیادی شیء موقت (temporary objects) در حافظه می‌شود که باید بعداً توسط Garbage Collector جمع‌آوری شوند. این سربار (overhead) می‌تواند به طور قابل توجهی بر کارایی برنامه شما تأثیر بگذارد.


// مثال ناكارآمدی الحاق با + در حلقه
string result = "";
for (int i = 0; i < 1000; i++)
{
    result += i.ToString(); // در هر تکرار، یک رشته جدید ایجاد و کپی می‌شود
}
Console.WriteLine($"طول رشته نهایی (با +): {result.Length}");
// این عملیات بسیار کند خواهد بود برای تعداد تکرار بالا

StringBuilder: راه‌حل بهینه برای الحاق مکرر

برای سناریوهایی که نیاز به الحاق مکرر رشته‌ها دارید، به خصوص در حلقه‌ها یا توابع پرکاربرد، کلاس System.Text.StringBuilder راه‌حل ایده‌آلی است. برخلاف string، کلاس StringBuilder تغییرپذیر (Mutable) است. این بدان معناست که می‌تواند محتوای خود را بدون ایجاد مکرر اشیاء جدید در حافظه، تغییر دهد.

StringBuilder با تخصیص یک بافر (buffer) در حافظه شروع می‌کند. هنگامی که شما رشته‌ها را به آن اضافه می‌کنید، StringBuilder سعی می‌کند آن‌ها را در بافر موجود قرار دهد. تنها زمانی که بافر پر شود، StringBuilder یک بافر بزرگتر تخصیص داده و محتوای موجود را به آن کپی می‌کند. این کار به میزان قابل توجهی تعداد عملیات کپی و تخصیص حافظه را کاهش می‌دهد.


using System.Text;

// مثال کارآمدی الحاق با StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
    sb.Append(i.ToString()); // محتوا به بافر موجود اضافه می‌شود
}
string finalString = sb.ToString(); // در نهایت یک شیء رشته‌ای نهایی ساخته می‌شود
Console.WriteLine($"طول رشته نهایی (با StringBuilder): {finalString.Length}");

همانطور که مشاهده می‌کنید، با StringBuilder، شما تنها یک بار در انتها با فراخوانی ToString() یک شیء string واقعی ایجاد می‌کنید. این امر به طور چشمگیری کارایی را برای عملیات الحاق زیاد بهبود می‌بخشد.

چه زمانی از StringBuilder استفاده کنیم؟

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

در غیر این صورت، برای تعداد کمی از الحاقات (مثلاً 1 یا 2 الحاق)، استفاده از عملگر + یا string.Concat() کاملاً قابل قبول و حتی کمی ساده‌تر است.

string.Join: الحاق با جداکننده

متد استاتیک string.Join() یک راه بسیار مفید و کارآمد برای الحاق عناصر یک آرایه یا مجموعه (IEnumerable) با استفاده از یک جداکننده (separator) است. این متد بهینه‌سازی‌های داخلی خود را برای جلوگیری از مشکلات کارایی انجام می‌دهد و اغلب جایگزین مناسبی برای StringBuilder در سناریوهای خاص است.


string[] names = { "Alice", "Bob", "Charlie", "David" };
string commaSeparatedNames = string.Join(", ", names); // "Alice, Bob, Charlie, David"
Console.WriteLine($"نام‌ها با کاما: {commaSeparatedNames}");

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
string joinedNumbers = string.Join("-", numbers); // "1-2-3-4-5"
Console.WriteLine($"اعداد با خط تیره: {joinedNumbers}");

قالب‌بندی رشته‌ها: String.Format و Interpolated Strings

قالب‌بندی رشته‌ها به معنای ایجاد رشته‌هایی با محتوای پویا و ساختار مشخص است، جایی که مقادیر متغیرها در جایگاه‌های از پیش تعیین شده قرار می‌گیرند. C# دو روش اصلی و قدرتمند برای این کار ارائه می‌دهد: String.Format و Interpolated Strings.

String.Format: قالب‌بندی سنتی

متد استاتیک String.Format() از زمان‌های اولیه دات‌نت وجود داشته و به شما اجازه می‌دهد تا رشته‌هایی را با استفاده از placeholders ({0}, {1} و غیره) و آرگومان‌ها قالب‌بندی کنید. این متد بسیار انعطاف‌پذیر است و امکان کنترل دقیق بر نحوه نمایش اعداد، تاریخ‌ها و سایر انواع داده را فراهم می‌کند.


string name = "Ali";
int age = 30;
double temperature = 25.75;
DateTime now = DateTime.Now;

// مثال ساده
string message1 = String.Format("Hello, {0}! You are {1} years old.", name, age);
Console.WriteLine(message1); // Output: Hello, Ali! You are 30 years old.

// قالب‌بندی اعداد
string message2 = String.Format("Temperature: {0:F2}°C", temperature); // F2 برای دو رقم اعشار
Console.WriteLine(message2); // Output: Temperature: 25.75°C

// قالب‌بندی تاریخ و زمان
string message3 = String.Format("Today is {0:yyyy-MM-dd} at {0:HH:mm:ss}", now);
Console.WriteLine(message3); // Output: Today is 2023-10-27 at 14:30:45 (مثال)

// تراز بندی (Alignment) و عرض فیلد
// {index, alignment:formatString}
string item = "Laptop";
decimal price = 1250.99m;
int quantity = 2;
string invoiceLine = String.Format("{0,-15} {1,10:C} {2,5}", item, price, quantity);
Console.WriteLine(invoiceLine); // Output: Laptop          $1,250.99     2
// -15 به معنی تراز از چپ با حداقل عرض 15
// 10:C به معنی تراز از راست با حداقل عرض 10 و فرمت ارزی (Currency)

نکات در مورد String.Format:

  • Placeholders: اعداد درون آکولادها ({0}, {1}) به ترتیب به آرگومان‌های بعد از رشته فرمت اشاره می‌کنند.
  • Format Specifiers: می‌توانید پس از شماره placeholder و یک دو نقطه (:)، یک رشته فرمت (مانند F2، C، D، X، yyyy-MM-dd) اضافه کنید تا نحوه نمایش داده‌ها را کنترل کنید.
  • Alignment: با استفاده از {index, alignment} می‌توانید تراز متن (چپ یا راست) و حداقل عرض فیلد را مشخص کنید. عدد مثبت برای تراز راست و منفی برای تراز چپ است.

Interpolated Strings ($""): راهی مدرن و خوانا

از C# 6.0 به بعد، Interpolated Strings (رشته‌های درون‌یابی شده) معرفی شدند که راهی بسیار خوانا و ساده‌تر برای قالب‌بندی رشته‌ها ارائه می‌دهند. با پیشوند $ قبل از رشته، می‌توانید متغیرها یا عبارات را مستقیماً درون آکولادها ({}) در رشته قرار دهید.


string name = "Sara";
int age = 25;
double score = 95.8;
DateTime eventDate = new DateTime(2024, 7, 15);

// مثال ساده
string message1 = $"Hello, {name}! You are {age} years old.";
Console.WriteLine(message1); // Output: Hello, Sara! You are 25 years old.

// با فرمت‌دهی (Format Specifiers)
string message2 = $"Your score is {score:N1}."; // N1 برای یک رقم اعشار و جداکننده هزارگان
Console.WriteLine(message2); // Output: Your score is 95.8.

// فرمت‌دهی تاریخ
string message3 = $"The event is on {eventDate:MMMM dd, yyyy}.";
Console.WriteLine(message3); // Output: The event is on July 15, 2024.

// تراز بندی و عرض فیلد
string product = "Keyboard";
decimal unitPrice = 75.50m;
int units = 3;
string orderLine = $"{product,-20} {unitPrice,10:C} {units,5}";
Console.WriteLine(orderLine); // Output: Keyboard            $75.50    3

// عبارات پیچیده‌تر درون آکولادها
string status = $"The user is {(age >= 18 ? "an adult" : "a minor")}.";
Console.WriteLine(status); // Output: The user is an adult.

مزایای Interpolated Strings:

  • خوانایی بالا: کد بسیار شبیه به خروجی نهایی است. نیازی به شمارش ایندکس‌ها نیست.
  • کاهش خطا: به دلیل نزدیکی متغیرها به متن، احتمال خطای تایپی و ارجاع اشتباه به آرگومان‌ها کاهش می‌یابد.
  • پشتیبانی از عبارات: می‌توانید هر عبارت معتبر C# را درون آکولادها قرار دهید (متغیرها، فراخوانی متدها، عملیات ریاضی، عملگرهای شرطی و غیره).
  • عملکرد: در پشت صحنه، کامپایلر C# اغلب Interpolated Strings را به فراخوانی‌های String.Format (برای سناریوهای پیچیده) یا String.Concat (برای سناریوهای ساده) تبدیل می‌کند، بنابراین عملکرد مشابهی با String.Format دارند.

امروزه، Interpolated Strings به دلیل خوانایی و سهولت استفاده، روش ترجیحی برای قالب‌بندی رشته‌ها در C# محسوب می‌شوند.

مقایسه رشته‌ها: ظرافت‌ها و نکات امنیتی

مقایسه رشته‌ها در C#، به ظاهر ساده است، اما می‌تواند دارای ظرافت‌های مهمی باشد که در صورت عدم توجه به آن‌ها، منجر به باگ‌های نامحسوس و حتی آسیب‌پذیری‌های امنیتی شود. درک تفاوت بین متدهای مختلف مقایسه و تأثیر Culture (فرهنگ) بر آن‌ها، حیاتی است.

عملگر == و متد Equals()

در C#، برای مقایسه برابری رشته‌ها، می‌توانید از عملگر == یا متد Equals() استفاده کنید. برای انواع ارجاعی (Reference Types) به طور کلی، عملگر == به طور پیش‌فرض برابری ارجاع (Reference Equality) را بررسی می‌کند (آیا دو متغیر به یک شیء یکسان در حافظه اشاره می‌کنند؟). اما برای رشته‌ها، عملگر == overload شده است تا برابری مقدار (Value Equality) را بررسی کند.


string str1 = "apple";
string str2 = "apple";
string str3 = "Apple";

// مقایسه با عملگر ==
Console.WriteLine($"str1 == str2: {str1 == str2}"); // true (مقدار یکسان)
Console.WriteLine($"str1 == str3: {str1 == str3}"); // false (تفاوت حروف بزرگ/کوچک)

// مقایسه با متد Equals
Console.WriteLine($"str1.Equals(str2): {str1.Equals(str2)}"); // true
Console.WriteLine($"str1.Equals(str3): {str1.Equals(str3)}"); // false

در حالت پیش‌فرض، هم == و هم Equals() (بدون آرگومان) مقایسه‌ای حساس به حروف بزرگ و کوچک (Case-sensitive) و حساس به فرهنگ (Culture-sensitive) انجام می‌دهند. این بدان معناست که نتیجه مقایسه ممکن است بسته به تنظیمات منطقه‌ای سیستم عامل یا Culture جاری برنامه تغییر کند.

مشکلات Culture-sensitive در مقایسه

تصور کنید در یک برنامه بین‌المللی کار می‌کنید. حروف خاص در برخی زبان‌ها ممکن است در مقایسات حساس به فرهنگ به گونه‌ای متفاوت از آنچه انتظار دارید، رفتار کنند. به عنوان مثال، در ترکی استانبولی، حرف 'i' کوچک و 'I' بزرگ داریم، اما همچنین 'ı' (i بدون نقطه) و 'İ' (I با نقطه) وجود دارند. مقایسه ساده "file" و "FILE" ممکن است در یک Culture خاص نتیجه متفاوتی بدهد.


// مثال Culture-sensitive (برای سیستم‌هایی که دارای Culture خاصی هستند)
string turkishLowerI = "i"; // i با نقطه
string turkishUpperI = "İ"; // I با نقطه

// در Culture "tr-TR" (ترکی استانبولی)، این مقایسه ممکن است true برگرداند!
// در سایر Culture ها، معمولا false
Console.WriteLine($"turkishLowerI.Equals(turkishUpperI, StringComparison.CurrentCultureIgnoreCase): {turkishLowerI.Equals(turkishUpperI, StringComparison.CurrentCultureIgnoreCase)}");

این رفتار می‌تواند منجر به باگ‌های دشوار یا حتی مشکلات امنیتی (مانند Bypass کردن اعتبارسنجی) شود.

استفاده از StringComparison Enumeration

برای کنترل دقیق بر نحوه مقایسه رشته‌ها، به ویژه برای مقاصد امنیتی یا عملکردی، باید از متدهای Equals() یا Compare() که یک آرگومان StringComparison می‌گیرند، استفاده کنید. این enum گزینه‌های مختلفی برای نوع مقایسه ارائه می‌دهد:

  • StringComparison.Ordinal: مقایسه باینری (Binary/byte-by-byte comparison) است، کاملاً بدون در نظر گرفتن فرهنگ. بسیار سریع و مناسب برای مقایسه‌هایی که باید دقیقاً یکسان باشند (مانند کلیدها، مسیرهای فایل، پسوردها). همیشه غیر حساس به حروف بزرگ و کوچک نیست!
  • StringComparison.OrdinalIgnoreCase: مقایسه باینری و غیر حساس به حروف بزرگ و کوچک. بهترین گزینه برای مقایسه‌های غیرحساس به حروف بزرگ و کوچک که نیاز به ثبات و سرعت دارند (مثلاً برای مقایسه نام کاربری).
  • StringComparison.CurrentCulture: مقایسه حساس به حروف بزرگ و کوچک، بر اساس قواعد فرهنگ جاری (پیش‌فرض == و Equals()).
  • StringComparison.CurrentCultureIgnoreCase: مقایسه غیر حساس به حروف بزرگ و کوچک، بر اساس قواعد فرهنگ جاری.
  • StringComparison.InvariantCulture: مقایسه حساس به حروف بزرگ و کوچک، بر اساس قواعد فرهنگ Invariant (فرهنگ ثابت و بدون در نظر گرفتن منطقه جغرافیایی).
  • StringComparison.InvariantCultureIgnoreCase: مقایسه غیر حساس به حروف بزرگ و کوچک، بر اساس قواعد فرهنگ Invariant.

string s1 = "C# Programming";
string s2 = "c# programming";

Console.WriteLine($"s1 == s2: {s1 == s2}"); // false (Case-sensitive, CurrentCulture)
Console.WriteLine($"s1.Equals(s2, StringComparison.Ordinal): {s1.Equals(s2, StringComparison.Ordinal)}"); // false (Case-sensitive, byte-by-byte)
Console.WriteLine($"s1.Equals(s2, StringComparison.OrdinalIgnoreCase): {s1.Equals(s2, StringComparison.OrdinalIgnoreCase)}"); // true (Case-insensitive, byte-by-byte)
Console.WriteLine($"s1.Equals(s2, StringComparison.CurrentCultureIgnoreCase): {s1.Equals(s2, StringComparison.CurrentCultureIgnoreCase)}"); // true (Case-insensitive, Culture-sensitive)

متد Compare() و CompareTo()

برای مقایسه دو رشته و تعیین ترتیب آن‌ها (مثلاً برای مرتب‌سازی)، می‌توانید از متدهای String.Compare() (استاتیک) یا string.CompareTo() (عضو) استفاده کنید. این متدها یک عدد صحیح برمی‌گردانند:

  • 0: اگر رشته‌ها برابر باشند.
  • < 0: اگر رشته اول قبل از رشته دوم در ترتیب مرتب‌سازی قرار گیرد.
  • > 0: اگر رشته اول بعد از رشته دوم در ترتیب مرتب‌سازی قرار گیرد.

string A = "apple";
string B = "banana";
string a = "Apple";

// String.Compare
int result1 = String.Compare(A, B); // result1 < 0 (apple قبل از banana)
int result2 = String.Compare(A, a, StringComparison.OrdinalIgnoreCase); // result2 == 0 (apple و Apple یکسان با نادیده گرفتن حالت)
Console.WriteLine($"Compare(A, B): {result1}");
Console.WriteLine($"Compare(A, a, OrdinalIgnoreCase): {result2}");

// string.CompareTo
Console.WriteLine($"A.CompareTo(B): {A.CompareTo(B)}"); // < 0
Console.WriteLine($"A.CompareTo(a): {A.CompareTo(a)}"); // > 0 (Case-sensitive)

نکات امنیتی در مقایسه رشته‌ها

  • مقایسه پسوردها و توکن‌ها: همیشه از StringComparison.Ordinal یا StringComparison.OrdinalIgnoreCase استفاده کنید. هرگز از مقایسه‌های وابسته به Culture برای اطلاعات حساس استفاده نکنید، زیرا می‌تواند منجر به باگ‌های امنیتی شود (مثل اینکه یک کاراکتر خاص در یک فرهنگ برابر با کاراکتر دیگری در فرهنگ دیگر تشخیص داده شود). بهتر است پسوردها را به صورت هش شده (hashed) و با استفاده از SecureString مقایسه کنید.
  • مسیرهای فایل و URLها: برای مقایسه مسیرهای فایل و URLها، که اغلب Case-insensitive هستند اما باید باینری یکسان باشند، از StringComparison.OrdinalIgnoreCase استفاده کنید. این تضمین می‌کند که "C:\temp" و "c:\TEMP" یکسان در نظر گرفته شوند، اما "C:\temp" و "C:\temP" (در ویندوز) که ممکن است در برخی موارد به یک فایل اشاره کنند، به درستی مقایسه شوند.
  • امنیت در پایگاه داده: هنگام کوئری‌نویسی و مقایسه رشته‌ها در دیتابیس، مطمئن شوید که تنظیمات Collations در دیتابیس با منطق مقایسه شما در C# همخوانی دارد تا از نتایج غیرمنتظره جلوگیری شود.

در یک کلام، برای مقایسه‌های امنیتی و عملکردی، همیشه از StringComparison.Ordinal یا StringComparison.OrdinalIgnoreCase استفاده کنید. برای مقایسه‌هایی که باید برای کاربر نهایی منطقی و وابسته به زبان او باشند (مثلاً مرتب‌سازی نام‌ها در یک لیست نمایش داده شده)، از گزینه‌های CurrentCulture یا InvariantCulture استفاده کنید.

کار با Encoding و Decoding رشته‌ها

رشته‌ها در دات‌نت (و C#) به صورت داخلی با استفاده از UTF-16 (که گاهی اوقات به آن UCS-2 نیز گفته می‌شود) نمایش داده می‌شوند. هر کاراکتر در یک رشته C# یک نقطه کد (Code Point) یونیکد است که معمولاً به صورت 16 بیتی ذخیره می‌شود.

اما هنگامی که نیاز به تعامل با سیستم‌های خارجی (فایل‌ها، شبکه‌ها، پایگاه‌های داده) دارید، اغلب با فرمت‌های مختلفی از بایت‌ها سروکار خواهید داشت که نمایانگر متن هستند. اینجاست که مفهوم Encoding (رمزگذاری) و Decoding (رمزگشایی) اهمیت پیدا می‌کند. یک Encoding مجموعه‌ای از قوانین است که تعیین می‌کند چگونه کاراکترها به دنباله‌ای از بایت‌ها تبدیل شوند و بالعکس.

Encoding های رایج

  • ASCII: قدیمی‌ترین و ساده‌ترین Encoding. فقط 128 کاراکتر انگلیسی و نمادهای رایج را پشتیبانی می‌کند. هر کاراکتر 1 بایت است.
  • UTF-8: پرکاربردترین Encoding در وب و سیستم‌های فایل مدرن. یک Encoding با طول متغیر است؛ کاراکترهای ASCII را با 1 بایت، کاراکترهای اروپایی را با 2 بایت، و کاراکترهای زبان‌های دیگر (مانند فارسی، چینی، ژاپنی) را با 3 یا 4 بایت نشان می‌دهد. به دلیل بهینگی در فضای ذخیره‌سازی برای متون انگلیسی، بسیار محبوب است.
  • UTF-16: Encoding داخلی C#. هر کاراکتر معمولاً 2 بایت (16 بیت) است، اما برای کاراکترهای خارج از BMP (مانند ایموجی‌ها) ممکن است از 4 بایت (زوج‌های جایگزین - Surrogate Pairs) استفاده کند.
  • ISO-8859-1 (Latin-1): یک Encoding 1 بایتی که کاراکترهای زبان‌های اروپای غربی را پوشش می‌دهد.

کلاس System.Text.Encoding

کلاس System.Text.Encoding در C# به شما امکان می‌دهد تا رشته‌ها را به آرایه‌ای از بایت‌ها (Encode) و بایت‌ها را به رشته‌ها (Decode) تبدیل کنید.

برای دریافت یک نمونه از Encoding مورد نظر، می‌توانید از متدهای استاتیک کلاس Encoding استفاده کنید:

  • Encoding.UTF8
  • Encoding.Unicode (برای UTF-16)
  • Encoding.ASCII
  • Encoding.GetEncoding("encoding_name_or_code_page") (برای Encoding های دیگر)

مثال: Encode و Decode کردن رشته‌ها


using System.Text;

string originalString = "سلام دنیا! Hello World!";

// 1. Encoding به UTF-8
Encoding utf8 = Encoding.UTF8;
byte[] utf8Bytes = utf8.GetBytes(originalString);
Console.WriteLine($"UTF-8 Bytes: {BitConverter.ToString(utf8Bytes)}"); // نمایش بایت‌ها به صورت هگزادسیمال

// 2. Decoding از UTF-8
string decodedFromUtf8 = utf8.GetString(utf8Bytes);
Console.WriteLine($"Decoded from UTF-8: {decodedFromUtf8}");

Console.WriteLine();

// 3. Encoding به UTF-16 (Unicode)
Encoding unicode = Encoding.Unicode; // این همان UTF-16 است
byte[] unicodeBytes = unicode.GetBytes(originalString);
Console.WriteLine($"UTF-16 Bytes: {BitConverter.ToString(unicodeBytes)}");

// 4. Decoding از UTF-16
string decodedFromUnicode = unicode.GetString(unicodeBytes);
Console.WriteLine($"Decoded from UTF-16: {decodedFromUnicode}");

Console.WriteLine();

// 5. Encoding به ASCII (با از دست دادن اطلاعات)
// کاراکترهای فارسی و برخی نمادها در ASCII وجود ندارند.
Encoding ascii = Encoding.ASCII;
byte[] asciiBytes = ascii.GetBytes(originalString);
// کاراکترهایی که قابل تبدیل به ASCII نیستند، به '?' تبدیل می‌شوند.
string decodedFromAscii = ascii.GetString(asciiBytes);
Console.WriteLine($"ASCII Bytes: {BitConverter.ToString(asciiBytes)}");
Console.WriteLine($"Decoded from ASCII: {decodedFromAscii}"); // Output: ??? ???! Hello World!

همانطور که در مثال ASCII مشاهده شد، اگر رشته‌ای حاوی کاراکترهایی باشد که در Encoding مقصد وجود ندارند، ممکن است اطلاعات از بین برود یا کاراکترها به کاراکترهای جایگزین (مانند ?) تبدیل شوند. این نکته برای جلوگیری از مشکلات Mojibake (متن ناخوانا) بسیار مهم است.

کار با Encoding در File I/O و Network

هنگام خواندن یا نوشتن فایل‌ها، یا ارسال/دریافت داده از طریق شبکه، تعیین Encoding صحیح حیاتی است. متدهای File.ReadAllText()، File.WriteAllText()، StreamReader و StreamWriter دارای overload هایی هستند که به شما امکان می‌دهند Encoding مورد نظر را مشخص کنید.


string filePath = "my_text_file.txt";
string content = "این یک متن فارسی و English text است.";

// نوشتن با UTF-8
File.WriteAllText(filePath, content, Encoding.UTF8);
Console.WriteLine($"فایل '{filePath}' با UTF-8 نوشته شد.");

// خواندن با UTF-8
string readContent = File.ReadAllText(filePath, Encoding.UTF8);
Console.WriteLine($"محتوای خوانده شده (UTF-8): {readContent}");

// امتحان خواندن با Encoding اشتباه (مثلاً ASCII)
try
{
    string wrongReadContent = File.ReadAllText(filePath, Encoding.ASCII);
    Console.WriteLine($"محتوای خوانده شده (ASCII - غلط): {wrongReadContent}");
}
catch (Exception ex)
{
    Console.WriteLine($"خطا در خواندن با ASCII: {ex.Message}");
    Console.WriteLine("احتمالاً کاراکترهای فارسی به درستی رمزگشایی نشده‌اند.");
}

همواره سعی کنید از UTF-8 برای فایل‌ها و ارتباطات شبکه استفاده کنید، زیرا این Encoding به طور گسترده پشتیبانی می‌شود و کاراکترهای بین‌المللی را به خوبی مدیریت می‌کند.

عبارات باقاعده (Regular Expressions) برای پردازش پیشرفته رشته‌ها

عبارات باقاعده (Regex یا RegEx) ابزاری فوق‌العاده قدرتمند برای جستجو، جایگزینی، تقسیم و اعتبارسنجی الگوهای پیچیده در رشته‌ها هستند. کتابخانه System.Text.RegularExpressions در دات‌نت امکان استفاده از Regex را در C# فراهم می‌کند.

Regexها از یک سینتکس خاص برای تعریف الگوها استفاده می‌کنند. در اینجا برخی از عناصر رایج Regex آورده شده است:

  • .: هر کاراکتری (به جز Newline).
  • *: صفر یا بیشتر از عنصر قبلی.
  • +: یک یا بیشتر از عنصر قبلی.
  • ?: صفر یا یک از عنصر قبلی.
  • [abc]: هر کدام از کاراکترهای a, b یا c.
  • [a-z]: هر کاراکتر بین a تا z.
  • [^abc]: هر کاراکتری به جز a, b یا c.
  • \d: هر رقم (معادل [0-9]).
  • \D: هر کاراکتری که رقم نیست.
  • \w: هر کاراکتر کلمه (حروف، اعداد، زیرخط - معادل [a-zA-Z0-9_]).
  • \W: هر کاراکتری که کلمه نیست.
  • \s: هر کاراکتر فضای خالی (Space, Tab, Newline).
  • \S: هر کاراکتری که فضای خالی نیست.
  • ^: شروع رشته.
  • $: پایان رشته.
  • |: یا (OR).
  • ( ): گروه‌بندی و گرفتن (Capturing Group).
  • {n}: دقیقاً n بار تکرار.
  • {n,}: حداقل n بار تکرار.
  • {n,m}: بین n تا m بار تکرار.

کلاس Regex و متدهای اصلی

کلاس اصلی برای کار با عبارات باقاعده در C#، کلاس Regex است. این کلاس متدهای استاتیک و عضو (پس از ساخت نمونه) را ارائه می‌دهد:

  • Regex.IsMatch(input, pattern): بررسی می‌کند که آیا الگو در رشته ورودی وجود دارد یا خیر (برمی‌گرداند bool).
  • Regex.Match(input, pattern): اولین رخداد الگو را پیدا می‌کند و یک شیء Match برمی‌گرداند.
  • Regex.Matches(input, pattern): تمام رخدادهای الگو را پیدا می‌کند و یک مجموعه MatchCollection برمی‌گرداند.
  • Regex.Replace(input, pattern, replacement): تمام رخدادهای الگو را با یک رشته جایگزین می‌کند.
  • Regex.Split(input, pattern): رشته را بر اساس الگو تقسیم می‌کند و یک آرایه string برمی‌گرداند.

مثال‌های کاربردی با Regex

1. اعتبارسنجی آدرس ایمیل


using System.Text.RegularExpressions;

string email1 = "test@example.com";
string email2 = "invalid-email";
string patternEmail = @"^[^@\s]+@[^@\s]+\.[^@\s]+$"; // یک الگوی ساده برای ایمیل

Console.WriteLine($"'{email1}' یک ایمیل معتبر است؟ {Regex.IsMatch(email1, patternEmail)}"); // true
Console.WriteLine($"'{email2}' یک ایمیل معتبر است؟ {Regex.IsMatch(email2, patternEmail)}"); // false

2. استخراج اعداد از رشته


string textWithNumbers = "Product Code: P12345, Price: $99.50, Quantity: 10 units.";
string patternNumbers = @"\d+"; // یک یا چند رقم

MatchCollection matches = Regex.Matches(textWithNumbers, patternNumbers);
Console.WriteLine("اعداد استخراج شده:");
foreach (Match match in matches)
{
    Console.WriteLine($"- {match.Value}");
}
// Output:
// - 12345
// - 99
// - 50
// - 10

3. جایگزینی الگوها


string originalText = "The date is 2023-10-27.";
// تغییر فرمت تاریخ از YYYY-MM-DD به DD/MM/YYYY
string patternDate = @"(\d{4})-(\d{2})-(\d{2})"; // گروه‌های گرفتن برای سال، ماه، روز
string replacementDate = "$3/$2/$1"; // $1, $2, $3 به گروه‌های گرفته شده اشاره دارند

string modifiedText = Regex.Replace(originalText, patternDate, replacementDate);
Console.WriteLine($"متن اصلی: {originalText}");
Console.WriteLine($"متن تغییر یافته: {modifiedText}"); // Output: The date is 27/10/2023.

4. تقسیم رشته با چند جداکننده


string products = "Apple,Banana;Orange |Grape";
string patternSeparators = @"[,;|]"; // جداکننده‌های کاما، سمی‌کولون یا پایپ

string[] productArray = Regex.Split(products, patternSeparators);
Console.WriteLine("محصولات تقسیم شده:");
foreach (string product in productArray)
{
    Console.WriteLine($"- {product.Trim()}"); // Trim برای حذف فواصل اضافی
}
// Output:
// - Apple
// - Banana
// - Orange
// - Grape

RegexOptions

می‌توانید با استفاده از RegexOptions رفتار Regex را کنترل کنید، مانند:

  • RegexOptions.IgnoreCase: نادیده گرفتن حروف بزرگ و کوچک.
  • RegexOptions.Multiline: ^ و $ با شروع و پایان هر خط (نه فقط کل رشته) مطابقت پیدا کنند.
  • RegexOptions.Compiled: کامپایل کردن الگو به کد بومی (برای عملکرد بهتر در استفاده مکرر).

string email = "User@Example.COM";
string pattern = @"user@example.com";
// مقایسه Case-insensitive
bool isMatch = Regex.IsMatch(email, pattern, RegexOptions.IgnoreCase);
Console.WriteLine($"Email match (Case-insensitive): {isMatch}"); // true

استفاده از Regex می‌تواند پیچیده باشد، اما با تمرین و استفاده از ابزارهای آنلاین تست Regex، می‌توانید الگوهای قدرتمندی برای نیازهای پردازش رشته خود ایجاد کنید.

بهترین رویه‌ها و نکات پیشرفته در کار با رشته‌ها

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

1. استفاده صحیح از StringBuilder

همانطور که قبلاً اشاره شد، برای هر عملیات الحاق رشته که بیش از چند بار تکرار می‌شود (مثلاً در یک حلقه)، همیشه از StringBuilder استفاده کنید. این کار می‌تواند تأثیر قابل توجهی بر کارایی و مصرف حافظه برنامه شما داشته باشد. تفاوت در کارایی بین string + و StringBuilder به صورت نمایی با افزایش تعداد الحاقات، بیشتر می‌شود.


// اشتباه رایج:
// string result = "";
// for (int i = 0; i < 10000; i++) { result += "data"; } // بسیار کند و ایجاد 10000 شیء موقت

// روش صحیح:
// StringBuilder sb = new StringBuilder();
// for (int i = 0; i < 10000; i++) { sb.Append("data"); }
// string result = sb.ToString(); // فقط یک شیء نهایی

2. انتخاب درست StringComparison

برای مقایسه رشته‌ها، همیشه به این نکته توجه کنید که آیا مقایسه باید حساس به حروف بزرگ/کوچک باشد و آیا باید وابسته به Culture باشد. همانطور که در بخش مقایسه رشته‌ها توضیح داده شد:

  • برای مقایسه‌های امنیتی (مانند پسوردها، توکن‌ها، مسیرهای حساس) و مقایسه‌هایی که باید کاملاً باینری باشند (کلیدهای هش، GUID ها)، از StringComparison.Ordinal یا StringComparison.OrdinalIgnoreCase استفاده کنید.
  • برای نمایش و مرتب‌سازی داده‌ها برای کاربر نهایی که وابسته به زبان آن‌هاست، از گزینه‌های CurrentCulture استفاده کنید.
  • برای عملیات مستقل از زبان که نیاز به مقایسه Case-insensitive دارند (مانند جستجو در یک مجموعه داده داخلی که همیشه به یک شکل است)، از StringComparison.InvariantCultureIgnoreCase استفاده کنید.

3. پرهیز از استفاده بی‌مورد از رشته‌ها

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

4. استفاده از IsNullOrEmpty و IsNullOrWhiteSpace

همیشه قبل از انجام عملیات روی رشته‌ها، آن‌ها را برای null بودن یا خالی بودن (empty) بررسی کنید تا از NullReferenceException جلوگیری شود. متدهای استاتیک string.IsNullOrEmpty() و string.IsNullOrWhiteSpace() برای این منظور طراحی شده‌اند.

  • IsNullOrEmpty(str): اگر str برابر null یا "" (رشته خالی) باشد، true برمی‌گرداند.
  • IsNullOrWhiteSpace(str): اگر str برابر null، ""، یا فقط شامل فواصل خالی (Whitespace) باشد، true برمی‌گرداند. این بهترین گزینه برای اعتبارسنجی ورودی کاربر است.

string s1 = null;
string s2 = "";
string s3 = "   ";
string s4 = "Hello";

Console.WriteLine($"IsNullOrEmpty(s1): {string.IsNullOrEmpty(s1)}"); // true
Console.WriteLine($"IsNullOrEmpty(s2): {string.IsNullOrEmpty(s2)}"); // true
Console.WriteLine($"IsNullOrEmpty(s3): {string.IsNullOrEmpty(s3)}"); // false
Console.WriteLine($"IsNullOrEmpty(s4): {string.IsNullOrEmpty(s4)}"); // false

Console.WriteLine($"IsNullOrWhiteSpace(s1): {string.IsNullOrWhiteSpace(s1)}"); // true
Console.WriteLine($"IsNullOrWhiteSpace(s2): {string.IsNullOrWhiteSpace(s2)}"); // true
Console.WriteLine($"IsNullOrWhiteSpace(s3): {string.IsNullOrWhiteSpace(s3)}"); // true
Console.WriteLine($"IsNullOrWhiteSpace(s4): {string.IsNullOrWhiteSpace(s4)}"); // false

5. Span<char> و ReadOnlySpan<char> (برای .NET Core/.NET 5+)

در .NET Core و نسخه‌های جدیدتر دات‌نت (.NET 5+)، انواع Span<char> و ReadOnlySpan<char> معرفی شده‌اند که می‌توانند به طور چشمگیری کارایی پردازش رشته‌ها را در سناریوهای خاص بهبود بخشند. این انواع، راهی برای کار با بخش‌هایی از حافظه (از جمله رشته‌ها) بدون نیاز به کپی کردن داده‌ها ارائه می‌دهند. این می‌تواند سربار تخصیص حافظه و Garbage Collection را کاهش دهد.

ReadOnlySpan<char> به شما امکان می‌دهد تا به بخش‌هایی از یک رشته بدون تخصیص حافظه جدید یا کپی کردن داده‌ها دسترسی پیدا کنید و روی آن‌ها عملیات انجام دهید. این برای عملیات خواندنی (Read-only) مانند برش (slicing) یا جستجو در زیررشته‌ها بسیار کارآمد است.


// Requires .NET Core 2.1+ or .NET 5+
string longText = "This is a very long string that we want to slice efficiently.";

// استفاده از Substring: یک رشته جدید ایجاد می‌کند.
string subStringResult = longText.Substring(10, 5); // "very"

// استفاده از ReadOnlySpan<char>: هیچ کپی‌ای از داده‌ها ایجاد نمی‌کند.
ReadOnlySpan<char> span = longText.AsSpan();
ReadOnlySpan<char> spanSlice = span.Slice(10, 5); // "very"

Console.WriteLine($"Substring result: {subStringResult}");
Console.WriteLine($"Span slice result: {spanSlice.ToString()}"); // برای نمایش باید به string تبدیل کرد.

// مثال دیگر: اعتبارسنجی بدون تخصیص اضافی
bool IsPhoneNumber(ReadOnlySpan<char> phoneNumber)
{
    if (phoneNumber.Length != 11) return false;
    // فرض کنیم فقط شامل ارقام باشد
    foreach (char c in phoneNumber)
    {
        if (!char.IsDigit(c)) return false;
    }
    return true;
}

string phone1 = "09121234567";
string phone2 = "0912abcde";

Console.WriteLine($"'{phone1}' is valid phone number: {IsPhoneNumber(phone1.AsSpan())}"); // true
Console.WriteLine($"'{phone2}' is valid phone number: {IsPhoneNumber(phone2.AsSpan())}"); // false

استفاده از Span<char> و ReadOnlySpan<char> برای سناریوهایی که نیاز به پردازش حجم زیادی از متن یا انجام عملیات مکرر روی زیررشته‌ها بدون تولید زباله در حافظه دارید، توصیه می‌شود.

6. استفاده از String.Create (برای .NET Core/.NET 5+)

اگر نیاز به ساخت یک رشته جدید با محتوای سفارشی و پیچیده دارید، و نمی‌خواهید از StringBuilder استفاده کنید (مثلاً چون تعداد قطعات کم است یا منطق ساخت پیچیده است)، string.Create می‌تواند گزینه‌ای بهینه باشد. این متد به شما اجازه می‌دهد تا با یک SpanWriter یا delegate به طور مستقیم در حافظه رشته جدید بنویسید، بدون اینکه واسطه‌های کپی و تخصیص متعدد وجود داشته باشند.


// Requires .NET Core 2.1+ or .NET 5+
// string.Create برای ساخت رشته‌ای که نیازی به StringBuilder ندارد اما محتوایش پویاست
string CreateFormattedString(string name, int id)
{
    return string.Create(name.Length + 10, (name, id), (span, state) =>
    {
        // در اینجا می توانید مستقیما در Span بنویسید
        // state.name و state.id در اینجا قابل دسترسی هستند
        int charsWritten = 0;
        state.name.CopyTo(span);
        charsWritten += state.name.Length;
        span[charsWritten++] = '-';
        span[charsWritten++] = 'I';
        span[charsWritten++] = 'D';
        span[charsWritten++] = ':';
        state.id.TryFormat(span.Slice(charsWritten), out int idCharsWritten);
        charsWritten += idCharsWritten;
    });
}

string formatted = CreateFormattedString("Developer", 12345);
Console.WriteLine($"Created with String.Create: {formatted}"); // Developer-ID:12345

این متد برای سناریوهای با کارایی بالا که نیاز به کنترل دقیق بر تخصیص حافظه دارند، بسیار مفید است.

خطاهای رایج و راه‌حل‌ها

در کار با رشته‌ها، برخی خطاها و سوءتفاهم‌ها رایج هستند. درک آن‌ها به شما کمک می‌کند تا کد قوی‌تر و پایدارتری بنویسید.

1. NullReferenceException

این خطا زمانی رخ می‌دهد که شما سعی می‌کنید روی یک متغیر رشته‌ای که مقدار null دارد، متدی را فراخوانی کنید یا به خاصیتی دسترسی پیدا کنید.


string myString = null;
// int length = myString.Length; // NullReferenceException!

// راه‌حل: همیشه null بودن را بررسی کنید.
if (myString != null)
{
    int length = myString.Length;
    Console.WriteLine($"طول رشته: {length}");
}
else
{
    Console.WriteLine("رشته null است.");
}

// یا از متدهای کمکی مانند IsNullOrEmpty/IsNullOrWhiteSpace استفاده کنید:
if (!string.IsNullOrEmpty(myString))
{
    int length = myString.Length;
    Console.WriteLine($"طول رشته: {length}");
}
else
{
    Console.WriteLine("رشته null یا خالی است.");
}

2. ArgumentOutOfRangeException در Substring و IndexOf

این خطا زمانی پرتاب می‌شود که ایندکس شروع یا طول ارائه شده به متد Substring()، IndexOf() (با شروع ایندکس) یا Remove() از محدوده معتبر رشته خارج شود.


string data = "programming";
// string part = data.Substring(15); // ArgumentOutOfRangeException! (ایندکس خارج از محدوده)
// string part2 = data.Substring(0, 15); // ArgumentOutOfRangeException! (طول خارج از محدوده)

// راه‌حل: همیشه طول رشته و ایندکس‌ها را بررسی کنید.
if (data.Length >= 5)
{
    string part = data.Substring(0, 5); // "progr"
    Console.WriteLine($"قسمت: {part}");
}

// در IndexOf: اگر آیتم پیدا نشود، -1 برمی‌گرداند.
int index = data.IndexOf("xyz");
if (index != -1)
{
    Console.WriteLine($"'xyz' در ایندکس: {index}");
}
else
{
    Console.WriteLine("'xyz' در رشته یافت نشد.");
}

3. مشکلات Encoding و Mojibake

هنگام خواندن/نوشتن فایل‌ها یا تبادل داده با سیستم‌های خارجی، عدم تطابق Encoding می‌تواند منجر به نمایش نادرست کاراکترها (Mojibake) شود.


// مثال: نوشتن با UTF-8 و خواندن با Encoding اشتباه (مثلاً Latin1)
string filePath = "test_encoding.txt";
string originalContent = "سلام"; // کاراکترهای فارسی

// نوشتن با UTF-8
File.WriteAllText(filePath, originalContent, Encoding.UTF8);

// خواندن با Encoding اشتباه (ISO-8859-1)
string corruptedContent = File.ReadAllText(filePath, Encoding.GetEncoding("iso-8859-1"));
Console.WriteLine($"محتوای اصلی: {originalContent}");
Console.WriteLine($"محتوای خراب شده (Mojibake): {corruptedContent}"); // خروجی ناخوانا خواهد بود

// راه‌حل: همیشه از Encoding مناسب برای خواندن و نوشتن استفاده کنید.
string correctContent = File.ReadAllText(filePath, Encoding.UTF8);
Console.WriteLine($"محتوای صحیح: {correctContent}");

توصیه: همیشه تا جای ممکن از UTF-8 استفاده کنید، مگر اینکه دلیل محکمی برای استفاده از Encoding دیگری داشته باشید. همچنین، در ارتباطات بین سیستم‌ها، Encoding را به وضوح مشخص و توافق کنید.

4. نادیده گرفتن مفهوم Immutability

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


// اشتباه (نادیده گرفتن Immutability در حلقه):
// string log = "";
// for (int i = 0; i < 10000; i++)
// {
//     log += $"Log entry {i}\n"; // ایجاد 10000 شیء رشته‌ای جدید
// }

// راه‌حل (استفاده از StringBuilder):
// StringBuilder logBuilder = new StringBuilder();
// for (int i = 0; i < 10000; i++)
// {
//     logBuilder.AppendFormat("Log entry {0}\n", i);
// }
// string log = logBuilder.ToString();

5. مشکلات عملکردی با Regex های پیچیده

Regexها قدرتمند هستند اما می‌توانند بسیار کند باشند، به خصوص اگر الگو پیچیده باشد و داده ورودی بزرگ باشد (مشکل "redos" یا "catastrophic backtracking").


// مثال یک Regex مشکل‌ساز (مثلاً برای آدرس ایمیل ساده شده):
// string problematicPattern = "(a+)+"; // الگو با back-tracking بالا
// Regex.IsMatch("aaaaaaaaaaaaaaaaaaaaaaaaaaaaa!", problematicPattern); // بسیار کند!

// راه‌حل:
// 1. از الگوهای Regex کارآمد استفاده کنید.
// 2. از RegexOptions.Compiled برای الگوهایی که مکرراً استفاده می‌شوند، استفاده کنید.
// 3. در صورت امکان، از متدهای ساده‌تر string به جای Regex برای عملیات ساده استفاده کنید.
// 4. برای ورودی‌های نامعتبر یا بزرگ، یک timeout برای Regex تنظیم کنید.
//    Regex regexWithTimeout = new Regex(pattern, RegexOptions.None, TimeSpan.FromSeconds(1));
//    regexWithTimeout.IsMatch(input);

تنظیم Regex.MatchTimeout یک اقدام دفاعی بسیار مهم برای جلوگیری از حملات DoS مبتنی بر Regular Expression (ReDoS) است.

با درک این خطاها و رویه‌های صحیح، می‌توانید کدی بنویسید که نه تنها عملکرد بهتری دارد، بلکه کمتر مستعد باگ است و نگهداری آن آسان‌تر است.

نتیجه‌گیری

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

مرور کوتاهی بر نکات کلیدی:

  • تغییرناپذیری: به یاد داشته باشید که هر عملیات روی رشته، یک رشته جدید ایجاد می‌کند. این مفهوم اساس درک کارایی رشته‌هاست.
  • StringBuilder: قهرمان الحاق رشته‌ها در حلقه‌ها و سناریوهای با کارایی بالا است. از آن برای جلوگیری از تخصیص حافظه و کپی‌های غیرضروری استفاده کنید.
  • Interpolated Strings: بهترین دوست شما برای قالب‌بندی خوانا و ساده رشته‌ها.
  • StringComparison: برای مقایسه‌های امن و دقیق، حتماً نوع مقایسه (Ordinal، InvariantCulture، CurrentCulture و Case-sensitivity) را با دقت انتخاب کنید.
  • Encoding: برای جلوگیری از مشکلات Mojibake در ورودی/خروجی، Encoding صحیح را درک و اعمال کنید. UTF-8 انتخاب اول شما باشد.
  • Regular Expressions: ابزاری قدرتمند برای الگوبرداری و پردازش پیچیده متن، اما با آگاهی از مسائل کارایی آن‌ها.
  • بهترین رویه‌ها: همیشه رشته‌ها را برای null/empty بررسی کنید و در دات‌نت‌های جدیدتر، Span<char> را برای کارایی بیشتر در نظر بگیرید.

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

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان