private static string toPersianNumbersUsingReplace(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return data .Replace("0", "\u06F0") .Replace("1", "\u06F1") .Replace("2", "\u06F2") .Replace("3", "\u06F3") .Replace("4", "\u06F4") .Replace("5", "\u06F5") .Replace("6", "\u06F6") .Replace("7", "\u06F7") .Replace("8", "\u06F8") .Replace("9", "\u06F9"); }
جایگزین کردن حروف با استفاده از Replace معمولی توسط رشتهها
نگارش اصلی تبدیل تمام اعداد موجود در یک رشته به اعداد فارسی، به صورت زیر است که در آن یک دست سازی اعداد عربی هم درنظر گرفته شدهاند (برای مثال طرز نگارش عدد 4 فارسی و عربی متفاوت است):
private static string toPersianNumbersUsingReplace(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return toEnglishNumbers(data) .Replace("0", "\u06F0") .Replace("1", "\u06F1") .Replace("2", "\u06F2") .Replace("3", "\u06F3") .Replace("4", "\u06F4") .Replace("5", "\u06F5") .Replace("6", "\u06F6") .Replace("7", "\u06F7") .Replace("8", "\u06F8") .Replace("9", "\u06F9"); } private static string toEnglishNumbers(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return data.Replace("\u0660", "0") //٠ .Replace("\u06F0", "0") //۰ .Replace("\u0661", "1") //١ .Replace("\u06F1", "1") //۱ .Replace("\u0662", "2") //٢ .Replace("\u06F2", "2") //۲ .Replace("\u0663", "3") //٣ .Replace("\u06F3", "3") //۳ .Replace("\u0664", "4") //٤ .Replace("\u06F4", "4") //۴ .Replace("\u0665", "5") //٥ .Replace("\u06F5", "5") //۵ .Replace("\u0666", "6") //٦ .Replace("\u06F6", "6") //۶ .Replace("\u0667", "7") //٧ .Replace("\u06F7", "7") //۷ .Replace("\u0668", "8") //٨ .Replace("\u06F8", "8") //۸ .Replace("\u0669", "9") //٩ .Replace("\u06F9", "9"); //۹ }
جایگزین کردن حروف با استفاده از Replace معمولی توسط کاراکترها
اینبار همان حالت قبل را درنظر بگیرید؛ با این تفاوت که بجای رشتهها از کاراکترها استفاده شود. برای مثال بجای:
.Replace("\u0669", "9") //٩
.Replace('\u0669', '9') //٩
جایگزین کردن حروف با استفاده از String Builder
در ادامه بجای استفاده از متد Replace متداول، آرایهای از حروف قابل جایگزینی را توسط یک StringBuilder ایجاد کرده و حروف را یکی یکی تبدیل میکنیم و به این ترتیب برخلاف متد Replace، هربار برای جایگزینی یک مورد خاص، مجددا از ابتدای رشته شروع به جستجو نمیشود:
private static string toPersianNumbersUsingStringBuilder(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; var strBuilder = new StringBuilder(data); for (var i = 0; i < strBuilder.Length; i++) { switch (strBuilder[i]) { case '0': case '\u0660': strBuilder[i] = '\u06F0'; break; case '1': case '\u0661': strBuilder[i] = '\u06F1'; break; case '2': case '\u0662': strBuilder[i] = '\u06F2'; break; case '3': case '\u0663': strBuilder[i] = '\u06F3'; break; case '4': case '\u0664': strBuilder[i] = '\u06F4'; break; case '5': case '\u0665': strBuilder[i] = '\u06F5'; break; case '6': case '\u0666': strBuilder[i] = '\u06F6'; break; case '7': case '\u0667': strBuilder[i] = '\u06F7'; break; case '8': case '\u0668': strBuilder[i] = '\u06F8'; break; case '9': case '\u0669': strBuilder[i] = '\u06F9'; break; default: strBuilder[i] = strBuilder[i]; break; } } return strBuilder.ToString(); }
جایگزین کردن حروف با استفاده از ToCharArray
متد زیر دقیقا شبیه به حالت استفاده از String Builder است؛ با یک تفاوت مهم: بجای استفاده از String Builder برای تهیهی آرایهای از حروف قابل تغییر، از متد ToCharArray استفاده شدهاست:
private static string toPersianNumbersUsingToCharArray(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; var letters = data.ToCharArray(); for (var i = 0; i < letters.Length; i++) { switch (letters[i]) { case '0': case '\u0660': letters[i] = '\u06F0'; break; // مانند قبل } } return new string(letters); }
جایگزین کردن حروف با استفاده از string.Create
string.Create یکی از تازههای NET Core. است که امکان تغییر مستقیم یک قطعه string را میسر میکند:
private static string toPersianNumbersUsingStringCreate(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return string.Create(data.Length, data, (chars, context) => { for (var i = 0; i < data.Length; i++) { switch (context[i]) { case '0': case '\u0660': chars[i] = '\u06F0'; break; // مانند قبل } } }); }
توضیحات بیشتر:
در دات نت، رشتهها نوعهای ارجاعی (reference type) غیرقابل تغییر (immutable) هستند. به این معنا که هر زمانیکه ایجاد شدند، دیگر نمیتوان محتوای آنها را تغییر داد. به همین جهت است که مجبور هستیم آنها را برای مثال توسط ToCharArray به یک آرایه تبدیل کنیم و سپس این آرایهی قابل تغییر را ویرایش کنیم. در حین کار با رشتهها، این غیرقابل تغییر بودن، سبب تخصیص حافظههای بیش از حدی میشوند. اگر بخواهیم قسمتی از یک رشته را جدا و یا جایگزین کنیم و یا تعدادی رشته را با هم جمع بزنیم، نتیجهی آن نیاز به یک تخصیص حافظهی جدید را دارد. راه حل استاندارد مواجه شدن با این مشکل، استفاده از StringBuilder است که از یک بافر داخلی برای انجام کارهای خودش استفاده میکند و زمانیکه نتیجهی نهایی را از آن درخواست میکنیم، تخصیص حافظهای را برای تولید رشتهی حاصل انجام میدهد. البته این مورد نیاز به اندازه گیری دارد و ارزش StringBuilder با حجم بالایی از اطلاعات متنی مشخص میشود؛ وگرنه همانطور که مشاهده میکنید (در نتیجهی نهایی بحث در ادامه)، الزاما کدهای سریعتری را به همراه نخواهد داشت.
هدف از string.Create، ایجاد رشتهها از دادههای موجود است. هدف اصلی آن کاهش تخصیصهای حافظه و کپی کردن اطلاعات است و امضای آن به صورت زیر میباشد:
public static string Create<TState> (int length, TState state, System.Buffers.SpanAction<char,TState> action);
هنگام کار با این متد، chars ای که در اختیار ما قرار میگیرد، یک <Span<char اشاره کننده به رشتهی نهایی است که قرار است بازگشت داده شود (در ابتدای کار بر اساس اندازهای که مشخص میشود، یک رشتهی خالی تخصیص داده میشود، اما بافر پر کردن آن اینبار در دسترس است و نیازی به تخصیص و کپی جداگانهای را ندارد). بنابراین روش کار با این متد، پر کردن بافر درونی رشتهی بازگشتی (همان chars در اینجا) به صورت مستقیم است؛ کاری که با یک رشتهی معمولی نمیتوان انجام داد.
State یا همان پارامتر دوم این متد، هر چیزی میتواند باشد. اگر نیاز است چندین رشته را در اینجا دریافت کنید تا بتوان بر اساس آن رشتهی نهایی را تشکیل داد، یک struct را تعریف کرده و بجای state به آن ارسال کنید. سپس این state توسط پارامتر context مربوط به SpanAction<char, string> action قابل دریافت و استفادهاست که در این مثال، context همان data ارسالی به این متد است.
سؤال: در حین کار با string.Create، باید از پارامتر data استفاده کنیم و یا از context دریافتی؟ به نظر در مثال فوق، data و context یکی هستند. اکنون داخل action delegate مهیا که جهت ساخت رشتهی نهایی بکار میرود، باید از data استفاده کرد و یا از context؟
return string.Create(data.Length, data, (chars, context) => {});
نتیجهی نهایی بررسی کارآیی روشهای مختلف جایگزین کردن حروف در یک رشته
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: ReplacePerformanceTests.zip
ستون op/s در اینجا، مهمترین ستون گزارش است و به معنای تعداد عملیات قابل انجام در یک ثانیه است. از 670 هزار عملیات در ثانیه با Replace معمولی، به 5 میلیون عملیات در ثانیه رسیدهایم که بسیار قابل توجهاست.
همانطور که مشاهده میکنید، string.Create، سریعترین نگارش موجود است. در این بین نگارشی که از ToCharArray استفاده میکند، قابلیت انتقال بیشتری را دارد؛ از این جهت که نگارشهای دیگر NET. هنوز دسترسی به string.Create را ندارند. همچنین نگارش کاراکتری متد Replace، از متد رشتهای آن سریعتر عمل کردهاست.