سلسلة net. للمحترفين العمليات على السلاسل النصية Strings في dot NET


رضوى العربي

عدم قابلية السلاسل النصية للتغيير

تُعدّ السلاسل النصية غير قابلة للتغيير immutable؛ بمعنى أنه لا يمكن التلاعب بسلسلة نصية بتغيير أحد المحارف فيها. عند إجراء أي عملية على السلسلة النصية، يُنشَئ نسخة جديدة منها بعد التعديل؛ مما يعني أنه عند استبدال حرف واحد بـسلسة نصية طويلة، ستُعيَّن مساحة بالذاكرة للقيمة الجديدة.

// تعيين مساحة بالذاكرة
string veryLongString = ...
// حذف أول حرف من السلسلة النصية
string newString = veryLongString.Remove(0,1); 

إذا أردت إجراء عدد كبير من العمليات على قيمة سلسلة نصية، استخدم الصنف StringBuilder المُعدّ خصيصًا للتعديل على السلاسل النصية بكفاءة.

var sb = new StringBuilder(someInitialString);
foreach(var str in manyManyStrings)
{
    sb.Append(str);
}
var finalString = sb.ToString();

عد الحروف

إذا كنت بحاجة إلى عَدّ الحروف بسلسلة نصية، فلا يُمكِنك ببساطة استخدام الخاصية Length؛ لأنها تَحسِب عدد العناصر بمصفوفة محارف. لا تُمثِّل هذه المصفوفة الحروف كما نعهدها، وإنما تُمثِّل العدد البتي للمحرف code-unit (ليس محارف اليونيكود Unicode code-points ولا الوحدات الكتابية graphemes). لذلك فإن الشيفرة الصحيحة تكون كالتالي:

int length = text.EnumerateCharacters().Count();

يُمكِن لتحسين بسيط إعادة كتابة التابع المُوسِّع EnumerateCharacters؛ بحيث يكون مُعَدّ خصيصًا لحِساب عدد المحارف:

public static class StringExtensions
{
    public static int CountCharacters(this string text)
    {
        if (String.IsNullOrEmpty(text))
            return 0;
        int count = 0;
        var enumerator = StringInfo.GetTextElementEnumerator(text);
        while (enumerator.MoveNext())
            ++count;
        return count;
    }
}

عد الحروف غير المكررة

على سبيل المثال، إذا استخدمت text.Distinct().Count()‎‎، سَتحصُل على نتائج خاطئة. الشيفرة الصحيحة كالتالي:

int distinctCharactersCount = text.EnumerateCharacters().Count();

خطوة إضافية هي عَدّ مرات تكرار كل حرف على حدى. يُمكِنك ببساطة استخدام الشيفرة التالية إذا لم يكن عامل اﻷداء مهمًا:

var frequencies = text.EnumerateCharacters()
    .GroupBy(x => x, StringComparer.CurrentCultureIgnoreCase)
    .Select(x => new { Character = x.Key, Count = x.Count() };

تحويل السلسلة النصية من وإلى ترميز آخر

السلاسل النصية بالـ ‎.NET هي بالأساس مصفوفة محارف System.Char (عبارة عن بتات الترميز UTF-16). إذا أردت حفظ النصوص أو التعامل معها بترميز آخر، يجب أن تتعامل مع مصفوفة بايتات System.Byte.

تُحَوِّل أصناف مُشتَقَة من System.Text.Encoder و System.Text.Decoder النصوص من وإلى ترميز آخر (من مصفوفة بايتات إلى سلسلة نصية بترميز UTF-16 والعكس).

من المُعتاد أن تُستَدعَى كلًا من أداة الترميز وفكه سويًا، لذلك تُتضَمَن داخل أصناف مُشتَقَة من System.Text.Encoding؛ بحيث تُحوِّل هذه الأصناف من وإلى الترميزات الشائعة (UTF-8 و UTF-16 ..إلخ)

أمثلة

تحويل سلسلة نصية إلى ترميز UTF-8:

byte[] data = Encoding.UTF8.GetBytes("This is my text");

تحويل بيانات بترميز UTF-8 إلى سلسلة نصية:

var text = Encoding.UTF8.GetString(data);

تغيير ترميز ملف نصي:

تقرأ الشيفرة التالية محتويات ملف نصي بترميز UTF-8، ثم تحفظها مجددًا، ولكن بترميز UTF-16. إذا كان حجم الملف كبيرًا، فإن هذه الشيفرة ليست الحل الأمثل؛ لأنها ستنقل جميع محتويات الملف إلى الذاكرة:

var content = File.ReadAllText(path, Encoding.UTF8);
File.WriteAllText(content, Encoding.UTF16);

موازنة السلاسل النصية

بالرغم من أن النوع String نوع مَرجِعِي باﻷساس، يُقارِن عامل المساواة == قيمة السلسلة النصية وليس مَرجِعها.

ربما تعلم أن السلسلة النصية ليست سوى مصفوفة محارف. لكن في المقابل، إن كنت تظن أن اختبار مساواة السلاسل النصية ومقارنتها النسبية يتم على مستوى المحارف حرفًا بحرف، فأنت على خطأ. في الواقع، تختلف عمليات الاختبار بحسب اللغة؛ فمن الممكن أن تتكافئ أكثر من متتالية محارف اعتمادا على اللغة.

عند اختبار مساواة سلسلتين نصيتين، فكر مليًا قبل الاعتماد على تساوي خاصية الطول Length للسلسلتين بِحُسبَانِها دارة قصيرة short circuiting.

تُستخدم الدارة القصيرة مع التعبيرات المنطقية logical expressions لاختصار حِساب قيمة التعبير إن كان ذلك مُمكنًا. فمثلًا، تُعيد العملية AND القيمة true فقط إذا آل كِلا مُعامليها للقيمة true. وبالتالي، إذا آل المعامل اﻷول للقيمة false، يُمكِن للدارة القصيرة أن تُعيد القيمة false كقيمة إجمالية للتعبير المنطقي دون الحاجة إلى حساب قيمة المعامل الثاني؛ ﻷن قيمته لم تعد مُؤثرة.

إذا أردت تغيير السلوك الافتراضي لعملية الموازنة، يُمكِنك استخدام التحميلات الزائدة (method overloading) للتابع String.Equal، والتي تَستَقبِل مُعامِلًا إضافيًا من النوع StringComparison.

عد مرات تكرار حرف معين

لا يُمكِنك ببساطة استخدام الشيفرة التالية لحساب عدد مرات تكرار حرف معين (إلا إذا كنت ترغب بعَدّ مرات تكرار العدد البتي للمحرف):

int count = text.Count(x => x == ch);

في الواقع، تحتاج لدالة أكثر تعقيدًا مثل الدالة التالية:

public static int CountOccurrencesOf(this string text, string character)
{
    return text.EnumerateCharacters()
        .Count(x => String.Equals(x, character, StringComparer.CurrentCulture));
}

لاحظ أنه من الضروري أن تُقارَنّ السلاسل النصية وفقًا لقواعد لغة بعينها، بعكس مقارنة الحروف والتي يمكن أن تتباين بحسب اللغة.

تقسيم السلسلة النصية إلى كتل متساوية

لا يُمكِنك تقسيم السلسلة النصية تَقسِيمًا عشوائيًا؛ لأن أي محرف System.Char بداخل المصفوفة قد يكون غير صالح بمفرده؛ إِمَّا لكونه محرف دمج أو جزء من زوج بدل surrogate pair. لذلك لابد للشيفرة أن تأخذ هذا بالحسبان.

لاحظ أن المقصود من كتل متساوية هو تساوي عدد الوحدات الكتابية graphemes وليس عدد البتات للمحرف code-units.

public static IEnumerable<string> Split(this string value, int desiredLength)
{
    var characters = StringInfo.GetTextElementEnumerator(value);
    while (characters.MoveNext())
        yield return String.Concat(Take(characters, desiredLength));
}

private static IEnumerable<string> Take(TextElementEnumerator enumerator, int count)
{
    for (int i = 0; i < count; ++i)
    {
        yield return (string)enumerator.Current;
        if (!enumerator.MoveNext())
            yield break;
    }
}

التابع الإفتراضي Object.ToString

كل الأنواع الموجودة في NET. هي أنواع مشتقة من النوع Object في نهاية المطاف الذي يحوي التابع ToString، لذا ترثه تلك الأنواع المشتقة تلقائيًا ويمكن إعادة كتابة تنفيذ خاص به لنوع محدَّد والخروج عن التنفيذ الافتراضي الذي يعيد اسم النوع افتراضيًا إن لم يكن هنالك ما يحوّله.

المثال التالي يُعرف الصنف Foo، ونظرًا ﻷنه لم يُعيد كتابة تنفيذ التابع ToString، تم استدعاء التنفيذ الافتراضي فيكون الخْرج هو اسم الصنف Foo:

public class Foo
{    
}


var foo = new Foo();
Console.WriteLine(foo); // outputs Foo

يُستَدعَى التابع ToString ضمنيًا عند ضم قيمة إلى سلسلة نصية:

public class Foo
{
    public override string ToString()
    {
        return "I am Foo";
    }
}
var foo = new Foo();
Console.WriteLine("I am bar and "+foo); // I am bar and I am Foo

تُسجِل أدوات تنقيح الأخطاء debugging tools الأحَدَاث الحاصلة في البرنامج logs، لذلك فإنها عادة ما تحتاج للإشارة إلى الكائنات المُتفاعلة ضمن الحَدَث. لذا تلجأ لاستخدام التابع ToString. إذا أردت، لسبب ما، أن تُخصِّص طريقة عرض المُنقح debugger للقيم بدون أن تُعيد تعريف التابع، استخدم السمة DebuggerDisplay، انظر MSDN:

// [DebuggerDisplay("Person = FN {FirstName}, LN {LastName}")]
[DebuggerDisplay("Person = FN {"+nameof(Person.FirstName)+"}, LN {"+nameof(Person.LastName)+"}")]
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set;}
    // ...
}

التعبيرات النمطية (Regular Expressions)

التابع Regex.IsMatch لاختبار تطابق نمط (pattern)

يَفحص التابع Regex.IsMatch ما إذا كانت السِلسِلة النصية المُعطاة متطابقة مع نَمط معين، ويُعيد قيمة منطقية تُحدِّد ذلك، كالمثال التالي:

public bool Check()
{
    string input = "Hello World!";
    string pattern = @"H.ll. W.rld!";

    // true
    return Regex.IsMatch(input, pattern);
}

تَتوفَّر بَصمة أُخْرى من التابع Regex.IsMatch تَسمَح بتمرير بعض الخيارات للتحكم بعملية الفحص، كالتالي:

public bool Check()
{
    string input = "Hello World!";
    string pattern = @"H.ll. W.rld!";

    // true
    return Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.Singleline);
}

التابع Regex.Match لاستخراج أول تطابق (match)

يبحث التابع Regex.Match عن نمط (pattern) معين بالسِلسِلة النمطية المُعطاة، ويُعيد كائن من النوع Match يُمثِل أول ظهور (occurrence) للنمط بالسِلسِلة.

يُمكِن الولوج للخاصيتين Value و Index بالقيمة المُعادة. تَحوي الأولى القيمة الكاملة لأول ظهور للنمط بينما تَحوي الثانية مَوضِع (position) ذلك الظهور أي أين ظهرت القيمة المتطابقة.

تَستطيع أيضًا الولوج للخاصية Groups من النوع GroupCollection. تَحمِل هذه الخاصية دومًا عنصر واحد على الأقل، سيحتوي هذا العنصر على القيمة الكاملة لأول ظهور للنمط إن وجد أو قيمة فارغة في حالة عدم وجوده. قد تتواجد عناصر اخرى ضِمْن التَجمِيعَة إذا كنت قد عَرَّفت أي مجموعات بالنمط المُمرَّر مثل Subject كالمثال التالي:

string input = "Hello World!";
string pattern = @"H.ll. (?<Subject>W.rld)!";
Match match = Regex.Match(input, pattern);

Console.WriteLine(match.Value);                        // Hello World!
Console.WriteLine(match.Index);                        // 0
Console.WriteLine(match.Groups["Subject"].Value);     // World

التابع Regex.Matches لاستخراج جميع التطابقات (matches)

يَعمَل بصورة مشابهة للتابع Regex.Match لكنه لا يَقتصِر على إعادة أول ظهور (occurrence) للنمط بالسِلسِلة النصية، وإنما يُعيد قيمة من النوع GroupCollection تحتوي على جميع التطابقات (matches). انظر المثال التالي:

using System.Text.RegularExpressions;

static void Main(string[] args)
{
    string input = "Carrot Banana Apple Cherry Clementine Grape";
    // Find words that start with uppercase 'C'
    string pattern = @"\bC\w*\b";
    MatchCollection matches = Regex.Matches(input, pattern);

    foreach (Match m in matches)
        Console.WriteLine(m.Value);
}

الخْرج:

Carrot
Cherry
Clementine

التابع Regex.Replace للاستبدال وفقًا لنمط

يَستبدِل التابع Regex.Replace أجزاء من سِلسِلة نصية مُعطاة وفقًا لنمط يُمرَّر إليه، ثم يُعيد السِلسِلة النصية بعد عملية الاستبدال، كالمثال التالي:

public string Check()
{
    string input = "Hello World!";
    string pattern = @"W.rld";

    // Hello Stack Overflow!
    return Regex.Replace(input, pattern, "Stack Overflow");
}

يُمكن استخدام التابع Regex.Replace لحَذْف الحروف الأبْجَعَددية (alphanumeric) من سِلسِلة نصية عن طريق تمرير النمط [^a-zA-Z0-9] للتابع وطلب استبداله بسِلسِلة نصية فارغة، كالتالي:

public string Remove()
{
    string input = "Hello./!";

    return Regex.Replace(input, "[^a-zA-Z0-9]", "");
}

ترجمة -وبتصرف- للفصل DateTime parsing من كتاب ‎.NET Framework Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن