Rust: دليل شامل لسلاسل النصوص في لغة Rust

آخر تحديث: 2026-08-26

لا يوجد في لغة Rust نوع واحد من سلاسل الأحرف، بل نوعان — لأن برمجة الأنظمة تتطلب تحكماً دقيقاً في الذاكرة.

تعد معالجة السلاسل في لغة Rust مصدرًا للارتباك بالنسبة للعديد من المبتدئين: لماذا يوجد نوعان — String و&str؟ وكيف يختلفان عن char* في لغة C؟ سيساعدك هذا الدرس على فهم الموضوع بشكل كامل.


1. ما ستتعلمه



2. قصة أمينة مكتبة

(1) المشكلة: بيانات JSON طويلة جدًّا بحيث لا تتسع لها المساحة المتاحة

ماريا هي أمينة مكتبة تعمل على تطوير نظام صغير للبحث عن الكتب لإدارة 5,000 كتاب.

«سيكون من الرائع جدًّا لو كانت هناك لغات برمجة تدير الذاكرة تلقائيًّا...» وقد وجدت الإجابة لاحقًا في لغة «Rust».

(2) حل «Rust» لمشكلة السلسلة

RUST
fn main() {
    // &str: String Literals, Present in the binary at compile time
    let title_book1: &str = "One Hundred Years of Solitude";

    // String: Allocated on the heap, Variable length, Automatic Memory Management
    let mut title_book2 = String::from("How to Make a Complete Map of an Infinite Universe");
    title_book2.push_str(" (Second Edition)");  // Additional Content

    println!("Book 1: {} (Length: {} Bytes)", title_book1, title_book1.len());
    println!("Book 2: {} (Length: {} Bytes)", title_book2, title_book2.len());
    // String Automatically released when leaving the scope, No manual free required
}

String في لغة Rust تشبه سلاسل النصوص في لغة Java — فهي تدير الذاكرة تلقائيًا؛ أما &str فهي تشبه const char* في لغة C — أي المراجع المخصصة للقراءة فقط. وتحقق هاتان الميزتان معًا توازنًا بين الأداء والمرونة.



3. الفرق بين &str و String

(1) نموذج الذاكرة

100%
graph LR
    subgraph "&str (String Literals)"
        S1_ptr[ptr ──→ ...rust...]
        S1_len[len: 4]
    end
    subgraph "String (Heap Allocation)"
        S2_ptr[ptr ──→ Rust is...]
        S2_len[len: 10]
        S2_cap[cap: 16]
    end
    subgraph "Storage Location"
        STACK[Stack: &str<br>Read-Only Borrow]
        HEAP[Heap: String<br>Ownership]
    end
    S1_ptr -.->|Determined at compile time| STACK
    S2_ptr -.->|Runtime Allocation| HEAP
الخصائص &str String
المالك حق الوصول للقراءة فقط الملكية الكاملة
القابلية للتغيير غير قابل للتغيير يمكن إضافته أو تعديله
موقع التخزين مقطع بيانات للقراءة فقط (في وقت التحويل البرمجي) الـ«هيّب» (يُخصص في وقت التشغيل)
طريقة الإنشاء حرفي "hello" String::from() أو .to_string()
حالات الاستخدام معلمات سلاسل نصية للقراءة فقط الحاجة إلى تعديل السلسلة أو تولي ملكيتها

(2) مرجع سريع لأساليب السلسلة الشائعة

الطريقة نوع القيمة المرجعة الوصف مثال
push_str(&str) () جزء إضافي من سلسلة الأحرف s.push_str("abc")
push(char) () إضافة حرف واحد s.push('!')
len() usize طول البايت المرتجع s.len()
is_empty() bool هل هو فارغ؟ s.is_empty()
trim() &str إزالة المسافات في بداية ونهاية النص s.trim()
contains(&str) bool ما إذا كانت السلسلة الفرعية موجودة أم لا s.contains("rust")
replace(&str, &str) String استبدال جميع النتائج المطابقة s.replace("a", "b")
تقسيم حسب الفاصل split(char) Split s.split(',')
تحويل الأحرف إلى أحرف صغيرة
الأحرف الكبيرة to_uppercase() String التبديل إلى الأحرف الكبيرة
chars() Chars التكرار حسب الحرف s.chars()
bytes() Bytes التكرار بالبايت s.bytes()
مسح السلسلة
capacity() usize العودة إلى سعة المخزن المؤقت s.capacity()

(3) طريقة الإنشاء

RUST
// &str Create
let s1: &str = "hello, world";       // String Literals
let s2: &str = r"raw string\n";      // Original string(Do not escape)

// String Create
let s3 = String::from("hello");
let s4 = "world".to_string();
let s5 = format!("{}-{}", s3, s4);   // Format and Create
let s6: String = "hi".into();         // Through Type Conversion

(4) مرجع سريع لطرق تحويل السلاسل

نوع المصدر نوع الهدف طريقة التحويل بدون عبء إضافي
تخصيص ذاكرة الكومة
تخصيص ذاكرة الكومة
تخصيص ذاكرة الكومة
String &str &s أو s.as_str() بدون تكلفة (اقتراض)
String &str &s[..] بدون تكلفة (شريحة)
&str &[u8] "hello".as_bytes() بدون تكلفة (اقتراض)
String Vec<u8> s.into_bytes() بدون تكاليف إضافية (نقل الملكية)
Vec<u8> String String::from_utf8(v) بدون تكاليف إضافية (نقل الملكية؛ يلزم التحقق من ترميز UTF-8)


4. العمليات الشائعة على السلاسل

(1) ▶ المثال:إنشاء السلاسل وتعديلها (مستوى الصعوبة ⭐)

RUST
// ============================================
// String Creation, Append and Concatenation
// ============================================

fn main() {
    // Create
    let mut s = String::from("Hello");

    // Add
    s.push_str(", ");       // Append a string slice
    s.push('R');            // Add a single character
    s.push_str("ust!");
    println!("After the addition: {}", s);

    // Concatenation (+ operator consumes the left-hand side String)
    let s1 = String::from("Hello, ");
    let s2 = String::from("World!");
    let s3 = s1 + &s2;      // s1 Moved, It can't be used anymore.
    println!("After concatenation: {}", s3);

    // format! macro (Does not consume any variables)
    let a = String::from("tic");
    let b = String::from("tac");
    let c = String::from("toe");
    let result = format!("{}-{}-{}", a, b, c);
    println!("format Results: {}", result);
}

الناتج:

TEXT 📖 للعرض فقط
After the addition: Hello, Rust!
After concatenation: Hello, World!
format Result: tic-tac-toe

يستهلك (ينقل) المشغل + المتغير String الموجود على يساره، لذا لا يمكن استخدام s1 بعد عملية التسلسل. أما format! فلا يستهلك أي متغيرات؛ حيث يتم الاحتفاظ بملكية جميع المتغيرات.


(2) ▶ المثال:تقطيع السلاسل وفهرستها (مستوى الصعوبة ⭐⭐)

RUST
// ============================================
// String Slicing -- Note UTF-8 Boundary!
// ============================================

fn main() {
    let s = String::from("Rust🦀Code");

    // Get the length of a string (Number of bytes, Not the number of characters)
    println!("Byte Length: {}", s.len());         // 11 (4 Byte ASCII + 4 Byte UTF-8 emoji + 4 Byte ASCII)

    // String Slicing (Index by Byte, Must be at a character boundary)
    let slice1 = &s[0..4];                     // "Rust" (first 4 bytes fall exactly on a character boundary)
    println!("First 4 Byte Slice: {}", slice1);

    // &s[0..5] will panic! (Because bytes 4-5 are in the middle of the emoji)

    // Iterate by character count
    for c in s.chars() {
        print!("{} ", c);
    }
    println!();

    // Iterate by byte
    for b in s.bytes() {
        print!("{:02x} ", b);
    }
    println!();
}

الناتج:

TEXT 📖 للعرض فقط
Byte Length: 11
First 4 Byte Slice: Rust
R u s t 🦀 C o d e
52 75 73 74 f0 9f a6 80 43 6f 64 65

لا تدعم سلاسل Rust الوصول المباشر القائم على الفهرس (مثل s[0]) لأن أحرف UTF-8 ذات طول متغير. يجب عليك التكرار عبر الأحرف باستخدام .chars() أو استخدام شرائح البايت. يجب أن تتوافق الشرائح مع حدود الأحرف؛ وإلا فسيحدث تعطل في وقت التشغيل.


(3) ▶ المثال:الطرق الشائعة للسلاسل (مستوى الصعوبة ⭐⭐)

RUST
// ============================================
// Common String Queries, Replace, Cutting Methods
// ============================================

fn main() {
    let s = "  Hello, Rust World!  ";

    // Remove leading and trailing spaces
    println!("trim: '{}'", s.trim());

    // Includes a check
    println!("contains 'Rust': {}", s.contains("Rust"));

    // Replace
    let replaced = s.replace("Rust", "Go");
    println!("replace: '{}'", replaced);

    // Split
    let parts: Vec<&str> = "apple,banana,cherry".split(',').collect();
    println!("split: {:?}", parts);

    // Case Conversion
    let lower = "HELLO".to_lowercase();
    let upper = "world".to_uppercase();
    println!("lower: {}, upper: {}", lower, upper);
}

الناتج:

TEXT 📖 للعرض فقط
trim: 'Hello, Rust World!'
contains 'Rust': true
replace: '  Hello, Go World!  '
split: ["apple", "banana", "cherry"]
lower: hello, upper: WORLD

تُرجع الدالتان trim() وsplit() القيمة &str دون إنشاء سلسلة جديدة. أما الدالة replace() فتُرجع قيمة String جديدة تمامًا. ضع في اعتبارك العبء الإضافي على الأداء عند إجراء التعديلات.


(4) ▶ المثال:التكرار عبر سلاسل UTF-8 والتحقق من الحدود (مستوى الصعوبة ⭐⭐⭐)

RUST
// ============================================
// UTF-8 Bytes in a string, Character and Grapheme Clusters
// ============================================

fn main() {
    let s = "Rust🦀Code";

    println!("String: {}", s);
    println!("Byte Length: {}", s.len());
    println!("Number of characters: {}", s.chars().count());

    println!("\n--- Character-by-character traversal ---");
    for (i, c) in s.chars().enumerate() {
        println!("Character {}: '{}' (Unicode: U+{:04X})", i, c, c as u32);
    }

    println!("\n--- Traverse byte by byte ---");
    for (i, b) in s.bytes().enumerate() {
        print!("{:02x} ", b);
        if (i + 1) % 8 == 0 { println!(); }
    }
    println!();

    println!("\n--- Safe Slices ---");
    let ascii_part = &s[0..4];
    println!("ASCII Part &s[0..4]: '{}'", ascii_part);

    let emoji_start = 4;
    let emoji_end = emoji_start + 4;
    let emoji_part = &s[emoji_start..emoji_end];
    println!("Emoji Part &s[{}..{}]: '{}'", emoji_start, emoji_end, emoji_part);

    let code_start = emoji_end;
    let code_end = code_start + 4;
    let code_part = &s[code_start..code_end];
    println!("Code Part &s[{}..{}]: '{}'", code_start, code_end, code_part);

    let combined = format!("{}{}{}", ascii_part, emoji_part, code_part);
    println!("Concatenated Reconstruction: {}", combined);
}

الناتج:

TEXT 📖 للعرض فقط
String: Rust🦀Code
Byte Length: 12
Number of characters: 8

--- Character-by-character traversal ---
Character 0: 'R' (Unicode: U+0052)
Character 1: 'u' (Unicode: U+0075)
Character 2: 's' (Unicode: U+0073)
Character 3: 't' (Unicode: U+0074)
Character 4: '🦀' (Unicode: U+1F980)
Character 5: 'C' (Unicode: U+0043)
Character 6: 'o' (Unicode: U+006F)
Character 7: 'd' (Unicode: U+0064)
Character 8: 'e' (Unicode: U+0065)

--- Traverse byte by byte ---
52 75 73 74 f0 9f a6 80 43 6f 64 65

--- Safe Slices ---
ASCII Part &s[0..4]: 'Rust'
Emoji Part &s[4..8]: '🦀'
Code Part &s[8..12]: 'Code'
Concatenated Reconstruction: Rust🦀Code

في ترميز UTF-8، تشغل أحرف ASCII 1 بايت، بينما تشغل الرموز التعبيرية 4 بايت. يجب أن تتم عملية التقطيع عند حدود الأحرف؛ وإلا فسيحدث خطأ فادح أثناء وقت التشغيل. استخدم .chars().enumerate() لمعالجة الأحرف واحدًا تلو الآخر بأمان.


(5) ▶ المثال:تحليل السلاسل وتنسيقها عمليًّا (مستوى الصعوبة: ⭐⭐)

RUST
// ============================================
// String Parsing, Combined Use of Formatting and Linking
// ============================================

fn main() {
    let raw_data = "name=Alice;age=30;city=Beijing";

    println!("Raw Data: {}", raw_data);
    println!("\n--- Parsing Key-Value Pairs ---");

    let pairs: Vec<&str> = raw_data.split(';').collect();
    for pair in pairs {
        let parts: Vec<&str> = pair.split('=').collect();
        if parts.len() == 2 {
            let key = parts[0].trim();
            let value = parts[1].trim();
            println!("{} => {}", key, value);
        }
    }

    println!("\n--- Construct a New String ---");
    let name = "Alice";
    let age = 30;
    let city = "Beijing";
    let summary = format!("{}, {} years old, From {}", name, age, city);
    println!("Abstract: {}", summary);

    let mut report = String::from("Personnel Report\n");
    report.push_str(&format!("Name: {}\n", name));
    report.push_str(&format!("Age: {}\n", age));
    report.push_str(&format!("City: {}\n", city));
    report.push_str("--- End ---");
    println!("\n{}", report);

    let csv_line = ["Alice", "30", "Beijing"].join(",");
    println!("\nCSV Format: {}", csv_line);
}

الناتج:

TEXT 📖 للعرض فقط
Raw Data: name=Alice;age=30;city=Beijing

--- Parsing Key-Value Pairs ---
name => Alice
age => 30
city => Beijing

--- Construct a New String ---
Abstract: Alice, 30 years old, From Beijing

Personnel Report
Name: Alice
Age: 30
City: Beijing
--- End ---

CSV Format: Alice,30,Beijing

split() + collect() هما النمطان الأساسيان لتحليل النصوص المنظمة؛ وformat! مناسب للتسلسل لمرة واحدة؛ وpush_str() مناسب لبناء النصوص الطويلة تدريجيًّا؛ وjoin() مناسب لربط المصفوفات باستخدام الفواصل.



❓ أسئلة شائعة

س لماذا توجد في لغة Rust نوعان من السلاسل؟
ج لتحقيق التوازن بين الملكية والأداء. &str هي سلسلة مقتبضة للقراءة فقط (بدون أي عبء إضافي)، بينما String هي سلسلة قابلة للتعديل مملوكة (مع عبء تخصيص الذاكرة المؤقتة). ويوفر استخدام &str لمعلمات الدوال أقصى قدر من المرونة، حيث يمكن تحويل String تلقائيًا إلى &str.
س كيف يتم التحويل بين String و&str؟
ج لتحويل &str إلى String، استخدم .to_string() أو String::from()؛ ولتحويل String إلى &str، استخدم &s أو s.as_str(). جميع عمليات التحويل لا تكلف شيئًا (فهي تمثل ببساطة الذاكرة من منظور مختلف).
س لماذا لا يمكن لـ s[0] الوصول إلى الحرف الأول؟
ج لأن UTF-8 هو ترميز متغير الطول. s[0] يصل إلى البايت الأول، الذي لا يمثل بالضرورة حرفًا كاملاً. وقد صُممت لغة Rust لتكون أكثر أمانًا: فهي تفضل إصدار خطأ في وقت الترجمة على السماح بحدوث سلوك غير متوقع في وقت التشغيل.
س هل يجب عليّ استخدام علامة + أم format! لتسلسل السلاسل النصية؟
ج استخدم علامة + لتسلسل عدد قليل من السلاسل النصية، واستخدم format! لتسلسل عدة مقاطع. يستهلك + السلسلة الموجودة على اليسار ويعيد استخدام مخزنها المؤقت (لتحقيق أداء عالٍ)، بينما لا يستهلك format! المتغير بل ينشئ سلسلة جديدة تمامًا. استخدم علامة + لربط مقطعين أو ثلاثة مقاطع، واستخدم format! لربط أكثر من ثلاثة مقاطع.
س هل يمكن تعديل السلاسل النصية؟ أيهما يمكن تعديله، &str أم String؟
ج لا يمكن تعديل سوى String (بعد إعلان mut، يمكنك استخدام طرق مثل .push_str() و.push() و.insert()، إلخ). &str هي مرجع للقراءة فقط؛ ولا يمكن تعديل البيانات التي تشير إليها. إذا كنت بحاجة إلى تعديل سلسلة داخل دالة، فيجب أن يكون نوع المعلمة هو &mut String.

📖 ملخص


📝 تمارين

  1. الصعوبة ⭐: أنشئ String، ثم استدعِ العوامل .push_str() و.push() و+ وformat! بالترتيب، وقارن نتائج الطرق الأربع.
  2. الصعوبة ⭐⭐: اكتب دالة fn first_word(s: &str) -> &str تُرجع الكلمة الأولى في سلسلة (مفصولة بمسافات). استدعها باستخدام &str وString، على التوالي.
  3. الصعوبة ⭐⭐⭐: استكشف مشكلات حدود UTF-8 — أنشئ سلسلة تحتوي على الرمز التعبيري 🦀 "Rust🦀"، وحاول تقسيمها باستخدام &s[0..5] و&s[0..6]، ولاحظ أيهما ينجح وأيهما يتسبب في تعطل البرنامج.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%