الـ string هو نوع من البيانات بنستخدمه عشان نخزن فيه نصوص أو text. كل حرف في الـ string بيستهلك 2 Bytes من الذاكرة.

string greeting = "Hello";
string greeting2 = "Nice to meet you!";

الـ string بيعتبر من الـ Reference Types، وده معناه إنه class مش مجرد نوع بيانات بسيط، والـ class ده بيكون جواه properties و methods نقدر نستخدمها.

Why is String a Reference Type?

في معظم لغات البرمجة، الـ string بيعتبر reference type مش value type. السبب الرئيسي هو إننا لما بنعرّف متغير string، مش بنبقى عارفين حجمه هيكون قد إيه بالظبط في الـ memory.

  • لما بتعرّف متغير زي int x، الـ compiler بيكون عارف إنه هيحجز 4 bytes في الـ memory علطول.
  • لكن لما بتعرّف string str;، في اللحظة دي هو مجرد reference أو مؤشر فاضي. لما بتخصص له قيمة زي str = "ABC";، ساعتها بس بيتم حجز المساحة المطلوبة في الـ memory.
string str; // This is just a reference
 
// Here, memory is allocated for the value "ABC"
str = "ABC"; 

في C#، اللغة بتهتم بعملية الـ allocation دي تلقائيًا، فمش بنحتاج نستخدم كلمة new زي ما بنعمل مع الـ objects التانية.

string vs. String

يمكن تلاحظ إن string بتتكتب بطريقتين: string بحرف s صغير، وString بحرف S كبير.

الـ string (بحرف s صغير) هو مجرد alias أو اسم مختصر للـ class الأساسي اللي هو System.String (بحرف S كبير). من ناحية الأداء، مفيش أي فرق بينهم.

كممارسة كويسة (best practice):

  • استخدم string لما تعرّف متغيرات.
  • استخدم String لما تستدعي static methods من الـ class نفسه، زي String.Format() أو String.IsNullOrEmpty().

Immutability of Strings

واحدة من أهم خصائص الـ strings في C# هي إنها Immutable، يعني غير قابلة للتعديل.

لما بتعمل أي تعديل على string، زي إنك تضيف عليه نص أو تحذف منه جزء، اللي بيحصل في الحقيقة مش تعديل للـ string الأصلي، لأ، ده بيتم إنشاء string جديد خالص في الذاكرة بالنتيجة الجديدة، والـ reference بتاعك بيشاور على الـ object الجديد ده. الـ string القديم بيستنى الـ Garbage Collector عشان يمسحه.

ده بيأثر على أداء البرنامج والذاكرة، خصوصًا لو بتعمل عمليات تعديل متكررة في loop.

graph TD
    A[str = Hello] --> B(Memory: Hello);

    subgraph After Modification
        C[str = Hello World] --> D(Memory: Hello World);
        B[Becomes Garbage];
    end

Performance Impact Example

لو شغلنا كود بيعمل loop عدد كبير من المرات وبيغير قيمة string في كل مرة، هنلاحظ إن العملية بتاخد وقت طويل جدًا.

using System;
using System.Diagnostics;
 
// ...
 
string str = "";
Console.WriteLine("Loop Started");
var stopwatch = new Stopwatch();
stopwatch.Start();
 
for (int i = 0; i < 30000000; i++)
{
    // A new string object is created in each iteration
    str = Guid.NewGuid().ToString();
}
 
stopwatch.Stop();
Console.WriteLine("Loop Ended");
 
// Execution time will be very high (e.g., 26000 ms)
Console.WriteLine("Loop Execution Time in MS :" + stopwatch.ElapsedMilliseconds);
 
// Without changing => 48 ms
// with changing => 3800 ms

على عكس كدة، لو استخدمنا value type زي int، القيمة بتتعدل في نفس المكان في الذاكرة، وده بيخليه أسرع بكتير.

int ctr = 0;
Console.WriteLine("Loop Started");
var stopwatch = new Stopwatch();
stopwatch.Start();
 
for (int i = 0; i < 30000000; i++)
{
    // Value is modified in the same memory location
    ctr = ctr + 1;
}
 
stopwatch.Stop();
Console.WriteLine("Loop Ended");
 
// Execution time will be very low (e.g., 84 ms)
Console.WriteLine("Loop Execution Time in MS :" + stopwatch.ElapsedMilliseconds);

String Interning

طيب لو بنخصص نفس القيمة للـ string بشكل متكرر، هل برضه الأداء هيكون بطيء؟ الإجابة لأ، وده بسبب حاجة اسمها String Interning.

الـ .NET CLR بيحتفظ بـ pool أو مخزن للـ strings اللي تم استخدامها. لما بتيجي تنشئ string جديد، الأول بيشوف لو فيه string بنفس القيمة موجود بالفعل في الـ pool ده. لو موجود، بيرجعلك reference لنفس الـ object الموجود بدل ما ينشئ واحد جديد.

String Interning Example

لو شغلنا نفس الـ loop اللي فات بس بنخصص قيمة ثابتة كل مرة، هنلاقي الأداء اتحسن جدًا.

string str = "";
Console.WriteLine("Loop Started");
var stopwatch = new Stopwatch();
stopwatch.Start();
 
for (int i = 0; i < 30000000; i++)
{
    // The CLR reuses the same string from the intern pool
    str = "DotNet Tutorials";
}
 
stopwatch.Stop();
Console.WriteLine("Loop Ended");
 
// Execution time is very low due to interning (e.g., 95 ms)
Console.WriteLine("Loop Execution Time in MS :" + stopwatch.ElapsedMilliseconds);

Concatenation

لدمج النصوص (Concatenation)، بنستخدم علامة +.

string firstName = "Pop";
string lastName = "Corn";
string name = firstName + lastName; // Result: "PopCorn"
Console.WriteLine(name);

ممكن كمان نستخدم الـ method اللي اسمها string.Concat().

string firstName = "Pop";
string lastName = "Corn";
string name = string.Concat(firstName, lastName);
Console.WriteLine(name);

بس خلي بالك، دمج النصوص جوه loop باستخدام + بيسبب مشكلة أداء كبيرة بسبب الـ immutability، لإن في كل مرة بيتم إنشاء string جديد.

The Solution: StringBuilder

الحل الأمثل لعمليات الدمج المتكررة هو استخدام class اسمه Cs StringBuilder. الـ StringBuilder يعتبر mutable (قابل للتعديل)، يعني لما بتضيف عليه نص، هو بيعدّل على نفس الـ object في الذاكرة من غير ما ينشئ واحد جديد كل مرة.

Performance with StringBuilder

لو قارنا أداء StringBuilder مع الـ string العادي في عملية دمج متكررة، هنلاقي فرق ضخم.

using System.Text;
using System.Diagnostics;
 
// ...
 
StringBuilder stringBuilder = new StringBuilder();
Console.WriteLine("Loop Started");
var stopwatch = new Stopwatch();
stopwatch.Start();
 
for (int i = 0; i < 30000; i++)
{
    // Appends to the same object, very efficient
    stringBuilder.Append("DotNet Tutorials");
}
 
stopwatch.Stop();
Console.WriteLine("Loop Ended");
 
// Execution time is extremely low (e.g., 1 ms)
Console.WriteLine("Loop Execution Time in MS :" + stopwatch.ElapsedMilliseconds);

Why are Strings Immutable?

السؤال المهم، ليه أصلًا خلّوا الـ strings في C# بالصفة دي Immutable؟ السبب الرئيسي هو الأمان في بيئة تعدد الـ Threads أو Thread Safety.

لو كان الـ string قابل للتعديل (mutable)، وأكتر من Thread بيحاولوا يعدلوا على نفس الـ string object في نفس الوقت، ده كان هيسبب مشاكل كبيرة جدًا وتضارب في البيانات. كون الـ string غير قابل للتعديل بيخلي مشاركته بين الـ threads عملية آمنة تمامًا.

Common String Methods

Length

دي property بترجعلك عدد الحروف اللي في الـ string. لاحظ إننا مش بنكتب بعدها قوسين () لإنها property مش method.

string txt = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
Console.WriteLine(txt.Length); // Outputs: 26

Formatting

  • الـ ToUpper() و ToLower(): بيحولوا كل الحروف لـ UPPERCASE أو lowercase.

    string txt = "Hello World";
    Console.WriteLine(txt.ToUpper()); // HELLO WORLD
    Console.WriteLine(txt.ToLower()); // hello world
  • الـ Trim(): بتشيل أي مسافات فاضية في أول أو آخر الـ string.

    string val1 = " a";
    string val2 = "a ";
    Console.WriteLine(val1.Trim() == val2.Trim()); // True

Searching

  • الـ Contains()، StartsWith()، EndsWith(): بتعمل بحث وبترجع true أو false.

    string pangram = "The quick brown fox jumps over the lazy dog.";
    Console.WriteLine(pangram.Contains("fox")); // True
    Console.WriteLine(pangram.Contains("cow")); // False
  • الـ IndexOf() و LastIndexOf(): بترجع الـ index بتاع أول أو آخر ظهور لحرف أو كلمة.

    string myString = "Hello";
    Console.WriteLine(myString.IndexOf('e')); // Outputs: 1

Substrings

  • الـ Substring(startIndex): بتقطع الـ string من index معين لحد الآخر.
  • الـ Substring(startIndex, length): بتقطع جزء من الـ string بتبدأه من index معين وبطول محدد.
// Full name
string name = "John Doe";
 
// Location of the letter D
int charPos = name.IndexOf("D");
 
// Get last name from charPos to the end
string lastName = name.Substring(charPos);
 
// Print the result
Console.WriteLine(lastName); // Outputs: Doe

Replacing

  • الـ Replace('a', '!'): بتبدل كل حروف a بعلامة !.
  • الـ Replace("old", "new"): بتبدل كلمة بكلمة تانية.

Null Checking

  • الـ String.IsNullOrEmpty(str): بتتحقق لو الـ string كان null أو فاضي "".
  • الـ String.IsNullOrWhiteSpace(str): بتتحقق لو كان null أو فاضي أو بيحتوي على مسافات بس.

Splitting

  • الـ str.Split(' '): بتقسم الـ string لمصفوفة من الـ strings بناءً على فاصل معين (زي المسافة).

Converting Types

نقدر نحول من أرقام لـ string والعكس باستخدام Explicit Casting.

أو ممكن نستخدم i.ToString(format) عشان نتحكم في شكل الـ string الناتج.

  • ملحوظة: الفورمات C0 في المثال معناه Currency (عملة) مع 0 أرقام بعد العلامة العشرية. لو استخدمت C1 هيظهر رقم واحد بعد العلامة، وهكذا.

String Formatting

فيه طرق مختلفة عشان ننسق شكل النصوص النهائية.

  • String.Format:

    string str = String.Format("Hello World {0}", x);
    Console.Write(String.Format("Hello World {0}", x));
  • String Interpolation ($): دي الطريقة الأحدث والأسهل.

    string str = $"Hello World {x}";
  • الـ Verbatim Identifier (@): بتخلي الـ string يتطبع زي ما هو بالظبط من غير ما يعمل escape لأي characters زي \. مفيدة جدًا في مسارات الملفات.

    Console.WriteLine($@"C:\Output\{projectName}\Data");

Accessing Strings

ممكن نوصل لأي حرف في الـ string عن طريق الـ index بتاعه، زي الـ array بالظبط.

string myString = "Hello";
Console.WriteLine(myString[0]);  // Outputs: H
Console.WriteLine(myString[1]);  // Outputs: e

Empty String

أحيانًا بنحتاج نبدأ بـ string فاضي عشان نجمع فيه قيم بعدين. أفضل طريقة لعمل ده هي باستخدام string.Empty لإنها أوضح ومش بتعمل allocation جديدة في الذاكرة.

string numbers = string.Empty;
 
for (int i = 0; i < 10; i++)
{
    numbers = numbers + i.ToString();
}