Rust: دليل شامل لسلاسل النصوص في لغة Rust
آخر تحديث: 2026-08-26
لا يوجد في لغة Rust نوع واحد من سلاسل الأحرف، بل نوعان — لأن برمجة الأنظمة تتطلب تحكماً دقيقاً في الذاكرة.
تعد معالجة السلاسل في لغة Rust مصدرًا للارتباك بالنسبة للعديد من المبتدئين: لماذا يوجد نوعان — String و&str؟ وكيف يختلفان عن char* في لغة C؟ سيساعدك هذا الدرس على فهم الموضوع بشكل كامل.
1. ما ستتعلمه
- الفرق بين القيم الثابتة للسلاسل
&strوالسلاسل المخصصة في الـ«هيوب»String - الإنشاء والإضافة والتسلسل والتنسيق
String - تقطيع السلاسل والوصول المفهرس
- طرق السلسلة الشائعة (len، contains، replace، trim)
- تأثير ترميز UTF-8 على عمليات معالجة السلاسل
2. قصة أمينة مكتبة
(1) المشكلة: بيانات JSON طويلة جدًّا بحيث لا تتسع لها المساحة المتاحة
ماريا هي أمينة مكتبة تعمل على تطوير نظام صغير للبحث عن الكتب لإدارة 5,000 كتاب.
- يختلف طول عنوان كل كتاب؛ وأطولها يبلغ 200 حرفًا
- في البداية، كانت تستخدم سلاسل ذات طول ثابت لتخزين عناوين الكتب (مثل
char[50]في لغة البرمجة C). - كما اتضح، فإن عنوان «مائة عام من العزلة» يظهر بالكامل، أما عنوان «كيفية رسم خريطة كاملة لكون لانهائي» فقد تم اقتطاع جزء منه.
- بعد التبديل إلى
char*، واجهتُ مشكلة أخرى تتعلق بـتحرير الذاكرة — فقد نسيتُ استدعاءfreeفي موضع واحد، مما أدى إلى تعطل النظام.
«سيكون من الرائع جدًّا لو كانت هناك لغات برمجة تدير الذاكرة تلقائيًّا...» وقد وجدت الإجابة لاحقًا في لغة «Rust».
(2) حل «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) نموذج الذاكرة
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) طريقة الإنشاء
// &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) ▶ المثال:إنشاء السلاسل وتعديلها (مستوى الصعوبة ⭐)
// ============================================
// 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);
}
الناتج:
After the addition: Hello, Rust!
After concatenation: Hello, World!
format Result: tic-tac-toe
يستهلك (ينقل) المشغل
+المتغيرStringالموجود على يساره، لذا لا يمكن استخدامs1بعد عملية التسلسل. أماformat!فلا يستهلك أي متغيرات؛ حيث يتم الاحتفاظ بملكية جميع المتغيرات.
(2) ▶ المثال:تقطيع السلاسل وفهرستها (مستوى الصعوبة ⭐⭐)
// ============================================
// 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!();
}
الناتج:
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) ▶ المثال:الطرق الشائعة للسلاسل (مستوى الصعوبة ⭐⭐)
// ============================================
// 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);
}
الناتج:
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 والتحقق من الحدود (مستوى الصعوبة ⭐⭐⭐)
// ============================================
// 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);
}
الناتج:
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) ▶ المثال:تحليل السلاسل وتنسيقها عمليًّا (مستوى الصعوبة: ⭐⭐)
// ============================================
// 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);
}
الناتج:
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()مناسب لربط المصفوفات باستخدام الفواصل.
❓ أسئلة شائعة
&str هي سلسلة مقتبضة للقراءة فقط (بدون أي عبء إضافي)، بينما String هي سلسلة قابلة للتعديل مملوكة (مع عبء تخصيص الذاكرة المؤقتة). ويوفر استخدام &str لمعلمات الدوال أقصى قدر من المرونة، حيث يمكن تحويل String تلقائيًا إلى &str..to_string() أو String::from()؛ ولتحويل String إلى &str، استخدم &s أو s.as_str(). جميع عمليات التحويل لا تكلف شيئًا (فهي تمثل ببساطة الذاكرة من منظور مختلف).s[0] يصل إلى البايت الأول، الذي لا يمثل بالضرورة حرفًا كاملاً. وقد صُممت لغة Rust لتكون أكثر أمانًا: فهي تفضل إصدار خطأ في وقت الترجمة على السماح بحدوث سلوك غير متوقع في وقت التشغيل.+ السلسلة الموجودة على اليسار ويعيد استخدام مخزنها المؤقت (لتحقيق أداء عالٍ)، بينما لا يستهلك format! المتغير بل ينشئ سلسلة جديدة تمامًا. استخدم علامة + لربط مقطعين أو ثلاثة مقاطع، واستخدم format! لربط أكثر من ثلاثة مقاطع.String (بعد إعلان mut، يمكنك استخدام طرق مثل .push_str() و.push() و.insert()، إلخ). &str هي مرجع للقراءة فقط؛ ولا يمكن تعديل البيانات التي تشير إليها. إذا كنت بحاجة إلى تعديل سلسلة داخل دالة، فيجب أن يكون نوع المعلمة هو &mut String.📖 ملخص
- تحتوي لغة Rust على نوعين من السلاسل:
&str(استعارة للقراءة فقط) وString(ملكية) Stringهي سلسلة UTF-8 مخصصة من الذاكرة المؤقتة وقابلة لتغيير الحجم، ويتم إلغاء تخصيصها تلقائيًا عند خروجها من نطاقها&strعبارة عن شريحة سلسلة تشير إلى تسلسل متجاور من بايتات UTF-8 صالحة- نظرًا لترميز UTF-8، لا تدعم السلاسل الوصول المباشر عن طريق الفهرس؛ لذا يجب عليك استخدام
.chars()أو شرائح البايت. - الطرق الشائعة:
.len()،.trim()،.contains()،.replace()،.split() - عندما تقبل معلمات الدالة سلاسل نصية، استخدم
&strكلما أمكن ذلك لضمان أقصى قدر من المرونة
📝 تمارين
- الصعوبة ⭐: أنشئ
String، ثم استدعِ العوامل.push_str()و.push()و+وformat!بالترتيب، وقارن نتائج الطرق الأربع. - الصعوبة ⭐⭐: اكتب دالة
fn first_word(s: &str) -> &strتُرجع الكلمة الأولى في سلسلة (مفصولة بمسافات). استدعها باستخدام&strوString، على التوالي. - الصعوبة ⭐⭐⭐: استكشف مشكلات حدود UTF-8 — أنشئ سلسلة تحتوي على الرمز التعبيري 🦀
"Rust🦀"، وحاول تقسيمها باستخدام&s[0..5]و&s[0..6]، ولاحظ أيهما ينجح وأيهما يتسبب في تعطل البرنامج.