`string.Length` چرا به کاربر دروغ می گوید؟ نکته ای در .NET برای متن فارسی و انگلیسی

26 مهر 1405 - خواندن 10 دقیقه - 18 بازدید

وقتی یک زبان آموز در فرم آنلاین آزمون آزمایشی می نویسد: «می خواهم speaking را بهتر کنم 🌱»، سامانه چه چیزی را می شمارد؟ برنامه نویس شاید بگوید طول رشته را می سنجم و بنویسد text.Length. اما در .NET، این مقدار الزاما تعداد نویسه هایی نیست که کاربر روی صفحه می بیند. این تفاوت کوچک در کد می تواند در فرم پاسخ، محدودیت کلمه، جست وجوی فارسی، زیرنویس و ابزارهای ارزیابی زبان به تجربه ای بزرگ و آزاردهنده تبدیل شود.



این یادداشت از یک سوال فنی شروع می کند: رشته در C# از چه چیزی ساخته شده است؟ و بعد می پرسد این مدل فنی وقتی با زبانی مانند فارسی روبه رو می شود که نیم فاصله، گونه های نوشتاری و متن های آمیخته با انگلیسی دارد، چه پیامدی برای کاربر و مدرس پیدا می کند؟



چه چیزی را می شمارد؟



در .NET نوع string دنباله ای از واحدهای UTF-16 است. خاصیت Length تعداد همین واحدهای ۱۶بیتی را می دهد، نه تعداد واژه ها، حروف یا شکل های دیداری متن. نوع char نیز یک واحد UTF-16 است؛ بعضی مقدارهای Unicode در یک char جا می گیرند و بعضی دیگر با یک جفت surrogate نمایش داده می شوند. بنابراین یک ایموجی می تواند در Length دو واحد بشمارد، با آنکه کاربر آن را یک نماد می بیند. یک حرف به همراه نشانه ترکیبی هم ممکن است از بیش از یک مقدار Unicode ساخته شود.



برای برنامه نویس این رفتار باگ نیست؛ پاسخ به پرسش دیگری است. Length برای اندیس گذاری و عملیات مبتنی بر واحدهای UTF-16 معنا دارد. اما اگر برچسب رابط کاربری می گوید «تعداد نویسه ها»، باید پیش از نمایش عدد روشن کنیم که منظور کدام تعریف از نویسه است. مستندات رسمی .NET تفاوت میان char، Rune و واحد متنی را شرح می دهد. (Microsoft, “Introduction to character encoding in .NET”)



سه سطح را از هم جدا کنیم:

• واحد UTF-16 یا char: همان چیزی که string.Length می شمارد.

• Rune: یک مقدار Unicode؛ برای پیمایش مقدارهای متنی بدون نصف کردن جفت surrogate مناسب تر است.

• Text element: یک واحد متنی قابل مشاهده در بسیاری از کاربردهای رابط کاربری، معمولا با درنظرگرفتن حروف و نشانه های ترکیبی کنار هم.






در C# چگونه انتخاب کنیم؟



اگر هدف پیمایش مقدارهای Unicode است، می توان از Rune استفاده کرد. اگر هدف برش پیش نمایش یا شمارش واحدهایی است که برای نمایش به کاربر مناسب ترند، StringInfo و text elementها را بررسی می کنیم:



using System.Globalization;

using System.Linq;

using System.Text;



string text = "Learning فارسی 🌱";



int utf16Units = text.Length;

int unicodeValues = text.EnumerateRunes().Count();



var enumerator = StringInfo.GetTextElementEnumerator(text);

int textElements = 0;

while (enumerator.MoveNext())

{

 textElements++;

}



Console.WriteLine($"UTF-16: {utf16Units}");

Console.WriteLine($"Unicode values: {unicodeValues}");

Console.WriteLine($"Text elements: {textElements}");



این کد سه عدد مختلف می دهد، چون سه پرسش مختلف مطرح کرده ایم. Rune کمک می کند مقدارهای Unicode را بشماریم، اما تعداد Runeها الزاما برابر تعداد شکل های دیداری نیست؛ بعضی شکل ها از چند مقدار ساخته می شوند. StringInfo برای کار با text elementها ساخته شده و در متن های ترکیبی می تواند مرزهای نمایشی مناسب تری از برش با اندیس خام char فراهم کند. مستندات نسخه .NET مورد استفاده را هم باید بررسی کرد، چون رفتارهای Unicode در چارچوب نسخه دار معنا دارند و نباید ویژگی یک نسخه را بی قید به همه محیط ها تعمیم داد.



پیش از برش متن، نیاز را تعریف کنیم



فرض کنید پاسخ زبان آموز برای نمایش در فهرست نتایج باید حداکثر ۱۲۰ نویسه باشد. کد text[..120] ممکن است درست کار کند؛ اما اگر مرز ۱۲۰ وسط یک جفت surrogate قرار بگیرد، یا واحدی از یک خوشه متنی را جدا کند، پیش نمایش خراب یا نازیبا می شود. چنین خطایی در مرورگر ممکن است به صورت نماد جایگزین، ایموجی ناقص یا فاصله گذاری غیرمنتظره دیده شود.



راه امن تر این است که اول تعریف محصول را مشخص کنیم: محدودیت بر اساس بایت است؟ واحد UTF-16 است؟ مقدار Unicode است؟ یا چیزی نزدیک به شکل دیداری مورد انتظار کاربر؟ سپس برش را بر همان تعریف پیاده کنیم و برای مرزها آزمون بنویسیم. اگر محدودیت برای ذخیره سازی یا تبادل داده است، ممکن است اندازه بایتی اهمیت داشته باشد. اگر محدودیت برای خوانایی رابط است، text elementها انتخاب مرتبط تری اند. یک عدد واحد به نام «طول» همه این کاربردها را پوشش نمی دهد.



نیم فاصله و جست وجوی فارسی



موضوع تنها ایموجی نیست. نیم فاصله یا ZWNJ در واژه هایی مانند «می روم» بخشی از نگارش فارسی است. کاربر ممکن است همان واژه را با فاصله معمولی یا بدون فاصله هم وارد کند. اگر سامانه همه نویسه های نامرئی را پاک کند، متن نمایشی و ساخت واژه را تغییر می دهد. اگر هیچ قاعده ای نداشته باشد، جست وجوی همان عبارت ممکن است بسته به شیوه تایپ نتیجه های متفاوتی بدهد.



مقاله Matina، پیکره بزرگ فارسی با ۷۳ میلیارد توکن، در شرح خط لوله پردازش خود به همسان سازی فاصله ها می پردازد، اما در برخی واژه های فارسی نیم فاصله را حفظ می کند. این انتخاب نشان می دهد پاک سازی متن فارسی تصمیم زبان شناختی هم هست، نه فقط حذف نشانه های اضافی. مقاله در مجموعه مقالات NAACL 2025 منتشر شده است؛ شناسه مجموعه مقالات: 2025.naacl-long.462. این پژوهش نسخه واحدی برای همه فرم ها تجویز نمی کند، اما نمونه روشنی است از اینکه سیاست فاصله باید با داده و کاربرد هماهنگ باشد. (Bourbour Hosseinbeigi et al., 2025)



همین احتیاط درباره Normalize در .NET هم لازم است. نرمال سازی Unicode می تواند صورت هایی را که طبق قواعد Unicode هم ارز شناخته می شوند همسان کند، اما نباید فرض کرد خودکار هر شکل شبیه به هم فارسی و عربی را به یک حرف تبدیل می کند. اگر برای جست وجو می خواهیم «ی» و «ی» یا «ک» و «ک» هم ارز شوند، باید این سیاست را جداگانه تعریف کنیم. این تبدیل برای کلید جست وجو شاید مفید باشد؛ برای متن اصلی پاسخ زبان آموز یا ارزیابی املایی می تواند اطلاعات لازم را از بین ببرد. متن اصلی را حفظ کنیم و در صورت نیاز، نمایه جست وجوی نرمال شده جدا بسازیم.



یک سناریو از کلاس تا کد



در سامانه آزمون ماک، زبان آموز پاسخ نوشتاری را در تلفن همراه وارد می کند. شمارنده کنار کادر می گوید ۲۰۰ نویسه باقی مانده است. اگر شمارنده بر اساس string.Length باشد ولی پیام محصول برای کاربر «حرف» یا «نویسه» وعده دهد، ممکن است با یک ایموجی یا متن چندزبانه عدد ناگهانی تغییر کند. اگر پاسخ هنگام ذخیره از حد پایگاه داده بیشتر شود، کاربر ممکن است فقط پس از ارسال خطا ببیند. اگر متن در گزارش مدرس بریده شود، شاید جمله ای که منظور پاسخ را روشن می کند ناپدید شود.



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



چک لیست کوچک برای تیم وب



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



در رابط کاربری هم نام گذاری دقیق مهم است. اگر عدد بر مبنای واحدهای UTF-16 است، بهتر است آن را بی توضیح «تعداد نویسه» ننامیم. اگر بر مبنای text element است، همان قاعده را در اعتبارسنجی سمت سرور هم هماهنگ کنیم. نمایش یک معیار و اعمال معیار دیگری، سرچشمه خطای گیج کننده است.



در نهایت، string.Length اشتباه نمی کند؛ ما گاهی از آن پاسخ پرسشی را می خواهیم که اصلا نمی شمارد. وقتی برنامه آموزشی با فارسی واقعی روبه رو می شود، جزئیات Unicode از حاشیه فنی بیرون می آیند و به بخشی از تجربه زبان آموز تبدیل می شوند. انتخاب درست میان char، Rune و text element یعنی تصمیم آگاهانه درباره این که برنامه متن را برای چه کاری می خواند. یک شمارنده درست شاید کوچک به نظر برسد، اما اعتماد کاربر به فرم و نتیجه آزمون از همین جزئیات ساخته می شود.



منابع:

۱. Microsoft. “Introduction to character encoding in .NET.” مستندات رسمی .NET، درباره UTF-16، string.Length، Rune و StringInfo.

۲. Unicode Consortium. Unicode Standard Annex #29: Unicode Text Segmentation. استاندارد قطعه بندی خوشه های گرافیکی و مرزهای متنی.

۳. Bourbour Hosseinbeigi, S., Taherinezhad, F., Faili, H., Baghbani, H., Nadi, F., & Amiri, M. (2025). “Matina: A Large-Scale 73B Token Persian Text Corpus.” Proceedings of NAACL 2025: Human Language Technologies, 9143–9157. ACL Anthology ID: 2025.naacl-long.462.