DanLevy.net

اختبار: إدارة الذاكرة الأساسية في Rust

تحقق من نفسك قبل أن تُفسد نفسك! 🦀

مستعد لاختبار مهاراتك في إدارة الذاكرة بـ Rust؟ 🦀

هذا الاختبار سيختبر فهمك لنظام الملكية في Rust، قواعد الاستعارة، أوقات الحياة، والمؤشرات الذكية.

ملاحظة: تم تنسيق الأسئلة بعرض ~50 عمودًا لضمان قابلية القراءة على جميع الأجهزة. (نرحب باقتراحات التحسين!)

سواء كنت مبرمج Rust مخضرم أو بدأت للتو في إدارة الذاكرة، سيساعدك هذا الاختبار على تعزيز معرفتك. هيا نبدأ! 🦀

ماذا يحدث عندما تشغّل هذا الكود؟ حاول توقع النتيجة أو الخطأ:

fn main() {
let philosopher =
String::from("Zeno of Citium");
let greeting = philosopher;
println!("Hello, {}!", philosopher);
}

يفشل هذا الكود في التجميع بسبب قواعد الملكية في Rust. عندما نُعيّن philosopher إلى greeting، تنتقل ملكية الـ String إلى greeting. بعد هذا النقل، لا يعود philosopher صالحًا للاستخدام.

إليك ثلاث طرق لإصلاح ذلك:

  1. استنساخ السلسلة (ينشئ نسخة جديدة):
let greeting = philosopher.clone();
  1. استخدم مرجعًا (يستعير القيمة):
let greeting = &philosopher;
  1. استخدم شريحة سلسلة (تستعير جزءًا من السلسلة):
let greeting = &philosopher[..];

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

ماذا يحدث عندما تشغّل هذا الكود؟ فكر في نقل الملكية:

fn take_knowledge(knowledge: String) {
println!("Knowledge: {}", knowledge);
}
fn main() {
let wisdom = String::from("know thyself");
take_knowledge(wisdom);
// What happens to our wisdom?
println!("Do you {}", wisdom);
}

يفشل الكود في التجميع لأن ملكية wisdom انتقلت إلى take_knowledge وبالتالي لا يمكن استخدامها لاحقًا.

إليك ثلاث طرق لإصلاح هذه المشكلة:

  1. المرور بالمرجع (استعارة القيمة):
fn borrow_it(text: &String) {
println!("Inside: {}", text);
}
borrow_it(&wisdom); // Now wisdom can be used after
  1. استنساخ القيمة (إنشاء نسخة جديدة):
take_knowledge(wisdom.clone()); // Original wisdom remains valid
  1. إرجاع الملكية من الدالة:
fn take_and_return(text: String) -> String {
println!("Inside: {}", text);
text // Return ownership back
}
let wisdom = take_and_return(wisdom); // Reassign returned ownership

كل نهج له حالات استخدام مختلفة:

  • المراجع: الأكثر كفاءة، لكن تحتاج إلى إدارة العمر
  • الاستنساخ: بسيط لكنه قد يكون مكلفًا
  • إرجاع الملكية: مفيد لتحويل القيم

الممارسة المثلى: استخدم المراجع ما لم تكن بحاجة إلى نقل الملكية.

ماذا يحدث عند وجود مراجع قابلة للتغيير متعددة؟

fn main() {
let mut wisdom = String::from("He who laughs at");
let ref1 = &mut wisdom; // First mutable borrow
let ref2 = &mut wisdom; // Second mutable borrow
ref1.push_str(" himself never runs");
ref2.push_str(" out of things to laugh at.");
}

فكّر في قواعد Rust للمراجع القابلة للتغيير.

هذا الكود ينتهك قواعد الاستعارة الأساسية في Rust:

  • مرجع قابل للتغيير واحد فقط للقيمة في كل مرة
  • أو أي عدد من المراجع غير القابلة للتغيير
  • لا يمكن للمراجع أن تعيش بعد كائنها

إليك كيفية إصلاح الكود:

  1. استخدم نطاقًا تسلسليًا:
let mut wisdom = String::from("He who laughs at");
{
let ref1 = &mut wisdom;
ref1.push_str(" himself never runs");
} // ref1 goes out of scope
let ref2 = &mut wisdom; // Now this is valid
ref2.push_str(" out of things to laugh at.");
  1. أو عدّل السلسلة في استعارة واحدة:
let mut wisdom = String::from("He who laughs at");
let ref1 = &mut wisdom;
ref1.push_str(" himself never runs out of things to laugh at.");

هذه القواعد تمنع سباقات البيانات أثناء التجميع، مما يجعل Rust آمنًا من الخيوط بشكل افتراضي.

خطأ شائع: محاولة استخدام مراجع قابلة للتغيير متعددة لتجنب النسخ أو لتعديل أجزاء مختلفة من نفس القيمة في آن واحد.

هل سيُجمع هذا الكود؟ إذا نعم، لماذا؟ إذا لا، ما الخطأ؟

fn first_word(s: &str) -> &str { // No explicit lifetimes?
match s.find(' ') {
Some(pos) => &s[0..pos],
None => s,
}
}
fn main() {
let name = String::from("Seneca the Younger");
let first = first_word(&name);
println!("Hello, {}", first);
}

هذا الكود ينجح في التجميع بفضل قواعد إسقاط الأعمار في Rust. تسمح هذه القواعد للمترجم باستنتاج الأعمار تلقائيًا في الأنماط الشائعة.

القواعد الثلاثة لإسقاط الأعمار هي:

  1. كل معلمة تحصل على معلمة عمر خاصة بها
  2. إذا كان هناك معلمة عمر واحدة فقط كمدخل، تُعطى هذه العمر لجميع معلمات العمر في المخرجات
  3. إذا كان هناك عدة معلمات عمر كمدخل، ولكن إحداها هي &self أو &mut self، يُعطى عمر self لجميع معلمات العمر في المخرجات

هذه الدالة تعادل:

fn first_word<'a>(s: &'a str) -> &'a str {
// ... same implementation
}

أنماط شائعة حيث يعمل الإسقاط:

// These don't need explicit lifetimes
fn get_str(s: &str) -> &str { s }
fn get_first(s: &str) -> &str { &s[0..1] }
// These would need explicit lifetimes
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

أفضل ممارسة: دع الإسقاط يعمل لك عندما يكون ممكنًا، لكن افهم متى تكون الأعمار الصريحة ضرورية.

ما الخطأ في تعريف هذا النوع المتكرر؟

#[derive(Debug)]
enum CatList {
Cons(i32, CatList), // Recursive without indirection
Nil,
}
fn main() {
let catlist = CatList::Cons(1,
CatList::Cons(2,
CatList::Cons(3,
CatList::Nil)));
}

يفشل هذا الكود لأن المترجم لا يستطيع تحديد حجم CatList أثناء التجميع. الطبيعة المتكررة للنوع تعني أنه قد يكون لا نهائيًا!

إليك طريقة إصلاحه باستخدام Box<T>:

#[derive(Debug)]
enum CatList {
Cons(i32, Box<CatList>), // Box provides a fixed-size pointer
Nil,
}
fn main() {
let catlist = CatList::Cons(1,
Box::new(CatList::Cons(2,
Box::new(CatList::Cons(3,
Box::new(CatList::Nil))))));
}

لماذا Box<T> يعمل:

  1. يوفر Box مؤشرًا ثابت الحجم (عادة 8 بايت على الأنظمة 64‑بت)
  2. تُخزن البيانات الفعلية على الكومة
  3. الآن يعرف المترجم بالضبط مقدار المساحة التي يجب تخصيصها

حالات الاستخدام الشائعة لـ Box<T>:

  • هياكل بيانات متكررة (قوائم مرتبطة، أشجار)
  • بيانات كبيرة تريد التأكد من تخصيصها على الكومة
  • كائنات Traits عندما تحتاج إلى استدعاء ديناميكي

أفضل ممارسة: استخدم Box<T> عندما تحتاج إلى:

  • أنواع متكررة
  • لضمان تخصيص على الكومة
  • لنقل بيانات كبيرة دون نسخ

ماذا سيطبع هذا الكود؟ احسب بعناية!

use std::rc::Rc;
fn main() {
let text = Rc::new(String::from("Meditations")); // Count: 1
let marcus = Rc::clone(&text); // What happens here?
let aurelius = Rc::clone(&text); // And here?
println!(
"Reference count: {}",
Rc::strong_count(&text)
);
}

لنشرح كيف يعمل Rc:

  1. الإنشاء الأولي باستخدام Rc::new(): العدد = 1
  2. النسخة الأولى لـ marcus: العدد = 2
  3. النسخة الثانية لـ aurelius: العدد = 3

خصائص Rc المهمة:

use std::rc::Rc;
fn demonstrate_rc() {
let original = Rc::new(String::from("Shared"));
println!("Count after creation: {}", Rc::strong_count(&original)); // 1
{
let copy = Rc::clone(&original);
println!("Count inside scope: {}", Rc::strong_count(&original)); // 2
} // copy is dropped here
println!("Count after scope: {}", Rc::strong_count(&original)); // 1
}

نقاط رئيسية:

  • Rc::clone() رخيص - فهو فقط يزيد العداد
  • Rc مخصص للسيناريوهات أحادية الخيط فقط
  • عندما يُسقط آخر مرجع، يتم تنظيف البيانات
  • استخدم المراجع الضعيفة (Weak) لمنع دورات المراجع

أفضل الممارسات:

  • استخدم Rc عندما تحتاج إلى ملكية مشتركة
  • فكر في Arc للسيناريوهات الآمنة عبر الخيوط
  • تجنب إنشاء دورات مراجع

هل سينجح تجميع تعريف هذا الهيكل؟ لماذا أو لماذا لا؟

struct Philosopher {
name: &str, // Reference without lifetime
quote: &str, // Another reference without lifetime
}
fn main() {
let phil = Philosopher {
name: "Seneca",
quote: "Luck happens when preparation meets opportunity",
};
}

يفشل الكود لأن الهياكل التي تحتوي على مراجع يجب أن تحدد الأعمار. إليك طريقة الإصلاح:

// Single lifetime parameter
struct Philosopher<'a> {
name: &'a str,
quote: &'a str,
}
// Or different lifetimes if needed
struct PhilosopherFlex<'n, 'q> {
name: &'n str,
quote: &'q str,
}

أنماط شائعة:

// Own the data instead
struct PhilosopherOwned {
name: String,
quote: String,
}
// Mixed ownership
struct PhilosopherMixed<'a> {
name: String, // Owned
quote: &'a str, // Borrowed
}

أفضل الممارسات:

  1. استخدم الأنواع المملوكة (String) عندما تحتاج إلى تخزين البيانات إلى أجل غير مسمى
  2. استخدم المراجع عندما يكون عمر الهيكل أقصر بوضوح من عمر البيانات
  3. فكر في عدة معاملات عمر عندما يمكن للمراجع أن تكون لها أعمار مختلفة
  4. وثّق علاقات الأعمار في الهياكل المعقدة

ماذا يحدث مع هذه الدالة التي تُعيد أطول شريحة نصية من شريحتين؟

fn longest(text1: &str, text2: &str) -> &str {
if text1.len() > text2.len() {
text1 // Returning a reference, but which lifetime?
} else {
text2 // Could be this reference instead
}
}
fn main() {
println!("{}", longest(
"Seneca the Younger",
"Marcus Aurelius"
));
}

يجب أن يبقى المرجع المعاد ضمن عمر صالح لكلا المدخلين، وألا يتجاوز عمر أي من القيمتين المستعارتين. تصف تعليقات العمر هذه العلاقة ولا تمدد الأعمار.

fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}

ماذا يحدث عندما يُنفّذ هذا الكود؟

use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let _borrow1 = data.borrow_mut(); // First mutable borrow
let _borrow2 = data.borrow_mut(); // Second mutable borrow
println!("Value: {}", _borrow2);
}

يفشل السؤال بـ panic عند استدعاء borrow_mut() الثاني لأن الاستعارة القابلة للتغيير الأولى لا تزال حية. يفرض RefCell هذه القاعدة أثناء التشغيل. في المثال المصحح أدناه، يحرر drop(second) الاستعارة القابلة للتغيير قبل إنشاء الاستعارتين المشتركتين.

use std::cell::RefCell;
fn main() {
let data = RefCell::new(42);
let mut second = data.borrow_mut();
*second += 1;
drop(second);
let read1 = data.borrow();
let read2 = data.borrow();
println!("{} {}", *read1, *read2);
}

ماذا سيطبع هذا الكود؟

use std::cell::Cell;
fn main() {
let life = Cell::new(42);
let meaning = &life; // Shared reference
println!("{}", life.get()); // What prints here?
meaning.set(43); // Mutation through shared ref
println!("{}", life.get()); // And here?
}

Cell و RefCell يخدمان أغراضًا مختلفة لتغيير داخلي:

use std::cell::{Cell, RefCell};
// Cell for Copy types
struct Counter {
count: Cell<i32>,
}
impl Counter {
fn increment(&self) {
self.count.set(self.count.get() + 1);
}
}
// RefCell for non-Copy types
struct Logger {
messages: RefCell<Vec<String>>,
}
impl Logger {
fn log(&self, msg: &str) {
self.messages.borrow_mut().push(msg.to_string());
}
}

الاختلافات الرئيسية:

  1. Cell:
  • يعمل بشكل أفضل مع الأنواع القابلة للنسخ
  • لا يوجد API للاستعارة
  • دائمًا ينسخ أو ينقل القيم
  1. RefCell:
  • يعمل مع أي نوع
  • يحتوي على API للاستعارة
  • فحص الاستعارة في وقت التشغيل

أفضل الممارسات:

  1. استخدم Cell للأنواع القابلة للنسخ البسيطة (أرقام، منطقية، إلخ)
  2. استخدم RefCell عندما تحتاج إلى استعارة المحتويات
  3. حافظ على الحد الأدنى من التغييرات عبر Cell/RefCell
  4. وثّق لماذا الحاجة إلى تغيير داخلي

متى يجب عليك استخدام Rc (العدّ المرجعي) في Rust؟

انظر إلى هذا المثال:

use std::rc::Rc;
struct SharedConfig {
name: String,
value: i32,
}
fn main() {
let config = Rc::new(SharedConfig {
name: "settings".to_string(),
value: 42,
});
let config2 = Rc::clone(&config);
// Both config and config2 share ownership
}

تم تصميم Rc (العدّ المرجعي) لسيناريوهات ذات خيط واحد حيث تحتاج إلى ملكية مشتركة.

حالات الاستخدام الشائعة:

use std::rc::Rc;
use std::cell::RefCell;
// Shared ownership in data structures
struct Node {
next: Option<Rc<Node>>,
value: i32,
}
// Combining with interior mutability
struct SharedState {
data: Rc<RefCell<Vec<String>>>,
}
// Multiple owners of same data
let original = Rc::new(vec![1, 2, 3]);
let clone1 = Rc::clone(&original);
let clone2 = Rc::clone(&original);

النقاط الرئيسية:

  1. استخدم Rc عندما:
  • تحتاج أجزاء متعددة من الشيفرة إلى ملكية
  • تعرف أن المشاركة ذات خيط واحد
  • لا يمكن تحديد عمر القيمة ثابتًا
  1. استخدم Arc بدلاً من ذلك عندما:
  • تحتاج إلى مشاركة آمنة عبر الخيوط
  • تحتاج خيوط متعددة إلى ملكية
  1. قيود Rc:
  • غير آمن عبر الخيوط
  • بعض الحمل الزمني أثناء التشغيل
  • لا يمكن كسر دورات المرجعية تلقائيًا

أفضل الممارسات:

  1. فضل الملكية الفريدة عندما يكون ذلك ممكنًا
  2. استخدم Rc للملكية المشتركة في خيط واحد
  3. استخدم Arc للسيناريوهات متعددة الخيوط
  4. دمج مع Weak لمنع دورات المرجعية

ما هو الفرق الرئيسي بين RefCell و RwLock في Rust؟

انظر إلى هذه الأمثلة:

use std::cell::RefCell;
use std::sync::RwLock;
// Example 1
let data = RefCell::new(vec![1, 2, 3]);
let borrowed = data.borrow_mut();
// Example 2
let shared = RwLock::new(vec![1, 2, 3]);
let locked = shared.write().unwrap();

يفحص RefCell<T> الاستعارات أثناء التشغيل ولا يحقق Sync، فلا تشارك مرجعًا إليه بين خيوط التنفيذ. يمكن نقل القيمة إلى خيط آخر عندما يكون T: Send. يوفر RwLock<T> وصولًا متزامنًا وفق قيود النوع.

ماذا يحدث عندما يُنفّذ هذا الكود؟

use std::sync::{Arc, Mutex};
fn main() {
let lock = Arc::new(Mutex::new(42));
let lock2 = Arc::clone(&lock);
let _guard1 = lock.lock().unwrap(); // First lock
let _guard2 = lock2.lock().unwrap(); // Second lock attempt
println!("Value: {}", _guard2);
}

قفل mutex نفسه مرتين في خيط واحد دون تحرير الحارس الأول لا يعود بشكل طبيعي. تسمح الواجهة بجمود متبادل أو panic. حرر الحارس قبل القفل مرة أخرى.

use std::sync::Mutex;
fn main() {
let lock = Mutex::new(42);
{ let mut guard = lock.lock().unwrap(); *guard += 1; }
let guard = lock.lock().unwrap();
println!("{}", *guard);
}

ماذا يحدث عندما تشغّل هذا الكود باستخدام المراجع الضعيفة؟

use std::rc::{Rc, Weak};
fn main() {
let data = Rc::new(String::from("Wisdom"));
let weak = Rc::downgrade(&data); // Create weak reference
drop(data); // Drop strong reference
println!("Value: {:?}", weak.upgrade());
}

المراجع الضعيفة لا تمنع تحرير أهدافها. إليك مثالًا مفصلاً:

use std::rc::{Rc, Weak};
use std::cell::RefCell;
// Parent-child tree structure avoiding reference cycles
struct Node {
next: Option<Rc<Node>>,
parent: RefCell<Weak<Node>>, // Weak to prevent cycles
value: i32,
}
impl Node {
fn new(value: i32) -> Rc<Node> {
Rc::new(Node {
next: None,
parent: RefCell::new(Weak::new()),
value,
})
}
fn set_parent(&self, parent: &Rc<Node>) {
*self.parent.borrow_mut() = Rc::downgrade(parent);
}
fn get_parent(&self) -> Option<Rc<Node>> {
self.parent.borrow().upgrade()
}
}

حالات الاستخدام الشائعة:

  1. هياكل شبيهة بالذاكرة المؤقتة حيث يمكن مسح الإدخالات
  2. هياكل شجرية مع مراجع للآباء
  3. أنماط المراقب حيث يمكن إسقاط المواضيع
  4. كسر دورات الإشارة في هياكل بيانات معقدة

أفضل الممارسات:

  1. استخدم المراجع الضعيفة للعلاقات الاختيارية
  2. تحقق من نتائج upgrade() قبل الاستخدام
  3. وثّق علاقات الملكية بوضوح
  4. فكر في بدائل مثل الفهارس للحالات الأبسط

ماذا يحدث لمقبض الملف في مثال RAII هذا؟

use std::fs::File;
struct FileWrapper {
file: File,
}
fn main() {
let file = File::create("test.txt").unwrap();
let wrapper = FileWrapper { file };
// ... use wrapper ...
// No Drop implementation
}

يضمن RAII في Rust إدارة الموارد بشكل صحيح. في هذا المثال، لا يحتاج FileWrapper إلى تنفيذ Drop مخصص لإغلاق مقبض الملف: حقل File يُحرّر تلقائيًا عندما يخرج المغلف من النطاق.

تقوم بتنفيذ Drop فقط عندما يكون للمغلف سلوك تنظيف إضافي يتجاوز تحرير حقوله:

use std::fs::File;
use std::io::{self, Write};
struct FileWrapper {
file: File,
path: String,
}
impl FileWrapper {
fn new(path: &str) -> io::Result<FileWrapper> {
Ok(FileWrapper {
file: File::create(path)?,
path: path.to_string(),
})
}
fn write(&mut self, content: &str) -> io::Result<()> {
self.file.write_all(content.as_bytes())
}
}
impl Drop for FileWrapper {
fn drop(&mut self) {
// Ensure file is properly closed
// Could also do cleanup like deletion
println!("Closing file: {}", self.path);
}
}

أنماط RAII:

  1. المُنشئ يحصل على الموارد
  2. الطرق تستخدم الموارد بأمان
  3. الحقول تُحرّر تلقائيًا عندما يخرج المالك من النطاق
  4. Drop مخصص يضيف تنظيفًا إضافيًا عند الحاجة
  5. استخدم ? لنشر الأخطاء

أفضل الممارسات:

  1. اعتمد على تنفيذات Drop في المكتبة القياسية عندما تكون تمثل المورد بالفعل
  2. اجعل إدارة الموارد بسيطة وواضحة
  3. استخدم أنواع المكتبة القياسية عندما يكون ذلك ممكنًا
  4. وثّق سلوك التنظيف
  5. فكر في استخدام أنماط الحراسة للعمليات ذات النطاق المحدد

ماذا يحدث عندما نستنسخ بنية Philosophy هذه؟

#[derive(Clone)]
struct Philosophy {
school: String,
founder: String,
}
fn main() {
let stoicism = Philosophy {
school: String::from("Stoicism"),
founder: String::from("Zeno of Citium")
};
let new_school = stoicism.clone();
println!("{} - {}",
stoicism.school, new_school.school);
}

ينفذ Copy نسخًا ضمنيًا بتًا ببت. أما Clone فيُستدعى صراحة وقد يحجز ذاكرة؛ هنا يحصل حقلا String على نسختين مستقلتين. وجود القيمة في المكدس أو الكومة لا يحدد تطبيق النوع لـ Copy.

على هدف Rust 64-بت شائع حاليًا، ما حجم هذا الهيكل؟

struct Metadata {
id: u32, // How many bytes?
name: String, // How many bytes?
active: bool // How many bytes + padding?
}

لنُحلل تخطيط الذاكرة للهيكل وتحسينه:

// Typical current 64-bit Rust layout: 32 bytes
struct Metadata {
id: u32, // 4 bytes
name: String, // 24 bytes on 64-bit systems
active: bool // 1 byte + padding/alignment
}
// Reordering fields may reduce padding for repr(C) structs,
// but default Rust layout is not a stable ABI guarantee.
struct OptimizedMetadata {
name: String, // 24 bytes
id: u32, // 4 bytes
active: bool // 1 byte + 3 padding
}
// Further optimization with packing
#[repr(packed)]
struct PackedMetadata {
id: u32,
active: bool,
name: String,
}

اعتبارات تخطيط الذاكرة:

  1. متطلبات المحاذاة:
  • u32: محاذاة 4 بايت
  • String: محاذاة 8 بايت وحجم 24 بايت على الأنظمة 64-بت الشائعة
  • bool: محاذاة بايت واحد
  1. استراتيجيات ترتيب الحقول:
  • تجميع الحقول ذات الأحجام المتشابهة
  • وضع المحاذاة الأكبر أولاً
  • النظر في تحسين خط الذاكرة المؤقتة (cache line)

أفضل الممارسات:

  1. للـ FFI أو افتراضات تخطيط ثابت، استخدم repr(...) المناسب
  2. استخدم أحجام أعداد صحيحة مناسبة
  3. فكر في استخدام Option للحقول الاختيارية
  4. قس حجم الهياكل الحساسة للذاكرة باستخدام std::mem::size_of
  5. استخدم #[repr(packed)] بحذر - قد يؤثر على الأداء

عند تفعيل التحسينات، ما الذي تتيحه تجريدات المكررات عديمة التكلفة في هذين التنفيذين؟

// Implementation A: Iterator
fn sum_iterator(v: &[i32]) -> i32 {
v.iter().fold(0, |acc, &x| acc + x)
}
// Implementation B: Raw loop
fn sum_loop(v: &[i32]) -> i32 {
let mut sum = 0;
for i in 0..v.len() {
sum += v[i];
}
sum
}

تتيح التجريدات عديمة التكلفة كودًا محسنًا فعالًا، لكنها لا تضمن زمنًا مطابقًا لكل مترجم أو مستوى تحسين. تحسب الدالتان المجموع نفسه. قس حمل العمل الفعلي عندما يكون الأداء مهمًا.

use std::ops::Range;
trait ZeroCost {
fn process(&self) -> u32;
}
impl ZeroCost for Range<u32> {
fn process(&self) -> u32 {
self.clone().fold(0, |acc, x| acc + x)
}
}

شكراً على مشاركتك في الاختبار! إذا استمتعت باختبار معرفتك بـ Rust، تفقد تحدياتي البرمجية الأخرى programming challenges! 🧠

هل تريد رفع مستوى مهاراتك في Rust؟ إليك بعض الموارد الموصى بها: