اذهب إلى المحتوى

البحث في الموقع

المحتوى عن 'البرمجة بلغة رست'.

  • ابحث بالكلمات المفتاحية

    أضف وسومًا وافصل بينها بفواصل ","
  • ابحث باسم الكاتب

نوع المحتوى


التصنيفات

  • الإدارة والقيادة
  • التخطيط وسير العمل
  • التمويل
  • فريق العمل
  • دراسة حالات
  • التعامل مع العملاء
  • التعهيد الخارجي
  • السلوك التنظيمي في المؤسسات
  • عالم الأعمال
  • التجارة والتجارة الإلكترونية
  • نصائح وإرشادات
  • مقالات ريادة أعمال عامة

التصنيفات

  • مقالات برمجة عامة
  • مقالات برمجة متقدمة
  • PHP
    • Laravel
    • ووردبريس
  • جافاسكربت
    • لغة TypeScript
    • Node.js
    • React
    • Vue.js
    • Angular
    • jQuery
    • Cordova
  • HTML
  • CSS
    • Sass
    • إطار عمل Bootstrap
  • SQL
  • لغة C#‎
    • ‎.NET
    • منصة Xamarin
  • لغة C++‎
  • لغة C
  • بايثون
    • Flask
    • Django
  • لغة روبي
    • إطار العمل Ruby on Rails
  • لغة Go
  • لغة جافا
  • لغة Kotlin
  • لغة Rust
  • برمجة أندرويد
  • لغة R
  • الذكاء الاصطناعي
  • صناعة الألعاب
  • سير العمل
    • Git
  • الأنظمة والأنظمة المدمجة

التصنيفات

  • تصميم تجربة المستخدم UX
  • تصميم واجهة المستخدم UI
  • الرسوميات
    • إنكسكيب
    • أدوبي إليستريتور
  • التصميم الجرافيكي
    • أدوبي فوتوشوب
    • أدوبي إن ديزاين
    • جيمب GIMP
    • كريتا Krita
  • التصميم ثلاثي الأبعاد
    • 3Ds Max
    • Blender
  • نصائح وإرشادات
  • مقالات تصميم عامة

التصنيفات

  • مقالات DevOps عامة
  • خوادم
    • الويب HTTP
    • البريد الإلكتروني
    • قواعد البيانات
    • DNS
    • Samba
  • الحوسبة السحابية
    • Docker
  • إدارة الإعدادات والنشر
    • Chef
    • Puppet
    • Ansible
  • لينكس
    • ريدهات (Red Hat)
  • خواديم ويندوز
  • FreeBSD
  • حماية
    • الجدران النارية
    • VPN
    • SSH
  • شبكات
    • سيسكو (Cisco)

التصنيفات

  • التسويق بالأداء
    • أدوات تحليل الزوار
  • تهيئة محركات البحث SEO
  • الشبكات الاجتماعية
  • التسويق بالبريد الالكتروني
  • التسويق الضمني
  • استسراع النمو
  • المبيعات
  • تجارب ونصائح
  • مبادئ علم التسويق

التصنيفات

  • مقالات عمل حر عامة
  • إدارة مالية
  • الإنتاجية
  • تجارب
  • مشاريع جانبية
  • التعامل مع العملاء
  • الحفاظ على الصحة
  • التسويق الذاتي
  • العمل الحر المهني
    • العمل بالترجمة
    • العمل كمساعد افتراضي
    • العمل بكتابة المحتوى

التصنيفات

  • الإنتاجية وسير العمل
    • مايكروسوفت أوفيس
    • ليبر أوفيس
    • جوجل درايف
    • شيربوينت
    • Evernote
    • Trello
  • تطبيقات الويب
    • ووردبريس
    • ماجنتو
    • بريستاشوب
    • أوبن كارت
    • دروبال
  • الترجمة بمساعدة الحاسوب
    • omegaT
    • memoQ
    • Trados
    • Memsource
  • برامج تخطيط موارد المؤسسات ERP
    • تطبيقات أودو odoo
  • أنظمة تشغيل الحواسيب والهواتف
    • ويندوز
    • لينكس
  • مقالات عامة

التصنيفات

  • آخر التحديثات

أسئلة وأجوبة

  • الأقسام
    • أسئلة البرمجة
    • أسئلة ريادة الأعمال
    • أسئلة العمل الحر
    • أسئلة التسويق والمبيعات
    • أسئلة التصميم
    • أسئلة DevOps
    • أسئلة البرامج والتطبيقات

التصنيفات

  • كتب ريادة الأعمال
  • كتب العمل الحر
  • كتب تسويق ومبيعات
  • كتب برمجة
  • كتب تصميم
  • كتب DevOps

ابحث في

ابحث عن


تاريخ الإنشاء

  • بداية

    نهاية


آخر تحديث

  • بداية

    نهاية


رشح النتائج حسب

تاريخ الانضمام

  • بداية

    نهاية


المجموعة


النبذة الشخصية

  1. بدأنا في مقال الجزء الأول ببناء مشروع عملي بلغة رست وهو عبارة عن خادم ويب متعدد مهام المعالجة، إذ بنينا الخادم الأساسي وكان أحادي خيط المعالجة، وعملنا في مقال الجزء الثاني على تحويله إلى خادم متعدد خيوط المعالجة، وسننهي في هذا المقال بناء الخادم ليصبح جاهزًا، فإذا لم تكن قرأت المقالات السابقة، فارجع لها قبل قراءة هذا المقال. الإغلاق الرشيق وتحرير الذاكرة تستجيب الشيفرة 20 للطلبات بصورةٍ غير متزامنة عبر استخدام مجمع خيط كما نريد، إذ نحصل على بعض التحذيرات من حقول workers و id و thread التي لن نستخدمها مباشرةً وتذكرنا أننا لم نحرر أي شيء من الذاكرة. عندما نستخدم الحل البدائي الذي هو استخدام مفتاحي "ctrl-c" لإيقاف الخيط الرئيسي، تتوقف الخيوط مباشرةً حتى لو كانوا يخدّمون طلبًا. سننفّذ سمة Drop لاستدعاء join على كل خيط في المجمع لكي ننهي الطلبات التي تعمل قبل الإغلاق، ثم سننفّذ طريقةً لإخبار الخيوط ألا تقبل طلبات جديدة قبل الإغلاق. لرؤية عمل هذا الكود سنعدّل الخادم ليقبل طلبين فقط قبل أن يغلق مجمع الخيط thread pool. تنفيذ سمة Drop على مجمع خيط لنبدأ بتنفيذ Drop على مجمع الخيط الخاص بنا. عندما يُسقط المجمع يجب أن تجتمع كل الخيوط للتأكد من أن عملهم قد انتهى. تظهر الشيفرة 22 المحاولة الأولى لتطبيق Drop، إذ لن تعمل الشيفرة حاليًا. اسم الملف: src/lib.rs impl Drop for ThreadPool { fn drop(&mut self) { for worker in &mut self.workers { println!("Shutting down worker {}", worker.id); worker.thread.join().unwrap(); } } } [الشيفرة 22: ضم كل خيط عندما يخرج المجمع خارج النطاق] أولًا، نمرّ على كل workers في مجمع الخيط، واستُخدمت ‎&mut هنا لأن self هو مرجع متغيّر، ونريد أيضًا تغيير worker. نطبع لكل عامل رسالةً تقول أن هذا العامل سيُغلق، ثم نستدعي join على خيط العمال. إذا فشل استدعاء join نستخدم unwrap لجعل رست تهلع وتذهب إلى إغلاق غير رشيق. سنحصل على هذا الخطأ عند تصريف هذه الشيفرة: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference --> src/lib.rs:52:13 | 52 | worker.thread.join().unwrap(); | ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call | | | move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait | note: this function takes ownership of the receiver `self`, which moves `worker.thread` --> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/src/thread/mod.rs:1581:17 For more information about this error, try `rustc --explain E0507`. error: could not compile `hello` due to previous error يوضّح الخطأ أننا لا يمكن أن نستدعي join لأنه لدينا استعارة متغيرة على كل worker وتأخذ join ملكية وسطائها، ولمعالجة هذه المشكلة نحن بحاجة لنقل الخيط خارج نسخة Worker التي تملك thread حتى تستطيع join استهلاك الخيط، وقد فعلنا ذلك في الشيفرة 15 من المقال تنفيذ نمط تصميمي Design Pattern كائني التوجه Object-Oriented في لغة رست. إذا احتفظ Worker بـ Option<thread::JoinHandle<()>>‎، يمكننا استدعاء تابع take على Option لنقل القيمة خارج المتغاير Some وإبقاء المتغاير None في مكانه، بمعنى آخر سيحتوي Worker عامل على متغاير Some في Thread الخاص به وعندما نريد تحرير ذاكرة Worker نستبدل Some بالقيمة None حتى لا يوجد لدى Worker أي خيط لينفذه. لذا نحن نعرف أننا نريد تحديث تعريف Worker على النحو التالي. اسم الملف: src/lib.rs struct Worker { id: usize, thread: Option<thread::JoinHandle<()>>, } الآن لنتابع المصرّف لنجد أية أماكن أُخرى تحتاج تغيير، وبالتحقق من الشيفرة نجد خطأين: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0599]: no method named `join` found for enum `Option` in the current scope --> src/lib.rs:52:27 | 52 | worker.thread.join().unwrap(); | ^^^^ method not found in `Option<JoinHandle<()>>` | note: the method `join` exists on the type `JoinHandle<()>` --> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/src/thread/mod.rs:1581:5 help: consider using `Option::expect` to unwrap the `JoinHandle<()>` value, panicking if the value is an `Option::None` | 52 | worker.thread.expect("REASON").join().unwrap(); | +++++++++++++++++ error[E0308]: mismatched types --> src/lib.rs:72:22 | 72 | Worker { id, thread } | ^^^^^^ expected enum `Option`, found struct `JoinHandle` | = note: expected enum `Option<JoinHandle<()>>` found struct `JoinHandle<_>` help: try wrapping the expression in `Some` | 72 | Worker { id, thread: Some(thread) } | +++++++++++++ + Some errors have detailed explanations: E0308, E0599. For more information about an error, try `rustc --explain E0308`. error: could not compile `hello` due to 2 previous errors لنعالج الخطأ الثاني الذي يشير إلى الشيفرة في نهاية Worker::new، إذ نريد تغليف قيمة thread في Some عندما ننشئ Worker جديد. أجرِ الخطوات التالية لتصحيح هذا الخطأ: اسم الملف: src/lib.rs impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { // --snip-- Worker { id, thread: Some(thread), } } } الخطأ الأول هو في تنفيذ Drop، وذكرنا سابقًا أننا أردنا استدعاء take على قيمة Option لنقل thread خارج worker. أجرِ التغييرات التالية لتصحيح هذا الخطأ: اسم الملف: src/lib.rs impl Drop for ThreadPool { fn drop(&mut self) { for worker in &mut self.workers { println!("Shutting down worker {}", worker.id); if let Some(thread) = worker.thread.take() { thread.join().unwrap(); } } } } كما تحدثنا سابقًا في المقال البرمجة كائنية التوجه OOP في لغة رست، يأخذ التابع take على Option المتغاير Some خارجًا ويبقي None بدلًا عنه. استخدمنا if let لتفكيك Some والحصول على الخيط، ثم استدعينا join على الخيط. إذا كان خيط العامل هو أساسًا None نعرف أن العامل قد حرًر ذاكرته ولا يحصل شيء في هذه الحالة. الإشارة للخيط ليتوقف عن الاستماع إلى الوظائف تُصرّف الشيفرة بدون تحذيرات بعد كل التغييرات التي أجريناها، ولكن الخبر السيء أنها لا تعمل كما أردنا. النقطة المهمة هي في منطق المغلفات المنفذة بواسطة خيوط نسخ Worker، إذ نستدعي حتى اللحظة join لكن لا تُغلق الخيوط لأننها تعمل في loop للأبد بحثًا عن وظائف. إذا أسقطنا Threadpool بتنفيذنا الحالي للسمة drop، سيُمنع الخيط الأساسي للأبد بانتظار الخيط الأول حتى ينتهي، ولحل هذه المشكلة نحتاج لتغيير تنفيذ drop في ThreadPool، ثم إجراء تغيير في حلقة Worker. أولًا، سنغير تنفيذ drop في ThreadPool ليسقِط صراحةً sender قبل انتظار الخيوط لتنتهي. تظهر الشيفرة 23 التغييرات في ThreadPool لتسقط صراحةً sender. استخدمنا نفس تقنياتOption و take كما فعلنا مع الخيط لكي يستطيع نقل sender خارج ThreadPool. اسم الملف: src/lib.rs: pub struct ThreadPool { workers: Vec<Worker>, sender: Option<mpsc::Sender<Job>>, } // --snip-- impl ThreadPool { pub fn new(size: usize) -> ThreadPool { // --snip-- ThreadPool { workers, sender: Some(sender), } } pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); self.sender.as_ref().unwrap().send(job).unwrap(); } } impl Drop for ThreadPool { fn drop(&mut self) { drop(self.sender.take()); for worker in &mut self.workers { println!("Shutting down worker {}", worker.id); if let Some(thread) = worker.thread.take() { thread.join().unwrap(); } } } } [الشيفرة 23: إسقاط sender صراحةً قبل جمع الخيوط الفعالة] يغلق إسقاط sender القناة، وهذا يشير بدوره إلى عدم إرسال أي رسائل إضافية، وعندما نفعل ذلك تعيد كل الاستدعاءات إلى recv التي تجريها الخيوط الفعالة في الحلقة اللانهائية خطأً. نغير حلقة Worker في الشيفرة 24 لتخرج من الحلقة برشاقة في تلك الحالة، يعني أن الخيوط ستنتهي عندما يستدعي join عليهم في تنفيذ drop في ThreadPool. اسم الملف: src/lib.rs impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { let thread = thread::spawn(move || loop { let message = receiver.lock().unwrap().recv(); match message { Ok(job) => { println!("Worker {id} got a job; executing."); job(); } Err(_) => { println!("Worker {id} disconnected; shutting down."); break; } } }); Worker { id, thread: Some(thread), } } } [الشيفرة 24: الخروج صراحةً من الحلقة عندما تعيد recv خطأ] لرؤية عمل هذه الشيفرة: سنعدل main لتقبل فقط طلبين قبل أن تُغلق الخادم برشاقة كما تظهر الشيفرة 25. اسم الملف: src/main.rs fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); let pool = ThreadPool::new(4); for stream in listener.incoming().take(2) { let stream = stream.unwrap(); pool.execute(|| { handle_connection(stream); }); } println!("Shutting down."); } [الشيفرة 25: إغلاق الخادم بعد خدمة طلبين عن طريق الخروج من الحلقة] لا نريد أن يتوقف خادم حقيقي بعد خدمة طلبين فقط، وتبين هذه الشيفرة أن الإغلاق الرشيق وتحرير الذاكرة يعملان بصورةٍ نظامية. تُعرّف دالة take في سمة Iterator وتحدد التكرار إلى أول عنصرين بالحد الأقصى. سيخرج ThreadPool خارج النطاق في نهاية main وستُطبَّق سمة drop. شغّل الخادم باستخدام cargo run وأرسل ثلاثة طلبات. سيعطي الطلب الثالث خطأ وسترى الخرج في الطرفية على النحو التالي: $ cargo run Compiling hello v0.1.0 (file:///projects/hello) Finished dev [unoptimized + debuginfo] target(s) in 1.0s Running `target/debug/hello` Worker 0 got a job; executing. Shutting down. Shutting down worker 0 Worker 3 got a job; executing. Worker 1 disconnected; shutting down. Worker 2 disconnected; shutting down. Worker 3 disconnected; shutting down. Worker 0 disconnected; shutting down. Shutting down worker 1 Shutting down worker 2 Shutting down worker 3 يمكن أن ترى ترتيبًا مختلفًا للخيوط الفعالة والرسائل المطبوعة. تعمل الشيفرة وفقًا لهذه الرسائل كما يلي: أخذ العاملان 0 و 3 الطلبين الأولين وتوقف الخادم عن قبول الاتصالات بعد ثاني اتصال، وبدأ تنفيذ Drop في العمل على ThreadPool قبل أخذ العامل 3 وظيفته. يفصل إسقاط sender كل العمال ويخبرهم أن يُغلقوا، ويطبع كل عامل رسالةً عندما يُغلقوا ويستدعي مجمع الخيط join لانتظار كل خيط عامل لينتهي. لاحظ ميّزة مهمة في هذا التنفيذ، إذ قام ThreadPool بإسقاط sender وجرّبنا ضم العامل 0 قبل أن يستقبل أي عامل خطأ. لم يتلق العامل 0 أي خطأ من recv بعد، لذا تنتظر كتلة الخيط الأساسية أن ينتهي العامل 0. في تلك الأثناء استقبل العامل 3 وظيفة ثم استقبلت كل الخيوط خطأ. ينتظر الخيط الأساسي باقي العمال لينتهوا عندما ينتهي العامل 0. وبحلول هذه النقطة يخرج كل عامل من حلقته ويتوقف. تهانينا، فقد أنهينا المشروع ولدينا الآن خادم ويب بسيط يستخدم مجمع خيط للاستجابة بصورةٍ غير متزامنة، ونستطيع إجراء إغلاق رشيق للخادم الذي يحرر من الذاكرة كل الخيوط في المجمع. هذه هي الشيفرة الكاملة بمثابة مرجع. اسم الملف: src/main.rs use hello::ThreadPool; use std::fs; use std::io::prelude::*; use std::net::TcpListener; use std::net::TcpStream; use std::thread; use std::time::Duration; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); let pool = ThreadPool::new(4); for stream in listener.incoming().take(2) { let stream = stream.unwrap(); pool.execute(|| { handle_connection(stream); }); } println!("Shutting down."); } fn handle_connection(mut stream: TcpStream) { let mut buffer = [0; 1024]; stream.read(&mut buffer).unwrap(); let get = b"GET / HTTP/1.1\r\n"; let sleep = b"GET /sleep HTTP/1.1\r\n"; let (status_line, filename) = if buffer.starts_with(get) { ("HTTP/1.1 200 OK", "hello.html") } else if buffer.starts_with(sleep) { thread::sleep(Duration::from_secs(5)); ("HTTP/1.1 200 OK", "hello.html") } else { ("HTTP/1.1 404 NOT FOUND", "404.html") }; let contents = fs::read_to_string(filename).unwrap(); let response = format!( "{}\r\nContent-Length: {}\r\n\r\n{}", status_line, contents.len(), contents ); stream.write_all(response.as_bytes()).unwrap(); stream.flush().unwrap(); } اسم الملف: src/lib.rs use std::{ sync::{mpsc, Arc, Mutex}, thread, }; pub struct ThreadPool { workers: Vec<Worker>, sender: Option<mpsc::Sender<Job>>, } type Job = Box<dyn FnOnce() + Send + 'static>; impl ThreadPool { /// Create a new ThreadPool. /// /// The size is the number of threads in the pool. /// /// # Panics /// /// The `new` function will panic if the size is zero. pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let receiver = Arc::new(Mutex::new(receiver)); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id, Arc::clone(&receiver))); } ThreadPool { workers, sender: Some(sender), } } pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); self.sender.as_ref().unwrap().send(job).unwrap(); } } impl Drop for ThreadPool { fn drop(&mut self) { drop(self.sender.take()); for worker in &mut self.workers { println!("Shutting down worker {}", worker.id); if let Some(thread) = worker.thread.take() { thread.join().unwrap(); } } } } struct Worker { id: usize, thread: Option<thread::JoinHandle<()>>, } impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { let thread = thread::spawn(move || loop { let message = receiver.lock().unwrap().recv(); match message { Ok(job) => { println!("Worker {id} got a job; executing."); job(); } Err(_) => { println!("Worker {id} disconnected; shutting down."); break; } } }); Worker { id, thread: Some(thread), } } } يمكننا إجراء المزيد إذا أردنا تحسين المشروع، وإليك بعض الأفكار: أضِف المزيد من التوثيق إلى ThreadPool وتوابعه العامة. أضِف بعض الاختبارات لوظيفة المكتبة. غيّر الاستدعاءات من unwrap إلى معالجة خطأ أكثر متانة. استخدم ThreadPool لتنفيذ أعمال غير خدمة طلبات الويب. ابحث عن وحدة مجمع خيط مصرفة على creats.io ونفذ خادم ويب باستخدام الوحدة المصرفة، ثم قارن واجهة برمجة التطبيقات API والمتانة بينها وبين مجمع الخيط الذي نفذناه. خاتمة عظيم جدًا! فقد وصلنا إلى نهاية سلسلة البرمجة بلغة رست . نريد أن نشكرك لانضمامك إلينا في هذه الجولة في رست. أنت الآن جاهز لتنفيذ مشاريع رست ومساعدة الآخرين في مشاريعهم. تذكر أنه هناك مجتمع مرحب من مستخدمي رست الذين يحبون المساعدة في أي صعوبة يمكن أن تواجهها في استعمالك رست. ترجمة -وبتصرف- لقسم من الفصل Final Project: Building a Multithreaded Web Server من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: بناء خادم ويب متعدد مهام المعالجة بلغة رست - الجزء الثاني تزامن الحالة المشتركة Shared-State Concurrency في لغة رست وتوسيع التزامن مع Send و Sync مقدمة إلى الخيوط Threads في جافا
  2. سنكمل في هذا المقال ما تحدثنا عنه في المقال السابق الجزء الأول عملية بناء خادم ويب متعدد مهام المعالجة، فإذا لم تكن قد قرأت المقال السابق، فاقرأه قبل قراءة هذا المقال. تحويل خادم ويب ذو خيط وحيد إلى خادم متعدد المهام يعالج الآن خادم الويب كل طلب بدوره، يعني أنه لن يعالج اتصال ثاني حتى ينتهي من معالجة الطلب الأول. سيصبح التنفيذ التسلسلي أقل كفاءةً كلما زادت الطلبات على الخادم؛ فإذا استقبل الخادم طلبًا يتطلب وقتًا طويلًا لمعالجته ستنتظر الطلبات التالية وقتًا أطول حتى ينتهي الطلب الطويل حتى لو كانت الطلبات التالية تُنفذ بسرعة. يجب حل هذه المشكلة ولكن أولًا لنلاحظها أثناء العمل. محاكاة طلب بطيء في تنفيذ الخادم الحالي لنلاحظ كيف يؤثر طلب بطيء المعالجة على الطلبات الأخرى المقدمة إلى تنفيذ الخادم الحالي. تنفذ الشيفرة 10 طلب معالجة إلى ‎/sleep بمحاكاة استجابة بطيئة التي تسبب سكون الخادم لخمس ثوانٍ قبل الاستجابة. اسم الملف: src/main.rs use std::{ fs, io::{prelude::*, BufReader}, net::{TcpListener, TcpStream}, thread, time::Duration, }; // --snip-- fn handle_connection(mut stream: TcpStream) { // --snip-- let (status_line, filename) = match &request_line[..] { "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"), "GET /sleep HTTP/1.1" => { thread::sleep(Duration::from_secs(5)); ("HTTP/1.1 200 OK", "hello.html") } _ => ("HTTP/1.1 404 NOT FOUND", "404.html"), }; // --snip-- } [الشيفرة 10: محاكاة استجابة بطيئة عن طريق سكون الخادم لخمس ثوان] بدلنا من if إلى match إذ لدينا ثلاث حالات. يجب أن نطابق صراحةً مع قطعة من request_line لمطابقة النمط مع قيم السلسلة النصية المجردة. لا تُسند match ولا تُحصل تلقائيًا كما تفعل توابع المساواة. تكون الذراع الأولى هي نفس كتلة if من الشيفرة 9، وتطابق الذراع الثانية الطلب إلى ‎/sleep ويسكن الخادم لخمس ثوان عندما يُستقبل الطلب قبل تصيير صفحة HTML الناجحة، والذراع الثالثة هي نفس كتلة else من الشيفرة 9. يمكن ملاحظة أن الخادم بدائي، لكن تعالج المكاتب الحقيقية طلبات متعددة بطريقة مختصرة أكثر. شغل الخادم باستخدام cargo run، ثم افتح نافذتي متصفح واحدة من أجل "/http://127.0.0.1:7878" وأُخرى من أجل "http://127.0.0.1:7878/sleep". إذا أدخلت ‎/ URI عدة مرات كما سابقًا سترى أنه يستجيب بسرعة، لكن إذا أدخلت ‎/sleep ومن ثم حمّلت "/" سترى أن "/" ينتظر حتى يسكن sleep خمس ثوان كاملة قبل أن يُحمّل. هناك تقنيات متعددة لتفادي التراكم خلف طلب بطيء، والطريقة التي سنتبعها هي مجمع خيط thread pool. تحسن الإنتاجية باستخدام مجمع خيط مجمع خيط thread pool هو مجموعة من الخيوط المُنشأة التي تنتظر معالجة مهمة. عندما يستقبل البرنامج مهمةً، يُعيّن واحد من الخيوط في المجمع لأداء المهمة ومعالجتها وتبقى باقي الخيوط في المجمع متاحةً لمعالجة أي مهمة تأتي أثناء معالجة الخيط الأول للمهمة، وعندما ينتهي الخيط من معالجة المهمة يعود إلى مجمع الخيوط الخاملة جاهزًا لمعالجة أي مهمة جديدة. يسمح مجمع خيط معالجة الاتصالات بصورةٍ متزامنة ويزيد إنتاجية الخادم. سنحدد عدد الخيوط في المجمع برقم صغير لحمايتنا من هجوم حجب الخدمة Denial of Service ‎ -أو اختصارًا DoS. إذا طلبنا من البرنامج إنشاء خيط لكل طلب قادم، يمكن لشخص إنشاء 10 مليون طلب مستهلكًا كل الموارد المتاحة وموقفًا معالجة الطلبات نهائيًا. بدلًا من إنشاء عدد لا نهائي من الخيوط، سننشئ عددًا محددًا من الخيوط في المجمع، وستذهب الطلبات المرسلة إلى المجمع للمعالجة. يحافظ المجمع على ترتيب الطلبات القادمة في رتل، كل خيط يأخذ طلبًا من الرتل يعالجه ثم يطلب من الرتل طلبًا آخر. يمكن معالجة N طلب بهذه الطريقة، إذ تمثّل N عدد الخيوط. إذا كان كل خيط يعالج طلبًا طويل التنفيذ يمكن أن تتراكم الطلبات في الرتل ولكننا بذلك نكون قد زدنا عدد الطلبات طويلة التنفيذ التي يمكن معالجتها قبل أن نصل إلى تلك المرحلة. هذه إحدى طرق زيادة إنتاجية خادم ويب، ويمكن استكشاف طرق أُخرى مثل نموذج اشتقاق/جمع fork/join أو نموذج الدخل والخرج للخيط الواحد غير المتزامن single-threaded async I/O أو نموذج الدخل والخرج للخيوط المتعددة غير المتزامن multi-threaded async I/O model. يمكنك قراءة وتنفيذ هذه الحلول إذا كنت مهتمًا بهذا الموضوع وكل هذه الخيارات ممكنة مع لغة برمجية ذات مستوى منخفض مثل رست. قبل البدء بتنفيذ مجمع خيط، لنتحدث كيف يجب أن يكون استخدام المجمع. عند بداية تصميم الشيفرة، تساعدك كتابة واجهة المستخدم في التصميم. اكتب واجهة برمجة التطبيق API للشيفرة بهيكلية تشبه طريقة استدعائها، ثم نفذ الوظيفة داخل الهيكل بدلًا من تنفيذ الوظيفة ثم بناء واجهة برمجة التطبيق العامة. سنستخدم طريقة تطوير مُقادة بالمصرف compiler-driven، هذه طريقة مشابهة لاستخدامنا التطوير المُقاد بالاختبار test-driven كما فعلنا في مشروع سابق في الفصل 12. سنكتب الشيفرة التي تستدعي الدالة المُرادة، ومن ثم ننظر إلى الأخطاء من المصرّف لتحديد ماذا يجب أن نغير حتى تعمل الشيفرة. يجب الحديث بدايةً عن التقنيات التي لن نستعملها. إنشاء خيط لكل طلب لنلاحظ بدايةً كيف ستكون الشيفرة في حال أنشأنا خيطًا لكل طلب. كما ذكرنا سابقًا، لن تكون هذه خطتنا النهائية وذلك لمشكلة إنشاء عدد لا نهائي من الخيوط ولكنها نقطة بداية لإنشاء خادم متعدد الخيوط قادر على العمل، ثم سنضيف مجمع الخيط مثل تحسين وستكون مقارنة الحلين أسهل. تظهر الشيفرة 11 التغييرات لجعل main تُنشئ خيط جديد لمعالجة كل مجرى داخل حلقة for. fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); thread::spawn(|| { handle_connection(stream); }); } } [الشيفرة 11: إنشاء خيط جديد لكل مجرى] كما تعلمنا سابقًا في مقال سابق استخدام الخيوط Threads لتنفيذ شيفرات رست بصورة متزامنة آنيًا، يُنشئ thread::spawn خيطًا جديدًا وينفذ الشيفرة في المُغلف في الخيط الجديد. إذا نفذت الشيفرة وحملت "‎/sleep" في المتصفح ومن ثم "/" في نافذتي متصفح أُخريين ستلاحظ أن الطلبات إلى "/" لا تنتظر "‎/sleep‎" لينتهي ولكن كما ذكرنا سابقًا سيطغى هذا على النظام لأننا ننشئ خيوطًا دون حد. إنشاء عدد محدد من الخيوط نريد من مجمع الخيط أن يعمل بطريقة مشابهة ومألوفة حتى لا يحتاج التبديل من الخيوط لمجمع خيط أي تعديلات كبيرة للشيفرة التي تستخدمها واجهة برمجة التطبيق. تظهر الشيفرة 12 واجهة افتراضية لهيكل ThreadPool الذي نريد استخدامه بدلًا عن thread::spawn. اسم الملف: src/main.rs fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); let pool = ThreadPool::new(4); for stream in listener.incoming() { let stream = stream.unwrap(); pool.execute(|| { handle_connection(stream); }); } } [الشيفرة 12: واجهة ThreadPool المثالية] استخدمنا ThreadPool::new لإنشاء مجمع خيط جديد بعدد خيوط يمكن تعديله وفي حالتنا أربعة. لدى pool.execute واجهة مماثلة للدالة thread:spawn في حلقة for إذ تأخذ مغلفًا يجب أن ينفذه المجمع لكل مجرى. نحتاج لتنفيذ pool.execute أن تأخذ مغلفًا وتعطيه لخيط في المجمع لينفذه. لن تُصرّف هذه الشيفرة ولكن سنجربها كي يدلنا المصرف عن كيفية إصلاحها. إنشاء مجمع خيط باستخدام التطوير المقاد بالمصرف أجرِ التغييرات في الشيفرة 12 على الملف src/main.rs واستخدم أخطاء المصرّف من cargo check لقيادة التطوير. هذه أول خطأ نحصل عليه: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0433]: failed to resolve: use of undeclared type `ThreadPool` --> src/main.rs:11:16 | 11 | let pool = ThreadPool::new(4); | ^^^^^^^^^^ use of undeclared type `ThreadPool` For more information about this error, try `rustc --explain E0433`. error: could not compile `hello` due to previous error عظيم، يبين هذ الخطأ أننا بحاجة إلى نوع أو وحدة ThreadPool. سيكون تنفيذ Threadpool الخاص بنا مستقل عن عمل خادم الويب، لذا لنبدّل الوحدة المصرفة hello من وحدة ثنائية مصرفة إلى وحدة مكتبة مصرفة لاحتواء تنفيذ Threadpool. يمكننا بعد ذلك استخدام مكتبة مجمع الخيط المنفصلة لفعل أي عمل نريده باستخدام مجمع خيط وليس فقط لطلبات خادم الويب. أنشئ src/lib.rs الذي يحتوي التالي، وهو أبسط تعريف لهيكل ThreadPool يمكن الحصول عليه. اسم الملف: src/lib.rs pub struct ThreadPool; ثم عدل ملف main.rs لجلب ThreadPool من المكتبة إلى النطاق بإضافة الشيفرة التالية في مقدمة الملف src/main.rs. اسم الملف: src/main.rs use hello::ThreadPool; لن تعمل هذه الشيفرة ولكن لننظر مجددًا إلى الخطأ التالي الذي نريد معالجته: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope --> src/main.rs:12:28 | 12 | let pool = ThreadPool::new(4); | ^^^ function or associated item not found in `ThreadPool` For more information about this error, try `rustc --explain E0599`. error: could not compile `hello` due to previous error يشير هذا الخطأ أننا نحتاج إلى انتاج دالة مرتبطة اسمها new من أجل ThreadPool. يمكننا معرفة أن new تحتاج معامل يقبل 4 مثل وسيط ويجب أن يعيد نسخة ThreadPool. لننفذ أبسط دالة new التي تحتوي هذه الصفات characteristics. اسم الملف: src/lib.rs pub struct ThreadPool; impl ThreadPool { pub fn new(size: usize) -> ThreadPool { ThreadPool } } اخترنا نوع usize للمعامل size لأننا نعرف أن العدد السالب للطلبات غير منطقي ونعرف أيضًا أننا سنستخدم 4 ليمثّل عدد العناصر في مجموعة الخيوط وهذا هو عمل نوع usize كما تحدثنا سابقًا في قسم "أنواع الأعداد الصحيحة" في المقال أنواع البيانات Data Types في لغة رست. لنتحقق من الشيفرة مجددًا: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope --> src/main.rs:17:14 | 17 | pool.execute(|| { | ^^^^^^^ method not found in `ThreadPool` For more information about this error, try `rustc --explain E0599`. error: could not compile `hello` due to previous error يحدث الخطأ الآن لأنه ليس لدينا تابع execute على ThreadPool. تذكر من قسم "إنشاء عدد محدد من الخيوط" أننا قررنا أن مجمع الخيط يجب أن يكون له واجهة تشبه thread::spawn، وقررنا أيضّأ أننا سننفذ التابع execute ليأخذ المغلف المعُطى له ويعطيه لخيط خامل في المجمع لينفذه. سنعرّف تابع execute على ThreadPool ليأخذ المغلف مثل معامل. تذكر من القسم "نقل القيم خارج المغلف وسمات Fn" في المقال المغلفات closures في لغة رست أننا بإمكاننا أخذ المغلفات مثل معاملات باستخدام ثلاث سمات هي Fn أو FnMut أو FnOnce. يجب أن نحدد أي نوع مغلف نريد استخدامه هنا، نحن نعرف أننا سنفعل شيئًا يشابه تنفيذ المكتبة القياسية لدالةthread::spawn، لذلك دعنا نرى ما هي القيود الموجودة لبصمة thread::spawn على معاملاتها. تظهر التوثيقات التالي: pub fn spawn<F, T>(f: F) -> JoinHandle<T> where F: FnOnce() -> T, F: Send + 'static, T: Send + 'static, المعامل F هو الذي يهمنا، والمعامل T متعلق بالقيمة المُعادة ولسنا مهتمين بها. يمكننا أن نرى أن spawn تستخدم FnOnce مثل قيد سمة على F، وهذا ما نريده أيضًأ لأننا نريد تمرير الوسيط الذي نأخذه في execute إلى spawn. يمكننا التأكد أيضًا أن FnOnce هي السمة المُراد استخدامها لأن خيط تنفيذ الطلب سينفِّذ فقط طلب المغلف مرةً واحدة، والذي يطابق Once في FnOnce. لدى معامل نوع F أيضًا قيد سمة Send وقيد دورة حياة static' المفيدان في حالتنا؛ فنحن بحاجة Send لنقل المغلف من خيط لآخر، و static' لأننا لا نعرف الوقت اللازم ليُنفذ الخيط. لننشئ تابع execute على ThreadPool التي تأخذ معامل معمم للنوع F مع هذه القيود. اسم الملف: src/lib.rs impl ThreadPool { // --snip-- pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { } } استخدمنا () بعد FnOnce لأن FnOnce تمثل مغلفًا لا يأخذ معاملات ويعيد نوع الوحدة (). يمكن إهمال النوع المُعاد من البصمة كما في تعريفات الدالة، ولكن حتى لو لم يوجد أي معاملات نحن بحاجة الأقواس. هذا هو أبسط تنفيذ لدالة execute، فهي لا تعمل شيئًا، لكننا فقط بحاجة أن تُصرّف شيفرتنا، لنتحقق منها مجددًا. $ cargo check Checking hello v0.1.0 (file:///projects/hello) Finished dev [unoptimized + debuginfo] target(s) in 0.24s إنها تُصرّف، لكن لاحظ إذا جربت cargo run وأجريت طلبًا في المتصفح، سترى الأخطاء في المتصفح نفسها التي رأيناها في بداية الفصل. لم تستدع المكتبة المغلف المُمرر إلى execute حتى الآن. ملاحظة: هناك مقولة عن لغات البرمجة ذات المُصرّفات الحازمة مثل هاسكل Haskell ورست وهي "إذا صُرفت الشيفرة فإنها تعمل" ولكن هذه المقولة ليست صحيحة إجمالًا، إذ يُصرّف مشروعنا، لكنه لا يعمل شيئًا إطلاقًا. إذا كنا نريد إنشاء مشروع حقيقي ومكتمل، الآن هو الوقت المثالي لكتابة وحدات اختبار للتحقق من أن الشيفرة تُصرّف ولها السلوك المرغوب. التحقق من صحة عدد الخيوط في new لن نغيّر شيئًا للمعاملين new و parameter. لننفذ متن الدوال بالسلوك الذي نريده، ولنبدأ بالدالة new. اخترنا سابقًا نوع غير مؤشر للمعامل size لأن مجمع بعدد خيوط سلبي هو غير منطقي، ولكن مجمع بعدد خيوط صفر ليس منطقيًا أيضًا ولكن unsize صالح. سنضيف الشيفرة التي تتحقق من أن size أكبر من الصفر قبل إعادة نسخة من ThreadPool وجعل البرنامج يهلع إذا حصل على قيمة صفر باستخدام ماكرو assert!‎ كما في الشيفرة 13. اسم الملف: src/lib.rs impl ThreadPool { /// Create a new ThreadPool. /// /// The size is the number of threads in the pool. /// /// # Panics /// /// The `new` function will panic if the size is zero. pub fn new(size: usize) -> ThreadPool { assert!(size > 0); ThreadPool } // --snip-- } [الشيفرة 13: تنفيذ Threadpool:new ليهلع إذا كان size صفر] أضفنا بعض التوثيق إلى ThreadPool باستخدام تعليقات doc. لاحظ أننا اتبعنا خطوات التوثيق الجيدة بإضافة قسم يستدعي الحالات التي يمكن للدالة أن تهلع فيها كما تحدثنا في الفصل 14. جرب تنفيذ cargo run --open واضغط على هيكل ThreadPool لرؤية كيف تبدو المستندات المُنشأة للدالة new. يمكننا تغيير new إلى build بدلًا من إضافة ماكرو assert!‎، ونعيد Result كما فعلنا في Config::build في مشروع الدخل والخرج في الشيفرة 9 في المقال كتابة برنامج سطر أوامر بلغة رست: إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء، لكننا قررنا في حالتنا أن إنشاء مجمع خيط بدون أي خيوط هو خطأ لا يمكن استرداده. إذا كنت طموحًا جرب كتابة دالة اسمها build مع البصمة التالية لمقارنته مع الدالة new. pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> { إنشاء مساحة لتخزين الخيوط لدينا الآن طريقة لمعرفة أنه لدينا عدد صالح من الخيوط لتخزينها في المجمع، إذ يمكننا إنشاء هذه الخيوط وتخزينها في هيكل ThreadPool قبل إرجاعها إلى الهيكل، ولكن كيف نخزن الخيوط؟ لنلاحظ بصمة thread::spawn. pub fn spawn<F, T>(f: F) -> JoinHandle<T> where F: FnOnce() -> T, F: Send + 'static, T: Send + 'static, يُعاد JoinHandle<T>‎ من الدالة spawn، إذ تمثّل T النوع الذي يعيده المغلف. لنستعمل JoinHandle أيضًا لنرى ما سيحدث، إذ سيعالج المغلف الذي نمرره إلى مجمع الخيط الاتصال ولا يعيد أي شيء لذا T ستكون نوع وحدة (). ستُصرّف الشيفرة في الشيفرة 14 ولكن لا تُنشئ أي خيوط. غيّرنا تعريف ThreadPool لتحتوي شعاعًا من نسخة thread::JoinHandle<()>‎ وهيأنا الشعاع بسعة size وضبطنا حلقة for التي تعيد بعض الشيفرة لإنشاء الخيوط وتعيد نسخة ThreadPool تحتويهم. اسم الملف: src/main.rs use std::thread; pub struct ThreadPool { threads: Vec<thread::JoinHandle<()>>, } impl ThreadPool { // --snip-- pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let mut threads = Vec::with_capacity(size); for _ in 0..size { // create some threads and store them in the vector } ThreadPool { threads } } // --snip-- } [الشيفرة 14: إنشاء شعاع للهيكل ThreadPool الذي يحتوي الخيوط] جلبنا std::thread إلى النطاق في وحدة المكتبة المصرفة لأننا نستخدم Thread::JoinHandle بمثابة نوع العنصر في الشعاع في ThreadPool. تُنشئ ThreadPool شعاعًا جديدًا يحتوي عناصر size عندما يُستقبل حجم صالح. تعمل الدالة with_capacity نفس مهام Vec::new ولكن بفرق مهم هو أنها تحجز مسبقًا المساحة في الشعاع لأننا نريد تخزين عناصر size في الشعاع. إجراء هذا الحجز مسبقًا هو أكثر كفاءة من استخدام Vec::new الذي يغير حجمه كلما اُضيفت عناصر. عندما تنفذ cargo check مجدّدًا ينبغي أن تنجح. هيكل عامل Worker Struct مسؤول عن ارسال شيفرة من مجمع الخيط إلى خيط تركنا تعليقًا في حلقة for متعلق بإنشاء الخيوط في الشيفرة 14. سننظر هنا إلى كيفية إنشاء الخيوط حقيقةً، إذ تؤمن المكتبة القياسية thread::spawn بمثابة طريقة لإنشاء الخيوط ويتوقع thread::spawn الحصول على بعض الشيفرة لكي ينفذها الخيط بعد إنشائه فورًا، ولكن نريد في حالتنا إنشاء خيوط وجعلهم ينتظرون شيفرةً سنرسلها لاحقًا. لا يقدم تنفيذ المكتبة القياسية للخيوط أي طريقة لعمل ذلك، إذ يجب تنفيذها يدويًا. سننفذ هذا السلوك عن طريق إضافة هيكلية بيانات جديدة بين ThreadPool والخيوط التي ستدير هذه السلوك الجديد، وسندعو هيكل البيانات هذا "العامل Worker" وهو مصطلح عام في تنفيذات مجمّع الخيوط. يأخذ العامل الشيفرة التي بحاجة لتنفيذ وينفّذها في خيط العامل. فكر كيف يعمل الناس في مطبخ المطعم، إذ ينتظر العاملون طلبات الزبائن ويكونوا مسؤولين عن أخذ هذه الطلبات وتنفيذها. بدلًا من تخزين شعاع نسخة JoinHandle<()>‎ في مجمع الخيط، نخزن نسخًا من هيكل Worker. يخزن كل Worker نسخة JoinHandle<()>‎ واحدة، ثم ننفذ تابع على Worker الذي يأخذ مغلف شيفرة لينفذه ويرسله إلى خيط يعمل حاليًا لينفذه. سنعطي كل عامل رقمًا معرّفًا id للتمييز بين العمال المختلفين في المجمع عند التسجيل أو تنقيح الأخطاء. هكذا ستكون العملية الجديدة عند إنشاء ThreadPool. سننفذ الشيفرة التي ترسل المغلف إلى الخيط بعد إعداد Worker بهذه الطريقة: عرّف هيكل Worker الذي يحتوي id و JoinHandle<()>‎. عدّل ThreadPool لتحتوي شعاع من نسخ Worker. عرّف دالة Worker::new التي تأخذ رقم id وتعيد نسخة Worker التي تحتوي id وخيط مُنشأ بمغلف فارغ. استخدم عداد حلقة for لإنشاء id وإنشاء Worker جديد مع ذلك الرقم id وخرن العامل في الشعاع. إذا كنت جاهزًا للتحدي، جرّب تنفيذ هذه التغييرات بنفسك قبل النظر إلى الشيفرة في الشيفرة 15. جاهز؟ يوجد في الشيفرة 15 إحدى طرق عمل التعديلات السابقة. اسم الملف:src/lib.rs use std::thread; pub struct ThreadPool { workers: Vec<Worker>, } impl ThreadPool { // --snip-- pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id)); } ThreadPool { workers } } // --snip-- } struct Worker { id: usize, thread: thread::JoinHandle<()>, } impl Worker { fn new(id: usize) -> Worker { let thread = thread::spawn(|| {}); Worker { id, thread } } } [الشيفرة 15: تعديل ThreadPool بحيث تحتوي نسخة Worker بدلًا من احتواء الخيط مباشرةً] عدّلنا اسم حقل ThreadPool من threads إلى workers لأنه يحتوي نسخ Worker بدلًا من نسخ JoinHandle<()>‎. استخدمنا العداد في حلقة for مثل وسيط لدالة Worker::new وخزّنا كل Worker جديد في شعاع اسمه workers. لا تحتاج الشيفرة الخارجية (كما في الخادم في src/main.rs) أن تعرف تفاصيل التنفيذ بما يتعلق باستخدام هيكل Worker داخل ThreadPool، لذا نجعل كل من هيكل Worker ودالة new خاصين private. تستخدم الدالة Worker::new المعرّف id المُعطى وتخزن نسخة JoinHandle<()>‎ المُنشأة عن طريق إنشاء خيط جديد باستخدام مغلف فارغ. ملاحظة: سيهلع thread::spawn إذا كان نظام التشغيل لا يستطيع إنشاء خيط بسبب عدم توفر موارد كافية، وسيؤدي هذا إلى هلع كامل الخادم حتى لو كان إنشاء بعض الخيوط ممكنًا. للتبسيط يمكن قبول هذا السلوك ولكن في تنفيذ مجمع خيط مُنتج ينبغي استخدام std::thread::Builder ودالة spawn الخاصة به التي تعيد Result. تُصرّف هذه الشيفرة وتخزن عددًا من نسخ Worker الذي حددناه مثل وسيط إلى ThreadPool::new. لكننا لم نعالج المغلف الذي نحصل عليه في execute. لنتعرف على كيفية عمل ذلك تاليًا. إرسال طلبات إلى الخيوط عن طريق القنوات المشكلة التالية التي سنتعامل معها هي أن المغلفات المُعطاة إلى thread::spawn لا تفعل شيئًا إطلاقًا، وسنحصل حاليًا على المغلف الذي نريد تنفيذه في تابع execute، لكن نحن بحاجة لإعطاء thread::spawn مغلفًا لينفذه عندما ننشئ كل Worker خلال إنشاء ThreadPool. نريد تشغيل هياكل Worker التي أنشأناها للبحث عن شيفرة من الرتل في ThreadPool وأن ترسل تلك الشيفرة إلى خيطها لينفّذها. ستكون القنوات التي تعلمناها سابقًا في المقال استخدام ميزة تمرير الرسائل Message Passing لنقل البيانات بين الخيوط Threads في لغة رست -والتي تُعد طريقة بسيطة للتواصل بين خيطين- طريقةً ممتازةً لحالتنا، إذ سنستعمل قناةً لتعمل مثل رتل للوظائف، وترسل execute وظيفة من ThreadPool إلى نسخة Worker التي ترسل بدورها الوظيفة إلى خيطها. ستكون الخطة على النحو التالي: يُنشئ ThreadPool قناة ويحتفظ بالمرسل. يحتفظ كل Worker بالمستقبل. ننشئ هيكل Job جديد يحتفظ بالمغلف الذي نريد إرساله عبر القناة. يرسل تابع execute الوظيفة المراد تنفيذها عبر المرسل. سيتكرر مرور Worker على المستقبل وينفذ المغلف لأي وظيفة يستقبلها في الخيط. لنحاول إنشاء قناة في ThreadPool::new والاحتفاظ بالمرسل في نسخة ThreadPool كما في الشيفرة 16. لا يحتوي هيكل Job أي شيء الآن، لكنه سيكون نوع العنصر المُرسل عبر القناة. اسم الملف: src/lib.rs use std::{sync::mpsc, thread}; pub struct ThreadPool { workers: Vec<Worker>, sender: mpsc::Sender<Job>, } struct Job; impl ThreadPool { // --snip-- pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id)); } ThreadPool { workers, sender } } // --snip-- } [الشيفرة 16: تعديل ThreadPool لتخزين مرسل القناة التي ترسل نسخ Job] أنشأنا القناة الجديدة في ThreadPool:new وجعلنا المجمع يحتفظ بالمرسل. ستصرّف هذه الشيفرة بنجاح. لنجرب تمرير مستقبل القناة إلى كل عامل عندما ينشئ مجمع الخيط القناة. نعرف أننا نريد استخدام المستقبل في الخيط الذي أنشأه العامل، لذا سنشير إلى معامل receiver في المغلف بمرجع reference. لن تُصرّف الشيفرة 17. اسم الملف: src/lib.rs impl ThreadPool { // --snip-- pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id, receiver)); } ThreadPool { workers, sender } } // --snip-- } // --snip-- impl Worker { fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker { let thread = thread::spawn(|| { receiver; }); Worker { id, thread } } } [الشيفرة 17: تمرير المستقبل إلى العمال workers] أجرينا بعض التغييرات الصغيرة والمباشرة، إذ مررنا المستقبل إلى Worker::new واستخدمناه داخل المغلف. عندما نتحقق من الشيفرة سنحصل على هذا الخطأ: $ cargo check Checking hello v0.1.0 (file:///projects/hello) error[E0382]: use of moved value: `receiver` --> src/lib.rs:26:42 | 21 | let (sender, receiver) = mpsc::channel(); | -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait ... 26 | workers.push(Worker::new(id, receiver)); | ^^^^^^^^ value moved here, in previous iteration of loop For more information about this error, try `rustc --explain E0382`. error: could not compile `hello` due to previous error تحاول الشيفرة تمرير receiver لنسخ متعددة من Worker ولكن هذا لن يعمل كما تتذكر سابقًا من المقال استخدام ميزة تمرير الرسائل Message Passing لنقل البيانات بين الخيوط Threads في لغة رست، إذ أن تنفيذ القناة المقدم من رست هو مُنتجين producer متعددين ومستهلك consumer واحد، وهذا يعني أنه لا يمكن نسخ الطرف المستهلك من القناة لإصلاح هذا الخطأ ولا نريد أيضًا إرسال رسائل متعددة لمستهلكين متعددين، بل نحتاج قائمة رسائل واحدة مع عمال متعددين لكي تعالج كل رسالة مرةً واحدةً فقط. إضافةً إلى ذلك، يتطلب أخذ وظيفة من رتل القناة تغيير receiver، لذا تحتاج الخيوط طريقةً آمنةً لتشارك وتعدل receiver، وإلا نحصل على حالات سباق (كما تحدثنا في الفصل السابق المشار إليه). تذكر المؤشرات الذكية الآمنة للخيوط التي تحدثنا عنها سابقًا في المقال تزامن الحالة المشتركة Shared-State Concurrency في لغة رست وتوسيع التزامن مع Send و Sync؛ فنحن بحاجة لاستخدام Arc<Mutex<T>>‎ لمشاركة الملكية لعدد من الخيوط والسماح للخيوط بتغيير القيمة. يسمح نوع Arc لعدد من العمال من مُلك المستقبل وتضمن Mutex حصول عامل واحد على الوظيفة من المستقبل. تظهر الشيفرة 18 التغييرات التي يجب عملها. اسم الملف: src/lib.rs use std::{ sync::{mpsc, Arc, Mutex}, thread, }; // --snip-- impl ThreadPool { // --snip-- pub fn new(size: usize) -> ThreadPool { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let receiver = Arc::new(Mutex::new(receiver)); let mut workers = Vec::with_capacity(size); for id in 0..size { workers.push(Worker::new(id, Arc::clone(&receiver))); } ThreadPool { workers, sender } } // --snip-- } // --snip-- impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { // --snip-- } } [الشيفرة 18: مشاركة المستقبل بين العمال باستخدام Arc و Mutex] نضع المستقبل فيThreadPool::new في Arc و Mutex، وننسخ Arc لكل عامل لتزيد عدّ المرجع ليستطيع العمال مشاركة ملكية المستقبل. تُصرّف الشيفرة بنجاح مع هذه التغييرات، لقد اقتربنا من تحقيق هدفنا. تنفيذ تابع التنفيذ execute لننفذ أخيرًا تابع execute على ThreadPool، إذ سغيّر أيضًا Job من هيكل إلى نوع اسم بديل لكائن السمة الذي يحتوي نوع المغلف الذي يستقبله execute. كما تحدثنا في قسم "إنشاء مرادفات للنوع بواسطة أسماء النوع البديلة" في المقال الأنواع والدوال المتقدمة في لغة رست، يسمح لنا نوع الاسم البديل بتقصير الأنواع الطويلة لسهولة الاستخدام كما في الشفرة 19. اسم الملف: src/lib.rs // --snip-- type Job = Box<dyn FnOnce() + Send + 'static>; impl ThreadPool { // --snip-- pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); self.sender.send(job).unwrap(); } } // --snip-- [الشيفرة 19: إنشاء نوع اسم بديل Job لـ Box يحتوي كل مغلف وارسال العمل عبر القناة] بعد إنشاء نسخة Job جديدة باستخدام المغلف نحصل على execute ونرسل الوظيفة عبر الطرف المرسل للقناة. نستدعي unwrap على send في حال فشل الإرسال؛ إذ يمكن حصول ذلك إذا أوقفنا كل الخيوط من التنفيذ، وهذا يعني توقُف الطرف المستقبل عن استقبال أي رسائل جديدة. لا يمكننا الآن إيقاف الخيوط من التنفيذ، إذ تستمر خيوطنا بالتنفيذ طالما المجمع موجود. سبب استخدام unwrap هو أننا نعرف أن حالة الفشل هذه لن تحصل ولكن المصرّف لا يعرف ذلك. لم ننتهي كليًا بعد، فالمغلف المُمرر إلى thread::spawn يسند الطرف المستقبل من القناة فقط، لكن نريد بدلًا من ذلك أن يتكرر المغلف للأبد ويسأل الطرف المستقبل من القناة عن وظيفة وينفذ الوظيفة عندما يحصل عليها. دعنا نجري التغييرات الموضحة في الشيفرة 20 للدالة Worker::new. اسم الملف: src/lib.rs // --snip-- impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { let thread = thread::spawn(move || loop { let job = receiver.lock().unwrap().recv().unwrap(); println!("Worker {id} got a job; executing."); job(); }); Worker { id, thread } } } [الشيفرة 20: استقبال وتنفيذ الوظائف في خيط العامل] نستدعي أولًا lock على receiver للحصول على mutex، ونستدعي unwrap ليهلع على أي خطأ. قد يفشل الحصول على قفل إذا كان mutex في حالة مسمومة poisoned، والتي تحصل إذا هلع أحد الخيوط عند احتفاظه بالقفل بدلًا من ترك القفل، وسيكون استدعاء unwrap في هذه الحالة هو العمل الأفضل. غيّر unwrap إلى expect على راحتك لتظهر رسالة خطأ ذات معنى. إذا حصلنا على القفل على mutex، نستدعي recv لاستقبال Job من القناة. يتخطى استدعاء unwrap الأخير أي أخطاء أيضًا والتي ربما قد تحصل إذا اُغلق، على نحوٍ مشابه لكيفية إعادة Err من قِبل تابع send إذا أُغلق المستقبل. إذا لم توجد أي وظيفة في استدعاء كتل recv، سينتظر الخيط حتى تتوفر وظيفة. يضمن Mutex<T>‎ أن يكون هناك خيط Worker واحد يطلب وظيفة. يعمل مجمع الخيط الآن، جرب cargo run وأرسل بعض الطلبات. $ cargo run Compiling hello v0.1.0 (file:///projects/hello) warning: field is never read: `workers` --> src/lib.rs:7:5 | 7 | workers: Vec<Worker>, | ^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(dead_code)]` on by default warning: field is never read: `id` --> src/lib.rs:48:5 | 48 | id: usize, | ^^^^^^^^^ warning: field is never read: `thread` --> src/lib.rs:49:5 | 49 | thread: thread::JoinHandle<()>, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ warning: `hello` (lib) generated 3 warnings Finished dev [unoptimized + debuginfo] target(s) in 1.40s Running `target/debug/hello` Worker 0 got a job; executing. Worker 2 got a job; executing. Worker 1 got a job; executing. Worker 3 got a job; executing. Worker 0 got a job; executing. Worker 2 got a job; executing. Worker 1 got a job; executing. Worker 3 got a job; executing. Worker 0 got a job; executing. Worker 2 got a job; executing. لقد نجحنا، ولدينا الآن مجمع خيط ينفذ الاتصالات على نحوٍ غير متزامن. لا يُنشئ أكثر من أربعة خيوط حتى لا يُحمَل النظام بصورةٍ زائدة إذا استقبل الخادم طلبات كثيرة. إذا أرسلنا طلبًا إلى ‎/‎sleep سيكون الخادم قادرًا على خدمة طلبات أُخرى بجعل خيط آخر ينفذهم. ملاحظة: إذا فتحنا ‎/sleep في نوافذ متعددة في المتصفح بنفس الوقت، ستُحمل واحدةٌ تلو الأُخرى بفواصل زمنية مدتها 5 ثواني لأن بعض المتصفحات تنفذ النسخ المتعددة لنفس الطلب بالترتيب لأسباب التخزين المؤقت. ليس الخادم هو سبب هذا التقصير. بعد أن تعلمنا عن حلقة while let في المقال الأنماط Patterns واستخداماتها وقابليتها للدحض Refutability في لغة رست، ربما تتساءل لماذا لم نكتب شيفرة الخيط العامل كما في الشيفرة 21. اسم الملف: src/lib.rs // --snip-- impl Worker { fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker { let thread = thread::spawn(move || { while let Ok(job) = receiver.lock().unwrap().recv() { println!("Worker {id} got a job; executing."); job(); } }); Worker { id, thread } } } [الشيفرة 21: طريقة تنفيذ مختلفة لدالة Worker::new باستخدام while let] تُصرّف الشيفرة وتُنفذ ولكن لا تعطي نتيجة عمل الخيوط المرغوبة، إذ يسبب الطلب البطيء انتظار باقي الطلبات لتُعالج، والسبب بسيطٌ إلى حد ما؛ فليس لدى هيكل Mutex دالة unlock عامة لأن ملكية القفل مبينةٌ على دورة حياة MutexGuard<T>‎ داخل LockResult<MutexGuard<T>>‎ التي يعيدها التابع lock. يطبق متحقق الاستعارة قاعدة أن المورد المحمي بهيكل Mutex لا يمكن الوصول له إلا إذا احتفظنا بالقفل وقت التصريف، ولكن بهذا التنفيذ يمكن أن يبقى القفل مُحتفظًا به أكثر من اللازم إذا لم نكن منتبهين إلى دورة حياة MutexGuard<T>‎. تعمل الشيفرة في الشيفرة 20 التي تستخدم let job = receiver.lock().unwrap().recv().unwrap();‎ إذ تُسقط أي قيمة مؤقتة مُستخدمة في التعبير على الطرف اليمين من إشارة المساواة "=" مع letعندما تنتهي تعليمة let، ولكن لا تُسقط while let (وأيضًا if let و match) القيم المؤقتة حتى نهاية الكتلة المرتبطة بها. يبقى القفل مُحتفظًا به حتى نهاية فترة استدعاء job()‎ يعني أن العمال الباقين لا يمكن أن يستقبلوا وظائف. ترجمة -وبتصرف- لقسم من الفصل Final Project: Building a Multithreaded Web Server من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: بناء خادم ويب متعدد مهام المعالجة بلغة رست - الجزء الأول مدخل إلى برمجة مواقع الويب من طرف الخادم أفضل 5 خوادم ويب مفتوحة المصدر
  3. بعد رحلة طويلة وصلنا إلى نهاية السلسلة البرمجة بلغة رست. سنبني في هذا القسم مشروعًا لتوضيح بعض المفاهيم التي تحدثنا عنها في المقالات السابقة وتذكر بعض الدروس السابقة. سنبني خادم ويب يعرض "hello" ويشبه الشكل 1 في متصفح الويب. [الشكل1: مشروعنا الأخير المشترك] هذه هي خطة بناء خادم الويب: مقدمة عن TCP و HTTP الاستماع إلى اتصالات TCP على المقبس socket تحليل عدد صغير من طلبات HTTP إنشاء استجابة HTTP مناسبة تطوير خرج الخادم بمجمع خيط thread pool قبل البدء، يجب التنويه على أن هذه الطريقة ليست أفضل طريقة لبناء خادم ويب باستخدام رست، إذ نشر أعضاء المجتمع وحدات مصرفة جاهزة للتطبيق على creats.io، والتي تقدم خوادم ويب أكثر اكتمالًا وتطبيقات لمجمع خيط أفضل من الذي سنبنيه، ولكن هدفنا من هذا الفصل هو مساعدتك على التعلم وليس اختيار الطريق الأسهل. يمكننا اختيار مستوى التجريد الذي نريد العمل معه لأن رست هي لغة برمجية للأنظمة ويمكن الانتقال لمستوى أدنى مما هو ممكن أو عملي في بعض اللغات الأُخرى، لذلك سنكتب خادم HTTP بسيط ومجمع الخيط يدويًا لنتعلم الأفكار والتقنيات العامة الموجودة في الوحدات المصرفة التي يمكن أن تراها في المستقبل. بناء خادم ويب أحادي الخيط سنبدأ بإنشاء خادم ويب أحادي الخيط، ولكن قبل أن نبدأ دعنا نراجع البروتوكولات المستخدمة في إنشاء خوادم الويب. تفاصيل هذه البروتوكولات هي خارج نطاق موضوعنا هنا إلا أن مراجعة سريعة ستمنحك المعلومات الكافية. البروتوكولان الأساسيان المعنيان في خوادم الويب هما بروتوكول نقل النصوص الفائقة Hypertext Transfer Protocol‏ -أو اختصارًا HTTP- وبروتوكول تحكم النقل Transmission Control Protocol‎ -أو اختصارًا TCP، وهما بروتوكولا طلب-استجابة؛ يعني أن العميل يبدأ الطلبات ويسمع الخادم الطلبات ويقدم استجابةً للعميل، ويُعرّف محتوى هذه الطلبات والاستجابات عبر هذه البروتوكولات. يصف بروتوكول TCP تفاصيل انتقال المعلومات من خادم لآخر ولكن لا يحدد نوع المعلومات. يبني HTTP فوق TCP عن طريق تعريف محتوى الطلبات والاستجابات. يمكن تقنيًا استخدام HTTP مع بروتوكولات أُخرى لكن في معظم الحالات يرسل HTTP البيانات على بروتوكول TCP. سنعمل مع البايتات الخام في طلبات واستجابات TCP و HTTP. الاستماع لاتصال TCP يجب أن يستمع خادم الويب إلى اتصال TCP لذا سنعمل على هذا الجزء أولًا. تقدم المكتبة القياسية وحدة std::net التي تسمح لنا بذلك. لننشئ مشروعًا جديدًا بالطريقة الاعتيادية: $ cargo new hello Created binary (application) `hello` project $ cd hello الآن اكتب الشيفرة 1 في الملف src/main.rs لنبدأ. ستسمع هذه الشيفرة إلى العنوان المحلي "127.0.0.1:7878" لمجرى TCP stream القادم، وعندما تستقبل مجرى قادم ستطبع Connection established!‎. اسم الملف: src/main.rs use std::net::TcpListener; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); println!("Connection established!"); } } [الشيفرة 1: الاستماع للمجاري القادمة وطباعة رسالة عند استقبال مجرى] يمكننا الاستماع لاتصال TCP على هذا العنوان "127.0.0.1:7878" باستخدام TcpListner، إذ يمثّل القسم قبل النقطتين عنوان IP الذي يمثل الحاسوب (هذا العنوان هو نفسه لكل الحواسيب وليس لحاسوب المستخدم حصريًا)، ورقم المنفذ هو 7878. اخترنا هذا المنفذ لسببين: لا يُقبل HTTP على هذا المنفذ لذا لا يتعارض الخادم بأي خدمة ويب ربما تحتاجها على جهازك، و 7878 هي كلمة rust مكتوبة على لوحة أرقام الهاتف. تعمل دالة bind في هذا الحالة مثل دالة new التي ترجع نسخة TcpListner جديدة. تسمى الدالة bind لأن الاتصال بمنفذ للاستماع إليه هي عملية تُعرف باسم الربط لمنفذ binding to a port. تعيد الدالة bind القيمة Results<T, E>‎ التي تشير أنه من الممكن أن يفشل الربط. يتطلب الاتصال بالمنفذ 80 امتيازات المسؤول (يستطيع غير المسؤولين فقط الاستماع في المنافذ الأعلى من 1023)، لذا لا يعمل الارتباط إذا حاولت الاتصال بالمنفذ 80 بدون كونك مسؤول، ولا يعمل الارتباط أيضًا إذا نفذنا نسختين من برنامجنا أي لدينا برنامجين يستمعان لنفس المنفذ. لا يلزمنا أن نتعامل مع هكذا أخطاء لأننا نكتب خادم بسيط لأغراض تعليمية فقط. نستعمل unwrap لإيقاف البرنامج إذا حصلت أي أخطاء. يعيد التابع incoming على TcpListner مكرّرًا iterator يعطي سلسلةً من المجاري (مجاري نوع TcpStream تحديدًا). يمثل المجرى الواحد اتصالًا مفتوحًا بين العميل والخادم، والاتصال هو الاسم الكامل لعملية الطلب والاستجابة التي يتصل فيها العميل إلى الخادم، وينشئ الخادم استجابةً ويغلق الاتصال. كذلك، سنقرأ من TcpStream لرؤية ماذا أرسل العميل وكتابة استجابتنا إلى المجرى لإرسال البيانات إلى العميل. ستعالج حلقة for عمومًا كل اتصال بدوره وتضيف سلسلة من المجاري لنتعامل معها. تتألف حتى الآن طريقتنا للتعامل مع المجرى من استدعاء unwrap لينهي البرنامج إذا كان للمجرى أي أخطاء، وإذا لم يكن هناك أخطاء يطبع البرنامج رسالة، وسنضيف وظائفًا إضافية في حالة النجاح في الشيفرة التالية. سبب استقبال أخطاء من تابع incoming عندما يتصل عميل بالخادم هو أننا نكرّر زيادةً عن حد محاولات الاتصال بدلًا من أن نكرّر أعلى من حد الاتصالات؛ فقد تفشل محاولات الاتصال لعدد من الأسباب ويتعلق العديد منها بنظام التشغيل، فمثلًا تحدّد الكثير من أنظمة التشغيل عدد الاتصالات المفتوحة بالوقت الذي تدعمها، وستعطي أي اتصالات جديدة خطأ حتى تُغلق أي اتصالات مفتوحة. لنحاول تنفيذ هذه الشيفرة، استدعِ cargo run في الطرفية وحمّل 127.0.0.1:7878 في متصفح الويب. يجب أن يظهر المتصفح رسالة الخطأ "إعادة ضبط الاتصال" لأن الخادم لا يرسل أي بيانات حاليًا، لكن عندما تنظر إلى الطرفية يجب أن ترى عدد من الرسائل المطبوعة عندما يتصل المتصفح بالخادم. Running `target/debug/hello` Connection established! Connection established! Connection established! سنرى أحيانًا عددًا من الرسائل المطبوعة لطلب متصفح واحد، ويعود سبب ذلك إلى أن المتصفح أنشأ طلبًا الصفحة وكذلك لعدد من الموارد الأخرى مثل أيقونة favicon.ico التي تظهر على صفحة المتصفح. يمكن أن تعني أيضًا أن المتصفح يحاول الاتصال بالخادم مرات متعددة لأنه لا يتجاوب مع أي بيانات. يُغلق الاتصال كجزء من تنفيذ drop عندما تخرج stream عن النطاق وتُسقط في نهاية الحلقة. تتعامل المتصفحات أحيانًا مع الاتصالات المغلقة بإعادة المحاولة لأن هذه المشكلة يمكن أن تكون مؤقتة. العامل المهم أنه حصلنا على مقبض لاتصال TCP. تذكر أن توقف البرنامج بالضغط على المفتاحين "ctrl-c" عندما تنتهي من تنفيذ نسخة معينة من الشيفرة، بعدها أعد تشغيل البرنامج باستدعاء أمر cargo run بعد إجراء أي تعديل على الشيفرة للتأكد من أنك تنفذ أحدث إصدار منها. قراءة الطلب دعنا ننفّذ وظيفةً لقراءة الطلب من المتصفح، إذ سنبدأ بدالة جديدة لمعالجة الاتصالات من أجل الفصل بين الحصول على اتصال وإجراء بعض الأعمال بالاتصال. سنقرأ دالة handle_connection البيانات من مجرى TCP وتطبعها لرؤية البيانات التي أُرسلت من المتصفح. غيّر الشيفرة لتصبح مثل الشيفرة 2. اسم الملف: src/main.rs use std::{ io::{prelude::*, BufReader}, net::{TcpListener, TcpStream}, }; fn main() { let listener = TcpListener::bind("127.0.0.1:7878").unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .take_while(|line| !line.is_empty()) .collect(); println!("Request: {:#?}", http_request); } [الشيفرة 2: القراءة من TcpStream وطباعة البيانات] نضيف std::io::prelude و std::io::BufReader إلى النطاق للحصول على سمات وأنواع تسمح لنا بالقراءة من والكتابة على المجرى. بدلًا من طباعة رسالة تقول أننا اتصلنا، نستدعي الدالة الجديدة handle_connection في حلقة for في الدالة main ونمرّر stream إليها. أنشأنا نسخة BufReader في دالة handle_connection التي تغلف المرجع المتغيّر إلى stream. يضيف BufReader تخزينًا مؤقتًا عن طريق إدارة الاستدعاءات إلى توابع سمة std::io::Read. أنشأنا متغيرًا اسمه http_request لجمع أسطر الطلب التي أرسله المتصفح إلى الخادم، ونشير أننا نريد جمع هذه الأسطر في شعاع عن طريق إضافة توصيف نوع Vec<_>‎. ينفّذ BufReader سمة std::io::BufRead التي تؤمن التابع lines، الذي يعيد مكرّر <Result<String, std::io::Error عن طريق فصل مجرى البيانات أينما ترى بايت سطر جديد. للحصول على كل String، نربط ونزيل تغليف unwarp كل Result. يمكن أن تكون Result خطأ إذا كانت البيانات ليست UTF-8 صالح أو كان هناك مشكلة في القراءة من المجرى. مجددًا، يمكن لبرنامج إنتاجي حل هذه المشكلات بسهولة ولكننا اخترنا إيقاف البرنامج في حالة الخطأ للتبسيط. يشير المتصفح إلى نهاية طلب HTTP عن طريق إرسال محرفي سطر جديد على الترتيب، لذا للحصول على طلب من المجرى نأخذ الأسطر حتى نصل إلى سلسلة نصية فارغة. عندما نجمع الأسطر في الشعاع سنطبعهم باستخدام تنسيقات جذابة pretty لتنقيح الأخطاء لكي ننظر إلى التعليمات التي يرسلها المتصفح إلى الخادم. لنجرب هذه الشيفرة. ابدأ البرنامج واطلب الصفحة في المتصفح مجددًا. لاحظ أنك ستحصل على صفحة خطأ في المتصفح، ولكن خرج البرنامج في الطرفية سيكون مشابهًا للتالي: $ cargo run Compiling hello v0.1.0 (file:///projects/hello) Finished dev [unoptimized + debuginfo] target(s) in 0.42s Running `target/debug/hello` Request: [ "GET / HTTP/1.1", "Host: 127.0.0.1:7878", "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0", "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language: en-US,en;q=0.5", "Accept-Encoding: gzip, deflate, br", "DNT: 1", "Connection: keep-alive", "Upgrade-Insecure-Requests: 1", "Sec-Fetch-Dest: document", "Sec-Fetch-Mode: navigate", "Sec-Fetch-Site: none", "Sec-Fetch-User: ?1", "Cache-Control: max-age=0", ] اعتمادًا على المتصفح يمكن أن تحصل على خرج مختلف قليلًا. نطبع الآن طلبات البيانات ويمكن مشاهدة لماذا نحصل على اتصالات متعددة من طلب واحد من المتصفح بتتبع المسار الذي بعد GET في أول سطر من الطلب. إذا كانت الاتصالات المتعددة كلها تطلب "/"، نعرف أن المتصفح يحاول إيجاد "/" باستمرار لأنه لم يحصل على استجابة من برنامجنا. لنفصّل بيانات الطلب لفهم ما يطلبه المتصفح من برنامجنا. نظرة أقرب على طلب HTTP بروتوكول HTTP أساسه نصي وتكون طلباته على الشكل التالي: Method Request-URI HTTP-Version CRLF headers CRLF message-body السطر الأول هو سطر الطلب الذي يحتوي معلومات عما يطلبه العميل، إذ يدل القسم الأول من سطر الطلب على التابع المستخدم مثل GET أو POST الذي يصف كيفية إجراء العميل لهذا الطلب. استخدم عميلنا طلب GET، وهذا يعني أنه يطلب معلومات؛ بينما يشير القسم الثاني "/" من سطر الطلبات إلى معرّف الموارد الموحد Uniform Resource Identifier‎ -أو اختصارًا URI- الذي يطلبه العميل. يشابه URI محدد الموارد الموحد Uniform Resource Locator -أو URL اختصارًا- ولكن ليس تمامًا، إذ أن الفرق بينهم ليس مهمًا لهذا المشروع، لكن تستخدم مواصفات HTTP المصطلح URI لذا نستبدل هنا URL بالمصطلح URI. القسم الأخير هو نسخة HTTP التي يستخدمها العميل، وينتهي الطلب بسلسلة CRLF (تعني CRLF محرف العودة إلى أول السطر والانتقال سطر للأسفل Carriage Return and Line Feed وهما مصطلحان من أيام الآلة الكاتبة). يمكن كتابة سلسلة CRLF مثل ‎\r\n إذ أن r\ هي محرف العودة إلى أول السطر و n\ هو الانتقال سطر للأسفل. تفصل سلسلة CRLF سطر الطلب من باقي بيانات الطلب. نلاحظ عندما تُطبع CRLF نرى بداية سطر جديد بدل ‎\r\n. عند ملاحظة سطر البيانات الذي استقبلناه من تنفيذ برنامجنا حتى الآن نرى أن التابع هو GET وطلب URI هو / والنسخة هي HTTP/1.1. الأسطر الباقية بدءًا من Host: وبعد هي ترويسات. طلب GET لا يحتوي متن. حاول عمل طلب من متصفح آخر أو طلب عنوان مختلف مثل 127.0.0.1:7878‎/test لترى كيف تتغير بيانات الطلب. بعد أن عرفنا ماذا يريد المتصفح لنرسل بعض البيانات. كتابة استجابة سننفّذ إرسال بيانات مثل استجابة لطلب عميل. لدى الاستجابات التنسيق التالي: HTTP-Version Status-Code Reason-Phrase CRLF headers CRLF message-body يحتوي السطر الأول الذي هو سطر الحالة نسخة HTTP المستخدمة في الاستجابة ورمز حالة status code عددية تلخص نتيجة الطلب وعبارة سبب تقدم شرحًا نصيًا عن رمز الحالة. يوجد بعد سلسلة CRLF ترويسات وسلسلة CRLF أُخرى ومتن الاستجابة. لدينا مثال عن استجابة تستخدم نسخة HTTP 1.1 ولديها رمز حالة 200 وعبارة سبب OK بلا ترويسة أو متن. HTTP/1.1 200 OK\r\n\r\n يُعد رمز الحالة 200 استجابة نجاح قياسية والنص هو استجابة نجاح HTTP صغيرة. لنكتب ذلك إلى المجرى مثل استجابة لطلب ناجح. أزل !println التي كانت تطبع طلب البيانات من الدالة handle_connection واستبدلها بالشيفرة 3. اسم الملف: src/main.rs fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .take_while(|line| !line.is_empty()) .collect(); let response = "HTTP/1.1 200 OK\r\n\r\n"; stream.write_all(response.as_bytes()).unwrap(); } [الشيفرة 3: كتابة استجابة نجاح HTTP صغيرة إلى المجرى] يعرّف أول سطر المتغير response الذي يحتوي بيانات رسالة النجاح، بعدها نستدعي as_bytes على response الخاص بنا لتحويل بيانات السلسلة النصية إلى بايتات. يأخذ تابع write_all على stream النوع ‏‏[u8]‏‏& ويرسل هذه البايتات مباشرةً نحو الاتصال لأن عملية write_all قد تفشل. نستعمل unwrap على أي خطأ ناتج كما فعلنا سابقًا. مُجددًا، يجب أن تتعامل مع الأخطاء في التطبيقات الحقيقية. لننفذ شيفرتنا بعد إجراء التعديلات ونرسل طلبًا. لا نطبع أي بيانات إلى الطرفية لذا لا نرى أي خرج ما عدا خرج Cargo. عند تحميل 127.0.0.1:7878 في متصفح الويب يجب أن يظهر صفحة فارغة بدلًا من خطأ، وبذلك تكون قد شفّرت يدويًا استقبال طلب HTTP وإرسال استجابة. إعادة HTML حقيقي لننفّذ وظيفة إعادة أكثر من صفحة فارغة. أنشئ الملف الجديد hello.html في جذر مسار مشروعك وليس في مسار src. يمكنك إدخال أي HTML تريده، تظهر الشيفرة 4 أحد الاحتمالات. اسم الملف:hello.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello!</title> </head> <body> <h1>Hello!</h1> <p>Hi from Rust</p> </body> </html> [الشيفرة 4: مثال ملف HTML يعيد استجابة] يمثّل هذا وثيقة HTML5 بسيطة مع ترويسة وبعض النصوص. سنعدّل الدالة handle_connection لإعادتها من الخادم عندما يُستقبل الطلب، كما في الشيفرة 5 وذلك لقراءة ملف HTML وإضافة الاستجابة مثل متن وإرساله. اسم الملف: src/main.rs use std::{ fs, io::{prelude::*, BufReader}, net::{TcpListener, TcpStream}, }; // --snip-- fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let http_request: Vec<_> = buf_reader .lines() .map(|result| result.unwrap()) .take_while(|line| !line.is_empty()) .collect(); let status_line = "HTTP/1.1 200 OK"; let contents = fs::read_to_string("hello.html").unwrap(); let length = contents.len(); let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); stream.write_all(response.as_bytes()).unwrap(); } [الشيفرة 5: إرسال محتوى hello.html مثل متن الاستجابة] أضفنا fs إلى تعليمة use لجلب وحدة نظام ملفات المكتبة القياسية إلى النطاق. يجب أن تكون الشيفرة لقراءة محتوى الملف إلى سلسلةً نصيةً مألوفة، إذ استخدمناها سابقًا في مقال كتابة برنامج سطر أوامر Command Line بلغة رست Rust عندما قرأنا محتوى ملف مشروع I/O في الشيفرة 4. استخدمنا !format لإضافة محتوى الملف على أنه متن استجابة النجاح، أضفنا الترويسة Content-Length التي تحدد حجم متن الاستجابة وفي حالتنا حجم hello.html لضمان استجابة HTTP صالحة لا. نفذ هذه الشيفرة مع cargo run وحمّل 1270.0.1:7878 في المتصفح، يجب أن ترى HTML الخاص بك معروضًا. نتجاهل حاليًا طلب البيانات في http_request ونرسل فقط محتوى ملف HTML دون شروط، هذا يعني إذا جربنا طلب 127.0.0.1:7878‎/something-else في المتصفح سنحصل على نفس استجابة HTML. في هذه اللحظة الخادم محدود ولا يفعل ما يفعله خوادم الويب، ونريد تعديل استجابتنا اعتمادًا على الطلب وإرسال ملف HTML فقط لطلب منسق جيدًا إلى "/". التحقق من صحة الطلب والاستجابة بصورة انتقائية يعيد خادم الويب الخاص بنا ملف HTML مهما كان طلب العميل، لنضف وظيفة التحقق أن المتصفح يطلب "/" قبل إعادة ملف HTML وإعادة خطأ في حال طلب المتصفح شيئًا آخر، لذا نحتاج لتعديل handle_connection كما في الشيفرة 6. تتحقق هذه الشيفرة الجديدة محتوى الطلب المُستقبل مع ما يشبه طلب "/" وتضيف كتل if و else لمعالجة الطلبات على نحوٍ مختلف. اسم الملف: src/main.rs // --snip-- fn handle_connection(mut stream: TcpStream) { let buf_reader = BufReader::new(&mut stream); let request_line = buf_reader.lines().next().unwrap().unwrap(); if request_line == "GET / HTTP/1.1" { let status_line = "HTTP/1.1 200 OK"; let contents = fs::read_to_string("hello.html").unwrap(); let length = contents.len(); let response = format!( "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" ); stream.write_all(response.as_bytes()).unwrap(); } else { // some other request } } [الشيفرة 6: معالجة الطلبات إلى / على نحوٍ مختلف عن الطلبات الأُخرى] سننظر فقط إلى السطر الأول من طلب HTTP لذا بدلًا من قراءة كامل الطلب لشعاع، نستدعي next ليأخذ العنصر الأول من المكرّر. تتعامل unwarp الأولى مع Option وتوقف البرنامج إذا لم يكن للمكرّر أي عنصر؛ بينما تتعامل unwarp الثانية مع Result ولها نفس تأثير unwarp التي كان في map المضافة في الشيفرة 2. نتحقق بعد ذلك من request_line لنرى إذا كانت تساوي سطر طلب GET إلى المسار"/"؛ فإذا ساوت تعيد كتلة if محتوى ملف HTML؛ وإذا لم تساوي، يعني ذلك أننا استقبلنا طلب آخر. سنضيف شيفرة إلى كتلة else بعد قليل لاستجابة الطلبات الأخرى. نفذ هذه الشيفرة واطلب 127.0.0.1:7878، يجب أن تحصل على HTML في hello.html. إذا طلبت أي شيء آخر مثل 127.0.0.1:7878‎/something-else ستحصل على خطأ اتصال مثل الذي تراه عند تنفيذ الشيفرة 1 و2. لنضيف الشيفرة في الشيفرة 7 إلى كتلة else لإعادة استجابة مع رمز الحالة 404 التي تشير إلى أن محتوى الطلب ليس موجودًا. سنعيد بعض HTML للصفحة لتصّير في المتصفح مشيرةً إلى جواب للمستخدم النهائي. اسم الملف: src/main.rs // --snip-- } else { let status_line = "HTTP/1.1 404 NOT FOUND"; let contents = fs::read_to_string("404.html").unwrap(); let length = contents.len(); let response = format!( "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}" ); stream.write_all(response.as_bytes()).unwrap(); } [الشيفرة 7: الاستجابة برمز الحالة 404 وصفحة خطأ إذا كان أي شيء عدا / قد طُلب] لدى استجابتنا سطر حالة مع رمز الحالة 404 وعبارة سبب NOT FOUND، يكون متن الاستجابة HTML في الملف ‎404.html. نحن بحاجة انشاء ملف ‎404.html بجانب hello.html لصفحة الخطأ، ويمكنك استخدام أي HTML تريده أو استخدم مثال HTML في الشيفرة 8. اسم الملف: ‎404.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Hello!</title> </head> <body> <h1>Oops!</h1> <p>Sorry, I don't know what you're asking for.</p> </body> </html> [الشيفرة 8: محتوى معين لصفحة كي تُرسل مع أي استجابة 404] شغّل الخادم مجددًا بعد هذه التغيرات. يجب أن يعيد محتوى hello.html عند طلب 127.0.0.1:7878 ويعيد خطأ HTML من ‎404.html في حال طلب آخر مثل 127.0.0.1:7878‎/foo. القليل من إعادة بناء التعليمات البرمجية في هذه اللحظة لدى كتلتي if و else الكثير من التكرار، فهما تقرأن الملفات وتكتبان محتوى الملفات إلى المجرى. الفرق الوحيد بينهما هو سطر الحالة واسم الملف. لنجعل الشيفرة أدق بسحب هذه الاختلافات إلى سطري if و else منفصلين، ليعينان القيم إلى المتغيرين سطر الحالة واسم الملف. يمكننا استخدام هذه المتغيرات دون قيود في الشيفرة لقراءة الملف وكتابة الاستجابة. تظهر الشيفرة 9 الشيفرة المُنتجة بعد استبدال كتل if و else الكبيرة. اسم الملف: src/main.rs // --snip-- fn handle_connection(mut stream: TcpStream) { // --snip-- let (status_line, filename) = if request_line == "GET / HTTP/1.1" { ("HTTP/1.1 200 OK", "hello.html") } else { ("HTTP/1.1 404 NOT FOUND", "404.html") }; let contents = fs::read_to_string(filename).unwrap(); let length = contents.len(); let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"); stream.write_all(response.as_bytes()).unwrap(); } [الشيفرة 9: إعادة بناء التعليمات البرمجية لكتل if و else لتحتوي فقط على الشيفرة المختلفة بين الحالتين] تعيد الآن كتلتا if و else فقط القيم المناسبة لسطر الحالة واسم الملف في الصف. نستخدم بعد ذلك التفكيك لتحديد هذه القيمتين إلى status_line و filename باستخدام الأنماط في تعليمة let كما تحدثنا في الفصل 18. الشيفرة المتكررة الآن هي خارج كتلتي if و else وتستخدم المتغيران status_line و filename. يسهّل هذا مشاهدة الفرق بين الحالتين ولدينا فقط مكان واحد لتعديل الشيفرة إذا أردنا تغيير كيفية قراءة الملفات وكتابة الاستجابة. سيكون سلوك الشيفرة في الشيفرة 9 مثل ماهو في الشيفرة 8. ممتاز، لديك الآن خادم ويب بسيط في حوالي 40 سطر من شيفرة رست الذي يستجيب لطلب واحد مع صفحة محتوى ويستجيب برمز حالة 404 لكل الطلبات الأُخرى. ينفذ الخادم حاليًا خيطًا واحدًا، بمعنى أنه يُخدّم طلبًا واحدًا كل مرة. لنفحص تاليًا كيف يمكن لذلك أن يسبب مشكلةً بمحاكاة بعض الطلبات البطيئة، ثم سنعالج هذه المشكلة لكي يعالج الخادم طلبات متعددة بالوقت ذاته. ترجمة -وبتصرف- لقسم من الفصل Final Project: Building a Multithreaded Web Server من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: الماكرو Macros في لغة رست إنشاء خادم ويب في Node.js باستخدام الوحدة HTTP دليل إعداد خادم ويب محلي خطوة بخطوة
  4. استخدمنا الماكرو مثل println!‎ سابقًا ضمن هذه السلسلة البرمجة بلغة رست، إلا أننا لم نتحدث بالكامل عما هو الماكرو وكيفية عمله، إذ تشير كلمة ماكرو إلى مجموعة من الميّزات في رست، ألا وهي الماكرو التصريحية declarative مع macro_rules!‎، إضافةً إلى ثلاثة أنواع من الماكرو الإجرائي procedural: ماكرو [derive]# مخصص يحدد شيفرة مضافة بسمة derive المستخدمة على الهياكل والمعدّدات. ماكرو شبيه بالسمة attribute، الذي يعرف سمات معينة تُستخدم على أية عنصر. ماكرو يشبه الدالة ويشابه استدعاءات الدالة ولكن يعمل على المفاتيح المحددة مثل وسائطها. سنتحدث عن كلِّ مما سبق بدوره ولكن لنتحدث أولًا عن حاجتنا للماكرو بالرغم من وجود الدوال. الفرق بين الماكرو والدوال الماكرو هو طريقة لكتابة شيفرة تكتب شيفرة أُخرى والمعروف بالبرمجة الوصفية metaprogramming، وحدثنا في الملحق "ت" عن سمة derive التي تنشئ تنفيذًا لسمات متعددة، واستخدمنا أيضًا ماكرو ‎‎‎‎‎println‎!‎‎‎ و ‎vec!‎ سابقًا. تتوسع كل هذه الماكرو لتضيف شيفرةً أكثر من الشيفرة التي كُتبت يدويًا. تفيد البرمجة الوصفية في تقليل كمية الشيفرة التي يجب كتابتها والمحافظة عليها وهو أيضًا أحد أدوار الدوال، لكن لدى الماكرو بعض القوى الإضافية غير الموجودة في الدوال. يجب أن تصرّح بصمة الدالة signature على عدد ونوع المعاملات الموجودة في الدالة، أما في حالة الماكرو فيمكن أن يأخذ عدد متغير من المعاملات، إذ يمكننا استدعاء println!("hello")‎ بوسيط واحد أو println!("hello {}", name)‎ بوسيطين. يتوسع أيضًا الماكرو قبل أن يفسر المصرف معنى الشيفرة لذا يمكن للماكرو مثلًا تنفيذ سمة على أي نوع مُعطى ولا يمكن للدالة فعل ذلك لأنها تُستدعى وقت التنفيذ وتحتاج لسمة لتُنفذ وقت التصريف. من مساوئ تنفيذ الماكرو بدلًا من الدالة هو أن تعاريف الماكرو أكثر تعقيدًا من تعاريف الدالة لأننا نكتب شيفرة رست لتكتب شيفرة رست، بالتالي تكون تعاريف الماكرو أكثر تعقيدًا للقراءة والفهم والمحافظة عليها من تعاريف الدالة. هناك فرق آخر مهم بين الماكرو والدوال هو أنه يجب تعريف الماكرو أو جلبه إلى النطاق في ملف قبل استدعائه، على عكس الدوال التي يمكنك تعريفها واستدعائها في كل وقت ومكان. الماكرو التصريحي مع macro_rules!‎ للبرمجة الوصفية العامة أكثر أنواع الماكرو استخدامًا في رست هو الماكرو التصريحي الذي يسمى أحيانًا "ماكرو بالمثال macros by example" أو "ماكرو macro_rules!‎" أو ببساطة "ماكرو". يسمح لك الماكرو التصريحي بكتابة شيء مشابه لتعبير match في رست بداخله. تعابير match -كما تحدثنا في المقال بنية match للتحكم بسير برامج لغة رست- هي هياكل تحكم تقبل تعبيرًا وتقارن القيمة الناتجة من التعبير مع النمط وبعدها تنفذ الشيفرة المرتبطة مع النمط المُطابق. يقارن الماكرو أيضًا قيمةً مع أنماط مرتبطة بشيفرة معينة، وتكون القيمة في هذه الحالة هي الشيفرة المصدرية لرست المُمَررة إلى الماكرو. تُقارن الأنماط مع هيكل الشيفرة المصدرية والشيفرة المرتبطة بكل نمط، وعند حدوث التطابق يستبدل الشيفرة المُمَررة إلى الماكرو، ويحصل كل ذلك وقت التصريف. نستخدم بنية macro_rules!‎ لتعريف الماكرو. دعنا نتحدث عن كيفية استخدام macro_rules!‎ بالنظر إلى كيفية تعريف ماكرو vec!‎، إذ تحدثنا سابقًا في المقال تخزين لائحة من القيم باستخدام الأشعة Vectors في لغة رست عن كيفية استخدام ماكرو vec!‎ من أجل إنشاء شعاع جديد بقيم معينة. ينشئ الماكرو التالي مثلًا شعاع جديد يحتوي على ثلاثة أعداد صحيحة. let v: Vec<u32> = vec![1, 2, 3]; يمكن استخدام الماكرو vec!‎ لإنشاء شعاع بعددين صحيحين أو شعاع بخمس سلاسل شرائح نصية string slice، ولا يمكننا فعل ذلك باستخدام الدوال لأننا لا نعرف عدد أو نوع القيم مسبقًا. تبين الشيفرة 28 تعريفًا مبسطًا لماكرو vec!‎. اسم الملف: src/main.rs #[macro_export] macro_rules! vec { ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( temp_vec.push($x); )* temp_vec } }; } [الشيفرة 28: نسخة مبسطة من تعريف ماكرو vec!‎] ملاحظة: يتضمن التعريف الفعلي لماكرو vec!‎ في المكتبة القياسية شيفرة للحجز الصحيح للذاكرة مسبقًا، وهذه الشيفرة هي تحسين لم نضفه هنا لجعل المثال أبسط. يشير توصيف ‏[macro_export]#‏‎ إلى أن هذا الماكرو يجب أن يبقى متاحًا عندما يجري إحضار الوحدة المُصرّفة crate المعرّفة داخلها الماكرو إلى النطاق، ولا يمكن إضافة الماكرو إلى النطاق دون هذا التوصيف. عندما نبدأ بتعريف الماكرو مع macro_rules!‎ ويكون اسم الماكرو الذي نعرّفه بدون علامة التعجب، يكون الاسم في هذه الحالة vec متبوعًا بقوسين معقوصين تدل على متن تعريف الماكرو. يشابه الهيكل في متن vec!‎ الهيكل في تعبير match، إذ لدينا هنا ذراع واحد مع النمط ‎( $( $x:expr ),* )‎‏‎‎‎ متبوعةً بالعامل ‎=>‎ وكتلة الشيفرة المرتبطة في النمط، وستُرسل الكتلة المرتبطة إذا تطابق النمط. بما أن هذا هو النمط الوحيد في الماكرو، هناك طريقة وحيدة للمطابقة، وأي أنماط أُخرى ستسبب خطأ، ويكون لدى الماكرو الأكثر تعقيدًا أكثر من ذراع واحدة. تختلف الصيغة الصحيحة في تعاريف الماكرو عن صيغة النمط المذكور سابقًا في المقال صياغة أنماط التصميم الصحيحة Pattern Syntax في لغة رست لأن أنماط الماكرو تُطابق مع هيكل شيفرة رست بدلًا من القيم. لنتحدث عن ماذا تعني أقسام النمط في الشيفرة 28. لقراءة صيغة نمط ماكرو الكاملة راجع مرجع رست. استخدمنا أولًا مجموعة أقواس لتغليف كامل النمط، واستخدمنا علامة الدولار ($) للتصريح عن متغير في نظام الماكرو الذي يحتوي على شيفرة رست مطابقة للنمط، إذ توضح إشارة الدولار أن هذا متغير ماكرو وليس متغير رست عادي. تأتي بعد ذلك مجموعةٌ من الأقواس التي تلتقط القيم التي تطابق النمط داخل القوسين لاستخدامها في الشيفرة المُستبدلة. توجد ‎$x:expr داخل ‎$()‎‎، التي تطابق أي تعبير رست وتعطي التعبير الاسم ‎$x. تشير الفاصلة التي تلي ‎‎‎$()‎ أنه يمكن أن يظهر هناك محرف فاصلة بعد الشيفرة الذي يطابق الشيفرة في ‎$()‎، وتشير * إلى أن هناك نمط يطابق صفر أو أكثر مما يسبق *. عندما نستدعي هذا الماكرو باستخدام vec![1, 2, 3];‎، يُطابق النمط ‎$x‎ ثلاث مرات مع التعابير الثلاث 1 و 2 و 3. لننظر إلى النمط الموجود في متن الشيفرة المرتبطة مع هذا الذراع، إذ تُنشَئ temp_vec.push()‎ داخل ‎$()*‎ لكل جزء يطابق ‎$()‎ في النمط صفر مرة أو أكثر اعتمادًا على كم مرة طابق النمط. تُبَدل ‎$‎x مع كل جزء مطابق، وعندما نستدعي الماكرو باستخدام vec![1, 2, ‎3];‎، ستكون الشيفرة المُنشأة التي تستبدل هذا الماكرو على النحو التالي: { let mut temp_vec = Vec::new(); temp_vec.push(1); temp_vec.push(2); temp_vec.push(3); temp_vec } عرّفنا الماكرو الذي يستطيع أن يأخذ أي عدد من الوسطاء من أي نوع ويستطيع إنشاء شيفرة لإنشاء شعاع يحتوي العناصر المحددة. لتعرف أكثر عن كيفية كتابة الماكرو، راجع وثائق ومصادر أُخرى على الشبكة مثل "الكتاب الصغير لماكرو رست The Little Book of Rust Macros" الذي بدأ فيه دانيل كيب Daniel Keep وتابعه لوكاس ويرث Lukas Wirth. الماكرو الإجرائي لإنشاء شيفرة من السمات الشكل الثاني من الماكرو هو الماكرو الإجرائي الذي يعمل أكثر مثل دالة (وهي نوع من الإجراءات). يقبل الماكرو الإجرائي بعض الشيفرة مثل دخل ويعمل على الشيفرة ويُنتج بعض الشيفرة مثل خرج بدلًا من مطابقة الأنماط وتبديل الشيفرة بشيفرة أُخرى كما يعمل الماكرو التصريحي. أنواع الماكرو الإجرائي الثلاث، هي: مشتقة مخصصة custom derive، أو مشابهة للسمة attribute-like، أو مشابهة للدالة function-like وتعمل كلها بطريقة مشابهة. عند إنشاء ماكرو إجرائي، يجب أن يبقى التعريف داخل الوحدة المصرّفة الخاصة به بنوع وحدة مصرّفة خاص، وذلك لأسباب تقنية معقدة نأمل أن نتخلص من وجودها مستقبلًا، تبين الشيفرة 29 كيفية تعريف الماكرو الإجرائي، إذ أن some_attribute هو عنصر مؤقت لاستخدام نوع ماكرو معين. اسم الملف: src/lib.rs use proc_macro; #[some_attribute] pub fn some_name(input: TokenStream) -> TokenStream { } [الشيفرة 29: مثال لتعريف ماكرو إجرائي] تأخذ الدالة التي تعرّف الماكرو الإجرائي TokenStream مثل دخل وتنتج TokenStream في الخرج. يُعرّف نوع TokenStream بالوحدة المصرّفة proc_macro المتضمنة في رست وتمثّل سلسلة من المفاتيح. هذا هو صلب الماكرو: تكون الشيفرة المصدرية التي يعمل فيها الماكرو هي الدخل TokenStream والشيفرة التي ينتجها الماكرو هي الخرج TokenStream. لدى الدالة سمة مرتبطة بها تحدد أي نوع من الماكرو الإجرائي يجب أن نُنشئ، ويمكن أيضًا الحصول على العديد من الماكرو الإجرائي في الوحدة المصرّفة ذاتها. لنتحدث عن الأشكال المختلفة من الماكرو الإجرائي. سنبدأ بالماكرو المشتق الخاص ونفسر الاختلافات البسيطة التي تجعل باقي الأشكال مختلفة. كيفية كتابة ماكرو derive مخصص لننشئ وحدة مصرّفة اسمها hello_macro التي تعرف سمةً اسمها HelloMacro مع دالة مرتبطة associated اسمها hello_macro، وبدلًا من إجبار المستخدمين على تنفيذ السمة HelloMacro لكل من أنواعهم، سنؤمن ماكرو إجرائي لكي يتمكن المستخدمين من توصيف نوعهم باستخدام ‏‏‏[derive(HelloMacro)‎]‏‏# للحصول على تنفيذ افتراضي للدالة hello_macro. سيطبع النفيذ الافتراضي: Hello, Macro! My name is TypeName!‎ إذ أن TypeName هو اسم النوع المُعرّفة عليه السمة، بمعنى آخر سنكتب وحدة مصرّفة تسمح لمبرمج آخر بكتابة الشيفرة باستخدام حزمتنا المصرفة كما في الشيفرة 30. اسم الملف:src/main.rs use hello_macro::HelloMacro; use hello_macro_derive::HelloMacro; #[derive(HelloMacro)] struct Pancakes; fn main() { Pancakes::hello_macro(); } [الشيفرة 30: الشيفرة التي يستطيع مستخدم الوحدة المصرفة فيها الكتابة عند استخدام الماكرو الإجرائي الخاص بنا] ستطبع الشيفرة عندما تنتهي ما يلي: Hello, Macro! My name is Pancakes!‎ الخطوة الأولى هي إنشاء وحدة مكتبة مصرّفة على النحو التالي: $ cargo new hello_macro --lib بعدها نعرّف سمة HelloMacro والدّالة التابعة لها. اسم الملف: src/lib.rs pub trait HelloMacro { fn hello_macro(); } لدينا السمة ودوالها، ويستطيع هنا مستخدم الوحدة المصرّفة تنفيذ السمة للحصول على الوظيفة المرغوبة على النحو التالي: use hello_macro::HelloMacro; struct Pancakes; impl HelloMacro for Pancakes { fn hello_macro() { println!("Hello, Macro! My name is Pancakes!"); } } fn main() { Pancakes::hello_macro(); } ولكن سيحتاج المستخدم لكتابة كتلة التنفيذ لكل نوع يرغب باستخدامه مع hello_macro، ونريد إعفائهم من ذلك. إضافةً إلى ذلك، لا نستطيع أن نؤمّن للتابع hello_macro التنفيذ الافتراضي الذي سيطبع اسم نوع السمة المُطبقة عليه، إذ ليس لدى رست قدرة على الفهم لذا لا تستطيع البحث عن اسم النوع وقت التنفيذ، وفي هذه الحالة نحن بحاجة لماكرو لإنشاء شيفرة وقت التنفيذ. الخطوة التالية هي تعريف الماكرو الإجرائي. يحتاج الماكرو الإجرائي حتى الآن إلى وحدة مصرّفة خاصة به، ربما سيُرفع هذا التقييد بالنهاية. يأتي اصطلاح الوحدات المصرّفة الهيكلية والوحدات المصرّفة للماكرو على النحو التالي: يسمى الماكرو الإجرائي الخاص المشتق foo_derive لاسم موحدة مصرفة foo. لنبدأ بإنشاء وحدة مصرّفة جديدة اسمها hello_macro_derive داخل المشروع hello_macro. $ cargo new hello_macro_derive --lib الوحدتان المصرّفتان مرتبطتان جدًا، لذلك سننشئ وحدةً مصرّفةً للماكرو الإجرائي داخل مجلد الوحدة المصرّفة hello_macro. يجب علينا تغيير تنفيذ الماكرو الإجرائي في hello_macro_derive إذا غيرنا تعريف السمة في hello_macro أيضًا. تحتاج الوحدتان المصرّفتان أن تُنشَرا بصورةٍ منفصلة ويجب أن يضيف مستخدمو هاتين الوحدتين المصرّفتين مثل اعتماديتين dependencies وجلبهما إلى النطاق. يمكن -بدلًا من ذلك- جعل الحزمة المصرّفة hello_macro تستخدم hello_macro_derive مثل اعتمادية وتعيد تصدير شيفرة الماكرو الإجرائي ولكن الطريقة التي بنينا فيها المشروع تسمح للمبرمجين استخدام hello_macro حتى لو كانوا لا يرغبون باستخدام وظيفة derive. يجب علينا التصريح عن الوحدة المصرفة hello_macro_derive مثل وحدة مصرفة لماكرو إجرائي ونحتاج أيضًا إلى وظائف من الوحدات المصرّفة syn و quote كما سنرى بعد قليل لذا سنحتاج لإضافتهم كاعتماديات. أضِف التالي إلى ملف Cargo.toml من أجل hello_macro_derive: اسم الملف: hello_macro_derive/Cargo.toml [lib] proc-macro = true [dependencies] syn = "1.0" quote = "1.0" لنبدأ بتعريف الماكرو الإجرائي. ضع الشيفرة 31 في ملف src/lib.rs من أجل الوحدة المصرّفة hello_macro-derive. لاحظ أن الشيفرة لن تصرّف حتى نضيف التعريف لدالة impl_hello_macro. اسم الملف: hello_macro_derive/src/lib.rs use proc_macro::TokenStream; use quote::quote; use syn; #[proc_macro_derive(HelloMacro)] pub fn hello_macro_derive(input: TokenStream) -> TokenStream { // إنشاء تمثيل لشيفرة رست مثل شجرة صيغة يمكننا التلاعب بها let ast = syn::parse(input).unwrap(); // بناء تنفيذ السمة impl_hello_macro(&ast) } [الشيفرة 31: الشيفرة التي تتطلبها معظم الوحدات المصرّفة للماكرو الإجرائي لكي تعالج شيفرة رست] لاحظ أننا قسّمنا الشيفرة إلى دالة hello_macro_derive المسؤولة عن تحليل TokenStream، ودالة Impl_hello_macro المسؤولة عن تحويل شجرة الصيغة syntax tree التي تجعل كاتبة الماكرو الإجرائي لتكون أكثر ملائمة. ستكون الشيفرة في الدالة الخارجية (في هذه الحالة hello_macro_derive) هي نفسها لمعظم الوحدات المصرّفة للماكرو الإجرائي الذي تراه أو تنشئه، وستكون الشيفرة التي تحددها في محتوى الدالة الداخلية (في هذه الحالة impl_hello_macro) مختلفة اعتمادًا على غرض الماكرو الإجرائي. أضفنا ثلاث وحدات مصرّفة هي proc_macro و syn و quote. لا نحتاج لإضافة الوحدة المصرفة proc_macro إلى الاعتماديات في Cargo.toml لأنها تأتي مع رست، وهذه الوحدة المصرفة هي واجهة برمجة التطبيق للمصرف التي تسمح بقراءة وتعديل شيفرة رست من شيفرتنا. تحلّل الوحدة المصرّفة syn شيفرة رست من سلسلة نصية إلى هيكل بيانات يمكننا إجراء عمليات عليه. تحوّل الوحدة المصرّفة quote هيكل بيانات syn إلى شيفرة رست. تسهّل هذه الوحدات المصرّفة تحليل أي نوع من شيفرة رست يمكن أن نعمل عليه. تُعد كتابة محلل parser كامل لرست أمرًا صعبًا. تُستدعى دالة hello_macro_derive عندما يحدد مستخدم مكتبتنا [derive(HelloMacro)‎]# على نوع، وهذا ممكن لأننا وصفّنا دالة hello_macro_dervie باستخدام proc_macro_dervie وحددنا اسم HelloMacro الذي يطابق اسم سِمتنا، وهذا هو الاصطلاح الذي يتبعه معظم الماكرو الإجرائي. تحوّل دالة hello_macro_derive أولًا input من TokenStream إلى هيكل بيانات يمكن أن نفسره ونجري عمليات عليه. هنا يأتي دور syn. تأخذ دالة parse في syn القيمة TokenStream وتُعيد هيكل DeriveInput يمثّل شيفرة رست المحلّلة. تظهر الشيفرة 32 الأجزاء المهمة من هيكل DeriveInput التي نحصل عليها من تحليل السلسلة النصية struct Pancakes;‎. DeriveInput { // --snip-- ident: Ident { ident: "Pancakes", span: #0 bytes(95..103) }, data: Struct( DataStruct { struct_token: Struct, fields: Unit, semi_token: Some( Semi ) } ) } [الشيفرة 32: نسخة DeriveInput التي نحصل عليها من تحليل الشيفرة التي فيها سمة الماكرو في الشيفرة 30] تظهر حقول هذا الهيكل بأن شيفرة رست التي حللناها هي هيكل وحدة مع ident (اختصارًا للمعرّف، أي الاسم) الخاصة بالاسم Pancakes. هناك حقول أخرى في هذا الهيكل لوصف كل أنواع شيفرة رست. راجع وثائق syn من أجل DeriveInput لمعلومات أكثر. سنعرِّف قريبًا دالة impl_hello_macro، التي سنبني فيها شيفرة رست الجديدة التي نريد ضمها، لكن قبل ذلك لاحظ أن الخرج من الماكرو المشتق الخاص بنا هو أيضًا TokenStream، إذ تُضاف TokenStream المُعادة إلى الشيفرة التي كتبها مستخدمو حزمتنا المصرّفة، لذلك سيحصلون عند تصريف الوحدة المصرّفة على وظائف إضافية قدمناها في TokenStream المعدلة. ربما لاحظت أننا استدعينا unwrap لتجعل الدالة hello_macro_derive تهلع إذا فشل استدعاء الدالة syn::parse. يجب أن يهلع الماكرو الإجرائي على الأخطاء، لأنه يجب أن تعيد الدالة proc_macro_derive الـقيمة TokenStream بدلًا من Result لتتوافق مع واجهة برمجة التطبيقات للماكرو الإجرائي. بسّطنا هذا المثال باستخدام unwrap، إلا أنه يجب تأمين رسالة خطأ محددة أكثر في شيفرة الإنتاج باستخدام panic!‎ أو expect. الآن لدينا الشيفرة لتحويل شيفرة رست الموصّفة من TokenStream إلى نسخة DeriveInput لننشئ الشيفرة التي تطبّق سمة HelloMacro على النوع الموصّف كما تظهر الشيفرة 33. اسم الملف: hello_macro_derive/src/lib.rs fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream { let name = &ast.ident; let gen = quote! { impl HelloMacro for #name { fn hello_macro() { println!("Hello, Macro! My name is {}!", stringify!(#name)); } } }; gen.into() } [الشيفرة 33: تنفيذ سمة HelloMacro باستخدام شيفرة رست المحلّلة] نحصل على نسخة هيكل Indent يحتوي على الاسم (المُعرّف) على النوع الموصّف باستخدام ast.ident. يظهر الهيكل في الشيفرة 32 أنه عندما نفّذنا دالة impl_hello_macro على الشيفرة 30 سيكون لدى ident التي نحصل عليها حقل ident مع القيمة "Pancakes"، لذلك سيحتوي المتغير name في الشيفرة 33 نسخة هيكل Ident، الذي سيكون سلسلة نصية "Pancakes" عندما يُطبع، وهو اسم الهيكل في الشيفرة 30. يسمح لنا ماكرو quote!‎ بتعريف شيفرة رست التي نريد إعادتها. يتوقع المصرف شيئًا مختلفًا عن النتيجة المباشرة لتنفيذ ماكرو quote!‎، لذا نحتاج لتحويله إلى TokenStream، وذلك عن طريق استدعاء تابع into الذي يستهلك التعبير الوسطي ويعيد القيمة من النوع TokenStream المطلوب. يؤمن ماكرو quote!‎ تقنيات قولبة templating جيدة، إذ يمكننا إدخال ‎#‎‎name ويبدّلها quote!‎ بالقيمة الموجودة في المتغير name، ويمكنك أيضًا إجراء بعض التكرارات بطريقة مشابهة لكيفية عمل الماكرو العادي. راجع توثيق الوحدة المصرّفة quote لتعريف وافي عنها. نريد أن يُنشئ الماكرو الإجرائي تنفيذًا لسمة HelloMacro للنوع الذي يريد توصيفه المستخدم، والذي نحصل عليه باستخدام ‎#‎‎name. يحتوي تنفيذ السمة دالةً واحدةً hello_macro تحتوي على الوظيفة المراد تقديمها ألا وهي طباعة Hello, ‎Macro! My name is وبعدها اسم النوع الموصَّف. الماكرو stringify!‎ المُستخدم هنا موجود داخل رست، إذ يأخذ تعبير رست مثل 1‎ + 2‎ ويحول التعبير إلى سلسلة نصية مجرّدة مثل "‎1 +2". هذا مختلف عن format!‎ و println!‎، الماكرو الذي يقيّم التعبير ويحول القيمة إلى String. هناك احتمال أن يكون الدخل ‎#‎name تعبيرًا للطباعة حرفيًا literally، لذا نستخدم stringify!‎، الذي يوفر مساحةً محجوزةً عن طريق تحويل ‎‎#‎name إلى سلسلة نصية مجرّدة وقت التصريف. الآن، يجب أن ينتهي cargo build بنجاح في كل من hello_macro و hello_macro_derive. لنربط هذه الوحدات المصرّفة مع الشيفرة في الشيفرة 30 لنرى كيفية عمل الماكرو الإجرائي. أنشئ مشروعًا ثنائيًا جديدًا في مجلد المشاريع باستخدام cargo new pancakes. نحتاج لإضافة hello_macro و hello_macro_derive مثل اعتماديات في ملف Cargo.toml الخاص بالوحدة المصرّفة pancakes. إذا نشرت النسخ الخاصة بك من hello_macro و hello_macro_derive إلى crates.io فستكون اعتماديات عادية، وإذا لم يكونوا كذلك فبإمكانك تحديدها مثل اعتماديات path على النحو التالي: hello_macro = { path = "../hello_macro" } hello_macro_derive = { path = "../hello_macro/hello_macro_derive" } ضع الشيفرة 30 في الملف src/main.rs ونفذ cargo run يجب أن تطبع Hello, Macro! My name is Pancakes!‎. كان تنفيذ سمة HelloMacro من الماكرو الإجرائي متضمنًا دون أن تحتاج الوحدة المصرفة pancakes أن تنفّذه. أضاف [‎‎derive(HelloMac‎ro)‎]# تنفيذ السمة. سنتحدث تاليًا عن الاختلافات بين الأنواع الأُخرى من الماكرو الإجرائي من الماكرو المشتق الخاص. الماكرو الشبيه بالسمة يشابه الماكرو الشبيه بالسمة الماكرو المشتق الخاص لكن بدلًا من إنشاء شيفرة لسمة derive يسمح لك بإنشاء سمات جديدة وهي أيضًا أكثر مرونة، تعمل derive فقط مع الهياكل والـتعدادات enums، يمكن أن تطبق السمات attributes على عناصر أُخرى أيضًا مثل الدوال. فيما يلي مثال عن استخدام الماكرو الشبيه بالسمة: لنقل أن لديك سمة اسمها route توصّف الدوال عند استخدام إطار عمل تطبيق ويب: #[route(GET, "/")] fn index() { تُعرَّف سمة ‏‏[route]# بإطار العمل مثل ماكرو إجرائي. ستكون بصمة دالة تعريف الماكرو على النحو التالي: #[proc_macro_attribute] pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { لدينا هنا معاملان من النوع TokenStream، الأول هو من أجل محتوى السمة (جزء GET, "/"‎)، والثاني هو لمتن العنصر الذي ترتبط به السمة والذي هو fn index‎() {}‎ في هذه الحالة والباقي هو متن الدالة. عدا عن ذلك، تعمل الماكرو الشبيهة بالسمة بنفس طريقة الماكرو المشتق الخاص عن طريق إنشاء وحدة مصرفة مع نوع الوحدة المصرّفة proc-macro وتنفذ الدالة التي تنشئ الشيفرة المرغوبة. الماكرو الشبيه بالدالة يعرّف الماكرو الشبيه بالدالة الماكرو ليشبه استدعاءات الدوال، وعلى نحوٍ مشابه لماكرو macro_rules!‎، فهي أكثر مرونة من الدوال؛ إذ يستطيع الماكرو أخذ عدد غير معروف من الوسطاء، ولكن يمكن أن يعرّف ماكرو macro_rules!‎ فقط باستخدام صيغة تشبه المطابقة التي تحدثنا عنها سابقًا في قسم "الماكرو التصريحي مع macro_rules!‎ للبرمجة الوصفية العامة". يأخذ الماكرو الشبيه بالدالة معامل TokenStream ويعدل تعريفها القيمة TokenStream باستخدام شيفرة رست كما يفعل الماكرو الإجرائي السابق. إليك مثالًا عن ماكرو شبيه بالدالة هو ماكرو sql!‎ التي يمكن استدعاؤه على النحو التالي: #[proc_macro_attribute] pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream { يحلل هذا الماكرو تعليمة SQL داخله ويتحقق إذا كانت صياغتها صحيحة، وهذه المعالجة أعقد مما يستطيع macro_rules!‎ معالجته ويكون تعريف ماكرو sql!‎ على النحو التالي: #[proc_macro] pub fn sql(input: TokenStream) -> TokenStream { يشابه التعريف بصمة الماكرو المشتق الخاص، إذ أخذنا المفاتيح التي داخل القوسين وأعدنا الشيفرة التي نريد إنشاءها. ترجمة -وبتصرف- لقسم من الفصل Advanced Features من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: الأنواع والدوال المتقدمة في لغة رست الأنماط Patterns واستخداماتها وقابليتها للدحض Refutability في لغة رست الماكرو Macro والمعالج المسبق Preprocessor في لغة سي
  5. نستعرض في هذه المقالة كل من الأنواع types والدوال functions المتقدمة في لغة رست. الأنواع المتقدمة يحتوي نظام نوع رست على بعض الميزات التي ذكرناها سابقًا إلا أننا لم نناقشها بالتفصيل بعد، وسنبدأ بمناقشة الأنواع الجديدة newtypes بصورةٍ عامة والنظر إلى فائدتها كأنواع، ثم ننتقل إلى كتابة الاختصارات وهي ميزة مشابهة للأنواع الجديدة ولكن بدلالات مختلفة قليلًا. سنناقش أيضًا النمط ! والأنواع ذات الحجم الديناميكي dynamically sized types. استخدام نمط النوع الجديد لأمان النوع والتجريد ملاحظة: يفترض هذا القسم أنك قرأت قسم ''استخدام نمط النوع الجديد لتنفيذ سمات الخارجية على الأنواع الخارجية'' من المقال السابق مفاهيم متقدمة عن السمات Trait في لغة رست. يُعد نمط النوع الجديد مفيدًا أيضًا للمهمات التي تتجاوز تلك التي ناقشناها حتى الآن بما في ذلك الفرض الصارم بعدم الخلط بين القيم وكذلك الإشارة إلى وحدات القيمة. رأيتَ مثالًا على استخدام أنواع جديدة للإشارة إلى الوحدات في الشيفرة 15 من المقال السابق، تذكر أن هياكل Millimeters و Meters تغلف قيم u32 في نوع جديد. إذا كتبنا دالة بمحدد من النوع Millimeters فلن نتمكن من تصريف برنامج حاولَ عن طريق الخطأ استدعاء هذه الدالة بقيمة من النوع Meters أو u32 عادي. يمكننا أيضًا استخدام نمط النوع الجديد للتخلص من بعض تفاصيل التطبيق الخاصة بنوع ما، ويمكن أن يكشف النوع الجديد عن واجهة برمجية عامة API تختلف عن الواجهة البرمجية للنوع الداخلي الخاص. يمكن أن تخفي الأنواع الجديدة أيضًا التطبيق الداخلي، إذ يمكننا على سبيل المثال يمكننا منح نوع People لتغليف <HashMap<i32, String الذي يخزن معرف الشخص المرتبط باسمه. تتفاعل الشيفرة التي تستخدم People فقط مع الواجهة البرمجية العامة التي نقدمها مثل تابع لإضافة سلسلة اسم إلى مجموعة People، ولن تحتاج هذه الشيفرة إلى معرفة أننا نعيِّن معرفًا i32 للأسماء داخليًا. يعد نمط النوع الجديد طريقةً خفيفةً لتحقيق التغليف لإخفاء تفاصيل التطبيق التي ناقشناها سابقًا في قسم "التغليف وإخفاءه لتفاصيل التنفيذ" من المقال البرمجة كائنية التوجه OOP في لغة رست. إنشاء مرادفات للنوع بواسطة اسماء النوع البديلة توفّر رست القدرة على التصريح عن اسم بديل للنوع type alias لمنح نوع موجود اسمًا آخر، ونستخدم لذلك الكلمة المفتاحية type. يمكننا على سبيل المثال منح الاسم البديل Kilometers للنوع i32 على النحو التالي: type Kilometers = i32; يصبح الاسم المستعار Kilometers الآن مرادفًا للنوع i32 على عكس أنواع Millimeters و Meters التي أنشأناها في الشيفرة 15، إذ أن Kilometers ليست نوعًا جديدًا منفصلًا. ستُعامل القيم ذات النوع Kilometers نفس معاملة قيم النوع i32: type Kilometers = i32; let x: i32 = 5; let y: Kilometers = 5; println!("x + y = {}", x + y); يمكننا إضافة قيم من كلا النوعين نظرًا لأن Kilometers و i32 من النوع ذاته، كما يمكننا تمرير قيم Kilometers إلى الدوال التي تأخذ معاملات i32. لن نحصل على مزايا التحقق من النوع التي نحصل عليها من نمط النوع الجديد الذي ناقشناه سابقًا إذا استخدمنا هذا التابع، أي بعبارة أخرى إذا خلطنا قيم Kilometers و i32 في مكان ما فلن يعطينا المصرف خطأ. حالة الاستخدام الرئيسة لأسماء النوع البديلة هي تقليل التكرار، على سبيل المثال قد يكون لدينا نوع طويل مثل هذا: Box<dyn Fn() + Send + 'static> يمكن أن تكون كتابة هذا النوع المطول في بصمات الدوال ومثل تعليقات توضيحية للنوع في جميع أنحاء الشيفرة أمرًا مملًا وعرضةً للخطأ. تخيل وجود مشروع مليء بالسطر السابق كما توضح الشيفرة 24. let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi")); fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) { // --snip-- } fn returns_long_type() -> Box<dyn Fn() + Send + 'static> { // --snip-- } [الشيفرة 24: استعمال نوع طويل في أماكن كثيرة] يجعل الاسم البديل للنوع هذه الشيفرة أكثر قابلية للإدارة عن طريق تقليل التكرار، إذ قدّمنا في الشيفرة 25 اسمًا بديلًا هو Thunk للنوع المطول ويمكننا استبدال جميع استخدامات النوع بالاسم البديل الأقصر Thunk. type Thunk = Box<dyn Fn() + Send + 'static>; let f: Thunk = Box::new(|| println!("hi")); fn takes_long_type(f: Thunk) { // --snip-- } fn returns_long_type() -> Thunk { // --snip-- } [الشيفرة 25: استخدام اسم بديل Thunk لتقليل التكرار] هذه الشيفرة أسهل في القراءة والكتابة، ويمكن أن يساعد اختيار اسم ذي معنى لاسم بديل للنوع على إيصال نيتك أيضًا، إذ أن thunk هي كلمة لشيفرة تُقيَّم في وقت لاحق لذا فهو اسم مناسب للمغلّف closure الذي يُخزَّن. تُستخدم الأسماء البديلة للنوع أيضًا كثيرًا مع نوع <Result<T, E لتقليل التكرار، خذ على سبيل المثال وحدة std::io في المكتبة القياسية، إذ غالبًا ما تُعيد عمليات الدخل والخرج النوع <Result<T, E للتعامل مع المواقف التي تفشل فيها العمليات هذه، وتحتوي هذه المكتبة على هيكل std::io::Error الذي يمثل جميع أخطاء الدخل والخرج المحتملة، وتعيد العديد من الدوال في std::io النوع<Result<T, E بحيث تكون قيمة E هي std::io::Error كما هو الأمر بالنسبة للدوال الموجودة في سمة Write: use std::fmt; use std::io::Error; pub trait Write { fn write(&mut self, buf: &[u8]) -> Result<usize, Error>; fn flush(&mut self) -> Result<(), Error>; fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>; fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>; } تكررت <Result<..., Error كثيرًا، كما احتوت الوحدة std::io على هذا النوع من التصريح: type Result<T> = std::result::Result<T, std::io::Error>; يمكننا استخدام الاسم البديل المؤهل كليًا <std::io::Result<T لأن هذا التصريح موجود في الوحدة std::io، ويعني النوع السابق وجود النوع <Result<T, E مع ملء E بقيمة std::io::Error. تبدو بصمة السمة Write بنهاية المطاف على النحو التالي: pub trait Write { fn write(&mut self, buf: &[u8]) -> Result<usize>; fn flush(&mut self) -> Result<()>; fn write_all(&mut self, buf: &[u8]) -> Result<()>; fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>; } يساعد الاسم البديل للنوع بطريقتين، فهو يجعل كتابة الشيفرات أسهل ويعطينا واجهةً متسقة عبر جميع أنواع std::io، إذ نظرًا لأن الاسم البديل هو <Result<T, E ببساطة فهذا يعني أننا نستطيع استخدام أي تابع يعمل على <Result<T, E بالإضافة إلى صيغة خاصة مثل العامل ?. النوع Never الذي لا يعيد أي قيمة تمتلك لغة رست نوعًا خاصًا يدعى !، وهذا النوع معروف في لغة نظرية النوع بالنوع الفارغ empty type لأنه لا يحتوي على أي قيم، إلا أننا نفضل أن نطلق عليه اسم ''أبدًا Never'' لأنه يحلّ مكان النوع المُعاد عندما لا تُعيد الدالة أي قيمة، إليك مثالًا على ذلك: fn bar() -> ! { // --snip-- } تُقرأ الشيفرة السابقة على أنّ الدالة bar لا تُعيد أي قيمة، وتسمى الدوال التي لا تُعيد أي قيمة بالدوال المتباينة diverging functions. لا يمكننا إنشاء قيم من النوع ! لذلك لا يمكن للدالة bar أن تُعيد أي شيء. لكن ما فائدة نوع لا يمكنك أبدًا إنشاء قيم له؟ تذكر الشيفرة 5 سابقًا من المقال لغة رست غير الآمنة Unsafe Rust التي كانت جزءًا من لعبة التخمين بالأرقام، ولنعيد إنتاج جزء منها هنا في الشيفرة 26. let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; [الشيفرة 26: بنية match بذراع ينتهي بتعليمة continue] تخطينا بعض التفاصيل في هذه الشيفرة، إذ ناقشنا سابقًا في المقال بنية match للتحكم بسير برامج لغة رست، أن أذرع match يجب أن تُعيد جميعها النوع ذاته، وهذا السبب وراء عدم عمل الشيفرة التالية: let guess = match guess.trim().parse() { Ok(_) => 5, Err(_) => "hello", }; يجب أن يكون النوع guess في هذه الشيفرة عددًا صحيحًا وسلسلة وتتطلب رست أن يكون guess نوعًا واحدًا فقط. إذًا، ماذا تُعيد continue؟ كيف سُمحَ لنا بإعادة u32 من ذراع مع وجود ذراع آخر ينتهي بالتعليمة continue في الشيفرة 26؟ لربّما خمّنت ذلك فعلًا، إذ للتعليمة continue قيمة !، وذلك يعني أنه عندما تحسب رست النوع guess فإنها تنظر إلى ذراعي التطابق، الأول بقيمة u32 والأخير بقيمة !، ولأنه ليس من الممكن للقيمة ! أن تكون لها قيمة أبدًا، تقرر رست أن النوع guess هو u32. الطريقة الرسمية لوصف هذا السلوك هي أنه يمكن إجبار التعبيرات من النوع ! على أي نوع آخر. يُسمح لنا بإنهاء ذراع match هذا بالكلمة المفتاحية continue لأن continue لا تُعيد قيمة، ولكن بدلًا من ذلك يُنقل عنصر التحكم مرةً أخرى إلى أعلى الحلقة، لذلك في حالة Err لا نعيّن قيمة للمتغير guess إطلاقًا. النوع "أبدًا never" مفيدٌ في ماكرو !panic أيضًا؛ تذكر دالة unwrap التي نستدعيها على قيم <Option<T لإنتاج قيمة أو هلع بهذا التعريف: impl<T> Option<T> { pub fn unwrap(self) -> T { match self { Some(val) => val, None => panic!("called `Option::unwrap()` on a `None` value"), } } } يحدث أمر مماثل في هذه الشيفرة للشيفرة 26 ضمن match، إذ ترى رست أن val لديه النوع T و !panic من النوع ! لذا فإن نتيجة تعبير match الكلي هي T. تعمل هذه الشيفرة لأن !panic لا تنتج قيمة تنهي البرنامج. لن نعيد قيمة من unwrap في حالة None لذا فإن هذه الشيفرة صالحة. يحتوي تعبير أخير على النوع ! ألا وهو الحلقة: print!("forever "); loop { print!("and ever "); } لا تنتهي هنا الحلقة أبدًا لذا فإن ! هي قيمة التعبير، ومع ذلك لن يكون هذا صحيحًا إذا ضمنّنا break لأن الحلقة ستنتهي عندما تصل إلى break. الأنواع ذات الحجم الديناميكي والسمة Sized تحتاج رست إلى معرفة تفاصيل معينة حول الأنواع المستخدمة، مثل مقدار المساحة المراد تخصيصها لقيمة من نوع معين، وهذا يجعل من أحد جوانب نظام النوع الخاص به مربكًا بعض الشيء في البداية، تحديدًا مفهوم الأنواع ذات الحجم الديناميكي Dynamically Sized Types، ويشار إليها أحيانًا باسم DST أو الأنواع غير محددة الحجم unsized types، إذ تتيح لنا هذه الأنواع كتابة الشيفرات باستخدام قيم لا يمكننا معرفة حجمها إلا وقت التنفيذ. لنتعمق في تفاصيل النوع ذو الحجم الديناميكي المسمى str الذي استخدمناه سابقًا في جميع أنحاء السلسلة البرمجة بلغة رست، لاحظ أننا لم نقل str& وإنما str بذاتها، إذ تُعدّ من الأنواع ذات الحجم الديناميكي. لا يمكننا معرفة طول السلسلة حتى وقت التنفيذ، مما يعني أنه لا يمكننا إنشاء متغير من النوع str، ولا يمكننا أخذ وسيط من النوع str. ألقِ نظرةً على الشيفرة التالية التي لا تعمل: let s1: str = "Hello there!"; let s2: str = "How's it going?"; تحتاج رست أن تعرف مقدار الذاكرة المراد تخصيصها لأي قيمة من نوع معين ويجب أن تُستخدم جميع قيم النوع نفس المقدار من الذاكرة. إذا سمحت لنا رست بكتابة هذه الشيفرة فستحتاج قيمتي str هاتين إلى شغل المقدار ذاته من المساحة، إلا أن للقيمتين أطوال مختلفة، إذ يحتاج s1 إلى 12 بايت من التخزين ويحتاج s2 إلى 15، ولهذا السبب لا يمكن إنشاء متغير يحمل نوعًا محدد الحجم ديناميكيًا. إذًا ماذا نفعل؟ يجب أن تعلم الإجابة مسبقًا في هذه الحالة، إذ أن الحلّ هو بإنشاء الأنواع s1 و s2و str& بدلًا من str. تذكر سابقًا من قسم "شرائح السلاسل النصية" في المقال المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست أن هيكل بيانات الشريحة يخزن فقط موضع البداية وطول الشريحة، لذلك على الرغم من أن T& هي قيمة واحدة تخزن عنوان الذاكرة الخاص بالمكان الذي يوجد فيه T إلا أن str& هي قيمتان، ألا وهما عنوان str وطولها، ويمكننا على هذا النحو معرفة حجم قيمة str& في وقت التصريف، وهي ضعف طول usize، أي أننا نعرف دائمًا حجم str& بغض النظر عن طول السلسلة التي تشير إليها. هذه هي الطريقة التي تُستخدم بها الأنواع ذات الحجم الديناميكي عمومًا في رست، إذ لهذه الأنواع مقدار إضافي من البيانات الوصفية metadata التي تخزن حجم المعلومات الديناميكية. القاعدة الذهبية للأنواع ذات الحجم الديناميكي هي أنه يجب علينا دائمًا وضع قيم للأنواع ذات الحجم الديناميكي خلف مؤشر من نوع ما. يمكننا دمج str مع جميع أنواع المؤشرات، على سبيل المثال <Box<str أو <Rc<str، وقد فعلنا ذلك سابقًا ولكن بنوع ذو حجم ديناميكي مختلف، ألا وهو السمات traits، فكل سمة هي نوع ذو حجم ديناميكي يمكننا الرجوع إليه باستخدام اسم السمة. ذكرنا سابقًا في المقال استخدام كائنات السمة Object Trait في لغة رست أنه يجب وضع السمات خلف مؤشر لاستخدامها مثل كائنات سمات، مثل dyn Trait& أو <Box<dyn Trait (يمكن استخدام <Rc<dyn Trait أيضًا). توفر رست سمة Sized للعمل مع الأنواع ذات الأحجام الديناميكية لتحديد ما إذا كان حجم النوع معروفًا أم لا في وقت التصريف، إذ تُطبق هذه السمة تلقائيًا لكل شيء يُعرف حجمه في وقت التصريف، كما تضيف رست ضمنيًا تقييدًا على Sized لكل دالة عامة. يُعامل تعريف دالة عامة مثل هذه: fn generic<T>(t: T) { // --snip-- } كما لو أننا كتبنا هذا: fn generic<T: Sized>(t: T) { // --snip-- } ستعمل الدوال العامة افتراضيًا فقط على الأنواع التي لها حجم معروف في وقت التصريف، ومع ذلك يمكنك استخدام الصيغة الخاصة التالية لتخفيف هذا التقييد: fn generic<T: ?Sized>(t: &T) { // --snip-- } الصفة مرتبطة بـ Sized? تعني أن "T قد تكون أو لا تكون Sized" وهذا الترميز يلغي الافتراض الذي ينص على وجود حجم معروف للأنواع العامة وقت التصريف. صيغة Trait? بهذا المعنى متاحة فقط للسمة Sized وليس لأي سمات أخرى. لاحظ أيضًا أننا بدّلنا نوع المعامل t من T إلى T&، نظرًا لأن النوع قد لا يكون Sized فنحن بحاجة إلى استخدامه خلف نوع من المؤشرات، وفي هذه الحالة اخترنا مرجعًا. الدوال functions والمغلفات closures المتقدمة حان الوقت للتحدث عن بعض الخصائص المتقدمة المتعلقة بالمغلّفات والدوال بما في ذلك مؤشرات الدوال والمغلفات الراجعة Returing Closures. مؤشرات الدوال تحدثنا سابقًا عن كيفية تمرير المغلفات للدوال، ويمكننا أيضًا تمرير الدوال العادية للدوال. تفيد هذه التقنية عندما نريد تمرير دالة عرّفناها مسبقًا بدلًا من تعريف مغلف جديد. تُجبَر الدوال بالنوع fn (بحرف f صغير) -لا تخلط بينه وبين مغلف السمة Fn- يسمى نوع fn مؤشر دالة function pointer، ويسمح لك تمرير الدوال بمؤشرات الدوال باستخدام الدوال مثل وسطاء لدوال أُخرى. تشابه صياغة مؤشرات الدوال لتحديد معامل مثل مؤشر صياغتها في المغلفات كما تبين الشيفرة 27، إذ عرّفنا تابع add_one الذي يضيف واحد إلى معامله. تأخذ الدالة do_twice معاملين، هما مؤشر دالة لأي دالة تأخذ معامل i32 وتعيد النوع i32، وقيمة i32 واحدة. تستدعي دالة do_twice الدالة f مرتين وتمرر قيمة arg وتضيف نتيجتَي استدعاء الدالة معًا، بينما تستدعي الدالة main الدالة do_twice مع الوسيطين add_one و 5. اسم الملف: src/main.rs fn add_one(x: i32) -> i32 { x + 1 } fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 { f(arg) + f(arg) } fn main() { let answer = do_twice(add_one, 5); println!("The answer is: {}", answer); } [الشيفرة 27: استخدام نوع fn لقبول مؤشر دالة مثل وسيط] تطبع الشيفرة السابقة ما يلي: The answer is: 12 حددنا أن المعامل f في do_twice هو fn الذي يأخذ معامل واحد من النوع i32 ويُعيد i32، ويمكن بعدها استدعاء f من داخل الدالة do_twice. يمكننا في main تمرير اسم الدالة add_one على أنه الوسيط الأول إلى do_twice. على عكس المغلفات، فإن fn هو نوع وليس سمة، لذا نحدد fn مثل نوع معامل مباشرة بدلًا من تصريح معامل نوع معمم generic مع واحدة من سمات fn على أنه قيد سمة trait bound. تطبّق مؤشرات الدالة سمات المغلفة الثلاثة (Fn و FnMut و FnOnce). يعني ذلك أنه بإمكانك دائمًا تمرير مؤشر الدالة مثل وسيط لدالة تتوقع مغلفًا. هذه هي الطريقة الأفضل لكتابة الدوال باستخدام النوع المعمم وواحد من مغلف السمات بحيث يمكن للدوال الأخرى قبول دوال أو مغلفات. هناك مثال واحد تستطيع فيه قبول fn فقط وليس المغلفات وهو عندما نتعامل مع شيفرة خارجية لا تحتوي على مغلفات. يمكن لدوال لغة البرمجة سي أن تقبل الدوال مثل وسطاء، لكن ليس لديها مغلفات. لنأخذ مثالًا عن مكان استخدام مغلف معرّف ضمنيًا أو دالة مسماة، ولنتابع كيفية استخدام تابع map مقدم بسمة Iterator في المكتبة القياسية. يمكننا استخدام المغلف لاستخدام دالة map لتحويل شعاع أرقام إلى شعاع سلاسل نصية على النحو التالي: let list_of_numbers = vec![1, 2, 3]; let list_of_strings: Vec<String> = list_of_numbers.iter().map(|i| i.to_string()).collect(); أو يتسمية التابع مثل وسيط map بدلًا من المغلف على النحو التالي: let list_of_numbers = vec![1, 2, 3]; let list_of_strings: Vec<String> = list_of_numbers.iter().map(ToString::to_string).collect(); لاحظ أنه يجب استخدام الصيغة المؤهلة كليًاالتي تحدثنا عنها سابقًا في قسم "السمات المتقدمة" لأنه يوجد دوال متعددة جاهزة اسمها to_string. استخدمنا هنا الدالة to_string المعرّفة في سمة ToString التي تطبّقها المكتبة القياسية لأي نوع يطبّق Display. تذكر سابقًا من القسم "قيم التعداد" في المقال التعدادات enums في لغة رست أن اسم كل متغاير variant في تعداد enum عرّفناه يصبح أيضًا دالة تهيئة. يمكننا استخدام دوال التهيئة هذه مثل مؤشرات دالة تطبّق مغلفات السمة، ما يعني أنه يمكننا تحديد دوال التهيئة مثل وسطاء للتوابع التي تقبل المغلفات على النحو التالي: enum Status { Value(u32), Stop, } let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect(); أنشأنا هنا نسخةً من Status::Value باستخدام كل قيمة من النوع u32 ضمن المجال الذي استُدعي إليه map باستخدام دالة التهيئة Status::Value. يفضّل بعض الناس هذه الطريقة وآخرون يفضلون استخدام المغلفات، النتيجة بعد التصريف مماثلة للطريقتين لذا استخدم الطريقة الأوضح بالنسبة لك. إعادة المغلفات تُمثل المغلفات بسمات، ما يعني أنه لا يمكن إعادة المغلفات مباشرةً، إذ يمكنك استخدام النوع الحقيقي الذي ينفذ السمة مثل قيمة معادة للدالة في معظم الحالات عندما تريد إعادة سمة بدلًا من ذلك، ولكن لا يمكنك فعل ذلك في المغلفات لأنها لا تحتوي نوعًا حقيقيًا يمكن إعادته. على سبيل المثال، يُمنع استخدام مؤشرات الدالة fn مثل نوع مُعاد. تحاول الشيفرة التالية إعادة مغلف مباشرةً، ولكنها لن تُصرّف. fn returns_closure() -> dyn Fn(i32) -> i32 { |x| x + 1 } يكون خطأ المصرّف على النحو التالي: $ cargo build Compiling functions-example v0.1.0 (file:///projects/functions-example) error[E0746]: return type cannot have an unboxed trait object --> src/lib.rs:1:25 | 1 | fn returns_closure() -> dyn Fn(i32) -> i32 { | ^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time | = note: for information on `impl Trait`, see <https://doc.rust-lang.org/book/ch10-02-traits.html#returning-types-that-implement-traits> help: use `impl Fn(i32) -> i32` as the return type, as all return paths are of type `[closure@src/lib.rs:2:5: 2:8]`, which implements `Fn(i32) -> i32` | 1 | fn returns_closure() -> impl Fn(i32) -> i32 { | ~~~~~~~~~~~~~~~~~~~ For more information about this error, try `rustc --explain E0746`. error: could not compile `functions-example` due to previous error يشير الخطأ إلى سمة Sized مجددًا. لا تعرف رست ما هي المساحة اللازمة لتخزين المغلف، وقد عرفنا حل هذه المشكلة سابقًا، إذ يمكننا استخدام كائن سمة. fn returns_closure() -> Box<dyn Fn(i32) -> i32> { Box::new(|x| x + 1) } تُصرّف الشيفرة بصورةٍ اعتيادية هنا. راجع المقال استخدام كائنات السمة Object Trait في لغة رست. سنتحدث لاحقًا عن الماكرو. ترجمة -وبتصرف- لقسم من الفصل Advanced Features من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: مفاهيم متقدمة عن السمات Trait في لغة رست تنفيذ نمط تصميمي Design Pattern كائني التوجه Object-Oriented في لغة رست الملكية Ownership في لغة رست
  6. ذكرنا سابقًا في الفصل تخزين لائحة من القيم باستخدام الأشعة Vectors وما بعده أن أحد قيود الشعاع vector هي تخزينه لعناصر من نوع واحد فقط، وقد أنشأنا حلًا بديلًا فيما بعد في الشيفرة 8 من الفصل الأخطاء والتعامل معها في لغة رست Rust، إذ عرّفنا التعداد SpreadsheetCell وداخله متغايرات variants تحتوي على أعداد صحيحة integers وعشرية floats ونص text، وهذا يعني أنه يمكننا تخزين أنواع مختلفة من البيانات في كل خلية مع المحافظة على شعاع يمثل صفًا من الخلايا. يعد هذا حلًا جيدًا عندما تمثّل العناصر القابلة للتبديل مجموعةً ثابتةً من الأنواع التي نعرّفها عند تصريف الشيفرة البرمجية الخاصة بنا. نريد أحيانًا أن يتمكن مستخدم مكتبتنا من توسيع مجموعة الأنواع الصالحة في حالة معينة، ولإظهار كيف يمكننا تحقيق ذلك سننشئ مثالًا لأداة واجهة المستخدم الرسومية graphical user interface ‎ -أو اختصارًا GUI- التي تتكرر من خلال قائمة من العناصر مستدعيةً تابع draw على كل عنصر لرسمه على الشاشة، وهي تقنية شائعة لأدوات واجهة المستخدم الرسومية. سننشئ وحدة مكتبة مصرّفة library crate تدعى gui تحتوي على هيكل مكتبة لأدوات واجهة المستخدم الرسومية GUI، وقد تتضمن هذه الوحدة المصرّفة بعض الأنواع ليستخدمها الأشخاص، مثل Button، أو TextField، كما سيرغب مستخدمو gui بإنشاء أنواعهم الخاصة التي يمكن رسمها، فعلى سبيل المثال قد يضيف أحد المبرمجين Image وقد يضيف آخر SelectBox. لن نبرمج كامل مكتبة GUI في هذا المثال لكننا سنبين كيف ستعمل الأجزاء معًا. لا يمكننا في وقت كتابة المكتبة معرفة وتعريف جميع الأنواع التي قد يرغب المبرمجون الآخرون بإنشائها، لكننا نعلم أن gui تحتاج إلى تتبُّع العديد من القيم ومن أنواع مختلفة وتحتاج إلى استدعاء تابع draw على كل من هذه القيم المكتوبة بصورةٍ مختلفة. لا يتطلب الأمر معرفة ماذا سيحدث فعلًا عندما نستدعي تابع draw، ومن الكافي معرفة أن القيمة ستحتوي على تابع متاح ضمنها يمكننا استدعاؤه. لتحقيق هذا الأمر في لغة برمجة تحتوي على خاصية التوريث قد نعرّف صنفًا يدعى Component له تابع يسمى draw. ترث الأصناف الأخرى، مثل Button، و Image، و SelectBox من Component، وبالتالي ترث تابع draw. يمكن لكل من الأصناف السابقة إعادة تعريف تابع draw لتعريف سلوكهم المخصص، لكن إطار العمل framework قد يتعامل مع جميع الأنواع كما لو كانت نُسخًا instance من Component ويستدعي التابع draw عليها، ولكن بما أن رست لا تحتوي على توريث، فنحن بحاجة إلى طريقة أخرى لهيكلة مكتبة gui للسماح للمستخدمين بتوسيعها بأنواع جديدة. تعريف سمة لسلوك مشترك سنعرّف سمةً باسم Draw يكون لها تابعٌ واحد يسمى draw، لتنفيذ السلوك الذي نريد من الوحدة المصرّفة gui أن تملكه. بعد ذلك، يمكننا تعريف شعاع يأخذ كائن سمة trait object، الذي يشير إلى نسخة من نوع ينفّذ السمة المحددة لدينا، إضافةً إلى جدول يُستخدم للبحث عن توابع السمات في هذا النوع في وقت التنفيذ. ننشئ كائن سمة عن طريق استخدام نوع من المؤشرات مثل المرجع &، أو المؤشر الذكي Box<T>‎، متبوعًا بالكلمة المفتاحية dyn، ثم تحديد السمة ذات الصلة. سنتحدث عن السبب الذي يجعل من المحتمل لكائنات السمة أن تستخدم مؤشرًا لاحقًا. يمكننا استخدام كائنات السمات بدلًا من النوع المعمم أو الحقيقي. يُضمّن نظام النوع في رست وقت التصريف أينما نستخدم كائن سمة، وإن أي قيمة مُستخدمة في هذا السياق ستنفّذ سمة كائن السمة، وبالتالي لا نحتاج إلى معرفة جميع الأنواع الممكنة وقت التصريف. لقد ذكرنا أننا نمتنع في رست عن تسمية الهياكل structs والتعدادات enums بالكائنات لتمييزها عن كائنات اللغات البرمجية الأخرى؛ إذ تُفصل البيانات الموجودة في البنية أو التعداد في حقول الهيكل والسلوك في كتل impl؛ بينما تُسمّى البيانات والسلوك معًا في اللغات الأخرى غالبًا مثل كائن. مع ذلك، تشبه كائنات السمة إلى حد كبير الكائنات في اللغات الأخرى بمعنى أنها تجمع بين البيانات والسلوك، لكن تختلف كائنات السمة عن الكائنات التقليدية في أنه لا يمكننا إضافة بيانات إلى كائن سمة. لا تعدّ كائنات السمة مفيدةً عمومًا مثل الكائنات في اللغات الأخرى، إذ أن الغرض المحدد منها هو السماح بالتجريد عبر سلوكها المشترك. توضّح الشيفرة 3 كيفية تعريف سمة تسمىDraw مع تابع واحد يسمى draw. اسم الملف: src/lib.rs pub trait Draw { fn draw(&self); } [الشيفرة 3: تعريف السمة Draw] يجب أن تبدو الشيفرة السابقة مألوفةً من حديثنا عن كيفية تعريف السمات سابقًا في الفصل مقدمة إلى مفهوم الأنواع المعممة Generic Types. إلا أن هناك بعض الأشياء الجديدة: إذ تعرٍّف الشيفرة 4 هيكلًا يدعى Screen يحمل شعاعًا باسم components. هذا الشعاع من النوع Box<dyn Draw>‎، وهو كائن سمة، ويُعدّ بديلًا لأي نوع داخل Box ينفّذ السمة Draw. اسم الملف: src/lib.rs pub struct Screen { pub components: Vec<Box<dyn Draw>>, } [الشيفرة 4: تعريف هيكل Screen مع حقل components الذي يحمل شعاعًا من كائنات سمة تطبّق السمة Draw] نعرّف على هيكل Screen تابعًا يدعى run يستدعي التابع draw على كل من components الخاصة به كما هو موضح في الشيفرة 5. اسم الملف: src/lib.rs impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } } [الشيفرة 5: تابع run على Screen الذي يستدعي تابع draw على كل مكون] يعمل هذا بصورةٍ مختلفة عن تعريف هيكل يستخدم معامل نوع معمّم generic type مع حدود السمة trait bounds، إذ لا يمكن استبدال معامل النوع المعمم إلا بنوع واحد صريح في كل مرة، بينما تسمح كائنات السمة بأنواع حقيقية متعددة لتحل مكان كائن السمة وقت التنفيذ. على سبيل المثال، كان من الممكن أن نعرّف هيكل Screen باستخدام نوع معمم وحدود سمة كما في الشيفرة 6. اسم الملف: src/lib.rs pub struct Screen<T: Draw> { pub components: Vec<T>, } impl<T> Screen<T> where T: Draw, { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } } [الشيفرة 6: تنفيذ بديل لهيكل Screen وتابعه run باستخدام أنواع معممة وحدود السمة] يقيّدنا هذا بنسخة Screen التي تحتوي على قائمة من المكونات جميعها من النوع Button، أو من النوع TextField؛ فإذا كان لديك مجموعات متجانسة homogeneous collections فقط، يُفضّل استعمال أنواع معممة وحدود السمات لأن التعريفات ستكون أحادية الشكل monomorphized في وقت التصريف لاستخدام الأنواع الفعلية. من جهة أخرى، يمكن لنسخة Screen واحدة أن تحمل النوع Vec<T>‎ باستخدام التابع الذي يستخدم كائنات السمة، الذي يحتوي بدوره على <Box<Button، إضافةً إلى <Box<TextField. لنلقي نظرةً على كيفية عمل ذلك، ثم سنتحدث عن الآثار المترتبة على وقت التنفيذ. تنفيذ السمة سنضيف الآن بعض الأنواع التي تنفّذ السمة Draw، وسنأخذ النوع Button مثالًا على هذه الأنواع. يُعد تنفيذ مكتبة GUI كما ذكرنا سابقًا خارج موضوعنا هنا، لذا لن يكون لتابع draw أي تنفيذ فعلي داخله. لنتخيل الشكل الذي قد يبدو عليه التنفيذ، فقد يحتوي هيكل Button على حقول لكل من width و height و label كما هو موضح في الشيفرة 7. اسم الملف: src/lib.rs pub struct Button { pub width: u32, pub height: u32, pub label: String, } impl Draw for Button { fn draw(&self) { // الشيفرة البرمجية المسؤولة عن رسم الزر } } [الشيفرة 7: هيكل Button الذي بطبّق السمة Draw] ستختلف حقول width و height و label الموجودة في Button عن الحقول الموجودة في المكونات الأخرى، فعلى سبيل المثال قد يحتوي النوع TextField نفس الحقول، إضافةً إلى حقل placeholder. ستنفّذ كل الأنواع التي نريد رسمها على الشاشة سمة Draw، لكن ستستعمل شيفرةً برمجيةً مختلفة في التابع draw لتعريف كيفية رسم ذلك النوع تحديدًا، كما في Button هنا (بدون شيفرة برمجية لمكتبة GUI فعلية كما ذكرنا سابقًا). قد يحتوي النوع Button على سبيل المثال كتلة impl إضافية تحتوي على توابع مرتبطة بما يحدث عندما يضغط مستخدم الزر، ولا تنطبق هذه الأنواع من التوابع على أنواع مثل TextField. إذا قرر شخص ما استعمال مكتبتنا لتطبيق هيكل SelectBox الذي يحتوي الحقولwidth و height و options، فسينفّذ سمة Draw على النوع SelectBox أيضًا كما هو موضح بالشيفرة 8. اسم الملف: src/main.rs use gui::Draw; struct SelectBox { width: u32, height: u32, options: Vec<String>, } impl Draw for SelectBox { fn draw(&self) { // الشيفرة البرمجية المسؤولة عن رسم صندوق الاختيار } } [الشيفرة 8: وحدة مصرفة أخرى تستعمل gui وتنفذ سمة Draw على هيكل SelectBox] يمكن لمستخدم مكتبتنا الآن كتابة الدالة main ليُنشئ نسخةً من Screen، ثم إضافة كل من SelectBox و Button لنسخة Screen بوضع كل واحدة منها في <Box<T لتصبح سمة كائن، ويمكنه بعد ذلك استدعاء التابع run على نسخةScreen التي ستستدعي draw على كل من المكونات، وتوضح الشيفرة 9 التطبيق المذكور. اسم الملف: src/main.rs use gui::{Button, Screen}; fn main() { let screen = Screen { components: vec![ Box::new(SelectBox { width: 75, height: 10, options: vec![ String::from("Yes"), String::from("Maybe"), String::from("No"), ], }), Box::new(Button { width: 50, height: 10, label: String::from("OK"), }), ], }; screen.run(); } [الشيفرة 9: استخدام كائنات السمة لتخزين قيم لأنواع مختلفة تنفذ السمة ذاتها] لم نفترض عند كتابتنا للمكتبة بأن شخصًا ما قد يضيف النوع SelectBox، إلا أن تطبيق Screen لدينا قادرٌ على العمل مع النوع الجديد ورسمه، وذلك لأن SelectBox ينفّذ سمة Draw، ما يعني أنه ينفّذ تابع draw. هذا المفهوم - المتمثل بالاهتمام فقط بالرسائل التي تستجيب لها القيمة بدلًا من النوع الحقيقي للقيمة - مشابهٌ لمفهوم كتابة البطة duck typing في اللغات البرمجية المكتوبة ديناميكيًا؛ بمعنى أنه إذا كان شيء ما يسير مثل البطة ويصدر صوتًا مثل البطة، فيجب أن يكون بطة لا محالة. لا يحتاج run إلى معرفة النوع الحقيقي لكل مكون عند تنفيذ run على Screen في الشيفرة 5، فهو لا يتحقق ما إذا كان المكوِّن نسخةً من النوع Button أو SelectBox بل يستدعي فقط التابع draw على المكوّن. عرّفنا Screen لتحتاج إلى قيم يمكننا استدعاء تابع draw عليها من خلال تحديد <Box<dyn Draw على أنه نوع القيم في الشعاع components. ميزة استخدام كائنات السمة ونظام نوع رست لكتابة شيفرة برمجية مشابهة للشيفرة البرمجية التي تستعمل كتابة البطة هي أننا لا نضطر أبدًا إلى التحقّق ما إذا كانت القيمة تنفذ تابعًا معينًا وقت التنفيذ، أو القلق بشأن حدوث أخطاء إذا كانت القيمة لا تنفّذ التابع، لكننا نستدعيه بغضّ النظر عن ذلك. لن تصرّف رست الشيفرة البرمجية الخاصة بنا إذا كانت القيم لا تنفّذ السمات التي تحتاجها كائنات السمة. تُظهر الشيفرة 10 على سبيل المثال ما يحدث إذا حاولنا إنشاء Screen مع String مثل مكوِّن: اسم الملف: src/main.rs use gui::Screen; fn main() { let screen = Screen { components: vec![Box::new(String::from("Hi"))], }; screen.run(); } [الشيفرة 10: محاولة استخدام نوع لا ينفّذ سمة كائن السمة] سنحصل على الخطأ التالي، وذلك لأن String لا ينفّذ السمة Draw: $ cargo run Compiling gui v0.1.0 (file:///projects/gui) error[E0277]: the trait bound `String: Draw` is not satisfied --> src/main.rs:5:26 | 5 | components: vec![Box::new(String::from("Hi"))], | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String` | = help: the trait `Draw` is implemented for `Button` = note: required for the cast from `String` to the object type `dyn Draw` For more information about this error, try `rustc --explain E0277`. error: could not compile `gui` due to previous error يتيح لنا هذا الخطأ معرفة ما إذا كنّا نمرّر شيئًا ما إلى Screen لم نقصد تمريره، ولذا يجب أن نمرّر نوعًا مختلفًا أو يجب أن نطبّق Draw على String حتى تتمكن Screen من استدعاء draw عليه. الإرسال الديناميكي لكائنات السمة باستذكار حديثنا عن عملية توحيد الشكل monomorphization المنفذة بواسطة المصرف عندما نستخدم حدود السمة على الأنواع المعممّة وذلك في قسم (أداء الشيفرة باستعمال الأنواع المعممة) في الفصل كيفية استخدام أنواع البيانات المعممة Generic Data Types: يولّد المصرّف تطبيقات غير معممّة للدوال والتوابع لكل نوع حقيقي نستخدمه بدلًا من معامل نوع مُحدّد. تنجز الشيفرة البرمجية الناتجة من عملية توحيد الشكل إيفادًا ساكنًا static dispatch، والذي يحدث عندما يعرِف المصرّف التابع الذي تستدعيه وقت التصريف. يتعارض هذا مع الإيفاد الديناميكي الذي يحدث عندما يتعذر على المصرف أن يخبرك بالتابع الذي تستدعيه وقت التصريف. يرسل المصرّف في حالات الإيفاد الديناميكي شيفرة برمجية تُحدّد في وقت التنفيذ التابع الذي يجب استدعاؤه. يجب أن تستخدم رست الإيفاد الديناميكي عندما نستخدم كائنات السمة، إذ لا يعرف المصرّف جميع الأنواع الممكن استعمالها مع الشيفرة البرمجية التي تستخدم كائنات السمة، لذا فهو لا يعرف التابع الذي يُنفّذ على النوع المطلوب استدعاؤه. وتستخدم رست بدلًا من ذلك المؤشرات داخل كائن السمة في وقت التنفيذ لمعرفة التابع الذي يجب استدعاؤه، ويتسبب هذا البحث بزيادة في وقت التنفيذ مقارنةً بالإيفاد الثابت. يمنع الإيفاد الديناميكي أيضًا المصرف من اختيار تضمين شيفرة التابع البرمجية التي تمنع بدورها بعض التحسينات، ومع ذلك فقد حصلنا على مزيد من المرونة في الشيفرة البرمجية التي كتبناها في الشيفرة 5 وتمكنا من دعمها في الشيفرة 9، لذا فهي مقايضة يجب أخذها بالحسبان. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming Features of Rust من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: البرمجة كائنية التوجه OOP في لغة رست مقدمة إلى مفهوم الأنواع المعممة Generic Types أنواع البيانات Data Types في لغة رست
  7. لا يوجد إجماع في مجتمع البرمجة حول الميزات التي يجب أن تكون موجودة في لغة البرمجة حتى تكون لغة كائنية التوجه، وتتأثر رست بالعديد من نماذج البرمجة programming paradigms بما في ذلك البرمجة كائنية التوجه، إذ اكتشفنا الميزات التي جاءت من البرمجة الوظيفية functional programming سابقًا بدءًا من الفصل المغلفات closures. يمكن القول أن اللغات كائنية التوجه تشترك ببعض الميزات المشتركة وهي الكائنات objects والتغليف encapsulation والوراثة inheritance، لنلقي نظرةً على ما تعنيه كل من هذه الميزات وما إذا كانت رست تدعمها. الكائنات واحتوائها على بيانات وسلوك يُعدّ الكتاب "أنماط التصميم: عناصر البرمجيات الموجهة للكائنات القابلة لإعادة الاستعمال Design Patterns: Elements of "Reusable Object-Oriented Software للمؤلفين إريك جاما Erich Gamma،وريتشارد هيلم Richard Helm، ورالف جونسون Ralph Johnson، وجون فليسيديس John Vlissides، التابع لدار النشر (Addison-Wesley Professional, 1994)، والذي يشار إليه بالعامية كتاب عصابة الأربعة The Gang of Four book، فهرسًا لأنماط التصميم كائنية التوجه، ويعرّف الكتاب البرمجة كائنية التوجّه بهذه الطريقة: بالنظر للتعريف السابق تكون رست لغة كائنية التوجه؛ إذ تحتوي الهياكل structs والتعدادات enums على البيانات، وتقدّم كتل impl توابعًا على الهياكل والتعدادات، وعلى الرغم من أن الهياكل والتعدادات ذات التوابع لا تدعى بالكائنات إلا أنها تقدّم الوظيفة نفسها وذلك بحسب تعريف الكائنات في كتاب عصابة الأربعة. ويمكنك الرجوع إلى توثيق أنماط التصميم العربي في موسوعة حسوب لمزيد من التفاصيل. التغليف وإخفاءه لتفاصيل التنفيذ هناك جانبٌ آخر مرتبط جدًا بالبرمجة كائنية التوجه وهو فكرة التغليف، والتي تعني أن تفاصيل تطبيق كائن ما لا يمكنها الوصول للشيفرة البرمجية من خلال هذا الكائن، لذا فإن الطريقة الوحيدة للتفاعل مع كائن ما هي من خلال واجهة برمجية عامة Public API خاصة به، وينبغي ألا تكون الشيفرة البرمجية التي يستخدمها الكائن قادرةً على الوصول إلى الأجزاء الداخلية للكائن وتغيير البيانات أو السلوك مباشرةً، وهذا يمكّن المبرمج من تغيير وإعادة تشكيل العناصر الداخلية للكائن دون الحاجة إلى تغيير الشيفرة البرمجية التي تستخدم الكائن. ناقشنا كيفية التحكم في التغليف سابقًا بدءًا من الفصل الحزم packages والوحدات المصرفة crates، إذ يمكننا استخدام الكلمة المفتاحية pub لتحديد أي من الوحدات modules والأنواع types والدوال functions والتوابع methods في الشيفرات البرمجية الخاصة بنا التي ينبغي أن تكون عامة، ويكون كل شيء آخر خاص افتراضيًا، فعلى سبيل المثال يمكننا تعريف هيكل AveragedCollection يحتوي على حقل يضمّ شعاعًا vector بقيم i32، كما يمكن للهيكل أيضًا أن يحتوي على حقل يضمّ متوسط القيم في الشعاع مما يعني أنه لا لزوم لحساب المتوسط عند الطلب كلما احتاجه أي أحد. بعبارة أخرى سيخزّن AveragedCollection المتوسط الناتج. تحتوي الشيفرة 1 على تعريف لهيكل AveragedCollection: اسم الملف: src/lib.rs pub struct AveragedCollection { list: Vec<i32>, average: f64, } [الشيفرة 1: هيكل AveragedCollection الذي يخزّن قائمة من الأعداد الصحيحة والمتوسط لعناصر التجميعة] الهيكل مُشار إليه بالكلمة المفتاحية pub، بحيث يمكن لشيفرة برمجية أخرى استخدام ذلك الهيكل، إلا أن الحقول الموجودة داخل الهيكل تبقى خاصة. هذا مهمٌ في هذه الحالة لأننا نريد التأكد من أنه كلما أُضيفت قيمة أو أُزيلت من الشيفرة يُحَدَّث المتوسط أيضًا، ونحقق ذلك من خلال تطبيق توابع add و remove و average كما هو موضح في الشيفرة 2: اسم الملف: src/lib.rs impl AveragedCollection { pub fn add(&mut self, value: i32) { self.list.push(value); self.update_average(); } pub fn remove(&mut self) -> Option<i32> { let result = self.list.pop(); match result { Some(value) => { self.update_average(); Some(value) } None => None, } } pub fn average(&self) -> f64 { self.average } fn update_average(&mut self) { let total: i32 = self.list.iter().sum(); self.average = total as f64 / self.list.len() as f64; } } [الشيفرة 2: تطبيق التوابع العامة add وremove وaverage على AveragedCollection] تعدّ التوابع العامة add و remove و average الوسائل الوحيدة للوصول إلى البيانات أو تعديلها في نسخ من AveragedCollection. يُستدعى التابع الخاص update_average عندما يضاف عنصر على list باستخدام التابع add أو يُزال باستخدام التابع remove، وهو التابع الذي يحدّث حقل average بدوره. نترك حقول list و average خاصة لكي لا يبقى أي وسيلة للشيفرة برمجية الخارجية أن تضيف أو تزيل عناصر إلى أو من حقل list مباشرةً، وإلّا، يمكن للحقل average ألّا يتوافق مع القيم عندما تتغير list. يعيد تابع average القيمة في حقل average مما يسمح للشيفرة البرمجية الخارجية أن تقرأ average دون أن تعدل عليها. بما أننا غلفنا تفاصيل تنفيذ الهيكل AveragedCollection، يمكننا بسهولة مستقبلًا تغيير بعض التفاصيل مثل هيكل البيانات، فعلى سبيل المثال، يمكننا استعمال <HashSet<i32 بدلًا من <Vec<i32 لحقل list. بما أن بصمات التوابع العامة add و remove و average بقيت على حالها، فلا ضرورة للتعديل على الشيفرة البرمجية التي تستخدم AveragedCollection. ولكنّ هذا الأمر لن يكون محققًا إذا جعلنا list عامة بدلًا من ذلك، إذ تملك <HashSet<i32 و <Vec<i32 توابعًا مختلفة لإضافة وإزالة العناصر بحيث ستحتاج إلى تغيير الشيفرة البرمجية الخارجية غالبًا في حال كانت تعدل على list مباشرةً. إذا كان التغليف جزءًا مطلوبًا للغة البرمجة حتى تصبح لغة كائنية التوجه، فإن رست تلبي هذا المطلب، إذ يتيح خيار استعمال pub أو عدم استعماله لأجزاء مختلفة من الشيفرة، التغليف لتفاصيل التنفيذ. الوراثة واستخدامها مثل نظام نوع ومشاركة الشيفرة البرمجية الوراثة هي آلية يمكن بواسطتها للكائن أن يرث عناصر من تعريف كائن آخر وبالتالي يكتسب بيانات الكائن الأصل وسلوكه دون الحاجة إلى تعريفها مرةً أخرى. إذا كانت الوراثة متطلبًا للغة برمجية حتى تكون اللغة كائنية التوجه فإن رست ليست بلغة كائنية التوجه، إذ لا توجد طريقة لتعريف هيكل يرث حقول وتطبيقات تابع الهيكل الأصلي دون استخدام ماكرو، ومع ذلك إذا كنت معتادًا على وجود الوراثة في اللغة البرمجية التي تتعامل معها، فيمكنك استخدام حلول أخرى في رست بحسب سبب حاجتك للوراثة. قد تحتاج للتوريث لسببين رئيسيين؛ أحدهما لإعادة استعمال الشيفرة البرمجية، ويمكنك في هذه الحالة تنفيذ سلوك معين لنوع واحد، ويمكّنُك التوريث بدوره من إعادة استعمال هذا التنفيذ لنوع مختلف. يمكنك استخدام هذه الوسيلة بطريقة محدودة في شيفرة رست باستخدام تطبيقات تابع السمة الافتراضية التي رأيتها في الشيفرة 14 من فصل السمات Traits عندما أضفنا تنفيذًا افتراضيًا لتابع summarize على السمة Summary. سيُتاح لأي نوع يطبق السمة Summary التابع summarize دون أي شيفرة برمجية إضافية، وهذا مشابه للصنف الأب parent class الذي يحتوي على تنفيذ لتابع وصنف ابن يرث الصف الأب ويحتوي على تنفيذ التابع. يمكننا أيضًا تجاوز التطبيق الافتراضي لتابع summarize عندما نطبق السمة Summary التي تشبه الصنف الابن الذي يُعيد تعريف تابع موروث من صنف أب. تتعلق الحاجة الأخرى لاستخدام التوريث بنظام النوع بتمكين استعمال نوع فرعي في نفس الأماكن مثل النوع الأصل. يسمى هذا أيضًا التعددية الشكلية polymorphism مما يعني أنه يمكنك استبدال كائنات متعددة ببعضها في وقت التنفيذ إذا كانت تشترك في خصائص معينة. التعددية الشكلية Polymorphism ينظر الكثير من الناس إلى التعددية الشكلية polymorphism بكونها مشابهة للوراثة لكنها فعليًا مفهوم أوسع، إذ تشير إلى الشيفرة البرمجية التي يمكنها العمل مع بيانات ذات أنواع مختلفة، بينما تكون هذه الأنواع بالنسبة للوراثة أصنافًا فرعيةً subclass عمومًا. يستخدم رست أنواع معممة generics بدلًا من هذا لتجريد الأنواع المختلفة الممكنة وحدود السمات trait bounds، وذلك لفرض قيود على ما يجب أن توفره هذه الأنواع، إذ يسمى ذلك أحيانًا بالتعددية الشكلية المحدودة المقيّدة bounded parametric polymorphism. فقد التوريث مكانته مؤخرًا مثل حل برمجي تصميمي في العديد من لغات البرمجة لأنه غالبًا ما يكون عرضةً لخطر مشاركة شيفرة برمجية زيادةً عن اللزوم. لا يجب أن تشترك الأصناف الابن دائمًا في جميع خصائص صنفها الأب ولكنها ستفعل ذلك مع التوريث، ومن شأن ذلك جعل تصميم البرنامج أقل مرونة. يُضيف ذلك أيضًا إمكانية استدعاء توابع على الأصناف الابن التي لا معنى لها أو التي تتسبب بأخطاء لأن التوابع لا تنطبق على الأصناف الابن. إضافةً إلى ذلك تسمح بعض اللغات فقط بالوراثة الفردية single inheritance (بمعنى أنه يمكن للصنف الابن أن يرث فقط من صنف واحد) مما يقيد مرونة تصميم البرنامج أكثر. تتخذ رست لهذه الأسباب طريقةً مختلفة في استعمال كائنات السمة بدلاً من الوراثة، وسنلقي نظرةَ على كيفية تمكين كائنات السمة من تعدد الأشكال في رست. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming Features of Rust من كتاب The Rust Programming Language. اقرأ أيضًا الفصل السابق: تزامن الحالة المشتركة Shared-State Concurrency في لغة رست وتوسيع التزامن مع Send و Sync البرمجة كائنية التوجه ما هي التعددية الشكلية polymorphism؟ كيفية تطبيق التعددية الشكلية (Polymorphism) على الأصناف في بايثون مقدمة إلى البرمجة الوظيفية Functional Programming
  8. يُعدّ تمرير الرسائل طريقةً جيدةً للتعامل مع التزامن ولكنها ليست الطريقة الوحيدة، إذ أن هناك طريقةٌ أخرى لوصول خيوط threads متعددة إلى ذات بيانات المُشاركة. ضع بالحسبان هذا الجزء من الشعار من توثيق لغة جو Go "لا تتواصل بمشاركة الذاكرة". كيف سيبدو التواصل من خلال مشاركة الذاكرة؟ وبالإضافة إلى ذلك، لماذا يحذر المدافعون عن تمرير الرسائل message-passing من استخدام مشاركة الذاكرة memory sharing؟ تشبه القنوات channels في أي لغة برمجة -بطريقة ما- الملكية الفردية لأنه بمجرد نقلك لقيمة ما إلى قناة يجب ألا تستخدم هذه القيمة بعدها. يشبه تزامن الذاكرة المشتركة الملكية المتعددة multiple ownership، إذ يمكن للخيوط المتعددة أن تصل إلى موقع الذاكرة ذاته في الوقت نفسه. كما رأينا سابقًا في مقال المؤشرات الذكية Smart Pointers في رست، فقد جعلت المؤشرات الذكية smart pointers الملكية المتعددة ممكنة، ويمكن للملكية المتعددة أن تعقد الأمر لأن الملّاك المختلفين بحاجة إلى إدارة. يساعد نظام رست وقواعد الملكية الخاصة به كثيرًا في جعل عملية الإدارة صحيحة. لنلقي على سبيل المثال نظرةً على كائنات المزامنة mutexes التي تعدّ واحدةً من أكثر بدائل التزامن شيوعًا للذاكرة المشتركة. استعمال كائنات المزامنة للسماح بالوصول للبيانات عن طريق خيط واحد بالوقت ذاته كائن المزامنة Mutex هو اختصارٌ للاستبعاد المتبادل mutual exclusion، أي يسمح كائن المزامنة لخيط واحد فقط أن يصل إلى بعض البيانات في أي وقت، وللوصول إلى البيانات في كائن المزامنة يجب أن يشير الخيط أولًا إلى أنه يريد الوصول عن طريق طلب الحصول على قفل كائن المزامنة mutex's lock؛ والقفل هو هيكل بيانات data structure يعد جزءًا من كائن المزامنة الذي يتتبع من لديه حاليًا وصولٌ حصري إلى البيانات، وبالتالي يُوصَف كائن المزامنة بأنه يحمي البيانات التي يحملها عبر نظام القفل. لكائنات المزامنة سمعة بأنها صعبة الاستعمال لأنك يجب أن تتذكر قاعدتين: يجب أن تحاول الحصول على القفل قبل استعمال البيانات. يجب أن تلغي قفل البيانات عندما تنتهي من البيانات التي يحميها كائن المزامنة حتى يتسنّى للخيوط الأخرى الحصول على القفل. لنأخذ تشبيهًا حقيقيًا لكائن المزامنة: تخيل حلقة نقاش في مؤتمر بميكروفون واحد فقط، إذ يجب على أحد أعضاء اللجنة أن يسأل أو يشير إلى أنه يريد استخدام الميكروفون عند رغبته بالتحدث وعندما يحصل على الميكروفون يمكنه التحدث بقدر ما يريد من الوقت ثم يسلّم الميكروفون إلى عضو اللجنة التالي الذي يطلب التحدث؛ وإذا نسي أحد أعضاء اللجنة تسليم الميكروفون عند الانتهاء منه فلن يتمكن أي شخص آخر من التحدث؛ إذا حدث خطأ في إدارة الميكروفون المشترك فلن تعمل حلقة النقاش على النحو المطلوب. قد تكون إدارة كائنات المزامنة صعبة جدًا وهذا هو سبب تفضيل الكثير من الناس للقنوات، ومع ذلك لا يمكنك الحصول على قفل وفتحه بصورةٍ خاطئة في رست بفضل نظامها وقواعد ملكيتها الخاصة. واجهة <Mutex<T البرمجية لنبدأ باستعمال كائن مزامنة بسياق خيط وحيد single-threaded مثالًا عن كيفية استعمال كائن مزامنة كما هو موضح في الشيفرة 12: اسم الملف: src/main.rs use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); *num = 6; } println!("m = {:?}", m); } الشيفرة 12: تجربة واجهة <Mutex<T البرمجية بسياق خيط وحيد لبساطته كما هو الحال مع العديد من الأنواع نُنشئ <Mutex<T باستعمال الدالة المرتبطة ‫new، ونستخدم التابع lock للوصول إلى البيانات داخل كائن المزامنة وذلك للحصول على القفل. سيحظر هذا الاستدعاء الخيط الحالي ولن تتمكن من فعل أي عمل حتى يحين دور الحصول على القفل. يفشل استدعاء lock إذا هلع خيط آخر يحمل القفل، وفي هذه الحالة لن يتمكن أي شخص إطلاقًا من الحصول على القفل لذلك اخترنا unwrap بحيث يهلع الخيط هذا إذا حصلت هذه الحالة. يمكننا أن نعالج القيمة التي حصلنا عليها التي تحمل الاسم num بعد حصولنا على القفل وستكون في هذه الحالة بمثابة مرجع متغير mutable يُشير إلى البيانات الموجودة بالداخل. يضمن نظام النوع type system حصولنا على قفل قبل استخدام القيمة في m، ونوع m هو <Mutex<i32 وليس i32 لذلك يجب علينا استدعاء lock حتى نتمكن من استخدام القيمة i32، ودعنا لا ننسى أن نظام النوع لن يسمح لنا بالوصول إلى قيمة i32 الداخلية بخلاف ذلك. كما تعتقد فإن <Mutex<T هو مؤشر ذكي، وبدقة أكبر، يُعيد استدعاء lock مؤشرًا ذكيًا يدعى MutexGuard مُغلَّفًا في LockResult الذي تعاملنا معه في استدعاء unwrap، يُطبّق المؤشر الذكي MutexGuard السمة Deref ليشير إلى بياناتنا الداخلية، كما يحتوي المؤشر الذكي أيضًا على تطبيق للسمة Drop يُحرّر القفل تلقائيًا عندما يخرج MutexGuard عن النطاق وهو الأمر الذي يحدث بنهاية النطاق الداخلي. نتيجة لذلك لا نخاطر بنسيان تحرير القفل وحجب استخدام كائن المزامنة بواسطة الخيوط الأخرى لأن تحرير القفل يحدث تلقائيًا. يمكننا طباعة قيمة كائن المزامنة بعد تحرير القفل وسنرى أننا تمكنا من تغيير القيمة ذات النوعi32 الداخلية إلى 6. مشاركة <Mutex<T بين خيوط متعددة دعنا نحاول الآن مشاركة قيمة بين خيوط متعددة باستخدام <Mutex<T، سنمرّ على عشرة خيوط ونجعل كل خيط منها يزيد قيمة العداد بمقدار 1 بحيث ينتقل العداد من القيمة 0 إلى 10. يحتوي المثال التالي في الشيفرة 13 على خطأ تصريفي compiler error، وسنستفيد من هذا الخطأ حتى نتعلم المزيد حول استعمال <Mutex<T وكيف سيساعدنا رست في استعماله بصورةٍ صحيحة. اسم الملف: src/main.rs use std::sync::Mutex; use std::thread; fn main() { let counter = Mutex::new(0); let mut handles = vec![]; for _ in 0..10 { let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); } الشيفرة 13: عشرة خيوط يزيد كل واحد منها عداد محميّ باستخدام <Mutex <T ننشئ متغيرًا ندعوه counter ليحمل قيمة من النوع i32 داخل <Mutex<T كما فعلنا في الشيفرة 12، بعد ذلك نُنشئ عشرة خيوط عبر المرور iterate على مجال من الأرقام، ونستخدم لتحقيق ذلك thread::spawn ونمنح كل خيط المغلّف ذاته الذي ينقل العداد باتجاه الخيط ويحصل على قفل على <Mutex<T عن طريق استدعاء التابع lock ومن ثم يضيف القيمة 1 إلى القيمة الموجودة في كائن المزامنة. عندما ينتهي الخيط من تنفيذ مغلّفه يخرج num عن النطاق ويحرر القفل بحيث يستطيع خيطٌ آخر الحصول عليه. نجمع كل مقابض الانضمام join handles في الخيط الرئيسي، ونستدعي بعد ذلك -كما فعلنا في الشيفرة 2 سابقًا- join على كل مقبض للتأكد من انتهاء جميع الخيوط، وعند هذه النقطة يحصل الخيط الرئيسي على القفل ويطبع نتيجة هذا البرنامج. لمّحنا إلى أن هذا المثال لن يُصرَّف، لنتعرف على السبب الآن: $ cargo run Compiling shared-state v0.1.0 (file:///projects/shared-state) error[E0382]: use of moved value: `counter` --> src/main.rs:9:36 | 5 | let counter = Mutex::new(0); | ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait ... 9 | let handle = thread::spawn(move || { | ^^^^^^^ value moved into closure here, in previous iteration of loop 10 | let mut num = counter.lock().unwrap(); | ------- use occurs due to use in closure For more information about this error, try `rustc --explain E0382`. error: could not compile `shared-state` due to previous error تشير رسالة الخطأ إلى أن قيمة counter نُقِلت في التكرار السابق للحلقة، وتخبرنا رست أنه لا يمكننا نقل ملكية قفل counter إلى خيوط متعددة. لنصحّح الخطأ التصريفي بطريقة الملكية المتعددة التي ناقشناها سابقًا. ملكية متعددة مع خيوط متعددة تكلمنا في مقال سابق عن المالكين المتعددين لقيمة باستخدام المؤشر الذكي <Rc<T لإنشاء قيمة مرجعية معدودة، لننفّذ الشيء ذاته هنا ونلاحظ النتيجة. نغلّف <Mutex<T داخل <Rc<T ضمن الشيفرة 14 ونستنسخ <Rc<T قبل نقل الملكية إلى الخيط. اسم الملف: src/main.rs use std::rc::Rc; use std::sync::Mutex; use std::thread; fn main() { let counter = Rc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Rc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); } الشيفرة 14: محاولة استعمال <Rc<T للسماح لخيوط متعددة بامتلاك <Mutex<T نصرّف الشيفرة البرمجية مرةً أخرى، ونحصل هذه المرة على أخطاء مختلفة، ويعلّمنا المصرّف الكثير من الأشياء. $ cargo run Compiling shared-state v0.1.0 (file:///projects/shared-state) error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely --> src/main.rs:11:36 | 11 | let handle = thread::spawn(move || { | ------------- ^------ | | | | ______________________|_____________within this `[closure@src/main.rs:11:36: 11:43]` | | | | | required by a bound introduced by this call 12 | | let mut num = counter.lock().unwrap(); 13 | | 14 | | *num += 1; 15 | | }); | |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely | = help: within `[closure@src/main.rs:11:36: 11:43]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>` note: required because it's used within this closure --> src/main.rs:11:36 | 11 | let handle = thread::spawn(move || { | ^^^^^^^ note: required by a bound in `spawn` For more information about this error, try `rustc --explain E0277`. error: could not compile `shared-state` due to previous error رسالة الخطأ هذه شديدة التعقيد، إليك الجزء المهم الذي يجب أن تركز عليه: `Rc<Mutex<i32>>` cannot be sent between threads safely يخبرنا المصرّف أيضًا عن السبب: the trait `Send` is not implemented for `Rc<Mutex<i32>>` سنتحدث عن Send في القسم التالي، إذ أنها أحد السمات التي تضمن أن الأنواع التي نستعملها مع الخيوط مخصصة للاستخدام في الحالات المتزامنة. لسوء الحظ فإن <Rc<T ليس آمنًا للمشاركة عبر الخيوط، فعندما يُدير <Rc<T عدد المراجع فإنه يضيف عدد كل استدعاء إلى clone ويطرح من العدد عندما تُحرَّر كل نسخة clone، إلا أنه لا يستعمل أي أنواع تزامن أولية للتأكد من أن التغييرات التي حدثت على العدد لا يمكن مقاطعتها بواسطة خيط آخر. قد يؤدي هذا إلى عمليات عدّ خاطئة -أخطاء خفية يمكن أن تؤدي بدورها إلى تسريب الذاكرة memory leak أو تحرير قيمة ما قبل أن ننتهي منها- وما نحتاجه هنا هو نوع مثل <Rc<T تمامًا ولكنه نوع يُجري تغييرات على عدد المراجع بطريقة آمنة للخيوط. عد المراجع الذري باستخدام <Arc<T لحسن الحظ، يعد <Arc<T نوعًا مثل <Rc<Tوهو آمن للاستخدام في الحالات المتزامنة، إذ يرمز الحرف a إلى ذرّي atomic مما يعني أنه يُعد نوع عدّ مرجع ذري atomically reference counted type. تعدّ الأنواع الذرية Atomics نوعًا إضافيًا من أنواع التزامن الأولية concurrency primitive التي لن نغطيها بالتفصيل هنا. راجع توثيق المكتبة القياسية للوحدة std::sync::atomic للمزيد من التفاصيل، من الكافي الآن معرفة أن الأنواع الذرية تعمل مثل الأنواع الأولية ولكن من الآمن مشاركتها عبر الخيوط. قد تتساءل عن عدم كون جميع الأنواع الأولية ذرية ولماذا أنواع المكتبات القياسية غير مُطبّقة لاستخدام <Arc<T افتراضيًا، والسبب هنا هو أن سلامة الخيوط تأتي مع ضريبة أداء تريد دفعها فقط عندما تحتاج إليها حقًا. إذا كنت تجري عمليات على القيم ضمن خيط واحد فيمكن لشيفرتك البرمجية أن تُنفَّذ بصورةٍ أسرع إذا لم تكن بحاجة لفرض الضمانات التي تقدمها الأنواع الذرية. بالعودة إلى مثالنا: للنوعين <Arc<T و <Rc<T الواجهة البرمجية API ذاتها لذلك نصحّح برنامجنا عن طريق تغيير سطر use واستدعاء new وكذلك استدعاء clone. نستطيع أخيرًا تصريف الشيفرة البرمجية في الشيفرة 15 وتنفيذها. اسم الملف: src/main.rs use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); } الشيفرة 15: استخدام <Arc<T لتغليف <Mutex<T بحيث نستطيع مشاركة الملكية عبر خيوط متعددة يكون خرج الشيفرة البرمجية السابقة على النحو التالي: Result: 10 وأخيرًا نجحنا، فقد عدَدنا من 0 إلى 10، وعلى الرغم من أن هذا قد لا يبدو رائعًا جدًا، إلا أنه علّمنا الكثير عن <Mutex<T وأمان الخيط. يمكنك أيضًا استخدام هيكل هذا البرنامج لإجراء عمليات أكثر تعقيدًا من مجرد زيادة العداد، ويمكننا باستخدام هذه الإستراتيجية تقسيم عملية حسابية إلى أجزاء مستقلة وتقسيم تلك الأجزاء عبر خيوط ثم استخدام <Mutex<T لجعل كل خيط يحدّث النتيجة النهائية بجزئه. لاحظ أنه إذا كنت تنفّذ عمليات عددية بسيطة فهناك أنواع أبسط من أنواع <Mutex<T التي توفرها وحدة المكتبة القياسية std::sync::atomic، إذ توفر هذه الأنواع وصولًا ذريًا آمنًا ومتزامنًا للأنواع الأولية، وقد اخترنا استخدام <Mutex<T مع نوع أولي لهذا المثال حتى نتمكن من التركيز على كيفية عمل <Mutex<T. التشابه بين <RefCell<T>/Rc<T و <Mutex<T>/Arc<T ربما لاحظت أن counter ثابت immutable ولكن يمكننا الحصول على مرجع متغيّر للقيمة الموجودة داخله، هذا يعني أن <Mutex<T يوفر قابلية تغيير داخلية كما تفعله عائلة Cell. نستخدم <Mutex<T بنفس الطريقة التي استخدمنا بها <RefCell<T سابقًا في مقال المؤشر الذكي Refcell‎ ونمط قابلية التغيير الداخلي interior mutability في لغة رست Rust للسماح لنا بتغيير المحتويات داخل <Rc<T وذلك لتغيير المحتويات داخل Arc<T>‎. هناك شيء آخر يجب ملاحظته ألا وهو أن رست لا يمكنها حمايتك من جميع أنواع الأخطاء المنطقية عند استخدام <Mutex<T. تذكر سابقًا في مقال المؤشر Rc‎ الذكي واستخدامه للإشارة إلى عدد المراجع في لغة رست Rust أن استخدام <Rc<T يأتي مع خطر إنشاء دورات مرجعية reference cycle، إذ تشير قيمتان <Rc<T إلى بعضهما بعضًا مما يتسبب في حدوث تسرب في الذاكرة. وبالمثل يأتي تطبيق <Mutex<T مع خطر خلق حالات مستعصية deadlocks، إذ تحدث هذه الحالات عندما تحتاج عملية ما إلى الحصول على مرجعين وحصل كل منهما على أحد الأقفال مما يتسبب في انتظارهما لبعضهما بعضًا إلى الأبد. إذا أثارت الحالات المستعصية اهتمامك وأردت رؤيتها عمليًا فحاول إنشاء برنامج رست به حالة مستعصية، ثم ابحث عن استراتيجيات التخفيف من الحالات المستعصية بالنسبة لكائنات المزامنة في أي لغة ونفذها في رست. يوفر توثيق واجهة المكتبة القياسية البرمجية لـ<Mutex<T و MutexGuard معلومات مفيدة. سنكمل هذا المقال بالحديث عن السمتين Send و Sync وكيف يمكننا استخدامها مع الأنواع المخصصة. التزامن الموسع مع السمة Sync والسمة Send من المثير للاهتمام أن لغة رست تحتوي على عدد قليل جدًا من ميزات التزامن، إذ تعد كل ميزة تزامن تحدثنا عنها حتى الآن جزءًا من المكتبة القياسية standard library وليست اللغة. لا تقتصر خياراتك للتعامل مع التزامن على اللغة أو المكتبة القياسية فيمكنك كتابة ميزات التزامن الخاصة بك أو استخدام تلك المكتوبة من قبل الآخرين. ومع ذلك، ضُمِّنَ مفهومان للتزامن في اللغة: سمتَا std::marker وهما Sync و Send. السماح بنقل الملكية بين الخيوط عن طريق Send تشير الكلمة المفتاحية للسمة Send إلى أنه يمكن نقل ملكية القيم من النوع الذي ينفّذ Send بين الخيوط، ويطبّق كل نوع في رست تقريبًا السمة Send، ولكن هناك بعض الاستثناءات بما في ذلك <Rc<T إذ لا يمكن تطبيق السمة Send عليه لأنه إذا استنسخت قيمة <Rc<T وحاولت نقل ملكية الاستنساخ إلى خيط آخر فقد يحدّث كلا الخيطين عدد المراجع reference count في الوقت ذاته. لهذا السبب تُنفَّذ <Rc<T للاستخدام في المواقف أحادية الخيط حيث لا تريد دفع غرامة الأداء الآمن. لذلك يؤكد نظام نوع رست وحدود السمات trait bounds أنه لا يمكنك أبدًا إرسال قيمة <Rc<T بطريق الخطأ عبر الخيوط بصورةٍ غير آمنة. عندما حاولنا فعل ذلك في الشيفرة 14 سابقًا حصلنا على الخطأ: the trait Send is not implemented for Rc<Mutex<i32>> وعندما استبدلنا النوع بالنوع <Arc<T الذي يطبّق السمة Send صُرِّفَت الشيفرة البرمجية بنجاح. أي نوع مكون بالكامل من أنواع Send يُميَّز تلقائيًا على أنه Send أيضًا، وتطبّق جميع الأنواع الأولية primitive types تقريبًا السمة Send بغض النظر عن المؤشرات الأولية primitive pointers التي سنناقشها لاحقًا في. السماح بالوصول لخيوط متعددة باستخدام السمة Sync تشير الكلمة المفتاحية للسمة Sync إلى أنه من الآمن للنوع المطبّق للسمة Sync أن يُشار إليه من خيوط متعددة، بمعنى آخر أي نوع T يطبّق السمة Sync إذا كان T& (مرجع غير قابل للتغيير إلى T) يطبّق Send أيضًا، مما يعني أنه يمكن إرسال المرجع بأمان إلى خيط آخر، وبصورةٍ مشابهة للسمة Send فإن الأنواع الأولية تطبّق السمة Sync والأنواع المكونة كاملًا من أنواع تطبّق السمة Sync هي أيضًا أنواع تطبّق Sync. لا يطبّق المؤشر الذكي <Rc<T أيضًا السمة Sync للأسباب ذاتها التي تجعل منه غير قابل لتطبيق السمة Send، كما أن النمط <RefCell<T (الذي تحدثنا عنه سابقًا وعائلة الأنواع المرتبطة بالنوع <Cell<T لا تطبّق السمة Sync. يعدّ تنفيذ فحص الاستعارة الذي تفعله <RefCell<T في وقت التنفيذ غير آمن للخيط. يطبّق المؤشر الذكي <Mutex<T السمة Sync ويمكن استخدامه لمشاركة الوصول مع خيوط متعددة كما رأيت سابقًا في قسم "مشاركة <Mutex<T بين خيوط متعددة". تطبيق السمتين Send و Sync يدويا غير آمن بما أن الأنواع المكونة من الأنواع التي تطبّق السمتين Send وSync هي أنواع تطبّق السمتين Send و Sync تلقائيًا، فلا يتوجب علينا تطبيق هاتين السمتين يدويًا؛ وبصفتهما سمتان علّامة marker traits، فهما سمتان لا تحتويان أيّ توابع تطبّقها، وهما مفيدتان فقط لتعزيز الثوابت المرتبطة بالمزامنة. يشمل تنفيذ هاتان السمتان يدويًا تنفيذ شيفرة رست غير آمنة، وسنتحدث عن استعمال شيفرة رست غير آمنة لاحقًا، ومن الكافي الآن معرفتك أن بناء أنواع متزامنة جديدة غير مؤلفة من أجزاء Send و Sync يتطلّب تفكيرًا حذرًا للمحافظة على ضمانات الأمان في لغة رست. يحتوي الكتاب “The Rustonomicon” معلومات أكثر عن هذه الضمانات وكيف يمكن التمسك بها. ترجمة -وبتصرف- لقسم من الفصل Fearless Concurrency من كتاب The Rust Programming Language. اقرأ أيضًا: المقال السابق: استخدام ميزة تمرير الرسائل Message Passing لنقل البيانات بين الخيوط Threads في لغة رست الملكية Ownership في لغة رست أنواع البيانات Data Types في لغة رست Rust الأخطاء والتعامل معها في لغة رست Rust استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust استخدام الخيوط Threads لتنفيذ شيفرات رست بصورة متزامنة آنيًا
  9. يُعد تمرير الرسائل message passing أحد الطرق الشائعة لضمان أمن التزامن، إذ تتواصل الخيوط threads أو المنفذون actors فيما بينهم بإرسال رسائل تحتوي على بيانات. يمكن توضيح هذه الفكرة باقتباس من شعار في توثيق لغة البرمجة جو Go: "لا تتواصل بمشاركة الذاكرة، شارك الذاكرة بالتواصل". تؤمّن مكتبة رست القياسية تنفيذًا للقنوات channels لتحقيق عملية تزامن إرسال الرسائل message-sending concurrency، والقناة هي مفهوم برمجي عام تُرسل به البيانات من خيط إلى آخر. يمكنك تخيل القناة في البرمجة مثل قناة مياه باتجاه واحد مثل جدول أو نهر، فإذا وضعت بطة مطاطية في النهر ستنتقل في المجرى إلى نهاية القناة المائية. تنقسم القناة إلى نصفين أحدها مُرسل transmitter والآخر مُستقبل receiver؛ والمُرسل هو القسم الموجود أعلى النهر الذي تضع فيه البطة المطاطية؛ والمُستقبل هو ما تصل إليه البطة المطاطية في نهاية النهر. يستدعي قسمٌ من الشيفرة البرمجية التوابع على المُرسل مع البيانات المراد إرسالها، والقسم الآخر يتحقق من وصول الرسالة في القسم المُستقبل. يمكن القول إن القناة قد أُغلقت إذا سقط أي من القسمين المُرسل أو المستقبل. سنعمل هنا على برنامج يحتوي على خيط واحد لتوليد القيم وإرسالها عبر القناة وخيط آخر سيستقبل القيم ويطبعها، وسنرسل قيمًا بسيطةً بين الخيوط باستخدام القناة وذلك بهدف توضيح هذه الميزة فقط، ويمكنك استخدام القناة -بعد أن نتعرف عليها جيدًا- على أي خيوط تحتاج لأن تتواصل مع بعضها بعضًا، مثل نظام محادثة أو نظام يكون فيه عدة خيوط تجري حسابات وإرسال هذه الأجزاء إلى خيط واحد يجّمع القيم. نُنشئ في الشيفرة 6 قناةً دون استخدامها. لاحظ أن الشيفرة البرمجية لن تُصرف بعد، لأن رست لا تستطيع معرفة ما هو نوع القيم التي نريد إرسالها عبر هذه القناة. اسم الملف: src/main.rs use std::sync::mpsc; fn main() { let (tx, rx) = mpsc::channel(); } الشيفرة 6: انشاء قناة وإسناد القسمين tx و rx إليها أنشأنا قناةً جديدةً باستخدام دالة mpsc::channel، ويعني الاختصار "mpsc" عدة منتجين multiple producer ومستهلك واحد single consumer. باختصار، طريقة تطبيق القنوات في مكتبة رست القياسية هي أن كل قناة تحتوي على عدة مُرسلين ينتجون القيم ولكن هناك مُستقبل واحد لاستهلاك تلك القيم. تخيّل عدة مجاري streams تلتقي في نهرٍ واحد كبير: أي سينتهي كل شيء يُرسل من المجاري في نهر واحد في النهاية. نبدأ بمنتج واحد وبعدها نضيف عدة منتجين بعد عمل هذا المثال. تُعيد الدالة mpsc::channel صفًا tuple، العنصر الأول هو طرف الإرسال (المُرسل) والعنصر الثاني هو طرف الاستقبال (المُستقبل)، وتُستخدم عادةً الاختصارات tx و rx في العديد من الحقول fields للمُرسل والمُستقبل على التوالي، لذا نُسمي متغيراتنا وفقًا لهذا الاصطلاح للدلالة على كل طرف. نستخدم التعليمة let بنمط pattern يدمّر هيكلية الصفوف، وسنتحدث عن استخدام الأنماط في تعليمة let وتدمير الهيكلية لاحقًا، ويكفي الآن معرفة أن استخدام تعليمة let هي الطريقة الأفضل لاستخراج أقسام من الصف المُعاد باستخدام mpsc::channel. لننقل الطرف المُرسل إلى خيط مُنشأ ولنجعله يرسل سلسلةً نصيةً لكي يتواصل الخيط المُنشأ مع الخيط الرئيسي كما هو موضح في الشيفرة 7. يُماثل هذا الأمر وضع بطة مطاطية أعلى النهر أو إرسال رسالة من خيط إلى آخر. اسم الملف: src/main.rs use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); }); } الشيفرة 7: نقل tx إلى خيط مُنشأ وإرسال "hi" استخدمنا thread::spawn مجددًا لإنشاء خيط جديد، ثم استخدمنا move لنقل tx إلى مُغلّف closure بحيث يمتلك الخيط المُنشأ القيمة tx. يجب على الخيط المُنشأ أن يمتلك الطرف المُرسل لكي يستطيع إرسال رسائل عبر القناة، ولدى المرسل تابع send الذي يأخذ القيمة المُراد إرسالها، إذ يعيد التابع send قيمةً من النوع Result<T, E>‎، لذا إذا كان المُستقبل قد اُسقط وليس هناك مكان لإرسال القيمة، تعيد عملية الإرسال خطأً. استدعينا في هذا المثال unwrap ليهلع في حال الخطأ، ولكن يجب التعامل مع حالة الهلع في التطبيقات الحقيقية بطريقة مناسبة، راجع مقال الأخطاء والتعامل معها في لغة رست لمراجعة استراتيجيات التعامل المناسب مع الأخطاء. سنحصل في الشيفرة 8 على القيمة من المُستقبل في الخيط الأساسي، وتشابه هذه العملية استرجاع البطة المطاطية من نهاية النهر أو استقبال الرسالة. اسم الملف: src/main.rs use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); }); let received = rx.recv().unwrap(); println!("Got: {}", received); } الشيفرة 8: استقبال القيمة "hi" في الخيط الأساسي وطباعتها للمستقبل تابعان مفيدان، هما recv و try_recv، استخدمنا recv -وهو اختصارٌ لكلمة استقبال receive- الذي يمنع تنفيذ الخيط الرئيسي وينتظر حتى تصل القيمة إلى نهاية القناة، ويعيد التابع recv بعد وصول القيمة إلى نهاية القناة القيمة ذاتها داخل النوع Result<T,E>‎، وعندما يُغلق المُرسل يعيد التابع recv خطأ للإشارة إلى أنه لا يوجد المزيد من القيم قادمة. لا يحجب التابع try_recv الخيط الرئيسي وإنما يعيد قيمةً من النوع Result<T,E>‎ مباشرةً، وتحتوي قيمة Ok رسالةً إذا كان هناك رسالة متوفرة وإلا فقيمة Err إذا لا يوجد أي رسائل هذه المرة. استخدام try_recv مفيدٌ إذا كان للخيط أعمالٌ أخرى لينفذها بينما ينتظر الرسائل، وبإمكاننا كتابة حلقة تستدعي try_recv بصورةٍ متكررة لتتعامل مع الرسائل في حال قدومها، أو تنفّذ أعمالًا أخرى قبل تحققها مجدداً. استخدمنا في مثالنا التابع recv لبساطته، وليس لدينا أي عمل آخر للخيط الرئيسي غير انتظار الرسائل، لذا فإن حجب الخيط الرئيس مناسب. عندما ننفذ الشيفرة البرمجية في الشيفرة 8 نلاحظ القيمة المطبوعة في الخيط الرئيسي: Got: hi هذا ممتاز! القنوات ونقل الملكية تلعب الملكية دورًا مهمًا في إرسال الرسائل لأنها تساعد في كتابة شيفرة آمنة ومتزامنة، إذ يتطلب منع الأخطاء في البرمجة المتزامنة الانتباه على الملكية ضمن برنامجك باستخدام لغة رست. لنكتب مثالًا يظهر كيف تعمل كل من القنوات والملكية لمنع المشاكل، وسنستخدم قيمة val في الخيط المُنشأ بعد أن أرسلناه عبر القناة. جرّب تصريف الشيفرة البرمجية في الشيفرة 9 لترى سبب كون هذه الشيفرة غير مسموحة. اسم الملف: src/main.rs use std::sync::mpsc; use std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from("hi"); tx.send(val).unwrap(); println!("val is {}", val); }); let received = rx.recv().unwrap(); println!("Got: {}", received); } الشيفرة 9: محاولة استخدام val بعد إرسالها عبر القناة حاولنا هنا طباعة val بعد أن أرسلناها عبر القناة باستخدام tx.send، وسيكون السماح بذلك فكرةً سيئة، إذ يستطيع الخيط التعديل على القيمة أو إسقاطها بعد إرسالها عبره، ومن الممكن أن يسبّب تعديل الخيط الآخر أخطاءً أو قيمًا غير متوقعة أو بيانات غير موجودة، إلا أن رست تعطينا رسالة خطأ إذا جربنا تصريف الشيفرة البرمجية في الشيفرة 9. $ cargo run Compiling message-passing v0.1.0 (file:///projects/message-passing) error[E0382]: borrow of moved value: `val` --> src/main.rs:10:31 | 8 | let val = String::from("hi"); | --- move occurs because `val` has type `String`, which does not implement the `Copy` trait 9 | tx.send(val).unwrap(); | --- value moved here 10 | println!("val is {}", val); | ^^^ value borrowed here after move | = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0382`. error: could not compile `message-passing` due to previous error حدث خطأ وقت التصريف نتيجة حدوث خطأ في التزامن. تأخذ دالة send ملكيتها من معاملاتها، وعندما تُنقل القيمة، يأخذ المستقبل ملكيتها، وهذا يمنعنا من استخدامها مرةً أخرى بعد إرسالها؛ وبالنتيجة يتحقق نظام الملكية من أن كل شيء على ما يرام. إرسال قيم متعددة مع انتظار المستقبِل استطعنا تصريف وتنفيذ الشيفرة 8، ولكن لم يظهر لنا أن خيطين مستقلين كانا يتكلمان مع بعضهما عبر القناة. أجرينا بعض التعديلات في الشيفرة 10 لنبين أن الشيفرة البرمجية في الشيفرة 8 تُنفَّذ بصورةٍ متزامنة. سيُرسل الخيط المُنشأ الآن عدة رسائل وسيتوقف لمدة ثانية بين كل رسالة. اسم الملف: src/main.rs use std::sync::mpsc; use std::thread; use std::time::Duration; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("thread"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); for received in rx { println!("Got: {}", received); } } الشيفرة 10: إرسال رسائل متعددة مع التوقف بين كل عملية إرسال للخيط المُنشأ هذه المرة شعاع vector من السلاسل النصية التي نريد إرسالها إلى الخيط الأساسي. نمرّ على السلاسل النصية ونرسلها بصورةٍ إفرادية ونتوقف بين كل واحدة وأخرى باستدعاء دالة thread::sleep مع قيمة Duration مساوية إلى 1 ثانية. لم نستدعِ في الخيط الأساسي الدالة recv صراحةً بل عاملنا rx مثل مكرّر iterator، إذ نطبع كل قيمة نستقبلها، وتنتهي عملية التكرار عندما تُغلق القناة. نلاحظ الخرج التالية عند تنفيذ الشيفرة البرمجية في الشيفرة 10 مع توقف لمدة ثانية بين كل سطر وآخر: Got: hi Got: from Got: the Got: thread يمكننا معرفة أن الخيط الأساسي ينتظر استلام القيم من الخيط المُنشأ لأنه ليس لدينا شيفرة تتوقف أو تتأخر في الحلقة for ضمن الخيط الأساسي. إنشاء عدة منتجين بواسطة نسخ المرسل قلنا سابقًا أن mpsc هو اختصار لعدة منتجين ومستهلك واحد، دعنا نستخدم mpsc للبناء على الشيفرة 10 وذلك لإنشاء خيوط متعددة ترسل كلها قيمًا المُستقبل ذاته، ونستطيع عمل ذلك بنسخ المُرسِل كما هو موضح في الشيفرة 11: اسم الملف: src/main.rs use std::sync::mpsc; use std::thread; use std::time::Duration; fn main() { // --snip-- let (tx, rx) = mpsc::channel(); let tx1 = tx.clone(); thread::spawn(move || { let vals = vec![ String::from("hi"), String::from("from"), String::from("the"), String::from("thread"), ]; for val in vals { tx1.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); thread::spawn(move || { let vals = vec![ String::from("more"), String::from("messages"), String::from("for"), String::from("you"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); for received in rx { println!("Got: {}", received); } // --snip-- } الشيفرة 11: إرسال عدّة رسائل من عدّة منتجين نستدعي هذه المرة clone على المُرسِل قبل إنشاء أول خيط، وسيمنحنا ذلك مُرسلًا جديدًا يمكننا تمريره إلى الخيط المُنشأ الأول، ثم نمرر المُرسل الأصلي إلى الخيط المنشأ الثاني، وهذا يعطينا خيطين يرسل كل منهما رسالة مختلفة إلى مستقبل واحد. يجب أن يكون الخرج كما يلي عندما ننفذ الشيفرة السابقة: Got: hi Got: more Got: from Got: messages Got: for Got: the Got: thread Got: you قد تجد القيم بترتيب مختلف حسب نظامك، وهذا ما يجعل التزامن مثيرًا للاهتمام وصعبًا في الوقت ذاته، وإذا حاولت تجربة التزامن باستخدام thread::sleep بإعطائه قيمًا مختلفة على خيوط مختلفة، فكل تنفيذ سيكون غير محدد أكثر، مما يعطيك خرج مختلف في كل مرة. الآن وبعد تعلمنا كيفية عمل القنوات سنتعلم في المقالات التالية نوعًا آخرًا من التزامن. ترجمة -وبتصرف- لقسم من الفصل Fearless Concurrency من كتاب The Rust Programming Language. اقرأ أيضًا: المقال السابق: استخدام الخيوط Threads لتنفيذ شيفرات رست بصورة متزامنة آنيًا الأخطاء والتعامل معها في لغة رست Rust استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust الحزم packages والوحدات المصرفة crates في لغة رست Rust
  10. تُنفّذ شيفرة البرنامج في معظم أنظمة التشغيل الحالية ضمن عملية process ويُدير نظام التشغيل عمليات متعددة في وقت واحد، يمكنك أيضًا العثور على أجزاء مستقلة تعمل بصورةٍ متزامنة داخل البرنامج، وتسمى الميّزات التي تنفّذ هذه الأجزاء المستقلة بالخيوط threads، على سبيل المثال يمكن أن يحتوي خادم الويب web server على خيوط متعددة بحيث يمكنه أن يستجيب لأكثر من طلب واحد في نفس الوقت. يمكن أن يؤدي تجزئة عمليات الحساب في برنامجك إلى خيوط متعددة لتنفيذ مهام متعددة في نفس الوقت إلى تحسين الأداء ولكنه يضيف تعقيدًا إضافيًا أيضًا. لا يوجد ضمان حول الترتيب الذي ستُنفَّذ فيه أجزاء من شيفرتك البرمجية في الخيوط المختلفة، وذلك بسبب إمكانية تنفيذ الخيوط بصورةٍ متزامنة، ويمكن لهذا أن يؤدي إلى مشاكل مثل: حالات التسابق race conditions، إذ يمكن للخيوط الوصول للبيانات أو المصادر بترتيب غير مضبوط. أقفال ميتة deadlocks، وهي الحالة التي ينتظر فيها الخيطان بعضهما بعضًا مما يمنع كلا الخيطين من الاستمرار. الأخطاء التي تحدث فقط في حالات معينة وتكون صعبة الحدوث دومًا والإصلاح بصورةٍ موثوقة. تحاول لغة رست التخفيف من الآثار السلبية لاستخدام الخيوط ولكن لا تزال البرمجة في سياق متعدد الخيوط تتطلّب تفكيرًا حذرًا ويتطلب هيكل شيفرة برمجية مختلف عن ذلك الموجود في البرامج المنفذة في خيط مفرد. تُنفّذ لغات البرمجة الخيوط بطرق مختلفة عن بعضها البعض، وتوفر العديد من أنظمة التشغيل واجهة برمجية يمكن للغة أن تستدعيها لإنشاء خيوط جديدة. تستخدم مكتبة رست القياسية نموذج 1:1 لتنفيذ الخيط، إذ يستخدم البرنامج خيط نظام تشغيل واحد لكل خيط لغة. هناك وحدات مصرفة تنفذ نماذج أخرى للخيوط لتقديم مقايضات مختلفة عن نموذج 1:1. إنشاء خيط جديد باستخدام spawn نستدعي الدالة thread::spawn لإنشاء خيط جديد ونمرر لها مغلَّف closure يحتوي على الشيفرة التي نريد أن ننفذها في الخيط الجديد (تحدثنا عن المغلفات سابقًا في المقال المغلفات closures في لغة رست). يطبع المثال في الشيفرة 1 نصًا ما من خيط رئيسي ونص آخر من خيط جديد: اسم الملف: src/main.rs use std::thread; use std::time::Duration; fn main() { thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } } الشيفرة 1: إنشاء خيط جديد لطباعة شيء ما بينما يطبع الخيط الرئيسي شيئًا آخر لاحظ أنه عندما يكتمل الخيط الرئيسي لبرنامج رست، تُغلَق كل الخيوط المُنشأة بغض النظر إذا أنهت التنفيذ أم لا، ويمكن لخرج هذا البرنامج أن يكون مختلفًا في كل مرة ولكنه سيكون مشابهًا لما يلي: hi number 1 from the main thread! hi number 1 from the spawned thread! hi number 2 from the main thread! hi number 2 from the spawned thread! hi number 3 from the main thread! hi number 3 from the spawned thread! hi number 4 from the main thread! hi number 4 from the spawned thread! hi number 5 from the spawned thread! تؤدي استدعاءات thread::sleep إلى إجبار الخيط على إيقاف تنفيذه لمدة قصيرة مما يسمح بتشغيل خيوط مختلفة، ومن المحتمل أن تتناوب الخيوط ولكن هذا الأمر غير مضمون الحدوث، إذ يعتمد ذلك على كيفية جدولة نظام التشغيل الخاص للخيوط. يُطبع في التنفيذ السابق الخيط الرئيسي أولًا على الرغم من ظهور عبارة الطباعة من الخيط الذي أُنشئ أولًا في الشيفرة البرمجية، وعلى الرغم من أننا أخبرنا الخيط المُنشأ أن يطبع حتى تصبح i مساوية للقيمة 9 إلا أنه وصل إلى 5 فقط قبل إغلاق الخيط الرئيسي. إذا نفَّذت هذه الشيفرة ورأيت فقط المخرجات من الخيط الرئيسي أو لم ترَ أي تداخل، فحاول زيادة الأرقام في المجالات لإنشاء المزيد من الفرص لنظام التشغيل للتبديل بين الخيوط. انتظار انتهاء كل الخيوط بضم المقابض Handles عن طريق join لا توقف الشيفرة البرمجية الموجودة في الشيفرة 1 الخيط المُنتَج قبل الأوان في معظم الأوقات بسبب انتهاء الخيط الرئيسي، وإنما بسبب عدم وجود ضمان على الترتيب الذي تُنفَّذ به الخيوط، إذ لا يمكننا أيضًا ضمان أن الخيط المُنتَج سيُنفذ إطلاقًا. يمكننا إصلاح مشكلة عدم تشغيل الخيط الناتج spawned أو انتهائه قبل الأوان عن طريق حفظ قيمة إرجاع thread::spawn في متغير، إذ يكون النوع المُعاد من thread::spawn هو JoinHandle؛ الذي يمثّل قيمةً مملوكةً، بحيث عندما نستدعي التابع join عليها ستنتظر حتى ينتهي الخيط الخاص بها. تُظهر الشيفرة 2 كيفية استعمال JoinHandle الخاص بالخيط التي أنشأناه في الشيفرة 1 واستدعاء join للتأكد من انتهاء الخيط المنتج قبل الخروج من الدالة main: اسم الملف: src/main.rs use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap(); } الشيفرة 2: حفظ JoinHandle من thread::spawn لضمان تنفيذ الخيط لحين الاكتمال يؤدي استدعاء join على المقبض handle إلى إيقاف تنفيذ الخيط الجاري حتى ينتهي الخيط الذي يمثله المقبض، ويعني إيقاف تنفيذ blocking الخيط أن الخيط ممنوع من أداء العمل أو الخروج منه. يجب أن ينتج تنفيذ الشيفرة 2 خرجًا مشابهًا لما يلي لأننا وضعنا استدعاء join بعد حلقة الخيط الرئيسي for: hi number 1 from the main thread! hi number 2 from the main thread! hi number 1 from the spawned thread! hi number 3 from the main thread! hi number 2 from the spawned thread! hi number 4 from the main thread! hi number 3 from the spawned thread! hi number 4 from the spawned thread! hi number 5 from the spawned thread! hi number 6 from the spawned thread! hi number 7 from the spawned thread! hi number 8 from the spawned thread! hi number 9 from the spawned thread! يستمرّ الخيطان بالتناوب، وينتظر الخيط الرئيسي بسبب استدعاء ()handle.join، ولا ينتهي حتى ينتهي الخيط الناتج. دعنا نرى ما سيحدث عندما ننقل ()handle.join إلى ما قبل حلقة for في main على النحو التالي: اسم الملف: src/main.rs use std::thread; use std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("hi number {} from the spawned thread!", i); thread::sleep(Duration::from_millis(1)); } }); handle.join().unwrap(); for i in 1..5 { println!("hi number {} from the main thread!", i); thread::sleep(Duration::from_millis(1)); } } سينتظر الخيط الرئيسي انتهاء الخيط الناتج، ثم سينفّذ الحلقة for الخاصة به لذلك لن تتداخل المخرجات بعد الآن كما هو موضح هنا: hi number 1 from the spawned thread! hi number 2 from the spawned thread! hi number 3 from the spawned thread! hi number 4 from the spawned thread! hi number 5 from the spawned thread! hi number 6 from the spawned thread! hi number 7 from the spawned thread! hi number 8 from the spawned thread! hi number 9 from the spawned thread! hi number 1 from the main thread! hi number 2 from the main thread! hi number 3 from the main thread! hi number 4 from the main thread! يمكن أن تؤثر التفاصيل الصغيرة على ما إذا كانت الخيوط الخاصة بك تُنفَّذ في نفس الوقت أم لا مثل استدعاء join. استعمال مغلفات move مع الخيوط نستخدم غالبًا الكلمة المفتاحية move مع المغلفات التي تُمرَّر إلى thread::spawn وذلك لأن المغلف سيأخذ بعد ذلك ملكية القيم التي يستخدمها من البيئة وبالتالي تُنقل ملكية هذه القيم من خيط إلى آخر، ناقشنا سابقًا في فقرة "الحصول على المعلومات من البيئة باستخدام المغلفات" من مقال المغلفات closures في لغة رست الكلمة المفتاحية move في سياق المغلفات، إلا أننا سنركز الآن أكثر على التفاعل بين move و thread::spawn. لاحظ في الشيفرة 1 أن المغلف الذي نمرّره إلى thread::spawn لا يأخذ أي وسطاء arguments، إذ أننا لا نستخدم أي بيانات من الخيط الرئيسي في شيفرة الخيط المنتج، ولاستخدام البيانات من الخيط الرئيسي في الخيط المُنتج يجب أن يحصل مغلّف الخيط الناتج على القيم التي يحتاجها. تُظهر الشيفرة 3 محاولة إنشاء شعاع vector في الخيط الرئيسي واستعماله في الخيط الناتج، ومع ذلك لن ينجح هذا الأمر كما سترى بعد لحظة. اسم الملف: src/main.rs use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(|| { println!("Here's a vector: {:?}", v); }); handle.join().unwrap(); } الشيفرة 3: محاولة استعمال شعاع منشأ بواسطة الخيط الرئيسي ضمن خيط آخر يستخدم المغلّف الشعاع v لذلك سوف يحصل على القيمة v ويجعلها جزءًا من بيئة المغلف، ونظرًا لأن thread::spawn ينفّذ المغلف في خيط جديد فيجب أن نكون قادرين على الوصول إلى v داخل هذا الخيط الجديد، ولكن عندما نصرِّف الشيفرة السابقة نحصل على الخطأ التالي: $ cargo run Compiling threads v0.1.0 (file:///projects/threads) error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function --> src/main.rs:6:32 | 6 | let handle = thread::spawn(|| { | ^^ may outlive borrowed value `v` 7 | println!("Here's a vector: {:?}", v); | - `v` is borrowed here | note: function requires argument type to outlive `'static` --> src/main.rs:6:18 | 6 | let handle = thread::spawn(|| { | __________________^ 7 | | println!("Here's a vector: {:?}", v); 8 | | }); | |______^ help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword | 6 | let handle = thread::spawn(move || { | ++++ For more information about this error, try `rustc --explain E0373`. error: could not compile `threads` due to previous error تستنتج رست كيفية الحصول على القيمة v ولأن !println تحتاج فقط إلى مرجع إلى v، يحاول المغلّف استعارة v، ومع ذلك هناك مشكلة، إذ لا يمكن لرست معرفة المدة التي سيُنفَّذ فيها الخيط الناتج لذلك لا تعرف ما إذا كان المرجع إلى v صالحًا دائمًا. تقدّم الشيفرة 4 سيناريو من المرجح به أن يحتوي على مرجع غير صالح إلى v: اسم الملف: src/main.rs use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(|| { println!("Here's a vector: {:?}", v); }); drop(v); // oh no! handle.join().unwrap(); } الشيفرة 4: خيط مع مغلف يحاول أن يحصل على مرجع يشير للشعاع v من خيط رئيسي يحرّر v إذا سمحت لنا رست بتنفيذ الشيفرة البرمجية السابقة فهناك احتمال أن يوضَع الخيط الناتج في الخلفية فورًا دون تنفيذ إطلاقًا. يحتوي الخيط الناتج على مرجع يشير إلى v من الداخل إلا أن الخيط الرئيسي يحرّر v فورًا باستخدام دالة drop التي ناقشناها سابقًا في مقال تنفيذ شيفرة برمجية عند تحرير الذاكرة cleanup باستخدام السمة Drop في لغة رست، وعندما يبدأ تنفيذ الخيط المنتج، لن تصبح v صالحة، لذلك فإن الإشارة إليها تكون أيضا غير صالحة، ولا نريد حدوث ذلك. لتصحيح الخطأ التصريفي في الشيفرة 3 نتّبع النصيحة المزودة لنا في رسالة الخطأ: help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword | 6 | let handle = thread::spawn(move || { | ++++ يمكننا من خلال من خلال إضافة الكلمة المفتاحية move قبل المغلف أن نفرض عليه الحصول على ملكية القيم التي يستعملها بدلًا من السماح لرست بالتدخل والاستنتاج أن عليها استعارة القيم. التعديل على الشيفرة 3 موضّح في الشيفرة 5 وسيُصرَّف البرنامج ويُنفَّذ وفق المطلوب: اسم الملف: src/main.rs use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!("Here's a vector: {:?}", v); }); handle.join().unwrap(); } الشيفرة 5: استعمال الكلمة المفتاحية move لنفرض على المغلف أن يأخذ ملكية القيم التي يستعملها قد نرغب في تجربة الأمر ذاته لتصحيح الشيفرة البرمجية في الشيفرة 4، إذ استدعى الخيط الرئيسي drop باستعمال مغلف move، ومع ذلك فإن هذا لن يعمل لأن ما تحاول الشيفرة 4 فعله غير مسموح به لسبب مختلف؛ وإذا أضفنا move إلى المغلف فسننقل v إلى بيئة المغلف ولن يعد بإمكاننا بعد ذلك استدعاء drop عليه في الخيط الرئيسي، وسنحصل على الخطأ التصريفي التالي بدلًا من ذلك: $ cargo run Compiling threads v0.1.0 (file:///projects/threads) error[E0382]: use of moved value: `v` --> src/main.rs:10:10 | 4 | let v = vec![1, 2, 3]; | - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait 5 | 6 | let handle = thread::spawn(move || { | ------- value moved into closure here 7 | println!("Here's a vector: {:?}", v); | - variable moved due to use in closure ... 10 | drop(v); // oh no! | ^ value used here after move For more information about this error, try `rustc --explain E0382`. error: could not compile `threads` due to previous error أنقذتنا قواعد الملكية في رست مرةً أخرى، إذ حصلنا على خطأ في الشيفرة 3 لأن رست كانت صارمة باستعارة v للخيط ذاته فقط مما يعني أن الخيط الرئيسي يمكنه نظريًا إبطال مرجع الخيط الناتج، ويمكننا بإخبار رست أن تنقل ملكية v إلى الخيط الناتج أن نضمن لرست أن الخيط الرئيسي لن يستخدم v بعد الآن. إذا عدّلنا الشيفرة 4 بالطريقة ذاتها فإننا بذلك ننتهك قواعد الملكية عندما نحاول استعمال v في الخيط الرئيسي. تتجاوز الكلمة المفتاحية move الوضع الافتراضي الصارم للاستعارة في رست، فلا يُسمح لنا بانتهاك قواعد الملكية. بعد فهمنا أساسيات الخيوط وواجهتها البرمجية، لننظر عما يمكننا فعله باستخدام الخيوط. ترجمة -وبتصرف- لقسم من الفصل Fearless Concurrency من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: حلقات المرجع Reference Cycles وتسببها بتسريب الذاكرة Memory Leak في لغة رست Rust أنواع البيانات Data Types في لغة رست Rust الأخطاء والتعامل معها في لغة رست Rust مقدمة إلى الخيوط Threads في جافا
  11. قد تكون عملية ملء الذاكرة دون تحريرها (العملية المعروفة بتسريب الذاكرة memory leak) صعبة الحدوث بمستوى أمان الذاكرة الذي تقدمه لغة رست Rust، إلا أن حدوث هذا الأمر غير مستحيل، إذ لا تضمن رست منع تسريب الذاكرة بصورةٍ كاملة، والمقصود هنا أن الذاكرة المُسرّبة آمنة في رست. نلاحظ أن رست تسمح بتسريب الذاكرة باستخدام Rc<T>‎ و RefCell<T>‎، إذ أنه من الممكن إنشاء مراجع تشير إلى بعضها ضمن حلقة cycle، وسيسبب هذا تسريب ذاكرة لأن عدد المراجع لكل عنصر لن يصل إلى 0 أبدًا، وبهذا لن تُحرَّر أي قيمة. إنشاء حلقة مرجع لنلاحظ كيف من الممكن أن نشكّل حلقة مرجع لتفادي هذا الأمر، بدءًا بتعريف معدّد List وتابع tail في الشيفرة 25: اسم الملف: src/main.rs use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match self { Cons(_, item) => Some(item), Nil => None, } } } fn main() {} الشيفرة 25: تعريف قائمة بنية تخزّن RefCell<T>‎ داخلها، بحيث نستطيع تعديل القيمة التي يشير إليها متغاير Cons نستخدم هنا نوعًا مختلفًا من تعريف List من الشيفرة 5، إذ يصبح العنصر الثاني من المتغاير Cons أي variant مساويًا للنوع RefCell<Rc<List>>‎، مما يعني أننا نحتاج تعديل قيمة List المشار إليها في Cons عوضًا عن حاجتنا لقابلية التعديل على قيمة النوع i32 كما فعلنا في الشيفرة 24 في المقال السابق، نضيف أيضًا التابع tail لتسهيل الوصول إلى العنصر الثاني إذا وُجد متغاير Cons. أضفنا في الشيفرة 26 الدالة main التي تستخدم التعريف الموجود في الشيفرة 25، إذ تُنشئ الشيفرة قائمةً في a وقائمة في b تشير إلى القائمة a، وثم تعدِّل القائمة في a لتشير إلى b لتنشئ بذلك حلقة مرجع. كتبنا أيضًا تعليمات println!‎ لتظهر قيمة عدد المراجع في نقاط مختلفة ضمن هذه العملية. اسم الملف: src/main.rs use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] enum List { Cons(i32, RefCell<Rc<List>>), Nil, } impl List { fn tail(&self) -> Option<&RefCell<Rc<List>>> { match self { Cons(_, item) => Some(item), Nil => None, } } } fn main() { let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); println!("a initial rc count = {}", Rc::strong_count(&a)); println!("a next item = {:?}", a.tail()); let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); println!("a rc count after b creation = {}", Rc::strong_count(&a)); println!("b initial rc count = {}", Rc::strong_count(&b)); println!("b next item = {:?}", b.tail()); if let Some(link) = a.tail() { *link.borrow_mut() = Rc::clone(&b); } println!("b rc count after changing a = {}", Rc::strong_count(&b)); println!("a rc count after changing a = {}", Rc::strong_count(&a)); // ألغِ تعليق السطر التالي لتلاحظ وجود حلقة المرجع، إذ ستتسبب الشيفرة البرمجية بطفحان المكدس // println!("a next item = {:?}", a.tail()); } الشيفرة 26: إنشاء حلقة مرجع بحيث تشير قيمتي List إلى بعضهما بعضًا أنشأنا نسخةً من Rc<List>‎ تحتوي على القيمة List في المتغير a مع قائمة مبدئية تحتوي على القيم 5,Nil، ثم أنشأنا نسخة Rc<List>‎ أخرى تحتوي قيمة List أخرى في المتغير b تحوي القيمة 10 وتشير إلى القائمة في a. عدّلنا a لتشير إلى b بدلًا من Nil لنحصل بذلك على حلقة، وحقّقنا ذلك باستخدام التابع tail للحصول على مرجع إلى RefCell<Rc<List>>‎ في a، والذي وضعناه بعد ذلك في المتغير link، ثم استخدمنا التابع borrow_mut على القيمة RefCell<Rc<List>>‎ لتغيير القيمة داخل Rc<List>‎ التي تخزّن القيمة Nil إلى القيمة Rc<List>‎ الموجودة في b. نحصل على الخرج التالي عندما ننفذ هذه الشيفرة البرمجية مع الإبقاء على تعليمة println!‎ الأخيرة معلّقة: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.53s Running `target/debug/cons-list` a initial rc count = 1 a next item = Some(RefCell { value: Nil }) a rc count after b creation = 2 b initial rc count = 1 b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) }) b rc count after changing a = 2 a rc count after changing a = 2 يبلغ عدد مراجع نُسخ Rc<List>‎ في كلٍّ من a و b القيمة 2 وذلك بعد تغيير القائمة في a لتشير إلى b. تُسقِط أو تحذف لغة رست في نهاية الدالة main المتغير b مما يغيّر من عدد مراجع نسخة b من Rc<List>‎ من 2 إلى 1، ولن تُحرّر الذاكرة التي تشغلها Rc<List>‎ على الكومة heap في هذه اللحظة لأن عدد المراجع هو 1 وليس 0، ومن ثم تُحرّر رست a، مما يُنقص عداد المرجع لنسخة a من Rc<List>‎ من 2 إلى 1 أيضًا. لا يُمكن تحرير ذاكرة النسخة هذه لأن نسخة Rc<List>‎ الأخرى لا تزال تشير إليها، وستبقى الذاكرة المحجوزة للقائمة شاغرة للأبد، ولتخيّل حلقة المرجع هذه بصريًا أنشأنا المخطط التالي في الشكل 4. الشكل 4: حلقة مرجع خاصة بقائمتين a و b، تشيران إلى بعضهما بعضًا إذا أزلت التعليق عن آخر تعليمة println!‎ ونفّذت البرنامج، فستحاول رست طباعة الحلقة مع إشارة القائمة a إلى القائمة b وإشارتها بدورها إلى القائمة a وهكذا دواليك حتى يطفح المكدّس. ليست عواقب إنشاء حلقة مرجعية هنا خطيرة مقارنةً بالبرامج الواقعية، إذ أن برنامجنا ينتهي بعد إنشائنا حلقة مرجع reference cycle، إلا أن البرنامج سيستخدم ذاكرة أكثر مما يحتاج إذا كان البرنامج أكثر تعقيدًا وشَغَل ذاكرة أكثر في الحلقة واحتفظ بها لفترة أطول، وربما سيتسبّب ذلك بتحميل النظام عبئًا كبيرًا مسببًا نفاد الذاكرة المتوفرة. إنشاء حلقات مرجع ليس بالأمر السهل ولكنه ليس مستحيلًا، فإذا كانت لديك قيم RefCell<T>‎ تحتوي على قيم Rc<T>‎ أو تشكيلات متداخلة مشابهة لأنواع مع قابلية التغيير الداخلي وعدّ المراجع، فيجب عليك التأكد أنك لم تنشئ حلقات، إذ لا يجب عليك الاعتماد على رست لإيجادها. إنشاء حلقة مرجع يحصل نتيجة خطأ منطقي في برنامجك ولذلك يجب عليك استخدام الاختبارات التلقائية ومراجعة الشيفرة البرمجية ووسائل تطوير البرامج الأخرى لتقليل احتمالية حدوثها. يمكن إعادة تنظيم هيكلية البيانات كحلٍ آخر لتفادي حلقات المرجع، بحيث تعبّر بعض المراجع عن الملكية بينما لا يعبّر بعضها الآخر، ونتيجةً لذلك سيكون لديك حلقات مكونة من بعض علاقات الملكية ownership وبعضها من علاقات لا ملكية non-ownership، بحيث تؤثر علاقات الملكية فقط إذا كانت القيمة ستُحرَّر. نريد من المتغاير Cons في الشيفرة 25 أن يملك قائمةً خاصةً به دائمًا، لذا فإن عملية إعادة تنظيم هيكلية البيانات ليست ممكنة. لنتابع مثالًا آخرًا باستخدام البيانات التخطيطية graphs بحيث تتكون من عُقد أب وعُقد أبناء لنرى كيف أن العلاقات اللا ملكية هي طريقة مناسبة لمنع حصول حلقات المرجع. منع حلقات المرجع: بتحويل Rc‎ إلى Weak‎ وضّحنا بحلول هذه النقطة أن استدعاء Rc::clone يزيد من قيمة strong_count الخاصة بالنسخة Rc<T>‎ وأن نسخة Rc<T>‎ تُحرَّر فقط عندما تكون قيمة strong_count هي 0. بإمكاننا أيضًا إنشاء مرجع ضعيف weak reference للقيمة داخل نسخة Rc<T>‎ وذلك باستدعاء Rc::downgrade وتمرير مرجع إلى Rc<T>‎. المراجع القوية هي الطريقة التي يمكنك بها مشاركة الملكية لنسخة Rc<T>‎، بينما لا تعبّر المراجع الضعيفة عن علاقة ملكية، ولا يتأثر عددها عندما تُحرَّر نسخة من Rc<T>‎، ولا يسبب حلقة مرجعية، إذ ستُكسر الحلقات المتضمنة لمراجع ضعيفة عندما تصبح قيمة عدد المراجع القوية مساويةً إلى 0. نحصل على مؤشر ذكي من النوع Weak<T>‎ عندما نستدعي Rc::downgrade، وبدلًا من زيادة strong_count في نسخة Rc<T>‎ بقيمة 1، سيزيد استدعاء Rc::downgrade من قيمة weak_count بمقدار 1. يستخدم النوع Rc<T>‎ القيمة weak_count لمتابعة عدد مراجع Weak<T>‎ الموجودة بصورةٍ مشابهة للقيمة strong_count، إلا أن الفرق هنا هو أن weak_count لا يحتاج أن يكون 0 لكي تُنظف نسخة <Rc<T. يجب علينا التأكد أن القيمة موجودة فعلًا إذا أردت إجراء أي عمليات على القيمة التي يشير إليها Weak<T>‎، وذلك لأن القيمة قد تُحرَّر، ويمكننا التحقق من ذلك باستدعاء تابع upgrade على نسخة Weak<T>‎ التي تعيد قيمةً من النوع Option<Rc<T>>‎، وسنحصل على نتيجة Some إذا لم تُحرَّر القيمة Rc<T>‎ ونتيجة None إذا حُرّرت القيمة، تضمن رست أن حالتي Some و None ستُعامل على النحو الصحيح ولن تصبح مؤشرًا غير صالح، وذلك لأن upgrade تعيد <Option<Rc<T>‎. لنأخذ مثالًا، فبدلًا من استخدام قائمة تعرف عناصرها العنصر الذي يليها فقط، سننشئ شجرةً تعرف عناصرها كلٍ من أبنائها وآبائها. إنشاء هيكل بيانات الشجرة يحتوي على عقدة مع عقد أبناء أولًا، ننشئ شجرة مع عقد nodes تعرف عقد أبنائها، ولتحقيق ذلك ننشئ بنيةً ندعوها Node تحتوي على قيم من النوع i32 إضافةً إلى مراجع تشير لقيم أبنائها Node: اسم الملف: src/main.rs use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, children: RefCell::new(vec![]), }); let branch = Rc::new(Node { value: 5, children: RefCell::new(vec![Rc::clone(&leaf)]), }); } نريد من البنية Node أن تمتلك أبنائها children، كما نريد كذلك مشاركة الملكية مع المتغيرات لكي نتمكن من الوصول إلى كل Node في الشجرة مباشرةً، ومن أجل تحقيق ذلك سوف نعرّف عناصر Vec<T>‎ بحيث تكون قيمًا من النوع Rc<Node>‎؛ كما نريد أيضًا تعديل العقد الأبناء لعقدة أخرى، لذلك سيكون لدينا RefCell<T>‎ في الأبناء children وحول Vec<Rc<Node>>‎. سنستخدم تعريف الهيكلية لإنشاء نسخة Node واحدة بالاسم leaf وقيمتها 3 دون أن تحتوي على أبناء، ونسخةً أخرى اسمها branch قيمتها 5 تحتوي على leaf بمثابة واحد من أبنائها كما هو موضح في الشيفرة 27: اسم الملف: src/main.rs use std::cell::RefCell; use std::rc::Rc; #[derive(Debug)] struct Node { value: i32, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, children: RefCell::new(vec![]), }); let branch = Rc::new(Node { value: 5, children: RefCell::new(vec![Rc::clone(&leaf)]), }); } الشيفرة 27: إنشاء عقدة leaf دون أبناء وعقدة branch مع leaf بمثابة واحد من أبنائها ننسخ القيمة Rc<Node>‎ الموجودة في leaf ونخزّنها في branch وبذلك تصبح Node في leaf مُمتلكةً من قبل مالكين، هما leaf و branch. يمكننا الانتقال من branch إلى leaf عبر branch.children ولكن لا يوجد طريقة للوصول الى leaf من branch، وسبب ذلك هو أن leaf لا تمتلك مرجع إلى branch، وبالتالي لا تعرف بأنها مرتبطة مع branch، إذًا نحن بحاجة لأن تعرف أن branch هي العقدة الأب وهذا ما سنفعله. إضافة مرجع يشير إلى عقدة ابن داخل عقدة أب نحتاج لإضافة حقل parent لجعل عقدة ابن تعرف بوجود آبائها وذلك ضمن تعريف الهيكل Node. تكمن صعوبة الأمر في اختيار النوع الذي يجب استخدامه لتخزين القيمة parent، إلا أننا نعلم أنه لا يمكن للقيمة أن تحتوي النوع Rc<T>‎ لأننا نحصل بذلك على حلقة مرجع تحتوي على القيمة leaf.parent مشيرةً إلى branch و branch.children مشيرةً إلى leaf مما يجعل قيم strong_count غير مساوية إلى القيمة 0 في أي من الحالات. لنفكّر بالعلاقات بطريقة أخرى؛ إذ يجب على العقدة الأب أن تمتلك أبنائها، إذا حُرِّرَت العقدة الأب فيجب على العُقد التابعة لها (الأبناء) أن تُسقط أيضًا، إلا أنه ليس من المفترض أن تمتلك عقدة ابن العقدة الأب، فإذا حرّرنا العقدة الابن يجب أن تبقى العقدة الأب موجودة، وهذه هي الحالة التي صُمّمت من أجلها المراجع الضعيفة. لذا نجعل النوع parent يستخدم Weak<T>‎ بدلًا من Rc<T>‎ وتحديدًا النوع RefCell<Weak<Node>>‎، وسيصبح تعريف الهيكل Node على النحو التالي: اسم الملف: src/main.rs use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); } يمكن للعقدة أن تُشير إلى العقدة الأب ولكن لا يمكن أن تمتلكها. عدّلنا من الدالة main في الشيفرة 28 بحيث تستخدم التعريف الجديد لكي تكون للعقدة leaf طريقة للإشارة إلى العقدة الأب branch: اسم الملف: src/main.rs use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); } الشيفرة 28: عقدة leaf مع مرجع ضعيف للعقدة الأب branch يبدو إنشاء العقدة leaf شبيهًا للشيفرة 27، باختلاف الحقل parent، إذ تبدأ العقدة leaf بدون أب وهذا يمكّننا من إنشاء نسخة لمرجع Weak<Node>‎ فارغ. عندما نحاول الحصول على مرجع للعقدة الأب الخاصة بالعقدة leaf وذلك باستخدام التابع upgrade، نحصل على قيمة None، ويمكن رؤية الخرج الناتج من أول تعليمة println!‎: leaf parent = None نحصل على مرجع Weak<Node>‎ جديد عندما نُنشئ العقدة branch وذلك في الحقل parent لأن branch لا تحتوي على عقدة أب. لا يزال لدينا leaf وهي عقدة ابن للعقدة branch، وحالما يوجد لدينا نسخة من Node في branch سيكون بإمكاننا تعديل leaf بمنحها مرجعًا إلى العقدة الأب الخاصة بها من النوع Weak<Node>‎. نستخدم التابع borrow_mut على النوع RefCell<Weak<Node>>‎ في حقل parent الخاص بالعقدة leaf، ومن ثم نستخدم الدالة Rc::downgrade لإنشاء مرجع من النوع Weak<Node>‎ يشير إلى العقدة branch من النوع Rc<Node>‎ في branch. نحصل على متغاير Some يحتوي على branch عندما نطبع أب العقدة leaf مجددًا، إذ يمكن للعقدة leaf الآن الوصول إلى العقدة الأب. نستطيع أيضًا تفادي إنشاء الحلقة التي ستتسبب أخيرًا في طفحان المكدّس عند طباعة leaf كما هو الحال في الشيفرة 26؛ إذ تُطبع مراجع Weak<Node>‎ مع الكلمة (Weak) جانبها: leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) }, children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) }, children: RefCell { value: [] } }] } }) يشير عدم وجود خرج لانهائي إلى أن الشيفرة لم تُنشئ حلقة مرجع، ويمكنك التأكد من ذلك عن طريق ملاحظة القيم التي نحصل عليها باستدعاء كل من Rc::strong_count و Rc::weak_count. مشاهدة التغييرات التي تحصل على strong_count و weak_count دعنا نلاحظ كيف تتغير قيم نُسخ Rc<Node>‎ لكل من النسخة strong_count و weak_count، وذلك عن طريق إنشاء نطاق داخلي جديد ونقل عملية إنشاء branch إلى هذا النطاق، إذ نستطيع بفعل ذلك مشاهدة ما الذي يحصل عند إنشاء العقدة branch وتحريرها بعد أن تخرج من النطاق. التعديلات موضحة في الشيفرة 29: اسم الملف: src/main.rs use std::cell::RefCell; use std::rc::{Rc, Weak}; #[derive(Debug)] struct Node { value: i32, parent: RefCell<Weak<Node>>, children: RefCell<Vec<Rc<Node>>>, } fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); { let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!( "branch strong = {}, weak = {}", Rc::strong_count(&branch), Rc::weak_count(&branch), ); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); } println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); println!( "leaf strong = {}, weak = {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); } الشيفرة 29: إنشاء branch في نطاق داخلي وفحص عدد المراجع القوية والضعيفة يبلغ عدد المراجع القوية للنوع Rc<Node>‎ القيمة 1 بعد إنشاء leaf، وعدد المراجع الضعيفة يساوي 0. نُنشئ في النطاق الداخلي العقدة branch ونربطها مع leaf وهذه هي النقطة التي نبدأ فيها بطباعة عدد المراجع، يبلغ عدد المراجع القوية الخاصة بالنوع Rc<Node>‎ في branch القيمة 1 وعدد المراجع الضعيفة 1 (لأن leaf.parent تشير إلى branch باستخدام قيمة من النوع <Weak<Node). نلاحظ تغيّر عداد المراجع القوية إلى 2 عندما نطبع عدد المراجع القوية والضعيفة في leaf وذلك لأن branch هي نسخةٌ من النوع Rc<Node>‎ من القيمة leaf ومخزّنة في branch.children، إلا أننا ما زلنا نحصل على عدد مراجع ضعيفة يساوي 0. تخرج branch عن النطاق عندما ينتهي النطاق الداخلي وينقص عدد المراجع القوي الخاص بالنوع Rc<Node>‎ إلى 0، لذا تُحرّر قيمة Node الخاصة به. لا يوجد تأثير لعدد المراجع الضعيفة البالغ 1 ضمن leaf.parent على تحرير القيمة Node أو عدم تحريرها ولذلك لا نحصل على تسريب للذاكرة. إذا أردنا الوصول إلى العقدة الأب الخاصة بالعقدة leaf في نهاية النطاق، فسنحصل على None مجددًا. عدد المراجع القوية الخاصة بالنوع Rc<Node>‎ في leaf هو 1، وعدد المراجع الضعيفة هو 0 في نهاية البرنامج، وذلك لأن المتغير leaf هو المرجع الوحيد للنوع Rc<Node>‎ مجددًا. المنطق الذي يُدير عدّ وتحرير القيم مُطبَّق في كلٍّ من Rc<T>‎ و Weak<T>‎، إضافةً إلى تنفيذ السمة Drop. سيجعل تحديد أن علاقة العقدة الابن بالعقدة الأب يجب أن تكون مرجعًا ضعيفًا من النوع Weak<T>‎ في تعريف Node، وجود عقدة أب تشير إلى عُقد ابن وبالعكس ممكنًا دون إنشاء حلقة مرجع والتسبُّب بتسريب ذاكرة. ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: المؤشر الذكي Refcell‎ ونمط قابلية التغيير الداخلي interior mutability في لغة رست Rust المؤشرات الذكية Smart Pointers في رست Rust أنواع البيانات Data Types في لغة رست Rust المؤشر Rc‎ الذكي واستخدامه للإشارة إلى عدد المراجع في لغة رست Rust مقدمة إلى مفهوم الأنواع المعممة Generic Types في لغة Rust
  12. تُعد قابلية التغيير الداخلي interior mutability نمط تصميم في رست يسمح لك بتغيير البيانات حتى في حالة وجود مراجع ثابتة immutable تشير لتلك البيانات، ولا تسمح قواعد الاستعارة بهذا الإجراء عادًة. يستخدم النمط شيفرة "unsafe" لتغيير البيانات وذلك داخل هيكل بيانات للتحايل على قواعد رست المعتادة التي تتحكم بقابلية التعديل والاستعارة، إذ تشير الشيفرة غير الآمنة للمصرّف أننا نتحقق من القواعد يدويًا عوضًا عن الاعتماد على المصرّف للتحقق منها لنا، وسنناقش الشيفرة البرمجية غير الآمنة بالتفصيل لاحقًا. يمكننا استخدام الأنواع التي تستخدم نمط قابلية التغيير الداخلي فقط عندما نتأكد أن قواعد الاستعارة تُتّبع في وقت التنفيذ، على الرغم من أن المصرّف لا يمكنه ضمان ذلك. تُغلّف wrap الشيفرة غير الآمنة "unsafe" ضمن واجهة برمجية API آمنة، بحيث يبقى النوع الخارجي ثابتًا. لنكتشف هذا المفهوم من خلال النظر إلى نوع <RefCell<T الذي يتبع نمط التغيير الداخلي. فرض قواعد الاستعارة عند وقت التنفيذ باستخدام <RefCell<T يمثل النمط <RefCell<T بعكس <Rc<T ملكيةً مفردةً للبيانات التي يحملها. إذًا ما الذي يجعل <RefCell<T مختلفًا عن نمط مثل <Box<T؟ تذكر قواعد الاستعارة التي تعلمناها سابقًا في المقال المراجع References والاستعارة Borrowing والشرائح Slices: يمكنك بأي وقت أن تمتلك إما مرجعًا متغيرًا واحدًا أو أي عدد من المراجع الثابتة ولكن ليس كليهما. يجب على المراجع أن تكون صالحة دومًا. تُفرض ثوابت قواعد الاستعارة borrowing rules’ invariants عند وقت التصريف مع المراجع و <Box<T، وتُفرض هذه الثوابت مع <RefCell<T في وقت التنفيذ. نحصل على خطأ تصريفي مع المراجع إذا خرقنا هذه القواعد، إلا أنه في حالة <RefCell<T سيُصاب البرنامج بالهلع ويتوقف إذا خرقت القواعد ذاتها. تتمثل إيجابيات التحقق من قواعد الاستعارة في وقت التصريف في أنه ستُكتشف الأخطاء مبكرًا في عملية التطوير ولا يوجد أي تأثير على الأداء وقت التنفيذ لأن جميع التحليلات قد اكتملت مسبقًا، لهذه الأسباب يُعد التحقق من قواعد الاستعارة في وقت التصريف هو الخيار الأفضل في معظم الحالات وهذا هو السبب في كونه الخيار الافتراضي في رست. تتمثل إيجابيات ميزة التحقق من قواعد الاستعارة في وقت التنفيذ بدلًا من ذلك، بأنه يُسمح بعد ذلك بسيناريوهات معينة خاصة بالذاكرة الآمنة إذ يجري منعها عادةً من خلال عمليات التحقق في وقت التصريف. يُعد التحليل الساكن static analysis صارمًا كما هو الحال في مصرّف رست. من المستحيل اكتشاف بعض خصائص الشيفرة البرمجية من خلال تحليلها والمثال الأكثر شهرة هو مشكلة التوقف halting problem التي تتجاوز نطاق موضوعنا هنا إلا أنه موضوع يستحق البحث عنه. قد ترفض رست البرنامج الصحيح إذا لم يكن مصرّف رست متأكدًا من أن الشيفرة البرمجية تتوافق مع قواعد الملكية، وذلك بفضل وجود عملية التحليل، وتوضّح هذه الحالة صرامة العملية، وإذا قبلت رست برنامجًا خاطئًا فلن يتمكن المستخدمون من الوثوق بالضمانات التي تقدمها رست، وعلى الجانب الآخر إذا رفضت رست برنامجًا صحيحًا فسيتسبب ذلك بإزعاج للمبرمج، ولكن لا يمكن أن يحدث أي شيء كارثي. يُعد النوع <RefCell<T مفيدًا عندما تكون متأكدًا من أن الشيفرة الخاصة بك تتبع قواعد الاستعارة ولكن المصرّف غير قادر على فهم وضمان ذلك. وكما في <Rc<T تُستخدم <RefCell<T فقط في السيناريوهات ذات الخيوط المفردة single-threaded وستعطيك خطأً وقت التنفيذ إذا حاولت استخدامه في سياق متعدد الخيوط multithreaded. سنتحدث عن كيفية الحصول على وظيفة <RefCell<T في برنامج متعدد الخيوط لاحقًا. فيما يلي ملخص لأسباب اختيار<Box<T أو <Rc<T أو <RefCell<T: يمكّن النوع <Rc<T وجود عدّة مالكين لنفس البيانات بينما يوجد للنوعين <RefCell<T و <Box<T مالك وحيد. يسمح النوع <Box<T بوجود استعارات متغيّرة أو ثابتة يُتحقَّق منها وقت التصريف، بينما يسمح النوع <RefCell<T باستعارات متغيّرة أو ثابتة وقت التنفيذ. بما أن النوع <RefCell<T يسمح باستعارات متغيّرة مُتحقق منها في وقت التنفيذ، يمكنك تغيير القيمة داخل <RefCell<T حتى عندما يكون النوع <RefCell<T ثابتًا. تغيير القيمة داخل قيمة ثابتة هو نمط التغيير الداخلي. لنلقي نظرةً على موقف يكون فيه نمط التغيير الداخلي مفيدًا ونفحص كيف يكون ذلك ممكنًا. التغيير الداخلي: استعارة متغيرة لقيمة ثابتة عندما يكون لديك قيمة ثابتة فإنك لا تستطيع استعارتها على أنها متغيرة وفقًا لقواعد الاستعارة، على سبيل المثال، لن تُصرَّف الشيفرة البرمجية التالية: fn main() { let x = 5; let y = &mut x; } إذا حاولت تصريف الشيفرة السابقة فسيظهر لك الخطأ التالي: $ cargo run Compiling borrowing v0.1.0 (file:///projects/borrowing) error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable --> src/main.rs:3:13 | 2 | let x = 5; | - help: consider changing this to be mutable: `mut x` 3 | let y = &mut x; | ^^^^^^ cannot borrow as mutable For more information about this error, try `rustc --explain E0596`. error: could not compile `borrowing` due to previous error على الرغم من ذلك هناك حالات قد يكون من المفيد فيها لقيمة ما أن تغير نفسها باستخدام توابعها methods الخاصة مع جعلها ثابتة بالنسبة للشيفرات الأخرى، بحيث لا تستطيع الشيفرة البرمجية خارج توابع القيمة تغيير القيمة. يُعد استخدام <RefCell<T إحدى الطرق للحصول على التغيير الداخلي إلا أن النوع <RefCell<T لا يتغلب على كامل قواعد الاستعارة، إذ يسمح مدقق الاستعارة في المصرّف بالتغيير الداخلي ويجري التحقق من قواعد الاستعارة في وقت التنفيذ بدلًا من ذلك. إذا خُرقت القواعد فسوف تحصل على حالة هلع panic!‎ بدلًا من خطأ تصريفي. لنعمل من خلال مثال عملي يمكّننا من استخدام <RefCell<T لتغيير قيمة ثابتة ومعرفة لماذا يُعد ذلك مفيدًا. حالة استخدام للتغيير الداخلي: الكائنات الزائفة Mock Objects يستخدم المبرمج نوعًا بدلًا من آخر في بعض الأحيان أثناء الاختبار من أجل مراقبة سلوك معين والتأكد من تنفيذه بصورةٍ صحيحة، ويُسمى هذا النوع من وضع القيمة المؤقتة placeholder بالاختبار المزدوج test double. فكِّر بهذا الأمر بسياق "دوبلير stunt double" في صناعة الأفلام، إذ يتدخل الشخص ويحل محل الممثل لإنجاز مشهد معين صعب. يُستخدم الاختبار المزدوج لأنواع أخرى عندما نجري الاختبارات. الكائنات الزائفة هي أنواع محددة من الاختبار المزدوج التي تسجل ما يحدث أثناء الاختبار حتى تتمكن من أن تتأكد أن الإجراءات الصحيحة قد أُنجزت. لا تحتوي رست على كائنات بنفس معنى الكائنات في لغات البرمجة الأخرى، ولا تحتوي رست على قدرة التعامل مع الكائنات الزائفة مُضمَّنة في المكتبة القياسية كما تفعل بعض اللغات الأخرى، ومع ذلك يمكنك بالتأكيد إنشاء هيكل يخدم الهدف من الكائنات الزائفة. إليك السيناريو الذي سنختبره: سننشئ مكتبةً تتعقب قيمةً مقابل قيمة قصوى وترسل رسائل بناءً على مدى قرب القيمة الحالية من القيمة القصوى، بحيث يمكن استخدام هذه المكتبة لتتبع حصة المستخدم لعدد استدعاءات الواجهة البرمجية المسموح بإجرائها على سبيل المثال. ستوفر مكتبتنا فقط وظيفة تتبع مدى قرب الحد الأعظمي للقيمة وما يجب أن تكون عليه الرسائل في أي وقت، ومن المتوقع أن توفّر التطبيقات التي تستخدم مكتبتنا آلية إرسال الرسائل، يمكن للتطبيق وضع رسالة في التطبيق، أو إرسال بريد إلكتروني، أو إرسال رسالة نصية، أو شيء آخر، إذ لا تحتاج المكتبة إلى معرفة هذا التفصيل، وكل ما تحتاجه هو شيء ينفِّذ سمة سنوفّرها تدعى Messenger. تظهر الشيفرة 20 الشيفرة البرمجية الخاصة بالمكتبة: اسم الملف: src/lib.rs pub trait Messenger { fn send(&self, msg: &str); } pub struct LimitTracker<'a, T: Messenger> { messenger: &'a T, value: usize, max: usize, } impl<'a, T> LimitTracker<'a, T> where T: Messenger, { pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> { LimitTracker { messenger, value: 0, max, } } pub fn set_value(&mut self, value: usize) { self.value = value; let percentage_of_max = self.value as f64 / self.max as f64; if percentage_of_max >= 1.0 { self.messenger.send("Error: You are over your quota!"); } else if percentage_of_max >= 0.9 { self.messenger .send("Urgent warning: You've used up over 90% of your quota!"); } else if percentage_of_max >= 0.75 { self.messenger .send("Warning: You've used up over 75% of your quota!"); } } } الشيفرة 20: مكتبة لتتبع مدى قرب قيمة من قيمة العظمى وإرسال تحذير عندما تصل القيمة لمقدار معيّن أحد الأجزاء المهمة من هذه الشيفرة هو أن سمة Messenger لها تابعٌ واحدٌ يدعى send يقبل مرجعًا ثابتًا يشير إلى self بالإضافة إلى نص الرسالة، وهذه السمة هي الواجهة التي يحتاج الكائن الزائف الخاص بنا إلى تنفيذها بحيث يمكن استخدام الكائن الزائف بنفس الطريقة التي يُستخدم بها الكائن الحقيقي؛ والجزء المهم الآخر هو أننا نريد اختبار سلوك التابع set_value على LimitTracker. يمكننا تغيير ما نمرّره لمعامل value، إلا أن set_value لا تُعيد لنا أي شيء لإجراء تأكيدات assertions عليه؛ إذ نريد أن نكون قادرين على القول أنه على المُرسل أن يرسل رسالة معيّنة إذا أنشأنا LimitTracker بشيء يطبّق السمة Messenger وقيمة معينة لـ max، عندما نمرر أرقامًا مختلفة مثل قيمة value. نحتاج هنا إلى كائن زائف لتتبُّع الرسائل التي يُطلب منه إرسالها عندما نستدعي send، وذلك بدلًا من إرسال بريد إلكتروني أو رسالة نصية. يمكننا إنشاء نسخة جديدة من كائن زائف وإنشاء LimitTracker يستخدم الكائن الزائف واستدعاء التابع set_value على LimitTracker ثم التحقق من أن الكائن الزائف يحتوي على الرسائل التي نتوقعها. تُظهر الشيفرة 21 محاولة تطبيق كائن زائف لفعل ذلك فقط إلا أن مدقق الاستعارة لن يسمح بذلك. اسم الملف: src/lib.rs #[cfg(test)] mod tests { use super::*; struct MockMessenger { sent_messages: Vec<String>, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: vec![], } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { let mock_messenger = MockMessenger::new(); let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); limit_tracker.set_value(80); assert_eq!(mock_messenger.sent_messages.len(), 1); } } الشيفرة 21: محاولة لتطبيق MockMessenger غير المسموح به بواسطة مدقق الاستعارة تعرِّف شيفرة الاختبار السابقة هيكل MockMessenger يحتوي على حقل sent_messages مع قيم Vec من نوع سلسلة نصية String لتتبع الرسائل المطلوب إرسالها؛ كما نعرّف أيضًا دالة مرتبطة associated function بالاسم new لتسهيل إنشاء قيم MockMessenger الجديدة التي تبدأ بلائحة فارغة من الرسائل، ثم نطبّق السمة Messenger على MockMessenger حتى نستطيع إعطاء ملكية MockMessenger لنسخة LimitTracker. نأخذ الرسالة الممرّرة مثل معامل في تعريف التابع send ونخزّنها في قائمة MockMessenger الخاصة بالحقل sent_messages. نختبر في الاختبار ما يحدث عندما يُطلب من LimitTracker تعيين value لشيء يزيد عن 75 بالمائة من max، إذ نُنشئ أولًا نسخةً جديدةً من MockMessenger تبدأ بقائمة فارغة من الرسائل، ثم نُنشئ LimitTracker جديد ونمرّر له مرجعًا يشير إلى MockMessenger الجديد وقيمة max هي 100. نستدعي التابع set_value على LimitTracker بقيمة 80 والتي تزيد عن 75 بالمئة من 100 ثم نتأكد أن قائمة الرسائل التي يتتبعها MockMessenger يجب أن تحتوي الآن على رسالة واحدة. ومع ذلك هناك مشكلة واحدة في هذا الاختبار كما هو موضح هنا: $ cargo test Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker) error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference --> src/lib.rs:58:13 | 2 | fn send(&self, msg: &str); | ----- help: consider changing that to be a mutable reference: `&mut self` ... 58 | self.sent_messages.push(String::from(message)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable For more information about this error, try `rustc --explain E0596`. error: could not compile `limit-tracker` due to previous error warning: build failed, waiting for other jobs to finish... لا يمكننا تعديل MockMessenger لتتبع الرسائل لأن التابع send يأخذ مرجعًا ثابتًا يشير إلى self، كما لا يمكننا أيضًا تنفيذ الاقتراح في نص الخطأ الذي يدلنا على استخدام ‎&mut self بدلًا من ذلك لأن بصمة التابع send لن تتطابق مع بصمة تعريف السمة Messenger (ننصحك بقراءة رسالة الخطأ بنفسك). هذه هي الحالة التي يُمكن أن يساعد فيها نمط التغيير الداخلي، إذ سنخزن sent_messages داخل <RefCell<T، ومن ثم سيتمكن التابع send من تعديل sent_messages لتخزين الرسائل التي رأيناها، وتظهر الشيفرة 22 كيف يبدو التغيير: اسم الملف: src/lib.rs #[cfg(test)] mod tests { use super::*; use std::cell::RefCell; struct MockMessenger { sent_messages: RefCell<Vec<String>>, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: RefCell::new(vec![]), } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.borrow_mut().push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { // --snip-- assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); } } الشيفرة 22: استخدام <RefCell<T لتغيير قيمة داخلية بينما تكون القيمة الخارجية ثابتة أصبح حقل sent_messages الآن من النوع <<RefCell<Vec<String بدلًا من <Vec<String. نُنشئ في الدالة new نسخةً جديدةً من <<RefCell<Vec<String حول الشعاع الفارغ. لا يزال المعامل الأول يمثّل استعارة self ثابتة تتطابق مع تعريف السمة، ولتطبيق التابع send نستدعي borrow_mut على <<RefCell<Vec<String في self.sent_messages للحصول على مرجع متغيّر للقيمة داخل <<RefCell<Vec<String التي تمثّل الشعاع، ومن ثم يمكننا أن نستدعي push على المرجع المتغيّر الذي يشير إلى الشعاع لتتبع الرسائل المرسلة أثناء الاختبار. التغيير الأخير الذي يتعين علينا إجراؤه هو ضمن التأكيد assertion لمعرفة عدد العناصر الموجودة في الشعاع الداخلي، ولتحقيق ذلك نستدعي borrow على <<RefCell<Vec<String للحصول على مرجع ثابت يشير إلى الشعاع. الآن بعد أن رأيت كيفية استخدام المؤشر <RefCell<T لنتعمق في كيفية عمله. تتبع عمليات الاستعارة وقت التنفيذ عن طريق <RefCell<T نستخدم عند إنشاء مراجع متغيّرة وثابتة الصيغة & و & mut على التوالي، بينما نستخدم في <RefCell<T التابعين borrow و borrow_mut اللذين يعدّان جزءًا من واجهة برمجية آمنة تنتمي إلى <RefCell<T. يُعيد التابع borrow نوع المؤشر الذكي <Ref<T ويُعيد التابع borrow_mut نوع المؤشر الذكي <RefMut<T، وينفّذ كلا النوعين السمة Deref لذلك يمكننا معاملتهما على أنهما مراجع نمطيّة regular references. يتعقّب المؤشر <RefCell<T عدد المؤشرات الذكية النشطة حاليًا من النوعين <Ref<T و <RefMut<T، وفي كل مرة نستدعي فيها التابع borrow يزيد المؤشر <RefCell<T من عدد عمليات الاستعارة النشطة الثابتة، وعندما تخرج قيمة من النوع <Ref<T عن النطاق، ينخفض عدد الاستعارات الثابتة بمقدار واحد. يتيح لنا المؤشر <RefCell<T الحصول على العديد من الاستعارات الثابتة أو استعارة واحدة متغيّرة في أي وقت تمامًا مثل قواعد الاستعارة وقت التصريف. إذا حاولنا انتهاك هذه القواعد، سيهلع تنفيذ <RefCell<T وقت التنفيذ بدلًا من الحصول على خطأ تصريفي كما اعتدنا حصوله مع المراجع. تظهر الشيفرة 23 تعديلًا في تطبيق الدالة send من الشيفرة 22، ونحاول هنا إنشاء استعارتين نشطتين متغيّرتين لنفس النطاق عمدًا لتوضيح أن <RefCell<T سيمنعنا من فعل ذلك وقت التنفيذ. اسم الملف: src/lib.rs impl Messenger for MockMessenger { fn send(&self, message: &str) { let mut one_borrow = self.sent_messages.borrow_mut(); let mut two_borrow = self.sent_messages.borrow_mut(); one_borrow.push(String::from(message)); two_borrow.push(String::from(message)); } } [الشيفرة 23: إنشاء مرجعين متغيّرين في النطاق ذاته لملاحظة هلع <RefCell<T>] نُنشئ متغير اسمه one_borrow للمؤشر الذكي <RefMut<T الذي أُعيد من borrow_mut، ثم نُنشئ استعارةً متغيّرة أخرى بنفس الطريقة في المتغير two_borrow، مما يؤدي إلى إنشاء مرجعين متغيّرين في النطاق ذاته وهو أمر غير مسموح به. عندما ننفذ الاختبارات لمكتبتنا، تُصرَّف الشيفرة البرمجية الموجودة في الشيفرة 23 دون أي أخطاء إلا أن الاختبار سيفشل: $ cargo test Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker) Finished test [unoptimized + debuginfo] target(s) in 0.91s Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde) running 1 test test tests::it_sends_an_over_75_percent_warning_message ... FAILED failures: ---- tests::it_sends_an_over_75_percent_warning_message stdout ---- thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_sends_an_over_75_percent_warning_message test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` لاحظ أن الشيفرة هلعت مع الرسالة already borrowed: BorrowMutError، وهذه هي الطريقة التي يتعامل بها المؤشر <RefCell<T مع انتهاكات قواعد الاستعارة عند وقت التنفيذ. اختيار اكتشاف أخطاء الاستعارة وقت التنفيذ بدلًا من وقت التصريف كما فعلنا هنا يعني أنك من المحتمل أن تجد أخطاءً في الشيفرة الخاصة بك لاحقًا في عملية التطوير، وربما حتى عند نشر الشيفرة البرمجية الخاصة بك ووصولها لمرحلة الإنتاج. قد تتكبّد شيفرتك البرمجية أيضًا خسارةً صغيرةً في الأداء عند وقت التنفيذ وذلك نتيجة لتتبع الاستعارات عند وقت التشغيل بدلًا من وقت التصريف، ومع ذلك فإن استخدام <RefCell<T يجعل من الممكن كتابة كائن زائف يمكنه تعديل نفسه لتتبع الرسائل التي شاهدها أثناء استخدامه في سياق يسمح فقط بالقيم الثابتة. يمكنك استخدام <RefCell<T على الرغم من المقايضات للحصول على وظائف أكثر مما توفره المراجع العادية. وجود عدة مالكين للبيانات المتغيرة عن طريق استخدام كل من <Rc <T و <RefCell<T هناك طريقة شائعة لاستخدام <RefCell<T بالاشتراك مع <Rc<T، تذكر أن <Rc<T يتيح لك وجود عدة مالكين لبعض البيانات إلا أنه يمنحك وصولًا ثابتًا إلى تلك البيانات. إذا كان لديك <Rc<T يحتوي على <RefCell<T فيمكنك الحصول على قيمة يمكن أن يكون لها عدة مالكين ويمكنك تغييرها. على سبيل المثال تذكر مثال قائمة البنية في الشيفرة 18 في المقالة السابقة، إذ استخدمنا <Rc<T للسماح لقوائم متعددة بمشاركة ملكية قائمة أخرى، ونظرًا لأن <Rc<T يحتوي على قيم ثابتة فقط، فلا يمكننا تغيير أي من القيم الموجودة في القائمة بمجرد إنشائها. دعنا نضيف <RefCell<T لاكتساب القدرة على تغيير القيم في القوائم. تُظهر الشيفرة 24 أنه يمكننا تعديل القيم المخزّنة في جميع القوائم وذلك باستخدام <RefCell<T داخل تعريف Cons: اسم الملف: src/main.rs #[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); *value.borrow_mut() += 10; println!("a after = {:?}", a); println!("b after = {:?}", b); println!("c after = {:?}", c); } الشيفرة 24: استخدام <<Rc<RefCell<i32 لإنشاء List يمكن تغييرها نُنشئ قيمة بحيث تكون نسخةً من النوع <<Rc<RefCell<i32 ونخزنها في متغير باسم value حتى نتمكن من الوصول إليها مباشرةً لاحقًا، ثم نُنشئ List في a مع متغاير Cons يحمل value، نحتاج هنا إلى استنساخ value بحيث يكون لكل من a و value ملكية للقيمة الداخلية 5 بدلًا من نقل الملكية من value إلى a أو استعارة a من value. نغلّف القائمة a داخل <Rc<T عند إنشاء القائمتين a و b بحيث يمكن لكلّ من القائمتين الرجوع إلى a وهو ما فعلناه في الشيفرة 18 سابقًا. نريد إضافة القيمة 10 إلى value بعد إنشاء القوائم في a و b و c، ونحقّق ذلك عن طريق استدعاء borrow_mut على value التي تستخدم ميزة التحصيل التلقائي automatic differencing التي ناقشناها سابقًا في المقال استخدام التوابع methods ضمن الهياكل structs ضمن فقرة بعنوان (أين العامل ‎->‎؟) لتحصيل <Rc<T إلى قيمة <RefCell<T الداخلية. يُعيد التابع borrow_mut المؤشر الذكي <RefMut<T، ومن ثم نستخدم عامل التحصيل عليه ونغير القيمة الداخلية. عندما ننفذ a و b و c، يمكننا أن نرى أن لجميعهم القيمة المعدلة 15 بدلًا من 5: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.63s Running `target/debug/cons-list` a after = Cons(RefCell { value: 15 }, Nil) b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil)) c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil)) يا لهذه الطريقة الجميلة، فقد أصبح لدينا قيمة List ثابتة خارجيًا باستخدام <RefCell<T>، إلا أنه يمكننا استخدام التوابع الموجودة في <RefCell<T التي توفر الوصول إلى قابلية التغيير الداخلية حتى نتمكن من تعديل بياناتنا عندما نحتاج إلى ذلك. تحمينا عمليات التحقق وقت التنفيذ لقواعد الاستعارة من حالات تسابق البيانات data race وفي بعض الأحيان يكون الأمر مستحقًا لمقايضة القليل من السرعة مقابل هذه المرونة في هياكل البيانات التي لدينا. لاحظ أن <RefCell<T لا يعمل مع الشيفرة متعددة الخيوط، ويعدّ <Mutex<T الإصدار الآمن من سلسلة <RefCell<T، وسنناقش <Mutex<T لاحقًا. ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: المؤشر Rc<T>‎ الذكي واستخدامه للإشارة إلى عدد المراجع في لغة رست Rust المؤشرات الذكية Smart Pointers في رست Rust الملكية Ownership في لغة رست البرمجة بلغة رست التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست
  13. مبدأ الملكية ownership واضح في معظم الحالات، إذ تسمح لك الملكية بمعرفة أي متغير يملك قيمةً ما، ولكن هناك حالات تكون فيها القيمة الواحدة مملوكةً من أكثر من مالك، فمثلًا في شعبة هيكل البيانات graph data structure قد تؤشر العديد من الأضلع edges إلى العقدة node ذاتها، وبالتالي تمتلك هذه العقدة كل الأضلع التي تشير إليها. لا يجب تحرير العقدة من الذاكرة إلا في حال عدم وجود أي ضلع يشير إليها وبالتالي عدم امتلاكها من قبل أحد. يمكنك تمكين وجود عدة مالكين صراحةً باستخدام النوع Rc<T>‎ وهو اختصار لعدّ المرجع reference counting، إذ يُحصي النوع Rc<T>‎ الخاص برست عدد المراجع التي تشير إلى قيمة محددة لتحديد فيما إذا كانت تلك القيمة قيد الاستخدام أم لا، وإذا لم يكن هناك أي مراجع تشير للقيمة، عندها يمكن تحرير القيمة دون التسبب بجعل أي مرجع غير صالح. تخيل Rc<T>‎ مثل تلفاز في غرفة الجلوس، فعندما يدخل شخص الغرفة ليشاهد التلفاز يشغله، كما يمكن لآخرين القدوم للغرفة والمشاهدة أيضَا، وعندما يغادر آخر شخص الغرفة يطفئ التلفاز لأنه لم يعد يُستخدم، ولكن إذا أطفأ أحدهم التلفاز بينما يشاهده الآخرون فسيغضب الذين يشاهدون التلفاز. نستخدم النوع Rc<T>‎ عندما نريد وضع بعض البيانات في الكومة heap بحيث يقرؤها عدة أجزاءٌ مختلفة من برنامجنا ولا نستطيع معرفة أي الأجزاء سينتهي من استخدام هذه البيانات أخيرًا عند تصريف البرنامج. نستطيع جعل الجزء الأخير مالك البيانات إذا كنّا نعرفه وبذلك تُطبَّق قواعد الملكية الاعتيادية لرست عند وقت التصريف. لاحظ أن Rc<T>‎ موجود فقط للاستخدام في حالات استخدام الخيط الواحد single-threaded، وسنتحدث عن عدّ المراجع في البرامج ذات الخيوط المتعددة multithreaded لاحقًا عندما نتحدث عن التزامن concurrency. استخدام Rc‎ لمشاركة البيانات لنعاود النظر إلى قائمة البنية cons list في الشيفرة 5 من مقال المؤشرات الذكية السابق، تذكّر أننا عرفنا القائمة باستخدام Box<T‎>‎ إلا أننا سنُنشئ هذه المرة لائحتين يتشاركان ملكية قائمة ثالثة كما يوضح الشكل 3. الشكل 3: قائمة b وقائمة c يتشاركان ملكية قائمة ثالثة a ننشئ القائمة a التي تحتوي على 5 وبعدها 10، ومن ثم ننشئ قائمتين؛ قائمة b تبدأ بالقيمة 3 وقائمة c تبدأ بالقيمة 4، إذ ستستمر قيم كل من القائمتين b و c ضمن القائمة الثالثة a التي تحتوي على 5 و10، أي ستتشارك القائمتان القائمة الأولى التي تحتوي على 5 و10. لن تنجح محاولة تنفيذ هذه الحالة باستخدام تعريفنا للنوع List مع النوع Box<T>‎ كما هو موضح في الشيفرة 17: اسم الملف: src/main.rs enum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); let c = Cons(4, Box::new(a)); } الشيفرة 17: شيفرة توضّح أنه ليس من المسموح استخدام قائمتين النوع Box<T>‎ مع مشاركة ملكيتهما لقائمة ثالثة عندما نصرّف الشيفرة السابقة نحصل على هذا الخطأ: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) error[E0382]: use of moved value: `a` --> src/main.rs:11:30 | 9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); | - move occurs because `a` has type `List`, which does not implement the `Copy` trait 10 | let b = Cons(3, Box::new(a)); | - value moved here 11 | let c = Cons(4, Box::new(a)); | ^ value used here after move For more information about this error, try `rustc --explain E0382`. error: could not compile `cons-list` due to previous error تمتلك متغايرات Cons البيانات التي تحتفظ بها، لذا عندما ننشئ القائمة b تنتقل القائمة a إلى القائمة b وتمتلك b القائمة a، ثم عندما نحاول استخدام a مجددًا عند إنشاء c لا يُسمح لنا، وذلك لأن القائمة a نُقلت. يمكننا تغيير تعريف Cons بحيث يخزّن المراجع عوضًا عن ذلك، ولكن علينا عندها تعريف معاملات دورة حياة lifetime parameters، إذ سيستطيع بذلك عنصر في القائمة أن يعيش مدّةً تساوي مدة حياة القائمة على الأقل، وهذه هي حالة العناصر الموجودة ضمن القائمة في الشيفرة 17 ولكن هذا لا ينطبق في كل حالة. عوضاً عن ذلك سنغير تعريف List لتستخدم Rc<T>‎ بدلاً من Box<T>‎ كما هو موضح في الشيفرة 18. يحفظ كل متغاير Cons قيمةً بالإضافة لمؤشر Rc<T>‎ يشير إلى List، عندما ننشئ b بدلًا من أخذ ملكية a، سننسخ Rc<List>‎ التي يحتفظ بها a وبذلك نزيد عدد المراجع من 1 إلى 2 ونجعل القائمة a والقائمة b تتشارك ملكية البيانات في Rc<List>‎، كما سننسخ أيضًا a عندما ننشئ c، وبذلك يزيد عدد المراجع من 2 إلى 3 مراجع، وفي كل مرة نستدعي Rc::clone سيزيد عدد المراجع للبيانات داخل Rc<List>‎ ولن تُحرَّر البيانات إلا إذا لم يكن هناك أي مرجع يشير إليها. اسم الملف: src/main.rs enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); let c = Cons(4, Rc::clone(&a)); } الشيفرة 18: تعريف List التي تستخدم Rc<T>‎ نحتاج لإضافة تعليمة use وذلك لإضافة Rc<T>‎ إلى النطاق لأنه غير موجود في البداية. أنشأنا في الدالة main قائمة تحتوي على 5 و10 وخزّناها في قيمة من نوع Rc<List>‎ جديدة ضمن a، ومن ثمّ استدعينا الدالة Rc::clone بعد أن أنشأنا b و c ومررنا مرجعًا مثل وسيط argument يشير إلى Rc<List>‎ في a. يمكننا استدعاء a.clone()‎ بدلًا من Rc::clone(&a)‎، إلا أن الطريقة الاصطلاحية في رست تستخدم Rc::clone في هذه الحالة. لا ينسخ تنفيذ Rc::clone نسخةً فعليةً deep copy للبيانات مثل باقي تنفيذات أنواع clone الأخرى، إذ يزيد استدعاء الدالة Rc::clone عدد المراجع وهو ما لا يأخذ وقتًا طويلًا، بينما يأخذ النسخ الفعلي للبيانات وقتًا طويلًا، إلا أنه يمكننا التمييز بصريًا بين النسخ الفعلي والنسخ الذي يزيد عدد المراجع باستخدام الدالة Rc::clone. نحتاج لأخذ موضوع النسخ الفعلية بالحسبان عندما نبحث عن مشاكل متعلقة بأداء الشيفرة البرمجية وذلك بإهمال استدعاءات Rc::clone. نسخ قيمة من النوع Rc‎ يزيد عدد المراجع لنغير مثالنا الموجود في الشيفرة 18 بحيث يمكننا رؤية تغيّر عدد المراجع عندما ننشئ ونحرّر مرجعًا إلى Rc<T>‎ في a. نغيّر من الدالة main في الشيفرة 19 بحيث تحتوي على نطاق داخلي حول القائمة c لكي نستطيع ملاحظة تغيّر قيمة عداد المراجع عندما تخرج c خارج النطاق. اسم الملف: src/main.rs enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!("count after creating a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("count after creating c = {}", Rc::strong_count(&a)); } println!("count after c goes out of scope = {}", Rc::strong_count(&a)); } الشيفرة 19: طباعة عدد المراجع نطبع عدد المراجع في كل نقطة يتغير فيه قيمته ضمن البرنامج، ونحصل على تلك القيمة باستدعاء الدالة Rc::strong_count. نسمّي الدالة strong_count بدلًا من count، وذلك لأن النوع Rc<T>‎ يحتوي أيضًا على weak_count، وسنستخدم weak_count في مقال لاحق عندما نتحدث عن منع دورات المراجع وتحويل Rc<T>‎ إلى Weak<T>‎. تطبع الشيفرة السابقة ما يلي: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.45s Running `target/debug/cons-list` count after creating a = 1 count after creating b = 2 count after creating c = 3 count after c goes out of scope = 2 نلاحظ أن Rc<List>‎ في a تحتوي على عدد مراجع مبدئي يساوي إلى 1، ثم يزداد العدد كل مرة نستدعي فيها clone بمقدار 1، وعندما تخرج c خارج النطاق ينقص العدد بمقدار 1. لسنا بحاجة لاستدعاء الدالة لإنقاص عدد المراجع كما نفعل عند استدعاء Rc::clone بهدف زيادة عدد المراجع، إذ يُنقص تنفيذ سمة drop من عدد المراجع تلقائيًا عندما تخرج قيمة Rc<T>‎ من النطاق. ما لا نستطيع رؤيته في هذا المثال هو خروج a من النطاق بعد خروج b في نهاية الدالة main، ويُضبط عدد المراجع فيما بعد إلى القيمة 0، وعندها تصبح Rc<List>‎ محرَّرة تمامًا. يسمح استخدام Rc<T>‎ بأن يكون لقيمة واحدة أكثر من مالك، كما أن عدد المراجع يؤكد أن القيمة لا تزال صالحةً طالما لا يزال هناك مالك. يسمح Rc<T>‎ عن طريق المراجع الثابتة immutable مشاركة البيانات بين أقسام متعددة من البرنامج للقراءة فقط. قد تخرق بعض قوانين الاستعارة -التي ناقشناها سابقًا في مقال المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست- في حلقات المرجع Reference Cycles وتسببها بتسريب الذاكرة Memory Leak في لغة رست Rust إذا سمح Rc<T>‎ بوجود عدة مراجع متغيّرة أيضًا؛ إذ قد تسبب الاستعارات المتعددة المتغيّرة لنفس المكان حالة تعارض وتناقض للبيانات data race، إلا أن القدرة على تغيير البيانات مفيدة جدًا. سنناقش في القسم القادم النمط الداخلي المتغيّر interior mutability pattern، إضافةً إلى النوع RefCell<T>‎ الذي يمكن استخدامه مع Rc<T>‎ لتجاوز قيد الثبات هذا. ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: تنفيذ شيفرة برمجية عند تحرير الذاكرة cleanup باستخدام السمة Drop في لغة رست الملكية Ownership في لغة رست المؤشرات الذكية Smart Pointers في رست Rust التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست البرمجة بلغة رست كيفية استخدام النوع HashMap لتخزين البيانات في رست Rust
  14. السمة الثانية المهمة لنمط المؤشرات الذكية smart pointer pattern هي السمة Drop، التي تسمح لك بتخصيص ماذا يحدث إذا كانت القيمة ستخرج من النطاق scope. يمكنك تأمين تنفيذ لسمة Drop على أي نوع، ويمكن استخدام هذه الشيفرة البرمجية لتحرير الموارد، مثل الملفات واتصالات الشبكة. قدّمنا السمة Drop في سياق المؤشرات الذكية، إذ تُستخدم وظيفة سمة Drop دائمًا عند تطبيق مؤشر ذكي، ومثال على ذلك: عندما يُحرَّر Box<T>‎ستحرّر المساحة المخصصة له في الكومة heap التي يشير إليها الصندوق box. يتوجب على المبرمج في بعض لغات البرمجة ولبعض الأنواع استدعاء الشيفرة البرمجية التي تحرّر مساحة تخزين أو موارد في كل مرة ينتهي من استخدام نسخة instance من هذه الأنواع، ومن الأمثلة على ذلك مقابض الملفات file handles والمقابس sockets أو الأقفال locks، وإذا نسي المبرمجون استدعاء تلك الشيفرة البرمجية (لتحرير مساحة التخزين والموارد)، سيزداد التحميل على النظام وسيتوقف النظام عن العمل بحلول نقطة معيّنة. يمكنك في لغة رست تحديد قسم معين من الشيفرة تُنفذ عندما تخرج قيمة ما من النطاق، إذ سيضيف المصرف هذه الشيفرة تلقائيًا ونتيجةً ذلك لا تحتاج أن تكون حذرًا بخصوص وضع شيفرة تحرير المساحة البرمجية cleanup code في كل مكان في البرنامج عندما تكون نسخة من نوع معين قد انتهت، أي لن يكون هناك أي هدر في الموارد. يمكنك تحديد الشيفرة البرمجية التي تريد تنفيذها عندما تخرج قيمة ما عن النطاق وذلك باستخدام تنفيذ سمة Drop، إذ تحتاج سمة Drop أن تطبّق تابع method واحد اسمه drop يأخذ مرجعًا متغيّرًا إلى self لينتظر استدعاء رست للدالة drop. دعنا ننفّذ drop مع تعليمات println!‎ في الوقت الحالي. توضّح الشيفرة 15 البنية CustomSmartPointer بوظيفة مخصّصة وحيدة هي طباعة Dropping CustomSmartPointer!‎ عندما تخرج النسخة عن النطاق لإظهار لحظة تنفيذ رست للخاصية drop. اسم الملف: src/main.rs struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("my stuff"), }; let d = CustomSmartPointer { data: String::from("other stuff"), }; println!("CustomSmartPointers created."); } [الشيفرة 15: بنية CustomSmartPointer التي تنفّذ السمة Drop عند مكان وضع شيفرة تحرير الذاكرة] السمة Drop مضمّنة في المقدمة لذا لا نحتاج لأن نضيفها إلى النطاق. ننفّذ سمة Drop على CustomSmartPointer ونقدّم تنفيذًا لتابع drop الذي يستدعي بدوره println!‎، ونضع في متن دالة drop أي منطق نريد تنفيذه عند تخرج نسخة من النوع خارج النطاق، كما أننا نطبع نصًا هنا لنوضح كيف تستدعي رست drop بصريًا. أنشأنا في main نسختين من CustomSmartPointer ومن ثم طبعنا CutsomSmartPointers created، سيخرج CustomSmartPointer بنهاية main خارج النطاق، مما يعني أن رست ستستدعي الشيفرة التي وضعناها في تابع drop مما سيتسبب بطباعة رسالتنا النهائية، مع ملاحظة بأننا لم نستدعي التابع drop صراحةً. نحصل على الخرج التالي عندما ننفذ هذا البرنامج: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example) Finished dev [unoptimized + debuginfo] target(s) in 0.60s Running `target/debug/drop-example` CustomSmartPointers created. Dropping CustomSmartPointer with data `other stuff`! Dropping CustomSmartPointer with data `my stuff`! استدعت رست drop تلقائيًا عندما خرجت نُسخنا عن النطاق واستدعى drop بدوره الشيفرة البرمجية التي حددناها. تُحرَّر المتغيرات عكس ترتيب انشائها لذا تُحرَّر d قبل c. يهدف هذا المثال إلى منحك دليلًا بصريًا مُلاحَظ لكيفية عمل التابع drop، إذ أنك تحدّد عادةً شيفرة تحرير الذاكرة التي تحتاجها بدلاً من طباعة رسالة. تحرير قيمة مبكرًا باستخدام دالة std::mem::drop لسوء الحظ، ليس من السهل تعطيل خاصية drop التلقائية، إلا أن تعطيل drop ليس ضروريًا عادةً، إذ أن الهدف من سمة Drop هي أنها تحدث تلقائيًا. نريد أحياناً تحرير قيمة ما مبكراً ومثال على ذلك هو استخدام المؤشرات الذكية لإدارة الأقفال، إذ قد تضطر لإجبار تابع drop على تحرير القفل لتستطيع الشيفرة البرمجية الموجودة في النطاق ذاته الحصول عليه. لا تتيح لك راست استدعاء تابع drop الخاص بسمة Drop يدويًا، بل يجب عليك بدلاً من ذلك استدعاء دالة std::mem::drop المضمّنة في المكتبة القياسية، إذا أردت تحرير قيمة قسريًا قبل أن تخرج عن نطاقها. نحصل على خطأ تصريفي إذا أردنا استدعاء التابع drop الخاص بسمة drop يدويًا وذلك بتعديل دالة main من الشيفرة 15 كما هو موضح: اسم الملف: src/main.rs struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("some data"), }; println!("CustomSmartPointer created."); c.drop(); println!("CustomSmartPointer dropped before the end of main."); } [الشيفرة 15: محاولة استدعاء تابع drop من السمة Drop يدوياً لتحرير الذاكرة المبكر] نحصل على الخطأ التالي عندما نحاول تصريف الشيفرة البرمجية السابقة: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example) error[E0040]: explicit use of destructor method --> src/main.rs:16:7 | 16 | c.drop(); | --^^^^-- | | | | | explicit destructor calls not allowed | help: consider using `drop` function: `drop(c)` For more information about this error, try `rustc --explain E0040`. error: could not compile `drop-example` due to previous error تبيّن لنا رسالة الخطأ هذه أنه من غير المسموح لنا استدعاء drop صراحةً. تستخدم رسالة الخطأ المصطلح مُفكّك destructor وهو مصطلح برمجي عام لدالة تنظف نسخة ما؛ والمفكّك هو مصطلح معاكس للباني constructor وهو الذي ينشئ نسخةً ما، ودالة drop في رست هي نوع من أنواع المفكّكات. لا تسمح لنا رست باستدعاء drop صراحةً لأن رست ستستدعي تلقائيًا التابع drop على القيمة في نهاية الدالة main مما سيسبب خطأ التحرير المزدوج double free لأن رست سيحاول تحرير القيمة ذاتها مرتين. لا يمكننا تعطيل إدخال drop التلقائي عندما تخرج قيمة ما عن النطاق، ولا يمكننا استدعاء التابع drop صراحةً، لذا نحن بحاجة لإجبار القيمة على أن تُنظف مبكرًا باستخدام الدالة std::mem::drop. تعمل دالة std::mem::drop بصورةٍ مختلفة عن التابع drop في سمة Drop، إذ نستدعيها بتمرير القيمة التي نريد تحريرها قسريًا مثل وسيط argument. الدالة مضمّنة في البداية لذا يمكننا تعديل الدالة main في الشيفرة 15 بحيث تستدعي الدالة drop كما في الشيفرة 16: اسم الملف: src/main.rs struct CustomSmartPointer { data: String, } impl Drop for CustomSmartPointer { fn drop(&mut self) { println!("Dropping CustomSmartPointer with data `{}`!", self.data); } } fn main() { let c = CustomSmartPointer { data: String::from("some data"), }; println!("CustomSmartPointer created."); drop(c); println!("CustomSmartPointer dropped before the end of main."); } [الشيفرة 16: استدعاء std::mem::drop لتحرير القيمة صراحةً قبل الخروج من النطاق] ينتج الخرج الآتي عن تنفيذ الشيفرة السابقة: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example) Finished dev [unoptimized + debuginfo] target(s) in 0.73s Running `target/debug/drop-example` CustomSmartPointer created. Dropping CustomSmartPointer with data `some data`! CustomSmartPointer dropped before the end of main. يُطبع النص "Dropping CustomSmartPointer with data some data!‎" بين النصين ".CustomSmartpointer created" و "CustomSmartPointer dropped before the end of main"، ويدلّ ذلك إلى أن شيفرة التابع drop استُدعيت لتحرير c في تلك النقطة. يمكنك استخدام شيفرة برمجية مخصصة في تطبيق سمة drop بطرق عديدة وذلك بهدف جعل عملية تحرير الذاكرة سهلة ومريحة، إذ يمكنك مثلًا استخدامها لإنشاء مُخَصص ذاكرة memory allocator خاص بك. ولا داعي لتذكر عملية تحرير الذاكرة مع سمة Drop وفي نظام رست للملكية وذلك لأن رست تفعل ذلك تلقائيًا، ولا داعي أيضًا للقلق من المشاكل الناتجة في حال تحرير قيم لا تزال قيد الاستخدام، إذ يضمن نظام الملكية أن المراجع صحيحة وأن drop يُستدعى فقط عندما تكون القيمة غير مستخدمة بعد الآن. الآن بعد أن رأينا Box<T>‎ وبعض من خصائص المؤشرات الذكية، لنرى بعض المؤشرات الذكية الأخرى المُعرفة في المكتبة القياسية. ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: معاملة المؤشرات الذكية Smart Pointers مثل مراجع نمطية Regular References باستخدام سمة Deref في لغة رست كتابة برنامج سطر أوامر Command Line بلغة رست Rust المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست
  15. يسمح لك تطبيق سمة Deref بتخصيص سلوك عامل التحصيل dereference operator * (انتبه عدم الخلط مع عامل عمليات الضرب أو عامل glob) يمكننا عند تطبيق deref بطريقة معينة تسمح بمعاملة المؤشرات الذكية smart pointers مثل مراجع نمطية، كتابة الشيفرة البرمجية بحيث تعمل على المراجع وتُستخدم بالمؤشرات الذكية أيضًا. لننظر أولًا إلى كيفية عمل عامل التحصيل مع المراجع النمطية regular references، ومن ثم سنحاول تعريف نوع مخصص يتصرف مثل Box<T>‎، وسنرى سبب عدم عمل عامل التحصيل مثل مرجع على النوع المخصص المعرف حديثاً. سنكتشف كيف يسمح تطبيق سمة Deref للمؤشرات الذكية بأن تعمل بطريقة مماثلة للمراجع، ثم سننظر إلى ميزة التحصيل القسري deref coercion في رست وكيف تسمح لنا بالعمل مع المراجع أو المؤشرات الذكية. ملاحظة: هناك فرق كبير بين النوع MyBox<T>‎ الذي سننشئه و Box<T>‎ الحقيقي: إذ لن يخزن إصدارنا منه البيانات على الكومة heap، وسنركز في مثالنا هذا على السمة deref، فمكان تخزين البيانات ليس مهمًا بقدر أهمية السلوك المشابه لسلوك المؤشر. تتبع المؤشر للوصول إلى القيمة المرجع النمطي هو نوع من المؤشرات، ويمكنك التفكير بالمؤشر على أنه سهم يشير إلى قيمة مخزنة في مكان آخر. أنشأنا في الشيفرة 6 مرجعًا إلى قيمة من النوع i32 ومن ثم استخدمنا عامل التحصيل لتتبع هذا المرجع وصولًا إلى القيمة. اسم الملف: src/main.rs fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); } [الشيفرة 6: استخدام عامل التحصيل لتتبع المرجع وصولًا إلى قيمة من النوع i32] يخزّن المتغير x قيمةً من النوع i32 هي 5. ضبطنا قيمة y بحيث تساوي مرجعًا إلى المتغير x، ويمكننا التأكد أن x تساوي 5، ولكن إذا أردنا التأكد من قيمة y علينا استخدام ‎*y لتتبع المرجع وصولًا للقيمة التي يدل عليها لتحصيلها (هذا هو السبب في حصول عملية التحصيل على اسمها)، وذلك لكي يستطيع المصرّف أن يقارنها مع القيمة الفعلية. نستطيع الحصول على قيمة العدد الصحيح y بعد تحصيل y وهي القيمة التي تشير على ما يمكن مقارنته مع 5. نحصل على الخطأ التالي عند التصريف إذا حاولنا كتابة assert_eq!(5,y);‎: $ cargo run Compiling deref-example v0.1.0 (file:///projects/deref-example) error[E0277]: can't compare `{integer}` with `&{integer}` --> src/main.rs:6:5 | 6 | assert_eq!(5, y); | ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}` | = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}` = help: the following other types implement trait `PartialEq<Rhs>`: f32 f64 i128 i16 i32 i64 i8 isize and 6 others = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0277`. error: could not compile `deref-example` due to previous error مقارنة مرجع لرقم مع رقم غير مسموح لأنهما من نوعين مختلفين ويجب علينا استخدام عامل التحصيل لتتبع المرجع وصولًا إلى القيمة التي يشير إليها. استخدام Box‎ مثل مرجع يمكننا إعادة كتابة الشيفرة البرمجية في الشيفرة 6 باسخدام Box<T>‎ بدلاً من مرجع، وذلك عن طريق استخدام عامل التحصيل الذي استخدمناه على Box<T>‎ كما هو موضح في الشيفرة 7 بطريقة مماثلة لعمل عامل التحصيل المُستخدم في الشيفرة 6: اسم الملف:src/main.rs fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); } [الشيفرة 7: استخدام عامل التحصيل على Box<i32>‎] الفرق الأساسي بين الشيفرة 7 والشيفرة 6 هو أننا حددنا y هنا لتكون نسخةً instance عن Box<T>‎ وتشير إلى قيمة منسوخة من x بدلًا من أن تكون مرجعًا يشير إلى قيمة x، ويمكننا في التوكيد assertion الأخير استخدام عامل التحصيل لتتبُّع مؤشر Box<T>‎ بالطريقة ذاتها التي اتبعناها عندما كان المرجع هو y. سنبحث تاليًا عن الشيء الذي يميّز Box<T>‎ ليسمح لنا باستخدام عامل التحصيل بتعريف نوع خاص بنا. تعريف المؤشر الذكي الخاص بنا دعنا نبني مؤشرًا ذكيًا خاصًا بنا بصورةٍ مماثلة للنوع Box<T>‎ الذي تزودنا به المكتبة القياسية لملاحظة كيف أن المؤشرات الذكية تتصرف على نحوٍ مختلف عن المراجع افتراضيًا، وسننظر بعدها إلى كيفية إضافة قدرة استخدام عامل التحصيل. النوع Box<T>‎ معرفٌ مثل هيكل صف tuple struct بعنصر واحد، لذا نعرّف في الشيفرة 8 نوع MyBox<T>‎ بالطريقة ذاتها، كما نعرف أيضاً دالة new لتطابق الدالة new المعرفة في Box<T>‎. اسم الملف: src/main.rs struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() {} [الشيفرة 8: تعريف النوع MyBox<T>‎] نعرف بنية بالاسم MyBox ونعرف معاملًا مُعمّمًا ‎‏generic ‏ ‎T‏ لأننا نريد لنوعنا أن يحتفظ بكل أنواع القيم. نوع MyBox هو صف tuple بعنصر واحد من النوع T. تأخذ دالة MyBox::new معاملًا واحدًا من النوع T وتُعيد نسخةً من MyBox تحتفظ بالقيمة المُمرّرة. لنجرب إضافة دالة main الموجودة في الشيفرة 7 إلى الشيفرة 8 ونعدلّها بحيث تستخدم النوع MyBox<T>‎ الذي عرفناه بدلًا من Box<T>‎. لن تُصّرف الشيفرة 9 لأن رست لا تعرف كيفية تحصيل قيمة MyBox. اسم الملف: src/main.rs struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); } [الشيفرة 9: محاولة استخدام Mybox<T>‎ بطريقة استخدام المراجع وBox<T>‎] إليك الخطأ التصريفي الناتج عن الشيفرة السابقة: $ cargo run Compiling deref-example v0.1.0 (file:///projects/deref-example) error[E0614]: type `MyBox<{integer}>` cannot be dereferenced --> src/main.rs:14:19 | 14 | assert_eq!(5, *y); | ^^ For more information about this error, try `rustc --explain E0614`. error: could not compile `deref-example` due to previous error لا يمكن تحصيل النوع MyBox<T>‎ الخاص بنا لأننا لم نطبق هذه الميزة على نوعنا، ولتطبيق التحصيل باستخدام العامل *، نطبّق السمة Deref. معاملة النوع مثل مرجع بتطبيق السمة Deref لتطبيق السمة نحن بحاجة تأمين تطبيقات لتوابع السمة المطلوبة كما ذكرنا سابقًا، إذ تحتاج السمة Deref الموجودة في المكتبة القياسية لتطبيق تابع واحد اسمه deref يستعير self ويعيد مرجعًا للبيانات الداخلية. تحتوي الشيفرة 10 على تطبيق Deref لإضافة تعريف MyBox: اسم الملف: src/main.rs use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); } [الشيفرة 10: تطبيق Deref على MyBox<T>‎] نعرّف في السطر type Target = T;‎ نوعًا مرتبط associated type لسمة Deref لتستخدمه، وتختلف الأنواع المرتبطة قليلًا في تعريف المعاملات المعمّمة، ولكن لا داعي للقلق بخصوصها حاليَا إذ سنتطرق لهذا الموضوع لاحقًا. نكتب في متن التابع deref المرجع ‎&self.0 بحيث يُعيد deref مرجعًا للقيمة التي نريد الوصول إليها باستخدام العامل * (تذكر سابقًا أن ‎.0 تصل إلى القيمة الأولى في هيكل الصف). تستطيع الدالة main في الشيفرة 9 التي تستدعي * على القيمة MyBox<T>‎ أن تُصرّف الآن مع نجاح التأكيدات. يستطيع المصرّف أن يحصّل المراجع & فقط بدون سمة Deref، إذ يعطي تابع deref المصرف القدرة على أن يأخذ القيم من أي نوع يطبق Deref وأن يستدعي التابع deref للحصول على مرجع & يعرف كيفية تحصيله. عندما أدخلنا ‎*y في الشيفرة 9، نفذت رست الشيفرة التالية خلف الكواليس: *(y.deref()) تستبدل رست العامل * باستدعاء للتابع deref ومن ثم إلى تحصيل عادي حتى لا نحتاج إلى التفكير فيما إذا كنا نريد استدعاء تابع deref، وتسمح لنا ميزة رست هذه بكتابة شيفرة برمجية تعمل بالطريقة نفسها سواءً أكان لدينا مرجع عادي أو نوع يطبق Deref. يعود السبب في إعادة تابع deref المرجع إلى قيمة وأن التحصيل العادي خارج القوسين في ‎*(y.deref())‎ لا يزال ضروريًا إلى نظام الملكية؛ فإذا أعاد التابع deref القيمة مباشرة بدلاً من مرجع للقيمة فإن القيمة ستنتقل خارج self، ولا نريد أن نأخذ ملكية القيمة الداخلية في MyBox<T>‎ في هذه الحالة وفي معظم الحالات التي نستخدم فيها معامل التحصيل. لاحظ أن المعامل * يُستبدل باستدعاءٍ للتابع deref ثم استدعاء للمعامل * مرةً واحدةً وذلك في كل مرة نستخدم * في شيفرتنا البرمجية. ينتهي بنا المطاف بقيمة من النوع i32 تُطابق "5" في assert_eq!‎ في الشيفرة 9 وذلك لأن عملية استبدال المعامل * لا تُنفّذ إلى ما لا نهاية. التحصيل القسري الضمني مع الدالات والتوابع يحوّل التحصيل القسري المرجع من نوع يطبق السمة Deref إلى مرجع من نوع آخر، فمثلاً يحوّل التحصيل القسري ‎&String إلى ‎&str لأن String يطبق السمة Deref بطريقة تُعيد ‎&str. التحصيل القسري هو عملية ملائمة في رست تُجرى على وسطاء arguments الدوال والتوابع وتعمل فقط على الأنواع التي تطبق السمة Deref، وتحصل هذه العملية تلقائيًا عندما نمرر مرجعًا لقيمة ذات نوع معين مثل وسيط لدالة أو تابع لا يطابق نوع المعامل في تعريف الدالة أو التابع. تحوِّل سلسلةً من الاستدعاءات إلى التابع deref النوع المُقدم إلى نوع يحتاجه المعامل. أُضيف التحصيل القسري إلى رست بحيث لا يضطر المبرمجون الذين يكتبون استدعاءات لدالات وتوابع إلى إضافة العديد من المراجع الصريحة والتحصيلات باستخدام & و *، كما تسمح لنا خاصية التحصيل القسري أيضاً بكتابة شيفرة برمجية تعمل لكل من المراجع أو المؤشرات الذكية في الوقت ذاته. لمشاهدة عمل التحصيل القسري عمليًا، نستخدم النوع MyBox<T>‎ الذي عرفناه في الشيفرة 8 بالإضافة إلى تطبيق Deref الذي أضفناه في الشيفرة 10. توضح الشيفرة 11 تعريف دالة تحتوي على معامل شريحة سلسلة نصية string slice: اسم الملف: src/main.rs fn hello(name: &str) { println!("Hello, {name}!"); } fn main() {} [الشيفرة 11: الدالة hello التي تحتوي على معامل name من النوع ‎&str] بإمكاننا استدعاء الدالة hello باستخدام شريحة سلسلة نصية بمثابة وسيط مثل hello(“Rust”);‎. يجعل التحصيل القسري استدعاء hello مع مرجع للقيمة MyBox<String>‎ ممكنًا كما هو موضح في الشيفرة 12: اسم الملف: src/main.rs use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); } [الشيفرة 12: استدعاء hello باستخدام مرجع إلى القيمة MyBox<String>‎ ويمكن تنفيذ ذلك بفضل التحصيل القسري] نستدعي هنا الدالة hello مع الوسيط ‎&m الذي يمثل مرجعًا إلى القيمة MyBox<String>‎، وتستطيع رست تحويل ‎&MyBox<String>‎ إلى ‎&String باستدعاء deref وذلك لأننا طبقنا السمة Deref على MyBox<T>‎ كما هو موضح في الشيفرة 10. تقّدم المكتبة القياسية تطبيقًا للسمة Deref على النوع String الذي يعيد لنا شريحة سلسلة نصية ويمكنك العثور على هذه التفاصيل في توثيق الواجهة البرمجية الخاصة بالسمة Deref. تستدعي رست التابع deref مجددًا لتحويل ‎&String إلى ‎&str الذي يطابق تعريف دالة hello. إذا لم تطبق رست التحصيل القسري فسيتوجب علينا كتابة الشيفرة 13 بدلًا من الشيفرة 12 لاستدعاء hello مع قيمة من النوع ‎&MyBox<String>‎. اسم الملف: src/main.rs use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&(*m)[..]); } [الشيفرة 13: الشيفرة التي يجب علينا كتابتها إذا لم تحتوي رست على ميزة التحصيل القسري] يحصّل (m*) النوع MyBox<String>‎ إلى String ومن ثم إلى & وتأخذ [..] شريحة سلسلة نصية String تساوي قيمة السلسلة النصية كاملةً وذلك لمطابقة بصمة الدالة hello. ستكون هذه الشيفرة البرمجية صعبة القراءة والفهم بدون التحصيل القسري وذلك مع كل الرموز اللازمة، إذ يسمح التحصيل القسري للغة رست بمعالجة هذه التحويلات تلقائياً نيابةً عنّا. عندما تُعرَّف سمة Deref للأنواع المستخدمة، ستحلّل رست هذه الأنواع وتستخدم Deref::deref بعدد المرات اللازم لتحصل على مرجع يطابق نوع المعامل، ويُعرَّف عدد مرات إضافة Deref::deref اللازمة عند وقت التصريف لذا لا يوجد أي عبء إضافي عند وقت التشغيل runtime مع التحصيل القسري. كيفية تعامل التحصيل القسري مع قابلية التغيير يمكننا استخدام السمة DerefMut لتجاوز عمل العامل * على المراجع المتغيّرة mutable references بصورةٍ مشابهة لاستخدامنا لسمة Deref لتجاوز عمل العامل * على المراجع الثابتة immutable. تنفّذ رست عملية التحصيل القسري عندما تجد تطبيقات لأنواع وسمات في ثلاث حالات معيّنة: من ‎&T إلى ‎&U عندما T: Deref<Target=U>‎. من ‎&mut T إلى ‎&mut U عندما T: DerefMut<Target=U>‎. من ‎&mut T إلى ‎&U عندما T: Deref<Target=U>‎. الحالتان الأولى والثانية متماثلتان مع فرق أن الثانية هي تطبيق لحالة متغيّرة، بينما تنصّ الحالة الأولى أنه إذا كان لديك ‎&T وتطبّق T سمة Deref لنوع ما من U، يمكن الحصول على ‎&U بوضوح، والحالة الثانية تشير إلى أن عملية التحصيل القسري ذاتها تحدث للمراجع المتغيّرة. تعدّ الحالة الثالثة أكثر تعقيدًا؛ إذ تجبر رست تحويل مرجع متغيّر إلى مرجع ثابت، إلا أن العكس غير ممكن، فالمراجع الثابتة لن تُحوّل قسريًا إلى مراجع متغيرة، وذلك بسبب قواعد الاستعارة، وإذا كان لديك مرجعًا متغيّرًا فإن هذا المرجع سيكون المرجع الوحيد لتلك البيانات (وإلا فلن يمكنك تصريف البرنامج). لن يكسرتحويل مرجع متغيّرإلى مرجع ثابت قواعد الاستعارة الافتراضية. تتطلب عملية تحويل المرجع الثابت إلى مرجع متغيّر أن يكون المرجع الثابت الابتدائي هو المرجع الوحيد الثابت للبيانات الخاصة به، إلا أن قوانين الاستعارة لا تضمن لك ذلك، وبالتالي لا يمكن لرست الافتراض بأن تحويل مرجع ثابت إلى مرجع متغيّر هي شيء ممكن. ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: المؤشرات الذكية Smart Pointers في رست Rust المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست
  16. يُعد المؤشر pointer مفهومًا عامًا لمتغيرٍ يحتوي على عنوان في الذاكرة، ويشير هذا العنوان أو "يؤشر إلى" بعض البيانات الأخرى. أكثر أنواع المؤشرات شيوعًا في رست هو المرجع reference، الذي تعلمناه سابقًا. يُحدّد المرجع بالرمز "&" وتُستعار القيمة التي يشير إليها، ولا يوجد للمؤشرات أي قدرات خاصة عدا الإشارة إلى البيانات، ولا يتطلّب استخدامها أي حِمل إضافي overhead. من جهة أخرى، تُعدّ المؤشرات الذكية smart pointers هياكل بيانات تعمل مثل مؤشر ولكن لها أيضًا بيانات وصفية metadata وقدرات إضافية، إذ لا يقتصر مفهوم المؤشرات الذكية على رست، فهي نشأت في لغة سي بلس بلس C++‎ وتوجد بلغات أخرى أيضًا. تحتوي رست على مجموعة متنوعة من المؤشرات الذكية المعرَّفة في المكتبة القياسية التي تقدم وظائف أكثر من تلك التي توفرها المراجع، وللتعرف على المفهوم العام، سنلقي نظرةً على بعض الأمثلة المختلفة للمؤشرات الذكية، بما في ذلك نوع مؤشر ذكي لعدّ المراجع reference counting. يمكّنك هذا المؤشر من السماح بوجود عدّة مالكين owners للبيانات من خلال تتبع عددهم، ويُحرّر البيانات في حال لم يتبقَّ أي مالكين. يوجد بمفهوم رست للملكية والاستعارة فرقٌ إضافي بين المراجع والمؤشرات الذكية؛ إذ بينما تستعير المراجع البيانات فقط، تمتلك المؤشرات الذكية في كثير من الحالات البيانات التي تشير إليها. صادفنا مسبقًا بعض المؤشرات الذكية -على الرغم من أننا لم ندعوها على هذا النحو في ذلك الوقت- بما في ذلك String و <Vec<T، وكلا النوعين مؤشرات ذكية لأنهما يمتلكان بعض الذاكرة و تسمحان لك بالتلاعب بها، إضافةً لوجود بيانات وصفية وإمكانيات أو ضمانات إضافية. تخزّن String على سبيل المثال سعتها على أنها بيانات وصفية ولديها قدرة إضافية لتضمن أن تكون بياناتها دائمًا بترميز UTF-8 صالح. تُطبَّق عادةً المؤشرات الذكية باستخدام الهياكل، وتنفّذ المؤشرات الذكية على عكس البنية العادية Deref و Drop، إذ تسمح سمة Deref لنسخة instance من هيكل المؤشر الذكي بالتصرف بمثابة مرجع حتى تتمكن من كتابة شيفرتك البرمجية للعمل مع المراجع أو المؤشرات الذكية، بينما تسمح لك سمة Drop بتخصيص الشيفرة التي تُنفَّذ عندما تخرج نسخة المؤشر الذكي عن النطاق، وسنناقش هنا كلًا من السمات traits ونوضح سبب أهميتها للمؤشرات الذكية. لن يغطي هذا المقال كل مؤشر ذكي موجود بما أن نمط المؤشر الذكي smart pointer pattern هو نمط تصميم عام يستخدم بصورةٍ متكررة في رست. تمتلك العديد من المكتبات مؤشراتها الذكية الخاصة بها، ويمكنك حتى كتابة المؤشرات الخاصة بك. سنغطي المؤشرات الذكية الأكثر شيوعًا في المكتبة القياسية: <Box<T لحجز مساحة خاصة بالقيم على الكومة heap. <Rc<T نوع عدّ مرجع يمكّن الملكية المتعددة. <Ref<T و <RefMut<T اللذين يمكن الوصول إليهما عن طريق <RefCell<T، وهو نمط يفرض قواعد الاستعارة وقت التنفيذ runtime بدلًا من وقت التصريف compile time. سنغطي بالإضافة إلى ذلك نمط قابلية التغيير الداخلي interior mutability pattern، إذ يعرّض النوع الثابت immutable واجهة برمجية لتعديل قيمة داخلية، كما سنناقش أيضًا دورات المرجع reference cycles، وسنرى كيف بإمكانها تسريب leak الذاكرة وكيفية منعها من ذلك. دعنا نبدأ. استخدام المؤشر Box‎ للإشارة إلى البيانات المخزنة على الكومة يُعد الصندوق "Box" واحدًا من أكثر المؤشرات الذكية وضوحًا وبساطةً، ويُكتب نوعه بالشكل <Box<T. تسمح لك الصناديق أن تخزن البيانات على الكومة بدلًا من المكدس stack، إذ يبقى المؤشر على المكدس الذي يشير بدوره للبيانات الموجودة على الكومة. عد إلى الفصل المراجع References والاستعارة Borrowing والشرائح Slices في لغة رست لمراجعة الفرق بين الكومة والمكدس. لا تملك الصناديق أي أفضلية في الأداء عدا أنها تخزن بياناتها على الكومة عوضًا عن المكدس، ولا تملك الكثير من الإمكانيات الإضافية. سنستخدمها غالبًا في أحد هذه الحالات: عندما يكون لديك نوع بحجم غير معروف وقت التصريف وتريد أن تستخدم قيمةً لهذا النوع في سياق يتطلب حجمه المحدد. عندما يكون لديك حجم كبير من البيانات وتريد أن تنقل ملكيتها ولكنك تريد التيقن أن البيانات لن تُنسَخ عندما تفعل هذا. عندما تريد أن تملك قيمةً ما وتهتم فقط أنها من نوع يناسب سمة محددة بدلًا عن كونها من نوع محدد. سنستعرض الحالة الأولى في فقرة "تمكين الأنواع التعاودية باستخدام الصناديق"، أما في الحالة الثانية فيمكن أن يأخذ نقل ملكية كبيرة من البيانات وقتًا طويلًا وذلك لأن البيانات نُسخت على المكدس، ويمكننا تخزين الكمية الكبيرة من البيانات على الكومة في صندوق لتحسين الأداء في هذه الحالة، وبذلك تُنسخ كميةٌ صغيرةٌ من بيانات المؤشر على المكدس، بينما تبقى البيانات التي تشير إليها في مكان واحد على الكومة. تُعرف الحالة الثالثة باسم "سمة الكائن" وسنتكلم عنها لاحقًا، إذ أنك ستطبق ما تعلمته هنا لاحقًا. استخدام Box‎ لتخزين البيانات على الكومة سنتكلم عن طريقة كتابة Box<T>‎ وكيفية تفاعل هذا النوع مع القيم المخزنة داخله قبل أن نناقش حالة استخدام تخزين الكومة للنوع Box<T>‎. توضح الشيفرة 1 كيفية استخدام صندوق لتخزين قيمة i32 على الكومة: اسم الملف: src/main.rs fn main() { let b = Box::new(5); println!("b = {}", b); } [الشيفرة 1: تخزين قيمة من النوع i32 على الكومة باستعمال صندوق box] نعرّف المتغير b ليملك القيمة Box التي تشير إلى القيمة "5" المخزنة على الكومة. سيطبع هذا البرنامج "b = 5" وفي هذه الحالة سنصل للبيانات الموجودة في الصندوق بطريقة مشابهة في حال كانت البيانات مخزنة على المكدس. ستُحرَّر القيمة deallocated كما في أي قيمة ممتلكة، عندما يخرج صندوق عن النطاق كما تفعل b في نهاية main، وتحدث عملية التحرير لكل من الصندوق (المخزن على المكدس) والبيانات التي يشير إليها (المخزنة على الكومة). وضع قيمة وحيدة على الكومة غير مفيد، لأنك لن تستخدم الصناديق بحد ذاتها كثيرًا، ووجود قيم مثل قيمة وحيدة من النوع i32 على المكدس -إذ تُخزن افتراضيًا هناك- مناسبٌ أكثر في أغلب الحالات. لننظر إلى حالة تسمح لنا الصناديق أن نعرّف أنواع لن يُسمح لنا بتعريفها إن لم يكن لدينا صناديق. تمكين الأنواع التعاودية باستخدام الصناديق يمكن للقيمة من نوع تعاودي recursive type أن تملك قيمةً أخرى من النوع ذاته مثل جزء من نفسها. تمثّل الأنواع التعاودية مشكلة إذ أن رست تحتاج لمعرفة المساحة التي يحتلها نوع ما وقت التصريف، ويمكن لتداخل قيم الأنواع التعاودية نظريًا أن يستمر إلى ما لا نهاية، لهذا لا يمكن أن تعرف رست كم تحتاج القيمة من مساحة، إلا أنه يمكننا استخدام الأنواع التعاودية بإدخال صندوق في تعريف النوع التعاودي نظرًا لأن الصناديق لها حجم معروف. لنكتشف قائمة البنية "cons list" مثالًا على نوع تعاودي، إذ أن نوع البيانات هذا موجود كثيرًا في لغات البرمجة الوظيفية. يُعَدّ نوع قائمة البنية بسيطًا وواضحًا باستثناء نقطة التعاود فيه، وبالتالي ستكون المفاهيم في الأمثلة التي سنعمل عليها مفيدةً في أي وقت ستصادف فيه حالات أكثر تعقيدًا من ضمنها الأنواع التعاودية. المزيد من المعلومات عن قائمة البنية تُعد قائمة البنية هيكل بيانات أتى من لغة البرمجة ليسب Lisp وشبيهاتها، وتتألف من أزواج متداخلة، وهي نسخة ليسب من القائمة المترابطة linked list، ويأتي اسم هيكل البيانات هذا من الدالة cons (اختصارًا لدالة البنية construct function) في ليسب التي تبني بدورها زوجًا جديدًا من وسيطين arguments. يمكننا بناء قوائم بنية مؤلفة من أزواج تعاودية عن طريق استدعاء cons على زوج يحتوي على قيمة وزوج آخر. إليك المثال التوضيحي pseudocode لقائمة بنية تحتوي على القائمة 1، 2، 3 مع وجود كل زوج داخل قوسين: (1, (2, (3, Nil))) يحتوي كل عنصر في قائمة البنية على عنصرين: القيمة للعنصر الحالي والعنصر التالي، إلا أن العنصر الأخير في القائمة يحتوي فقط على قيمة تُدعى Nil دون عنصر تالي. يمكن إنشاء قائمة البنية عن طريق استدعاء دالة cons بصورة تعاودية، والاسم المتعارف عليه للدلالة على الحالة الأساسية base case للتعاودية هو Nil، مع العلم أنه ليس خاضعًا لنفس مبدأ المصطلحين "null" أو "nil" الذين ناقشناهما سابقًا، فهما يمثلان مؤشرًا على قيمة غير موجودة أو غير صالحة. لا تعد قائمة بينة من هياكل البيانات المُستخدمة بكثرة في رست، إذ يُعد النوع <Vec<T خيارًا أفضل للاستعمال في معظم الوقت عندما تملك قائمة عناصر في رست، كما يوجد أنواع أخرى لبيانات تعاودية مفيدة في حالات متعددة، لكن من خلال البدء بقائمة البنية هنا، سنتعرف كيف تمكّننا للصناديق من تعريف نوع بيانات تعاودية دون ارتباك. تتضمن الشيفرة 2 على تعريف لمعدّد enum لقائمة بنية. لاحظ أن هذه الشيفرة لن تُصرَّف بعد لأن النوع List لا يملك حجمًا محددًا وهذا ما سنوضحه لاحقًا. اسم الملف: src/main.rs enum List { Cons(i32, List), Nil, } [الشيفرة 2: المحاولة الأولى لتعريف معدّد لتمثيل هيكل البيانات قائمة البنية لقيم من النوع i32] ملاحظة: نطبّق قائمة البنية التي تحمل فقط قيم من النوع i32 بهدف التوضيح، إذ يمكننا تنفيذها باستعمال الأنواع المعمّمة generics كما ناقشنا سابقًا وذلك لتعريف نوع قائمة بنية يخزّن قيمًا من أي نوع. يبدو استعمال النوع List لتخزين القائمة "1‎, 2, 3" كما توضح الشيفرة 3: اسم الملف: src/main.rs use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Cons(2, Cons(3, Nil))); } [الشيفرة 3: استعمال المعدّد List لتخزين القائمة "1‎, 2, 3"] تحمل قيمة Cons الأولى على "1" وقيمة List أخرى، قيمة List هذه هي قيمة Cons أخرى تحتوي على "2" وقيمة List أخرى، قيمة List هذه هي قيمة Cons أخرى تحتوي على "3" وقيمة List التي هي في النهاية Nil ألا وهو المتغاير variant غير التعاودي الذي يشير إلى نهاية القائمة. إذا حاولنا تصريف الشيفرة البرمجية الموجودة في الشيفرة 3، فسنحصل على الخطأ الموضح في الشيفرة 4: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) error[E0072]: recursive type `List` has infinite size --> src/main.rs:1:1 | 1 | enum List { | ^^^^^^^^^ recursive type has infinite size 2 | Cons(i32, List), | ---- recursive without indirection | help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable | 2 | Cons(i32, Box<List>), | ++++ + For more information about this error, try `rustc --explain E0072`. error: could not compile `cons-list` due to previous error [الشيفرة 4: الخطأ الذي نحصل عليه عندما نحاول تعريف معدّد تعاودي] يُظهِر الخطأ أن هذا النمط "له حجم لا نهائي"، والسبب هو تعريفنا للنوع List بمتغير تعاودي، أي أنه يحمل قيمةً أخرى لنفسه مباشرًة، ونتيجة لذلك، لا تستطيع رست معرفة مقدار المساحة التي يحتاجها لتخزين قيمة List. دعونا نوضح لماذا نحصل على هذا الخطأ. أولًا، سننظر إلى كيفية تحديد رست لمقدار المساحة التي يحتاجها لتخزين قيمة لنوع غير تعاودي. حساب حجم نوع غير تعاودي تذكر معدّد Message الذي عرّفناه سابقًا (الشيفرة 2 من الفصل التعدادات enums في لغة رست Rust)عندما ناقشنا تعريفات المعدّد: enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() {} تمر رست عبر كل من المتغيرات لمعرفة المتغير الذي يحتاج إلى أكبر مساحة وذلك لتحديد مقدار المساحة المراد تخصيصها لقيمة Message. ترى رست أن Message::Quit لا تحتاج إلى أي مساحة، بينما تحتاج Message::Move إلى مساحة كافية لتخزين قيمتين من نوع i32، وهكذا دواليك، ونظرًا لاستخدام متغير واحد فقط فإن أكبر مساحة تحتاجها قيمة Message هي المساحة التي ستأخذها لتخزين أكبر متغيراتها. قارن هذا مع ما يحدث عندما تحاول رست تحديد مقدار المساحة التي يحتاجها نوع تعاودي مثل المعدد List في الشيفرة 2، إذ يبدأ المصرّف بالنظر إلى المتغاير Cons الذي يحمل قيمةً من النوع i32 وقيمةً من النوع List، لذلك يحتاج Cons إلى مساحة مساوية لحجم النوع i32 إضافةً إلى حجم النوع List. لمعرفة مقدار الذاكرة التي يحتاجها النوع List ينظر المصرّف إلى المتغايرات بدءًا من المتغاير Cons، الذي يحمل قيمةً من النوع i32 وقيمةً من النوع List، وتستمر هذه العملية لما لا نهاية، كما هو موضح في الشكل 1. [الشكل 1: List لانهائية مؤلفة من متغايرات Cons لانهائية] استخدام <Box<T للحصول على نوع تعاودي بحجم معروف يعطينا المصرّف الخطأ التالي لأن رست لا يمكنها معرفة مقدار المساحة المراد تخصيصها لأنواع معرّفة بصورةٍ تعاودية مرفقًا مع هذا الاقتراح المفيد: help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable | 2 | Cons(i32, Box<List>), | ++++ + يعني "التحصيل indirection" -في هذا الاقتراح- أنه بدلًا من تخزين قيمة مباشرةً، يجب علينا تغيير هيكل البيانات المُستخدَم لتخزين القيمة بصورةٍ غير مباشرة عن طريق تخزين مؤشر يشير إلى القيمة عوضًا عن ذلك. نظرًا لأن <Box<T هو مؤشر فإن رست تعرف دائمًا مقدار المساحة التي يحتاجها <Box<T، إذ أن حجم المؤشر لا يتغير بناءً على كمية البيانات التي يشير إليها، وهذا يعني أنه يمكننا وضع <Box<T داخل المتغاير Cons بدلًا من قيمة List أخرى مباشرةً. سيشير <Box<T إلى قيمة List التالية التي ستكون على الكومة بدلًا من داخل المتغاير Cons. نظريًا، لا يزال لدينا قائمة أنشئت باستخدام قوائم تحتوي على قوائم أخرى، ولكن هذا التطبيق الآن أشبه بوضع العناصر بجانب بعضها بدلًا من وضع بعضها داخل الأخرى. يمكننا تغيير تعريف معدّد List في الشيفرة 2 باستخدام List في الشيفرة 3 كما هو موضح في الشيفرة 5 التي ستصرَّف بنجاح: اسم الملف: src/main.rs enum List { Cons(i32, Box<List>), Nil, } use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); } [الشيفرة 5: تعريف List التي تستخدم <Box<T للحصول على حجم معروف] يحتاج المتغاير Cons إلى حجمi32 بالإضافة إلى مساحة لتخزين بيانات مؤشر الصندوق، وبما أن المتغاير Nil لا يخزن أي قيم فهو يحتاج إلى مساحة أقل من المتغاير Cons. نعلم الآن أن أي قيمة List ستشغل حجم i32 إضافةً إلى حجم بيانات مؤشر الصندوق. كسرنا السلسلة اللانهائية التعاودية باستخدام الصندوق، بحيث يمكن للمصرف الآن معرفة الحجم الذي يحتاجه لتخزين قيمة List. يوضح الشكل 2 ما يبدو عليه متغاير Cons الآن. [الشكل 2: List ذات حجم محدد لأن Cons يحمل Box] توفر الصناديق التحصيل وتخصيص الكومة فقط، إذ لا تتوافر على أي إمكانيات خاصة أخرى مثل تلك التي سنراها مع أنواع المؤشرات الذكية الأخرى لاحقًا، كما أنها لا تتمتع بالأداء العام الذي تتحمله هذه الإمكانيات الخاصة لتكون مفيدةً في حالات مثل قائمة البنية، إذ تكون ميزة التحصيل هي الميزة الوحيدة التي نحتاجها. سنلقي نظرةً على المزيد من حالات استعمال الصناديق لاحقًا. النوع <Box<T هو مؤشر ذكي لأنه يطبق السمة Deref التي تسمح لقيم <Box<T أن تُعامَل بمثابة مراجع. عندما تخرج قيمة <Box<T عن النطاق، تُمسَح بيانات الكومة التي يشير إليها الصندوق بسبب تطبيق السمة Drop. ستبرز أهمية هاتان السمتان أكثر عندما نناقش أنواع المؤشرات الذكية الأخرى لاحقًا. لنكتشف هاتين السمتين بتفاصيل أكثر. ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: مساحة عمل كارجو Cargo Workspaces في لغة رست وتحميل حزمة من crates.io أنواع البيانات Data Types في لغة رست Rust المؤشرات الذكية (Smart Pointers) في Cpp
  17. بنينا سابقًا حزمة تتضمن وحدة تنفيذية مصرفة Binary Crate ووحدة مكتبة مصرفة Library Crate، وقد تجد مع تطور مشروعك أن وحدة المكتبة المصرفة تزداد حجمًا وستحتاج إلى تقسيم حزمتك إلى عدد من وحدات مكتبة مصرفة. يقدّم كارجو Cargo ميزة تدعى مساحات العمل Workspaces التي تساعد على إدارة حزم متعددة مرتبطة تُطوَّر بالترادف tandem أي واحدًا بعد الآخر. إنشاء مساحة عمل مساحة العمل هي مجموعة من الحزم التي تتشارك ملف Cargo.lock ومجلد الخرج ذاتهما. سنستخدم شيفرة برمجية بسيطة لإنشاء مشروع باستخدام مساحة العمل، بهدف التركيز على بُنية مساحة العمل أكثر. هناك الكثير من الطرق لبناء مساحة العمل ولذا سنعمل وفق الطريقة الشائعة. سيكون لدينا مساحة عمل تحتوي على وحدة ثنائية أو تنفيذية واحدة ومكتبتين؛ إذ ستؤمن الوحدة الثنائية الوظيفة الأساسية، وستعتمد بدورها على مكتبتين: مكتبة تؤمن دالة add_one، والثانية ستؤمن دالة add_two. ستكون الوحدات المصرفة الثلاثة في مساحة العمل ذاتها. نبدأ بعمل مسار جديد لمساحة العمل: $ mkdir add $ cd add ننشئ بعد ذلك في مجلد الخرج add الملف Cargo.toml الذي سيضبط مساحة العمل كاملةً. لن يكون لهذا الملف قسم [package]. بل سيبدأ بقسم [workspace] الذي سيسمح لنا بإضافة أعضاء إلى مساحة العمل عن طريق تحديد المسار للحزمة باستخدام الوحدة الثنائية أو التنفيذية المصرفة، وفي هذه الحالة المسار هو "adder": اسم الملف: Cargo.toml [workspace] members = [ "adder", ] ننشئ وحدة ثنائية مصرفة [adder] عن طريق تنفيذ cargo new في المجلد add: $ cargo new adder Created binary (application) `adder` package يمكننا الآن بناء مساحة العمل عن طريق تشغيل cargo build. الملفات في مجلد add يجب أن تكون على النحو التالي: ├── Cargo.lock ├── Cargo.toml ├── adder │ ├── Cargo.toml │ └── src │ └── main.rs └── target لمساحة العمل مجلد "target" واحد في بداية المستوى الذي ستوضع فيه أدوات التخطيط artifacts المصرفة، ولا تحتوي حزمة adder على مجلد "target". حتى لو نفّذنا cargo build داخل مجلد "adder"، ستكون أدوات التخطيط المصرفة في "add/target" بدلاً من "add/adder/target". يهيّئ كارجو المجلد target بالشكل هذا لأن الحزم المصرفة في مساحة العمل مهيئة لتعتمد على بعضها بعضًا. إذا كان لكل حزمة مصرفة مجلد "target" خاص بها، فهذا يعني أن كل حزمة مصرفة ستُعيد تصريف باقي الحزم المصرفة في مساحة العمل لوضع أدوات التخطيط في مجلد "target" الخاص بها، إلا أن الحزم تتجنب عملية إعادة البناء غير الضرورية بمشاركة مجلد "target" واحد. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن إنشاء الحزمة الثانية في مساحة العمل دعنا ننشئ حزمة عضو ثانية في مساحة العمل ونسميها add_one. غيِّر ملف Cargo.toml الموجود في المستوى العلوي ليحدد المسار add_one في القائمة members: اسم الملف: Cargo.toml [workspace] members = [ "adder", "add_one", ] أنشئ بعد ذلك حزمة مكتبة مصرفة اسمها add_one: $ cargo new add_one --lib Created library `add_one` package يجب أن يحتوي مجلد "add" الآن على المجلدات والملفات التالية: ├── Cargo.lock ├── Cargo.toml ├── add_one │ ├── Cargo.toml │ └── src │ └── lib.rs ├── adder │ ├── Cargo.toml │ └── src │ └── main.rs └── target نضيف في الملف add_one/scr/lib.rs الدالة add_one: اسم الملف: add_one/src/lib.rs pub fn add_one(x: i32) -> i32 { x + 1 } الآن بإمكاننا أن نجعل كلًا من الحزمة adder والوحدة الثنائية المصرفة تعتمدان على حزمة add_one التي تحتوي مكتبتنا. أولاً، نضيف اعتمادية مسار add_one إلى الملف adder/Cargo.toml. [dependencies] add_one = { path = "../add_one" } لا يفترض كارجو أن الحزم المصرفة تعتمد على بعضها في مساحة العمل، لذا نحتاج إلى توضيح علاقات الاعتمادية. لنستخدم بعدها دالة add_one (من الحزمة المصرفة add_one) في الحزمة المصرفة adder. افتح الملف adder/scr/main.rs وأضف سطر use في الأعلى لإضافة حزمة المكتبة المصرفة add_one الجديدة إلى النطاق. ثم عدِّل الدالة main بحيث تستدعي الدالة add_one كما في الشيفرة 7. اسم الملف: adder/src/main.rs use add_one; fn main() { let num = 10; println!("Hello, world! {num} plus one is {}!", add_one::add_one(num)); } [الشيفرة 7: استخدام حزمة المكتبة المصرفة add_one من الحزمة adder] دعنا نبني مساحة العمل بتنفيذ cargo build في مجلد "add" العلوي. $ cargo build Compiling add_one v0.1.0 (file:///projects/add/add_one) Compiling adder v0.1.0 (file:///projects/add/adder) Finished dev [unoptimized + debuginfo] target(s) in 0.68s يمكننا تحديد أي حزمة نريد تشغيلها في مساحة العمل باستخدام الوسيط ‎-p واسم الحزمة مع cargo run لتشغيل الحزمة الثنائية المصرفة من المجلد "add": $ cargo run -p adder Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/adder` Hello, world! 10 plus one is 11! يشغل هذا الأمر الشيفرة الموجودة في الملف "adder/scr/main.rs"، والتي تعتمد على الحزمة المصرفة add_one. الاعتماد على حزمة خارجية في مساحة العمل نلاحظ أن مساحة العمل تحتوي على ملف Cargo.lock واحد في المستوى الأعلى، بدلاً من أن يكون هناك ملف Cargo.lock في كل مسار حزمة مصرفة. يضمن ذلك أن كل حزمة مصرفة تستخدم الإصدار ذاته لكل الاعتماديات. إذا أضفنا حزمة rand للملفين "adder/Cargo.toml" و "add_one/Cargo.toml"، سيحوِّل كارجو كلاهما إلى إصدار واحد من rand، ثم سيسجل ذلك في Cargo.lock. جعل كل حزم المصرفة تستخدم نفس الاعتمادية يعني أن كل الحزم المصرفة ستكون متوافقة مع بعضها. دعنا نضيف الحزمة المصرفة rand إلى قسم [dependencies] في ملف add_one/Cargo.toml لكي نستخدم الحزمة المصرفة rand في الحزمة المصرفة add_one: اسم الملف: add_one/Cargo.toml [dependencies] rand = "0.8.5" يمكننا الآن إضافة use rand;‎ إلى الملف add_one/src/lib.rs، وبناء كامل مساحة العمل عن طريق تنفيذ cargo build في المجلد "add" الذي سيجلب ويصرِّف الحزمة المصرفة rand. نحصل على تحذير واحد لأننا لم نُشر إلى حزمة rand التي أضفناها إلى النطاق: $ cargo build Updating crates.io index Downloaded rand v0.8.5 --snip-- Compiling rand v0.8.5 Compiling add_one v0.1.0 (file:///projects/add/add_one) warning: unused import: `rand` --> add_one/src/lib.rs:1:5 | 1 | use rand; | ^^^^ | = note: `#[warn(unused_imports)]` on by default warning: `add_one` (lib) generated 1 warning Compiling adder v0.1.0 (file:///projects/add/adder) Finished dev [unoptimized + debuginfo] target(s) in 10.18s يحتوي ملف Cargo.lock في المستوى العلوي على معلومات عن الاعتمادية لكل من add_one و rand، ولكن وعلى الرغم من أننا نستخدم rand في مكان ما ضمن مساحة العمل إلا أننا لا نستطيع استخدامها في الحزم المصرفة الأخرى إلا إذا اضفنا rand إلى ملف Cargo.toml الخاص بها أيضاً. على سبيل المثال إذا أضفنا use rand;‎ إلى ملف adder/scr/main.rs من أجل الحزمة adder سنحصل على خطأ: $ cargo build --snip-- Compiling adder v0.1.0 (file:///projects/add/adder) error[E0432]: unresolved import `rand` --> adder/src/main.rs:2:5 | 2 | use rand; | ^^^^ no external crate `rand` لحل هذه المشكلة، عدِّل ملف Cargo.toml لحزمة adder وأشِر إلى أن rand هي اعتمادية لها أيضاً. بناء الحزمة adder سيضيف rand إلى لائحة اعتماديات adder في ملف cargo.lock، ولكن لن يجري أي تنزيل لنسخ إضافية من rand. يضمن كارجو أن كل حزمة مصرفة في كل حزمة في مساحة العمل تستخدم نفس الإصدار من الحزمة rand، وبالتالي ستقل المساحة التخزينية المستخدمة وسنضمن أن كل الحزم المصرفة في مساحة العمل ستكون متوافقة مع بعضها بعضًا. إضافة اختبار إلى مساحة العمل سنضيف اختبارًا للدالة add_one::add_one داخل الحزمة المصرفة add_one للمزيد من التحسينات: اسم الملف: add_one/src/lib.rs pub fn add_one(x: i32) -> i32 { x + 1 } #[cfg(test)] mod tests { use super::*; #[test] fn it_works() { assert_eq!(3, add_one(2)); } } نفّذ الأمر cargo test ضمن مجلد "add" العلوي، إذ سيؤدي تنفيذ cargo test في مساحة عمل مهيكلة بهذا الشكل إلى تنفيذ الاختبارات الخاصة بالحزم المصرفة في مساحة العمل: $ cargo test Compiling add_one v0.1.0 (file:///projects/add/add_one) Compiling adder v0.1.0 (file:///projects/add/adder) Finished test [unoptimized + debuginfo] target(s) in 0.27s Running unittests src/lib.rs (target/debug/deps/add_one-f0253159197f7841) running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/adder-49979ff40686fa8e) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s يُظهر أول قسم في الخرج نجاح اختبار it_works في الحزمة المصرفة add_one، بينما يظهر القسم الثاني أنه لم يُعثر على أي اختبار في الحزمة المصرفة adder، ويظهر القسم الأخير عدم العثور على اختبارات توثيق documentation tests في الحزمة المصرفة add_one. يمكن أيضاّ تنفيذ اختبارات لحزمة مصرفة محددة في مساحة عمل من المجلد العلوي باستخدام الراية ‎-p وتحديد اسم الحزمة المصرفة المراد اختبارها: $ cargo test -p add_one Finished test [unoptimized + debuginfo] target(s) in 0.00s Running unittests src/lib.rs (target/debug/deps/add_one-b3235fea9a156f74) running 1 test test tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s يظهر الخرج أن cargo test نفّذ فقط الاختبارات الموجودة في الحزمة المصرفة add_one ولم ينفّذ الاختبارات الموجودة في الحزمة المصرفة adder. إذا أردت نشر الحزم المصرفة في مساحة العمل على crates.io، فيجب على كل حزمة مصرفة في مساحة العمل أن تُنشر على حدة. نستطيع نشر حزمة مصرفة معينة في مساحة العمل باستخدام الراية ‎-p وتحديد اسم الحزمة المصرفة المراد نشرها بصورةٍ مماثلة للأمر cargo test. للتدرّب على العملية بصورةٍ أفضل، ضِف الحزمة المصرفة add_two لمساحة العمل هذه بنفس طريقة الحزمة المصرفة add_one. ضع في الحسبان استخدام مساحة العمل كلما كبر مشروعك، فمن الأسهل فهم مكونات صغيرة ومنفردة على كتلة كبيرة من الشيفرة البرمجية. إضافةً إلى ذلك، إبقاء الحزم المصرفة في مساحة عمل واحدة يجعل التنسيق بين الحزم المصرفة أسهل إذا كانت تُعدَّل باستمرار في نفس الوقت. تثبيت الملفات الثنائية binaries باستخدام cargo install يسمح لك أمر cargo install بتثبيت واستخدام الوحدات الثنائية المصرفة محليًا، وليس المقصود من ذلك استبدال حزم النظام، إذ أن الأمر موجود ليكون بمثابة طريقة ملائمة لمطوري رست لتثبيت الأدوات التي شاركها الآخرون على crates.io. لاحظ أنه يمكنك فقط تثبيت الحزم التي تحتوي أهداف ثنائية binary targets، والهدف الثنائي هو برنامج قابل للتشغيل يُنشأ إذا كانت الحزمة المصرفة تحتوي على ملف src/main.rs أو ملف آخر محدد على أنه ملف تنفيذي، على عكس هدف المكتبة library target الذي لا يمكن تشغيله لوحده، فهو موجود لضمِّه داخل برامج أخرى. تحتوي الحزم المصرفة عادةً على معلومات في ملف README وتدل هذه المعلومات فيما إذا كانت الوحدة المصرفة مكتبية أو تحتوي هدفًا ثنائيًا أو كلاهما. تُخزَّن كل الوحدات الثنائية المصرفة المثبتة عند تنفيذ cargo install في مجلد التثبيت الجذر الذي يدعى "bin". إذا ثبتّت رست باستخدام "rustup.rs" ولم يكن لديك أي إعدادات افتراضية فإن المجلد سيكون ‎$HOME/.cargo/bin. تأكد أن هذا المجلد في ‎$PATH الخاص بك لتكون قادراً على تشغيل البرامج التي ثبتتها باستخدام cargo install. ذكرنا سابقًا أن هناك تنفيذ لأداة grep بلغة رست اسمه ripgrep للبحث عن الملفات، ولتثبيت ripgrep يمكنك تنفيذ الأمر التالي: $ cargo install ripgrep Updating crates.io index Downloaded ripgrep v13.0.0 Downloaded 1 crate (243.3 KB) in 0.88s Installing ripgrep v13.0.0 --snip-- Compiling ripgrep v13.0.0 Finished release [optimized + debuginfo] target(s) in 3m 10s Installing ~/.cargo/bin/rg Installed package `ripgrep v13.0.0` (executable `rg`) يظهر السطر الثاني قبل الأخير من المخرجات مكان واسم الثنائية المثبتة، وهي rg في حالة ripgrep. إذا كان مجلد التثبيت موجودًا في ‎$PATH الخاص بك، فيمكنك تشغيل rg --help والبدء باستخدام أداة أسرع مكتوبة بلغة رست للبحث عن الملفات. توسيع استخدامات كارجو عن طريق أوامر مخصصة كارجو مصمم بحيث يمكن توسيع استخداماته بأوامر فرعية دون الحاجة لتعديله. إذا كان هناك وحدة ثنائية binary ضمن ‎$PATH اسمها cargo-something، فهذا يعني أنه يمكنك تشغيلها كما لو كانت أمر فرعي لكارجو عن طريق تنفيذ cargo something. تستطيع استعراض الأوامر المخصصة بتنفيذ cargo --list. قدرتك على استخدام cargo install لتثبيت الإضافات وتشغيلها كما في أدوات الكارجو المضمّنة built-in هي ميزة ملائمة جداً بتصميم كارجو. ترجمة -وبتصرف- لقسم من الفصل More About Cargo and Crates.io من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: تخصيص نسخ مشروع بلغة رست ونشرها على crates.io الأخطاء والتعامل معها في لغة رست الملكية Ownership في لغة رست
  18. سنتعرّف هنا على كيفية تخصيص نُسَخ المشروع builds المختلفة باستخدام ما يُدعى حسابات الإصدار release profiles، ثمّ سنستعرض كيفيّة إضافة مشاريعك بلغة رست على موقع crates.io. تخصيص نسخ مشروع مع حسابات الإصدار وضبطها في كارجو cargo حسابات الإصدار في رست هي حسابات قابلة للتعديل customizable ومعرّفة مسبقًا بالعديد من الإعدادات المختلفة التي تسمح للمبرمج بأن يمتلك تحكمًا أكبر على العديد من الخيارات لتصريف الشيفرة البرمجية، إذ أن كل حساب مضبوط بصورةٍ مستقلة عن الآخر. لدى كارجو حسابين أساسيين، هما: حساب dev وهو حساب يستخدمه كارجو عندما تنفّذ cargo build وحساب release يستخدمه كارجو عندما تنفّذ cargo build --release. حساب dev معرَّف بإعدادات تصلح لعملية التطوير، وحساب release مُعرَّف بإعدادت تصلح لإطلاق نسخ جديدة. قد تكون أسماء الحسابات هذه ضمن خرج نسخ مشروعك مألوفة: $ cargo build Finished dev [unoptimized + debuginfo] target(s) in 0.0s $ cargo build --release Finished release [optimized] target(s) in 0.0s حسابات dev و release هي الحسابات التي يستخدمها المصرّف. يمتلك كارجو إعدادات افتراضية لكلٍ من الحسابات التي تُطبَّق عندما لا تُحدد بوضوح بإضافة أية أقسام [profile.*‎] إلى ملف cargo.toml الخاص بالمشروع، إذ عند إضافة قسم [profile.*‎] لأي حساب تريد التعديل عليه، فأنت تُعيد الكتابة على أي من الإعدادات الفرعية الخاصة بالإعدادات الافتراضية. مثلاً، هذه هي القيم الأساسية لإعدادات opt-level لكل من الحسابين dev و release. اسم الملف: cargo.toml [profile.dev] opt-level = 0 [profile.release] opt-level = 3 تتحكم إعدادات opt-level بعدد التحسينات التي ستطبقها رست على شيفرتك البرمجية، بمجال من 0 إلى 3، مع الانتباه إلى أن تطبيق أي تحسينات إضافية سيزيد من وقت التصريف، لذا إذا كنت في مرحلة التطوير وتصرّف شيفرتك البرمجية بصورةٍ متكررة، فستحتاج إلى تحسينات أقل لتُصرف أسرع حتى لو كانت الشيفرة البرمجية الناتجة تعمل أبطأ. ستكون القيمة لكلٍ من opt-level و dev افتراضيًا هي 0، وعندما تكون جاهزاً لإصدار شيفرتك البرمجية، فمن الأفضل أن تقضي وقتاً أكثر بالتصريف إذ أنك ستصرّف الشيفرة البرمجية لمرة واحدة فقط في وضع الإصدار، لكنك ستشغّل البرنامج عدّة مرات، إذًا يُقايض وضع الإصدار وقت التصريف الطويل مقابل شيفرة برمجية تعمل على نحوٍ أسرع، وهذا هو السبب في كون القيمة الأساسية opt-level للحساب release هي 3. يمكنك تجاوز القيمة الافتراضية بإضافة قيمة مختلفة لها في Cargo.toml، فإذا أردنا مثلًا أن يكون مستوى التحسين هو 1 في حساب التطوير، يمكننا إضافة هذين السطرين في ملف Cargo.toml الخاص بالمشروع: اسم الملف: Cargo.toml [profile.dev] opt-level =1 تعيد هذه الشيفرة تعريف الإعداد الافتراضي 0، وعندما ننفّذ cargo build، سيستخدم كارجو الإعدادات الافتراضية لحساب dev إضافةً إلى التعديلات التي أجريناها على opt-level، ولأننا ضبطنا opt-level إلى القيمة 1، سيطبّق كارجو تحسينات أكثر من التحسينات التي يطبقها في الوضع الافتراضي، ولكن ليس أكثر من التحسينات الموجودة في إصدار البناء. للحصول على لائحة كاملة من خيارات الضبط والقيم الافتراضية لكل حساب راجع توثيق كارجو. نشر وحدة مصرفة crate على crates.io استخدمنا حزم من crates.io مثل اعتماديات dependencies لمشاريعنا، لكن يمكنك أيضًا أن تشارك شيفرتك مع أشخاص آخرين عن طريق نشر حزمتك الخاصة. يوزِّع تسجيل الوحدة المُصرَّفة crates في crates.io الشيفرة المصدرية للحزم الخاصة بك، بحيث تكون الشيفرة المُستضافة مفتوحة المصدر. لدى رست وكارجو ميزات تجعل حزمتك المنشورة أسهل إيجادًا من قبل الأشخاص واستخدامها وسنتحدث عن بعض هذه المزايا ثم سنشرح كيف تنشر حزمة. كتابة تعليقات توثيق مفيدة سيساعد توثيق حزمتك بدقة المستخدمين الآخرين على معرفة كيف ومتى يستخدمونها، لذا يُعد استثمار الوقت في كتابة التوثيق أمرًا مجديًا. ناقشنا سابقًا كيف تعلّق شيفرة رست باستخدام خطين مائلين "//". لدى رست أيضًا نوع محدد من التعليقات للتوثيق تعرف بتعليق التوثيق documentation comment والذي سيولّد بدوره توثيق HTML. يعرض الملف المكتوب باستخدام HTML محتويات تعليقات التوثيق لعناصر الواجهة البرمجية API العامة والموجهة للمبرمجين المهتمين بمعرفة كيفية استخدام وحدة مصرفة بغض النظر عن كيفية عملها وراء الكواليس. تستخدم تعليقات التوثيق ثلاثة خطوط مائلة "///" بدلاً من خطّين، وتدعم صيغة ماركداون Markdown لتنسيق النص، إذ يكفي وضع تعليقات النص مباشرةً قبل العنصر الذي يوَثّق. تُظهر الشيفرة 1 تعليقات التوثيق لدالة add_one في وحدة مصرفة تدعى my_crate. اسم الملف: src/lib.rs /// أضف واحد إلى الرقم المُعطى /// /// # أمثلة /// /// ``` /// let arg = 5; /// let answer = my_crate::add_one(arg); /// /// assert_eq!(6, answer); /// ``` pub fn add_one(x: i32) -> i32 { x + 1 } [الشيفرة 1- تعليق التوثيق لدالة] نقدم هنا وصفًا عما تفعله دالة add_one، ابدأ القسم بعنوان Examples ثم ضع شيفرة تعبّر عن كيفية استخدام الدالة add_one، ويمكننا توليد توثيق HTML من تعليق التوثيق هذا عن طريق تنفيذ cargo doc، إذ يشغّل هذا الأمر أداة rustdoc الموزّعة مع رست وتضع توثيق HTML المولّد في المجلد "target/doc". سيبني تنفيذ cargo doc --open ملف HTML للتوثيق الحالي لوحدتك المصرفة (وأيضًا توثيق كل ما يعتمد على وحدتك المصرفة) ويفتح النتيجة في متصفح ويب للسهولة. انتقل إلى الدالة add_one وسترى كيف يتحول النص في تعليقات التوثيق، كما يظهر في الشكل 1: [الشكل 1: توثيق HTML لدالة add_one] الأقسام شائعة الاستخدام استخدمنا عنوان ماركداون ‎# Examples في الشيفرة 1 لإنشاء قسم في ملف HTML بالعنوان "Examples" وهذه بعض الأجزاء الشائعة الأخرى التي يستخدمها مؤلفو الوحدات المصرفة في توثيقهم: الهلع Panics: السيناريوهات التي تتوقف بها الدوال المُضمنة بالتوثيق، ويجب على مستخدمي الدالة الذين لا يريدون لبرامجهم أن تتوقف ألا يستدعوا الدالة في هذه الحالات. الأخطاء Errors: إذا أعادت الدالة القيمة Result واصفةً أنواع الأخطاء التي قد حصلت للشيفرة البرمجية المُستدعاة والظروف التي قد تسببت بحدوث الأخطاء التي تعيدها، تكون هذه المعلومات مفيدة للمستخدمين ويمكنهم بهذا أن يكتبوا شيفرة تستطيع التعامل مع الأنواع المختلفة من الأخطاء بعدة طرق. الأمان Safety: إذا استُدعيَت الدالة unsafe (نناقش عدم الأمان Unsafety لاحقًا)، يجب أن يكون هناك قسمٌ يشرح سبب عدم أمان الدالة ويغطي الأنواع اللا متغايرة invariants التي تتوقعها الدالة من المستخدمين. لا تحتاج معظم تعليقات التوثيق لكل هذه الأقسام، لكن هذه لائحة جيدة تذكرك بالجوانب التي سيهتم مستخدمو شيفرتك البرمجية بمعرفتها. استخدام تعليقات التوثيق مثل اختبارات يساعد إضافة كُتَل من الشيفرات البرمجية على أنها مثال ضمن تعليقات التوثيق فهم كيفية استخدام مكتبتك، ولفعل هذا الأمر مزايا إضافية: إذ أن تنفيذ cargo test سينفذ بدوره أمثلة الشيفرة البرمجية في توثيقك مثل اختبارات. لا شيء أفضل من توثيق يحتوي على أمثلة، لكن لا شيء أسوأ من أمثلة لا تعمل لأن الشيفرة البرمجية قد تغيرت منذ وقت كتابة التوثيق. نحصل على قسم في نتائج الاختبارات إذا نفّذنا الأمر cargo test مع توثيق دالة add_one من الشيفرة 1 على النحو التالي: Doc-tests my_crate running 1 test test src/lib.rs - add_one (line 5) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s الآن إذا غيرنا إما الدالة أو المثال بحيث يهلع الماكرو assert_eq!‎ في المثال وينفّذ cargo test مرةً أخرى، سنرى أن اختبارات التوثيق تلاحظ أن شيفرة المثال والشيفرة البرمجية لا يتزامنان مع بعضهما بعضًا. تعليق العناصر المحتواة يضيف أسلوب التعليق "!//" التوثيق إلى العنصر الذي يحتوي على التعليقات بدلاً من العناصر التي تلي التعليقات، ونستخدم عادةً تعليقات المستند هذه داخل ملف الوحدة المصرفة الجذر (src/lib.rs اصطلاحًا)، أو داخل وحدة ما لتوثيق الوحدة المصرفة، أو الوحدة ككُل. على سبيل المثال، لإضافة التوثيق التي يصف الغرض من الوحدة المصرّفة my_crate التي تحتوي على الدالة add_one، نضيف تعليقات التوثيق التي تبدأ بـ "!//" إلى بداية الملف src/lib.rs، كما هو موضح في الشيفرة 2: اسم الملف: src/lib.rs //! # My Crate //! //! `my_crate` is a collection of utilities to make performing certain //! calculations more convenient. /// Adds one to the number given. // --snip-- [الشيفرة 2: توثيق الوحدة المصرفة my_crate ككل] لاحظ عدم وجود أي شيفرة برمجية بعد آخر سطر يبدأ بـ "!//"، وذلك لأننا بدأنا التعليقات بـ "!//" بدلاً من "///". نوثّق العنصر الذي يحتوي على هذا التعليق بدلاً من العنصر الذي يتبع هذا التعليق، وفي هذه الحالة، يكون هذا العنصر هو ملف src/lib.rs، وهو الوحدة المصرفة الجذر، إذ تصف هذه التعليقات كامل الوحدة المصرفة. عندما ننفّذ cargo doc --open، تظهر هذه التعليقات على الصفحة الأولى من توثيق my_crate أعلى قائمة العناصر العامة في الوحدة المصرفة، كما هو موضح في الشكل 2: [الشكل 2: التوثيق المولَّد للوحدة المصرّفة my_crate متضمنًا التعليق الذي يصف كل الوحدة المصرفة] تُعد تعليقات التوثيق داخل العناصر مفيدةً لوصف الوحدات المصرفة والوحدات خصوصًا. استخدمها لشرح الغرض العام من الحاوية container لمساعدة المستخدمين على فهم تنظيم الوحدة المصرفة. تصدير واجهة برمجية عامة Public API ملائمة باستخدام pub use يُعد هيكل الواجهة البرمجية العامة أحد النقاط المهمة عند نشر وحدة مصرفة، إذ يكون الأشخاص الذين يستخدمون الوحدة المصرفة الخاصة بك أقل دراية منك بهيكلية الوحدة، وقد يواجهون صعوبةً في إيجاد الأجزاء التي يريدون استخدامها إذا كانت الوحدة المصرفة الخاصة بك تحتوي على تسلسل هرمي كبير. تناولنا سابقًا كيفية جعل العناصر عامة باستخدام الكلمة المفتاحية pub، وإضافة العناصر إلى نطاق باستخدام الكلمة المفتاحية use. ومع ذلك، قد لا تكون الهيكلية المنطقية بالنسبة لك أثناء تطوير الوحدة المصرفة مناسبةً للمستخدمين، إذ قد ترغب في تنظيم الهياكل الخاصة بك ضمن تسلسل هرمي يحتوي على مستويات متعددة، ولكن قد يواجه بعض الأشخاص مشاكلًا بخصوص وجود نوع ما عرّفته في مكان عميق ضمن التسلسل الهرمي وذلك عندما يرغبون باستخدامه، كما قد يسبب الاضطرار إلى إدخال المسار التالي بعض الازعاج: use my_crate::some_module::another_module::UsefulType;‎ بدلًا من استخدام: use my_crate::UsefulType;‎ الخبر السار هو أنه إذا لم يكن الهيكل مناسبًا للآخرين لاستخدامه من مكتبة أخرى، فلن تضطر إلى إعادة ترتيب التنظيم الداخلي، إذ يمكنك إعادة تصدير العناصر بدلًا من ذلك لإنشاء هيكل عام مختلف عن هيكلتك الخاصة باستخدام pub use. تأخذ عملية إعادة التصدير عنصرًا عامًا من مكان ما وتجعله عامًا في مكان آخر، كما لو جرى تعريفه في موقع آخر عوضًا عن ذلك. على سبيل المثال، لنفترض أننا أنشأنا مكتبة باسم art لنمذجة المفاهيم الفنية، بحيث يوجد داخل هذه المكتبة وحدتان: وحدة kinds تحتوي على معدّدَين enums باسم PrimaryColor و SecondaryColor ووحدة utils تحتوي على دالة تدعى mix، كما هو موضح في الشيفرة 3: اسم الملف: src/lib.rs //! # Art //! //! مكتبة لنمذجة المفاهيم الفنية pub mod kinds { /// الألوان الأساسية طبقًا لنموذج‪ ‪‪‪RYB pub enum PrimaryColor { Red, Yellow, Blue, } /// الألوان الثانوية طبقًا لنموذج‪ RYB pub enum SecondaryColor { Orange, Green, Purple, } } pub mod utils { use crate::kinds::*; /// دمج لونين أساسيين بقيم متساوية لإنشاء لون ثانوي pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor { // --snip-- } } [الشيفرة 3: مكتبة art التي تحتوي على عناصر منظمة ضمن الوحدتين kinds و utils] يوضح الشكل 3 كيف ستبدو الصفحة الأولى لتوثيق الوحدة المصرفة التي أُنشئت بواسطة cargo doc: [الشكل 3: الصفحة الأولى لتوثيق art الذي توضّح الوحدتين kinds و utils] لاحظ أن النوعين PrimaryColor و SecondaryColor غير مُدرجين في الصفحة الأولى وكذلك دالة mix، إذ يجب علينا النقر على kinds و utils لرؤيتهما. ستحتاج وحدة مصرفة أخرى تعتمد على هذه المكتبة إلى عبارات use، لتجلب العناصر الموجودة في art إلى النطاق، وبالتالي تحديد هيكل الوحدة المعرّفة حاليًا. تُظهر الشيفرة 4 مثالاً على الوحدة المصرفة التي تستخدم عناصر PrimaryColor و mix من الوحدة المصرفة art: اسم الملف: src/main.rs use art::kinds::PrimaryColor; use art::utils::mix; fn main() { let red = PrimaryColor::Red; let yellow = PrimaryColor::Yellow; mix(red, yellow); } [الشيفرة 4: وحدة مصرفة تستخدم عناصر الوحدة المصرفة art مع تصدير هيكلها الداخلي] ينبغي على مؤلف الشيفرة في الشيفرة 4 التي تستخدم الوحدة المصرفة art أن يعرّف أن PrimaryColor موجود في الوحدة kinds وأن mix موجودة في الوحدة utils. هيكل الوحدة المصرفة art مناسب أكثر للمطورين العاملين على الوحدة المصرفة art مقارنةً بمن سيستخدمها، إذ لا يحتوي الهيكل الداخلي على أي معلومات مفيدة لشخص يحاول فهم كيفية استخدام الوحدة المصرفة art، بل يتسبب الهيكل الداخلي باللّبس لأن المطورين الذين يستخدمونها يجب أن يعرفوا أيّ المسارات التي يجب عليهم الذهاب إليها كما يجب عليهم تحديد أسماء الوحدات في عبارات use. لإزالة التنظيم الداخلي من الواجهة البرمجية العامة يمكننا تعديل شيفرة الوحدة المصرفة art في الشيفرة 3 لإضافة تعليمات pub use لإعادة تصدير العناصر للمستوى العلوي كما هو موضح في الشيفرة 5: اسم الملف: src/lib.rs //! # Art //! //! مكتبة لنمذجة المفاهيم الفنية pub use self::kinds::PrimaryColor; pub use self::kinds::SecondaryColor; pub use self::utils::mix; pub mod kinds { // --snip-- } pub mod utils { // --snip-- } [الشيفرة 5: إضافة تعليمات pub use لإعادة تصدير العناصر] سيُدرِج توثيق الواجهة البرمجية التي يولدها الأمر cargp doc لهذه الوحدة المصرفة ويعيد تصديرها على الصفحة الأولى كما هو موضح في الشكل 4 جاعلًا النوعَين PrimaryColor و SecondaryColor ودالة mix أسهل للإيجاد. [الشكل 4: الصفحة الأولى لتوثيق art التي تعرض عمليات إعادة التصدير] يمكن لمستخدمي الوحدة المصرفة art أن يروا ويستخدموا الهيكلة الداخلية من الشيفرة 3 كما هو موضح في الشيفرة 4 أو يمكنهم استخدام هيكل أكثر سهولة للاستخدام في الشيفرة 5 كما هو موضح في الشيفرة 6: اسم الملف: src/main.rs use art::mix; use art::PrimaryColor; fn main() { // --snip-- } [الشيفرة 6: برنامج يستخدم العناصر المعاد تصديرها من الوحدة المصرفة art] يمكن -في الحالات التي يوجد فيها العديد من الوحدات المتداخلة nested modules- أن تحدث عملية إعادة تصدير الأنواع في المستوى العلوي باستخدام pub use فرقًا واضحًا على تجربة الأشخاص في استخدام الوحدة المصرّفة. الاستخدام الشائع الآخر للتعليمة pub use هو إعادة تصدير تعريفات الاعتمادية في الوحدة المصرفة الحالية لجعل تعريفات تلك الوحدة المصرفة جزءًا من الواجهة البرمجية العامة لوحدتك المصرفة. يُعد إنشاء بنية واجهة برمجية عامة مفيدة فنًا أكثر من كونه علمًا، ويمكنك تكرار المحاولة حتى تعثر على واجهة برمجية تعمل بصورةٍ أفضل لمستخدميها، ويمنحك اختيار pub use مرونةً في كيفية هيكلة وحدتك المصرفة داخليًا وفصل هذه الهيكلة الداخلية عما تقدمه للمستخدمين. ألقِ نظرةً على الشيفرات البرمجية الخاصة ببعض الوحدات المصرفة التي ثبّتتها لمعرفة ما إذا كانت هيكلتها الداخلية مختلفة عن الواجهة البرمجية العامة. إنشاء حساب Crates.io قبل أن تتمكن من نشر أي وحدات مصرفة، تحتاج إلى إنشاء حساب على crates.io والحصول على رمز واجهة برمجية مميز API token، ولفعل ذلك، انقر على زر الصفحة الرئيسية على crates.io وسجّل الدخول عبر حساب غيت هب GitHub، إذ يُعد حساب غيت هب أحد المتطلبات حاليًا، ولكن قد يدعم الموقع طرقًا أخرى لإنشاء حساب في المستقبل. بمجرد تسجيل الدخول، اذهب إلى إعدادات حسابك على https://crates.io/me واسترجع مفتاح API. ثم نفّذ الأمر cargo login باستخدام مفتاح API الخاص بك، كما يلي: $ cargo login abcdefghijklmnopqrstuvwxyz012345 يُعلم هذا الأمر كارجو برمز API الخاص بك وتخزينه محليًا في "‎~/.cargo/credentials". لاحظ أن هذا الرمز هو سر، فلا تشاركه مع أي شخص آخر، وإذا شاركته مع أي شخص لأي سبب من الأسباب، فيجب عليك إبطاله وإنشاء رمز مميز جديد على crates.io. إضافة بيانات وصفية لوحدة مصرفة جديدة لنفترض أن لديك وحدة مصرفة تريد نشرها، ستحتاج قبل النشر إلى إضافة بعض البيانات الوصفية في قسم [package] داخل ملف Cargo.toml الخاص بالوحدة المصرفة. ستحتاج وحدتك المصرفة إلى اسم مميز، إذ يُمكنك تسمية الوحدة المصرفة أثناء عملك على وحدة مصرفة محليًا كما تريد، ومع ذلك، تُخصَّص أسماء الوحدات المصرفة على crates.io على أساس من يأتي أولًا يُخدم أولًا first-come, first-served. بمجرد اختيار اسم لوحدة مصرفة ما، لا يمكن لأي شخص آخر نشر وحدة مصرفة بهذا الاسم. قبل محاولة نشر وحدة مصرفة، ابحث عن الاسم الذي تريد استخدامه، فإذا كان الاسم مستخدمًا، ستحتاج إلى البحث عن اسم آخر وتعديل حقل name في ملف Cargo.toml في قسم [package] لاستخدام الاسم الجديد للنشر، كما يلي: اسم الملف: Cargo.toml [package] name = "guessing_game" حتى إذا اخترت اسمًا مميزًا، عند تنفيذ cargo publish لنشر الوحدة المصرفة في هذه المرحلة، ستتلقى تحذيرًا ثم خطأ: $ cargo publish Updating crates.io index warning: manifest has no description, license, license-file, documentation, homepage or repository. See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info. --snip-- error: failed to publish to registry at https://crates.io Caused by: the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata تحدث هذه الأخطاء بسبب افتقاد بعض المعلومات المهمة؛ إذ أن الوصف والترخيص مطلوبان حتى يعرف الأشخاص ما تفعله الوحدة المصرفة الخاصة بك وتحت أي شروط يمكنهم استخدامها. أضف وصفًا في Cargo.toml بحيث يكون مجرد جملة أو جملتين، لأنه سيظهر مع الوحدة المصرفة الخاصة بك في نتائج البحث، أما بالنسبة لحقل license، فأنت بحاجة لمنح قيمة معرّف الترخيص. تُدرج مؤسسة لينكس لتبادل بيانات حزم البرمجيات Linux Foundation’s Software Package Data Exchange -أو اختصارًا SPDX- المعرّفات التي يمكنك استخدامها لهذه القيمة. على سبيل المثال، لتحديد أنك رخّصت وحدتك المصرفة باستخدام ترخيص MIT، أضف معرف MIT: اسم الملف: Cargo.toml [package] name = "guessing_game" license = "MIT" إذا أردت استخدام ترخيص غير موجود في SPDX، فأنت بحاجة إلى وضع نص هذا الترخيص في ملف، وتضمين الملف في مشروعك، ثم استخدام license-file لتحديد اسم هذا الملف بدلاً من ذلك من استخدام المفتاح license. التوجيه بشأن الترخيص المناسب لمشروعك هو خارج نطاق هذا الكتاب. يرخِّص الكثير من الأشخاص في مجتمع رست مشاريعهم بنفس طريقة رست ألا وهي باستخدام ترخيص مزدوج من "MIT OR Apache-2.0". تدلّك هذه الممارسة على أنه بإمكانك أيضًا تحديد معرّفات ترخيص متعددة مفصولة بـ OR لتضمين تراخيص متعددة لمشروعك. باستخدام الاسم المميز والإصدار والوصف والترخيص المضاف، أصبح ملف Cargo.toml الخاص بالمشروع جاهزًا للنشر على النحو التالي: اسم الملف: Cargo.toml [package] name = "guessing_game" version = "0.1.0" edition = "2021" description = "A fun game where you guess what number the computer has chosen." license = "MIT OR Apache-2.0" [dependencies] يصف توثيق كارجو البيانات الوصفية الأخرى التي يمكنك تحديدها للتأكد من أن الآخرين يمكنهم اكتشاف واستخدام وحدة التصريف الخاصة بك بسهولة أكبر. النشر على Crates.io الآن وبعد أن أنشأت حسابًا، وحفظت رمز API، واخترت اسمًا للوحدة المصرفة، وحددت البيانات الوصفية المطلوبة، فأنت جاهزٌ للنشر، إذ يؤدي نشر وحدة مصرفة إلى رفع إصدار معين إلى crates.io ليستخدمه الآخرون. كن حذرًا، لأن النشر دائم، ولا يمكن الكتابة فوق الإصدار مطلقًا، ولا يمكن حذف الشيفرة البرمجية. يتمثل أحد الأهداف الرئيسة لموقع crates.io بالعمل مثل أرشيف دائم للشيفرة البرمجية بحيث تستمر عمليات إنشاء جميع المشاريع التي تعتمد على الوحدات المصرفة من crates.io في العمل، والسماح بحذف نسخة ما سيجعل تحقيق هذا الهدف مستحيلًا، ومع ذلك، لا يوجد حد لعدد إصدارات الوحدات المصرفة التي يمكنك نشرها. نفّذ الأمر cargo publish مرةً أخرى. يجب أن تنجح الآن: $ cargo publish Updating crates.io index Packaging guessing_game v0.1.0 (file:///projects/guessing_game) Verifying guessing_game v0.1.0 (file:///projects/guessing_game) Compiling guessing_game v0.1.0 (file:///projects/guessing_game/target/package/guessing_game-0.1.0) Finished dev [unoptimized + debuginfo] target(s) in 0.19s Uploading guessing_game v0.1.0 (file:///projects/guessing_game) تهانينا، فقد شاركت الآن الشيفرة الخاصة بك مع مجتمع رست، ويمكن لأي أحدٍ بسهولة إضافة الوحدة المصرفة الخاصة بك مثل اعتمادية لمشروعه. نشر نسخة جديدة لوحدة مصرفة موجودة مسبقا عندما تُجري تغييرات على الوحدة المصرفة الخاصة بك وتكون جاهزًا لطرح إصدار جديد، فإنك تغيّر قيمة version المحددة في ملف Cargo.toml الخاص بك وتعيد النشر. استخدم قواعد الإدارة الدلالية لنُسخ البرمجيات Semantic Versioning rules لتحديد رقم الإصدار التالي المناسب بناءً على التغييرات التي أجريتها، ومن ثم نفّذ cargo publish لرفع الإصدار الجديد. تعطيل النسخ من Crates.io باستخدام cargo yank على الرغم من أنه لا يمكنك إزالة الإصدارات السابقة للوحدة المصرفة، إلا أنه يمكنك منع أي مشاريع مستقبلية من إضافتها مثل اعتمادية جديدة، ويكون هذا مفيدًا عندما يُعطَّل إصدار الوحدة المصرفة لسبب أو لآخر، وفي مثل هذه الحالات، يدعم كارجو سحب yanking إصدار وحدة مصرفة. يمنع سحب إصدار ما المشاريع الجديدة من الاعتماد على هذا الإصدار مع السماح لجميع المشاريع الحالية التي تعتمد عليه بالاستمرار، إذ يعني السحب أن جميع المشاريع التي تحتوي على Cargo.lock لن تتعطّل، ولن تستخدم أي ملفات Cargo.lock المستقبلية المنشأة الإصدار المسحوب. لسحب نسخة من وحدة مصرفة، نفّذ cargo yank في دليل الوحدة المصرفة الذي نشرتَه سابقًا، وحدّد أي إصدار تريد إزالته. على سبيل المثال، إذا نشرنا وحدة مصرفة باسم guessing_game الإصدار 1.0.1 وأردنا انتزاعه، في مجلد المشروع guessing_game ننفّذ ما يلي: $ cargo yank --vers 1.0.1 Updating crates.io index Yank guessing_game@1.0.1 يمكنك أيضًا التراجع عن عملية السحب من خلال إضافة undo-- إلى الأمر والسماح للمشاريع بالاعتماد على الإصدار مرة أخرى: $ cargo yank --vers 1.0.1 --undo Updating crates.io index Unyank guessing_game_:1.0.1 لا تحذف عملية السحب أي شيفرة برمجية، إذ من غير الممكن على سبيل المثال حذف بيانات حساسة رُفعَت بالخطأ. إذا حدث ذلك، يجب عليك إعادة تعيين تلك البيانات على الفور. ترجمة -وبتصرف- لقسم من الفصل More About Cargo and Crates.io من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: الاختيار بين الحلقات Loops والمكررات Iterators في لغة رست كتابة برنامج سطر أوامر Command Line بلغة رست Rust المسارات paths والنطاق الخاص بها في لغة رست Rust
  19. تعرفنا في الفصل السابق "معالجة سلسلة من العناصر باستخدام المكررات iterators" والذي يليه "استخدام المكررات Iterators في تطبيق سطر أوامر" على المكررات Iterators وكيفية استخدامها عمليًا ولعل السؤال الذي خطر ببالك الآن بعد قراءتهما هو: أيّ طرق التطبيق ينبغي عليك استعمالها عند برمجتك لبرنامجك ولماذا؟ التطبيق الأصلي في الشيفرة 21، أم الإصدار باستخدام المكررات في الشيفرة 22 مقال "استخدام المكررات Iterators في تطبيق سطر أوامر بلغة رست"؟ يفضِّل معظم مبرمجي لغة رست استخدام المكررات، وعلى الرغم من أن هذه الطريقة أصعب فهمًا في البداية إلا أنك ستفضلها بعد الاعتياد عليها وعلى محولات المكررات المختلفة، إذ ستركّز عندها شيفرتك البرمجية على الهدف العام للحلقة بدلًا من إضاعة الوقت في ضبط الأجزاء المختلفة من الحلقات وإنشاء أشعة جديدة. تخلّصنا هذه الطريقة من الكثير من الشيفرات البرمجية الاعتيادية بحيث يكون من الأسهل رؤية المقصد الأساسي من الشيفرة البرمجية على نحوٍ فريد لكل استخدام، مثل ترشيح كل عنصر في المكرر بحسب شرط ما. لكن هل الطريقتان متساويتان حقًا؟ يقول الافتراض المنطقي: الحلقة التي يكون تطبيقها على مستوى منخفض أسرع. دعنا نتناقش بالتفاصيل. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن المقارنة بين أداء الحلقات والمكررات عليك معرفة أيّ التطبيقين أسرع، الحلقات أم المكررات؟ وذلك لتحديد متى يجب عليك استخدام أحد التطبيقين؛ هل إصدار الدالة search (من المقال السابق) باستخدام حلقة for أسرع أم إصدار المكررات؟ ننفّذ اختبار أداء بكتابة كامل محتويات رواية "مغامرات شيرلوك هولمز The Adventures of Sherlock Holmes" لكاتبها سير آرثر كونان دويل Sir Arthur Conan Doyle إلى String والبحث عن الكلمة "the" في المحتويات. إليك نتائج اختبار الأداء على كلا الإصدارين لدالة search باستخدام حلقة for، وباستخدام المكرّرات: test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700) test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200) كان إصدار المكررات أسرع قليلًا. لن نشرح الشيفرة البرمجية الخاصة باختبار الأداء هنا، لأن الهدف من المقال ليس برهنة أن الإصدارين متساويين بالأداء بل هو لملاحظة أداء كل طريقة بالنسبة للأخرى. ينبغي عليك التحقق من نصوص متفاوتة الطول للوسيط contents للحصول على اختبار أداء أكثر وضوًحا، واستخدام كلمات مختلفة من أطوال متفاوتة للوسيط query وتجربة مختلف أنواع الحالات. ما نريد الوصول إليه هنا هو التالي: على الرغم من أن المكررات تستخدم تطبيقًا مجرّدًا عالي المستوى، إلا أنها تُصرَّف إلى شيفرة برمجية مماثلة لشيفرة تطبيق منخفض المستوى كتبتها بنفسك، إذ أن المكررات هي من الطرق المجرّدة عديمة الحمل zero-cost abstraction في رست، وهذا يعني أن استخدام التجريد لن يؤثر على وقت التشغيل. هذا الأمر مماثل لكيفية تعريف بيارن ستروستروب Bjarne Stroustrup مصمّم لغة سي بلس بلس C++‎ ومطبّقها لمبدأ انعدام الحمل غير المباشر zero-overhead في كتابه "أساسيات سي بلس بلس Foundations of C++‎"‏ (2012): الشيفرة البرمجية التالية هي مثال آخر مأخوذ من برنامج فك ترميز صوت، إذ تستخدم خوارزمية فك الترميز عملية التوقع الخطي الرياضي لتوقّع القيم المستقبلية بناءً على دالة خطية تأخذ العينات السابقة. تستخدم هذه الشيفرة البرمجية سلسلة مكررات لإنجاز بعض العمليات الرياضية على ثلاثة متغيرات موجودة في النطاق scope، هي: buffer شريحة البيانات، ومصفوفة من 12 عنصر coefficients وأي مقدار إزاحة بالبيانات مُخزَّن في qlb_shift، وقد صرّحنا عن هذه المتغيرات داخل المثال دون إسناد أي قيمة لها. على الرغم من أن هذه الشيفرة البرمجية ليست مفيدة خارج سياقها إلا أنها مثال مختصر وواقعي على كيفية ترجمة رست للأفكار والتطبيقات عالية المستوى إلى مستوى منخفض. let buffer: &mut [i32]; let coefficients: [i64; 12]; let qlp_shift: i16; for i in 12..buffer.len() { let prediction = coefficients.iter() .zip(&buffer[i - 12..i]) .map(|(&c, &s)| c * s as i64) .sum::<i64>() >> qlp_shift; let delta = buffer[i]; buffer[i] = prediction as i32 + delta; } تمرّ الشيفرة البرمجية على القيم الاثنا عشر الموجودة في coefficients وتستخدم التابع zip لاقتران القيم الاثنا عشر الحالية مع القيم الاثني العشر السابقة الموجودة في buffer وذلك لحساب قيمة التوقع prediction، ثم نضرب قيمة كل زوج على حدى ونجمع النتيجة ونُزيح shift البتات bits في نتيجة الجمع بمقدار qlb_shift بت إلى اليمين. تركّز العمليات الحسابية في تطبيقات مثل فك ترميز الصوت على الأداء في المقام الأول. نُنشئ هنا مكررًا باستخدام محوّلَين adaptor ومن ثم نستهلك القيمة. ما هي شيفرة أسيمبلي Assembly التي ستُصرَّف إليها شيفرة رست هذه؟ تُصرَّف حتى الوقت الحالي إلى نفس شيفرة أسيمبلي التي قد تكتبها بنفسك. ليس هناك أي حلقة للمكرر الخاص بالقيم coefficients، إذ تعرف رست أن هناك 12 تكرارًا فقط، لذا فهي تنشر الحلقة؛ وعملية النشر unrolling هي عملية لتحسين الأداء، إذ يُزال فيها الحمل الإضافي غير المباشر الخاص بالشيفرة البرمجية المُتحكمة بالحلقة وتُولَّد شيفرة برمجية متكررة بدلًا من ذلك للشيفرة البرمجية في كل تكرار من الحلقة. تُخزَّن جميع المعاملات في المسجلات registers، مما يعني أن الوصول إلى القيم عملية سريعة، ولا يوجد هناك أي تحقق للقيود على مصفوفة الوصول access array خلال وقت التشغيل. ترفع جميع هذه الخطوات التي تفعلها رست لتحسين الأداء من فعالية أداء الشيفرة البرمجية الناتجة كثيرًا. الآن وبعد معرفتك لهذا، يمكنك استخدام المكررات والمغلفات دون أي خوف، إذ يجعلان من شيفرتك البرمجية ذات مستوى تطبيقي أعلى دون التفريط بسرعة الأداء عند وقت التشغيل. ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: استخدام المكررات Iterators في تطبيق سطر أوامر بلغة رست بنية match للتحكم بسير برامج لغة رست Rust التحكم بسير تنفيذ برامج راست Rust
  20. يمكننا الآن تحسين مشروع سطر الأوامر الذي نفذناه في فصل سابق بعنوان كتابة برنامج سطر أوامر Command Line بلغة رست Rust بعد تعرُّفنا على المكرّرات في المقال السابق معالجة سلسلة من العناصر باستخدام المكررات iterators، إذ سنستخدمها لجعل شيفرتنا البرمجية أكثر وضوحًا واختصارًا. دعنا نلقي نظرةً على طريقة تطبيق المكررات في مشروعنا والتي ستجعل منه إصدارًا محسنًا خاصةً على الدالتين Config::build و search. إزالة clone باستخدام المكرر أضفنا في الشيفرة 6 (من فصل كتابة برنامج سطر أوامر بلغة رست: إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء) شيفرةً برمجية تأخذ شريحةً slice من القيم ذات النوع String وأنشأنا بها نسخةً instance من الهيكل Config بالمرور على الشريحة ونسخ القيم والسماح بالهيكل Config بامتلاك هذه القيم. سنُعيد كتابة التطبيق ذاته الخاص بالدالة Config::build في الشيفرة 17 كما كانت في الشيفرة 23 (الفصل 12): اسم الملف: src/lib.rs impl Config { pub fn build(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let file_path = args[2].clone(); let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } } [الشيفرة 17: إعادة بناء الدالة Config::build من الشيفرة 23 (الفصل 12)] ذكرنا وقتها أنه ليس علينا القلق بخصوص استدعاءات clone غير الفعالة لأننا سنزيلها في المستقبل. حسنًا، أتى الوقت الآن. نحتاج clone هنا لوجود شريحة بعناصر String في المعامل args، إلا أن الدالة build لا تمتلك args، ولإعادة ملكية نسخة Config، اضطررنا لنسخ القيم من الحقول query و file_path الموجودة في الهيكل Config بحيث تمتلك نسخة Config القيم الموجودة في حقوله. أصبح بإمكاننا -بمعرفتنا الجديدة بخصوص المكررات- التعديل على الدالة build لأخذ ملكية مكرر مثل وسيط لها بدلًا من استعارة شريحة، وسنستخدم وظائف المكرر بدلًا من الشيفرة البرمجية التي تتحقق من طول السلسلة النصية وتمرّ على عناصرها في مواقع محددة، وسيوضِّح ذلك استخدام Config::build لأن المكرر يستطيع الحصول على هذه القيم. بعد أخذ الدالة Config::build لملكية المكرر وتوقف استخدامها لعمليات الفهرسة indexing التي تستعير القيم، أصبح بإمكاننا نقل قيم String من المكرر إلى الهيكل Config بدلًا من استدعاء clone وإنشاء تخصيص allocation جديد. إزالة المكرر المعاد مباشرة افتح ملف src/main.rs الخاص بمشروع الدخل والخرج، والذي يجب أن يبدو مماثلًا لما يلي: اسم الملف: src/main.rs fn main() { let args: Vec<String> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); // --snip-- } سنعدّل أولًا بداية الدالة main التي كانت موجودة في الشيفرة 24 من فصل سابق لتكون الشيفرة 18 التي تستخدم مكررًا، إلا أنها لن تُصرَّف بنجاح إلا بعد تعديلنا للدالة Config::build أيضًا. اسم الملف: src/main.rs fn main() { let config = Config::build(env::args()).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); // --snip-- } [الشيفرة 18: تمرير القيمة المعادة من env::args إلى Config::build] تُعيد الدالة env::args مكررًا، إذ يمكننا تمرير ملكية المكرّر المُعاد من env::args إلى Config::build الآن مباشرةً بدلًا من تجميع قيم المكرر في شعاع ثم تمرير شريحة إلى الدالة Config::build. نحتاج إلى تحديث تعريف الدالة Config::build في ملف src/lib.rs الخاص بمشروعنا. دعنا نغير بصمة الدالة Config::build لتبدو على نحوٍ مماثل للشيفرة 19، إلا أن هذا لن يُصرَّف بنجاح لأننا بحاجة لتحديث متن الدالة أيضًا. اسم الملف: src/lib.rs impl Config { pub fn build( mut args: impl Iterator<Item = String>, ) -> Result<Config, &'static str> { // --snip-- [الشيفرة 19: تحديث بصمة الدالة Config::build بحيث تتوقع تمرير مكرر] يوضّح توثيق المكتبة القياسية بالنسبة للدالة env::args بأن نوع المكرر المُعاد هو std::env::Args وأن هذا النوع يطبّق السمة Iterator ويُعيد قيمًا من النوع String. حدّثنا بصمة الدالة Config::build بحيث يحتوي المعامل args على نوع معمم generic type بحدّ السمة trait bound impl Iterator<Item = String>‎‎ بدلًا من ‎&[String]‎. ناقشنا الصيغة impl Trait سابقًا وهي تعني أن args يمكن أن يكون أي نوع يطبّق النوع Iterator ويُعيد عناصرًا من النوع String. يمكننا إضافة الكلمة المفتاحية mut إلى توصيف المعامل args لجعله متغيّرًا mutable بالنظر إلى أننا نأخذ ملكية args وسنعدّل args بالمرور ضمنه. استخدام توابع السمة Iterator بدلا من الفهرسة سنصحح متن الدالة Config::build، فنحن نعلم أنه بإمكاننا استدعاء التابع next على args لأنها تطبّق السمة Iterator. نحدّث في الشيفرة 20 الشيفرة البرمجية التي كانت موجودة في الشيفرة 23 بحيث نستخدم التابع next: اسم الملف: src/lib.rs impl Config { pub fn build( mut args: impl Iterator<Item = String>, ) -> Result<Config, &'static str> { args.next(); let query = match args.next() { Some(arg) => arg, None => return Err("Didn't get a query string"), }; let file_path = match args.next() { Some(arg) => arg, None => return Err("Didn't get a file path"), }; let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } } [الشيفرة 20: تعديل محتوى الدالة Config::build بحيث نستخدم توابع المكرر] تذكّر أن أول قيمة من القيم المُعادة من الدالة env::args هي اسم البرنامج، لذا نريد تجاهلها والحصول على القيمة التي تليها، ولذلك نستدعي next أولًا دون فعل أي شيء بالقيمة المُعادة، ثم نستدعي next مرةً أخرى للحصول على القيمة التي نريد وضعها في حقل query من الهيكل Config. إذا أعادت next القيمة Some، سنستخدم match لاستخلاص القيمة، أما إذا أعادت None، فهذا يعني عدم وجود وسطاء كافية من المستخدم وعندها نُعيد من الدالة القيمة Err مبكرًا، ونفعل ذلك مجددًا للتعامل مع القيمة file_path. جعل الشيفرة البرمجية أكثر وضوحا باستخدام محولات المكرر يمكننا أيضًا استغلال ميزة من مزايا المكررات في الدالة search ضمن مشروعنا، وهي موضّحة في الشيفرة 21 والشيفرة 19 (من فصل سابق): اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } [الشيفرة 21: تطبيق الدالة search من الشيفرة 19 (الفصل 12)] يمكننا كتابة هذه الشيفرة البرمجية بطريقة مختصرة باستخدام توابع محولات المكرر، إذ يسمح لنا ذلك أيضًا بوجود شعاع وسيط متغيّر نسميه results. يفضِّل أسلوب لغات البرمجة العمليّة تقليل كمية الحالات المتغيّرة لجعل الشيفرة البرمجية أكثر وضوحًا، إذ قد تفتح لنا إزالة الحالة المتغيّرة فرصةً لجعل عمليات البحث تُنفَّذ بصورةٍ متزامنة، لأنه لن يكون علينا حينها إدارة الوصول المتتالي إلى الشعاع results. توضح الشيفرة 22 هذا التغيير: اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { contents .lines() .filter(|line| line.contains(query)) .collect() } [الشيفرة 22: استخدام توابع محول المكرر في تطبيق الدالة search] تذكر أن الهدف من الدالة search هو إعادة جميع الأسطر الموجودة في contents التي تحتوي على query. تستخدم هذه الشيفرة البرمجية -بصورةٍ مشابهة لمثال filter في الشيفرة 16- محوّل filter للمحافظة على السطور التي يُعيد فيها التعبير line.contains(query)‎ القيمة true. يمكننا تجميع الأسطر الناتجة في شعاع آخر باستخدام collect. هذه الطريقة أبسط بكثير. جرّب تنفيذ التعديلات ذاتها بحيث تستخدم توابع المكرر في الدالة search_case_insensitive بصورةٍ مشابهة. ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: معالجة سلسلة من العناصر باستخدام المكررات iterators في لغة رست برمجة لعبة تخمين الأرقام بلغة رست Rust كتابة الاختبارات في لغة رست Rust
  21. يسمح لك نمط المكرّر iterator pattern بإنجاز مهمة ما على سلسلة من العناصر بصورةٍ متتالية، والمكرّر مسؤول عن المنطق الخاص بالمرور على كل عنصر وتحديد مكان انتهاء السلسلة، إذ ليس من الواجب عليك إعادة تطبيق هذا المنطق بنفسك عند استخدام المكررات. المكررات في رست كسولة، بمعنى أنها لا تمتلك أي تأثير حتى تستدعي أنت التابع الذي يستخدم المكرر، فعلى سبيل المثال نُنشئ الشيفرة 10 مكرّرًا على العناصر الموجودة في الشعاع ‏ v1 باستدعاء التابع iter المعرّف على Vec<T>‎، ولا تفعل تلك الشيفرة البرمجية أي شيء مفيد. let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); [الشيفرة 10: إنشاء مكرر] نخزّن المكرر في المتغير v1_iter، ويمكننا استخدامه بطرق عدّة بعد إنشائه. على سبيل المثال، استخدمنا حلقة for في الشيفرة 5 من فصل سابق للمرور على مصفوفة وذلك لتنفيذ شيفرة برمجية على كل من عناصرها، وفي الحقيقة كان المكرّر موجودًا ضمنيًا في تلك الشيفرة لتحقيق ذلك إلا أننا لم نتكلم عن ذلك سابقًا. نفصل في الشيفرة 11 إنشاء المكرر من استخدامه في الحلقة for، فعندما تُستدعى الحلقة for باستخدام المكرر v1_iter، يُستخدم كل عنصر في المكرر مرةً تلو الأخرى في كل دورة للحلقة، وهذا يطبع كل قيمة من قيم العناصر. let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!("Got: {}", val); } [الشيفرة 11: استخدام مكرر في حلقة for] يمكنك كتابة شيفرة برمجية تفعل الأمر نفسه في لغات البرمجة التي لا تستخدم المكررات في مكتبتها القياسية وذلك عن طريق البدء بالمتغير من الدليل‏ 0 واستخدام قيمة المتغير يمثابة دليلٍ للشعاع للحصول على قيمة ومن ثم زيادة قيمة المتغير في الحلقة حتى تصل قيمته لعدد العناصر الكلية في الشعاع. تتعامل المكررات مع المنطق البرمجي نيابةً عنك مما يقلل من الشيفرات البرمجية المتكررة التي من الممكن أن تتسبب بأخطاء، وتعطيك المكررات مرونةً أكبر في التعامل مع المنطق ذاته في الكثير من أنواع السلاسل وليس فقط هياكل البيانات data structures التي يمكنك الوصول إلى قيمها باستخدام دليل مثل الشعاع. دعنا نلقي نظرةً إلى كيفية تحقيق المكررات لكل هذا. سمة Iterator وتابع next تطبّق جميع المكررات السمة Iterator المُعرّفة في المكتبة القياسية، ويبدو تعريف السمة شيء مماثل لهذا: pub trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; // methods with default implementations elided } لاحظ أن هذا التعريف يستخدم صيغتان جديدتان، هما: type Item و Self::Item اللتان تُعرِّفان نوعًا مترابطًا associated type مع هذه السمة. سنتحدث عن الأنواع المترابطة بالتفصيل لاحقًا، ويكفي للآن معرفتك أن هذه الشيفرة البرمجية تقول أن تطبيق السمة Iterator يتطلب منك تعريف نوع Item أيضًا وهذا النوع مُستخدم مثل نوع مُعاد من التابع next، وبكلمات أخرى، سيكون النوع Item هو النوع المُعاد من المكرّر. تتطلب السمة Iterator ممن يطبّقها فقط أن يعرّف تابعًا واحدًا هو next، الذي يُعيد عنصرًا واحدًا من المكرر كل مرة ضمن Some وعندما تنتهي العناصر (ينتهي التكرار)، يُعيد None. يمكننا استدعاء التابع next على المكررات مباشرةً، وتوضح الشيفرة 12 القيم المُعادة من الاستدعاءات المتعاقبة على المكرّر باستخدام next وهو المكرر الموجود في الشعاع. اسم الملف: src/lib.rs #[test] fn iterator_demonstration() { let v1 = vec![1, 2, 3]; let mut v1_iter = v1.iter(); assert_eq!(v1_iter.next(), Some(&1)); assert_eq!(v1_iter.next(), Some(&2)); assert_eq!(v1_iter.next(), Some(&3)); assert_eq!(v1_iter.next(), None); } [الشيفرة 12: استدعاء التابع next على المكرر] لاحظ أننا احتجنا لإنشاء v1_iter متغيّر mutable، إذ يغيّر استدعاء التابع next على مكرر الحالة الداخلية التي يستخدمها المكرر لتتبع مكانه ضمن السلسلة، وبكلمات أخرى، تستهلك consumes الشيفرة البرمجية المكرّر، إذ يستهلك كل استدعاء للتابع next عنصرًا واحدًا من المكرر. لم يكن هناك أي حاجة لإنشاء v1_iter متغيّر عندما استخدمنا حلقة for لأن الحلقة أخذت ملكية ownership المكرر v1_iter وجعلته متغيّرًا ضمنيًا. لاحظ أيضًا أن القيم التي نحصل عليها من استدعاءات التابع next هي مراجع ثابتة immutable references للقيم الموجودة في الشعاع، ويعطينا التابع iter مكرًرًا على المراجع الثابتة. إذا أردنا إنشاء مكرر يأخذ ملكية v1 ويُعيد القيم المملوكة فيمكننا استدعاء into_iter بدلًا من iter، ويمكننا بصورةٍ مماثلة استدعاء iter_mut بدلًا من iter إذا أردنا المرور على مراجع متغيّرة. توابع تستهلك المكرر للسمة Iterator العديد من التوابع في تطبيقها الافتراضي والموجودة في المكتبة القياسية، ويمكنك الاطّلاع على هذه التوابع عن طريق النظر إلى توثيق واجهة المكتبة القياسية البرمجية للسمة Iterator. تستدعي بعض هذه التوابع التابع next في تعريفها وهو السبب في ضرورة تطبيق التابع next عند تطبيق السمة Iterator. تُسمّى التوابع التي تستدعي التابع next بالمحوّلات المُستهلكة consuming adaptors لأن استدعائها يستهلك المكرر. يُعد تابع sum مثال على هذه التوابع، فهو يأخذ ملكية المكرر ويمرّ على عناصره بصورةٍ متتالية مع استدعاء next مما يتسبب باستهلاك المكرر، وبينما يمرّ على العناصر فهو يجمع كل عنصر إلى قيمة كلية، ثم يعيد القيمة الكلية في النهاية. تحتوي الشيفرة 13 على اختبار يوضح استخدام التابع sum: اسم الملف: src/lib.rs #[test] fn iterator_sum() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); let total: i32 = v1_iter.sum(); assert_eq!(total, 6); } [الشيفرة 13: استدعاء التابع sum للحصول على القيمة الكلية لجميع العناصر في المكرر] لا يُسمح لنا باستخدام v1_iter بعد استدعاء sum لأن sum يأخذ ملكية المكرّر الذي نستدعي sum عليه. التوابع التي تنشئ مكررات أخرى محولات المكرر iterator adaptors هي توابع مُعرّفة على السمة Iterator ولا تستهلك المكرر، بل تُنشئ مكررات مختلفة بدلًا من ذلك عن طريق تغيير بعض خصائص المكرر الأصل. توضح الشيفرة 14 مثالًا عن استدعاء تابع محول مكرر map وهو تابع يأخذ مغلّفًا closure ويستدعيه على كل من العناصر بصورةٍ متتالية. يُعيد التابع map مكررًا جديدًا يُنشئ عناصرًا مُعدّلٌ عليها، ويُنشئ المغلف في هذه الحالة مكررًا جديدًا يزيد قيمة عناصره بمقدار 1 لكل عنصر في الشعاع: اسم الملف: src/main.rs let v1: Vec<i32> = vec![1, 2, 3]; v1.iter().map(|x| x + 1); [الشيفرة 14: استدعاء محول المكرر map لإنشاء مكرر جديد] إلا أن الشيفرة البرمجية السابقة تعطينا إنذارًا: $ cargo run Compiling iterators v0.1.0 (file:///projects/iterators) warning: unused `Map` that must be used --> src/main.rs:4:5 | 4 | v1.iter().map(|x| x + 1); | ^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_must_use)]` on by default = note: iterators are lazy and do nothing unless consumed warning: `iterators` (bin "iterators") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.47s Running `target/debug/iterators` لا تفعل الشيفرة 14 أي شيء، إذ أن المغلف الذي حددناه لا يُستدعى أبدًا، ويذكرنا الإنذار بسبب ذلك: إذ أن محولات المكرر كسولة ونحتاج لاستهلاك المكرر هنا. نستخدم التابع collect لتصحيح هذا الإنذار واستهلاك المكرر وهو ما استخدمناه في (الشيفرة 1 من فصل سابق) باستخدام env::args، إذ يستهلك هذا التابع المكرر ويجمّع القيم الناتجة إلى نوع بيانات تجميعة collection data type. نجمع في الشيفرة 15 النتائج من عملية المرور على المكرر والمُعادة من استدعاء التابع map على الشعاع، وسيحتوي هذا الشعاع في نهاية المطاف على جميع عناصره الموجودة مع زيادة على قيمة كل منها بمقدار 1. اسم الملف: src/main.rs let v1: Vec<i32> = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]); [الشيفرة 15: استدعاء التابع map لإنشاء مكرر جديد ومن ثم استدعاء التابع collect لاستهلاك المكرر الجديد وإنشاء شعاع] يمكننا تحديد أي عملية نريد إنجازها على كل عنصر بالنظر إلى أن map تتلقى مغلفًا بمثابة وسيط لها. هذا مثال عظيم عن كيفية إنجاز مهام معقدة بطريقة مقروءة، ولأن جميع المكررات كسولة، فهذا يعني أنك بحاجة استدعاء واحد من توابع المحولات المستهلكة للحصول على نتائج استدعاءات محولات المكرر. استخدام المغلفات التي تحصل على القيم من بيئتها تأخذ الكثير من محولات المكرر المغلفات مثل وسطاء لها، وستحصل هذه المغلفات التي تُحدد مثل وسطاء لمحولات المكرر على قيم من بيئتها غالبًا. نستخدم في هذا المثال التابع filter الذي يأخذ مغلّفًا، ويحصل المغلف على عنصر من المكرر ويُعيد bool، وتُضمّن هذه القيمة في التكرار المُنشئ بواسطة filter إذا أعاد المغلف القيمة true، وإذا أعاد المكرر القيمة false فلن تُضمَّن القيمة. نستخدم في الشيفرة 16 التابع filter بمغلّف يحصل على المتغير shoe_size من بيئته للمرور على تجميعة من نسخ instances الهيكل‏ Shoe، وسيُعيد فقط الأحذية ذات مقاس محدد. اسم الملف: src/lib.rs #[derive(PartialEq, Debug)] struct Shoe { size: u32, style: String, } fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> { shoes.into_iter().filter(|s| s.size == shoe_size).collect() } #[cfg(test)] mod tests { use super::*; #[test] fn filters_by_size() { let shoes = vec![ Shoe { size: 10, style: String::from("sneaker"), }, Shoe { size: 13, style: String::from("sandal"), }, Shoe { size: 10, style: String::from("boot"), }, ]; let in_my_size = shoes_in_size(shoes, 10); assert_eq!( in_my_size, vec![ Shoe { size: 10, style: String::from("sneaker") }, Shoe { size: 10, style: String::from("boot") }, ] ); } } [الشيفرة 16: استخدام التابع filter مع مغلف يحصل على القيمة shoe_size] تأخذ الدالة shoes_in_size ملكية شعاع الأحذية وقياس الحذاء مثل معاملات، وتُعيد شعاعًا يحتوي على الأحذية بالمقاس المحدد. نستدعي into_iter في متن الدالة shoes_in_size لإنشاء مكرر يأخذ ملكية الشعاع، ثم نستدعي filter لتحويل المكرّر إلى مكرر جديد يحتوي فقط على عناصر يُعيد منها المغلّف القيمة true. يحصل المغلف على المعامل shoe_size من البيئة ويقارنه مع قيمة كل مقاس حذاء مع إبقاء الأحذية التي يتطابق مقاسها، وأخيرًا يجمع استدعاء collect القيم المُعادة بواسطة محول المكرر إلى شعاع يُعاد من الدالة. يوضح هذا الاختبار أنه عندما نستدعي shoes_in_size، فنحن نحصل فقط على الأحذية التي تتطابق مقاساتها مع القيمة التي حددناها. ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: المغلفات closures في لغة رست Rust أنواع البيانات Data Types في لغة رست Rust التحكم بسير تنفيذ برامج راست Rust
  22. تمثّل المغلّفات في لغة رست دوالًا مجهولة anonymous يمكنك حفظها في متغير أو تمريرها مثل وسيط إلى دالة أخرى، ويمكنك إنشاء مغلف في مكان ما، ثم استدعاءه من مكان آخر ليُفَّذ بحسب سياق المكان، وعلى عكس الدوال فالمغلفات يمكنها الوصول إلى القيم الموجودة في النطاق المعرفة بها، وسنوضح كيف تسمح لنا مزايا المغلفات بإعادة استخدام شيفرتنا البرمجية وتخصيص سلوكها. الحصول على المعلومات من البيئة باستخدام المغلفات سنفحص أولًا كيفية استخدام المغلفات للحصول على القيم من البيئة التي عرّفناها فيها لاستخدام لاحق. إليك حالة استخدام ممكنة: لدينا شركة لبيع القمصان ونمنح شخصًا ما على قائمة مراسلة البريد الإلكتروني قميصًا حصريًا بين الحين والآخر مثل ترويج لشركتنا، ويمكن أن يُضيف الأشخاص على قائمة المراسلة لونهم المفضل إلى ملفهم بصورةٍ اختيارية، وإذا حدّد الشخص الذي سيحصل على قميص مجاني لونه المفضل فإنه يحصل على هذا اللون تحديدًا وإلا فإنه يحصل على اللون المتواجد بكثرة في المخزن. هناك عدة طرق لتطبيق ذلك، إذ يمكننا على سبيل المثال استخدام معدّد enum يدعى ShirtColor يحتوي على متغايرين variants هما‏ Red و Blue (حددنا لونين فقط للبساطة). نمثّل مخزن الشركة باستخدام الهيكل‏ Inventory الذي يحتوي على حقل يدعى shirts يحتوي على النوع Vec<ShirtColor>‎، الذي يمثّل لون القميص الموجود حاليًا في المخزن. يحصل التابع giveaway المُعرّف في Inventory على لون القميص المفضّل الاختياري للمستخدم من المستخدمين الرابحين القميص مجانًا ويُعيد لون القميص الذي سيحصل عليه المستخدم. التطبيق لكل ما سبق ذكره موضح في الشيفرة 1: اسم الملف: src/main.rss #[derive(Debug, PartialEq, Copy, Clone)] enum ShirtColor { Red, Blue, } struct Inventory { shirts: Vec<ShirtColor>, } impl Inventory { fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor { user_preference.unwrap_or_else(|| self.most_stocked()) } fn most_stocked(&self) -> ShirtColor { let mut num_red = 0; let mut num_blue = 0; for color in &self.shirts { match color { ShirtColor::Red => num_red += 1, ShirtColor::Blue => num_blue += 1, } } if num_red > num_blue { ShirtColor::Red } else { ShirtColor::Blue } } } fn main() { let store = Inventory { shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue], }; let user_pref1 = Some(ShirtColor::Red); let giveaway1 = store.giveaway(user_pref1); println!( "The user with preference {:?} gets {:?}", user_pref1, giveaway1 ); let user_pref2 = None; let giveaway2 = store.giveaway(user_pref2); println!( "The user with preference {:?} gets {:?}", user_pref2, giveaway2 ); } الشيفرة 1: شيفرة توزيع القمصان للمستخدمين يحتوي المتغير store المعرف في الدالة main على قميصين أحدهما باللون الأزرق والآخر باللون الأحمر للتوزيع ضمن حملة التسويق هذه. نستدعي التابع giveaway للمستخدم الذي يفضّل القميص الأحمر وللمستخدم الذي ليس لديه أي تفضيل معين. يمكن تطبيق الشيفرة البرمجية بمختلف الطرق، إلا أننا نركز هنا على استخدام المغلفات، لذا فقد التزمنا بالمفاهيم التي تعلمتها مسبقًا باستثناء ما بداخل التابع giveaway الذي يستخدم مغلّفًا. نحصل على تفضيل المستخدم مثل معامل من النوع Option<ShirtColor>‎ في التابع giveaway ونستدعي التابع unwrap_or_else على user_preference. التابع unwrap_or_else على النوع Option<T>‎ مُعرّف في المكتبة القياسية ويأخذ وسيطًا واحدًا ألا وهو مغلف دون أي وسطاء يعيد القيمة T (النوع ذاته المُخزن في المتغاير Some داخل النوع Option<T>‎، وفي هذه الحالة ShirtColor). إذا كان النوع Option<T>‎ هو المتغاير Some، سيُعيد التابع unwrap_or_else القيمة الموجودة داخل Some، وإذا كان المتغاير داخل النوع Option<T>‎ هو None، سيستدعي التابع المغلف ويُعيد القيمة المُعادة من المغلف. نحدد تعبير المغلف بالشكل ‎|| self.most_stocked()‎ مثل وسيط للتابع unwrap_or_else. لا يأخذ هذا المغلف أي معاملات، إذ نضع المعاملات بين الخطين العموديين إذا احتوى المغلف على معاملات. يستدعي متن المغلف التابع self.most_stocked()‎، ونعرّف هنا المغلف بحيث يُقيّم تطبيق unwrap_or_else المغلف لاحقًا إذا احتجنا للنتيجة. يطبع تنفيذ الشيفرة البرمجية السابقة الخرج التالي: $ cargo run Compiling shirt-company v0.1.0 (file:///projects/shirt-company) Finished dev [unoptimized + debuginfo] target(s) in 0.27s Running `target/debug/shirt-company` The user with preference Some(Red) gets Red The user with preference None gets Blue الجانب المثير للاهتمام هنا هو أننا نمرر مغلفًا يستدعي self.most_stocked()‎ على نسخة Inventory الحالية. لا تحتاج المكتبة القياسية لمعرفة أي شيء بخصوص النوعين Inventory أو ShirtColor الذين عرّفناهما، أو المنطق الذي نريد استخدامه في هذه الحالة، إذ أن المغلف يحصل على مرجع ثابث immutable reference إلى self الخاصة بنسخة Inventory، ثم يمرره إلى الشيفرة البرمجية التي كتبناها داخل التابع unwrap_or_else. إذا قارنّا هذه العملية بالتوابع، فالتوابع غير قادرة على الحصول على هذه المعلومات من البيئة بالطريقة ذاتها. استنتاج نوع المغلف وتوصيفه هناك المزيد من الاختلافات بين الدوال والمغلفات، إذ لا تتطلب المغلفات منك عادةً توصيف أنواع المعاملات، أو نوع القيمة المُعادة مثلما تتطلب الدوال fn ذلك، والسبب في ضرورة تحديد الأنواع في الدوال هو لأن الأنواع جزءٌ من واجهة صريحة مكشوفة لمستخدميك وتعريف الواجهة بصورةٍ دقيقة مهمٌ للتأكد من أن الجميع متفقٌ على أنواع القيم التي تستخدمها الدالة وتعيدها، بينما لا تُستخدم المغلّفات في الواجهات المكشوفة بهذه الطريقة، إذ أنها تُخزّن في متغيرات دون تسميتها وكشفها لمستخدمي مكتبتك. تكون المغلفات عادةً قصيرة وترتبط بسياق معين ضيق بدلًا من حالة عامة اعتباطية، ويمكن للمصرّف في هذا السياق المحدود استنتاج أنواع المعاملات والقيمة المعادة بصورةٍ مشابهة لاستنتاجه لمعظم أنواع المتغيرات، وهناك حالات نادرة يحتاج فيها المصرف وجود توصيف للأنواع في المغلفات أيضًا. يمكننا إضافة توصيف النوع إذا أردنا لزيادة دقة ووضوح المغلف كما هو الحال عند تعريف المتغيرات، إلا أن هذه الطريقة تتطلب كتابةً أطول غير ضرورية. يبدو توصيف الأنواع في المغلفات مثل التعريف الموجود في الشيفرة 2، ونعرّف في هذا المثال مغلّفًا في النقطة التي نمرّر فيها وسيطًا كما فعلنا في الشيفرة 1. اسم الملف: src/main.rs let expensive_closure = |num: u32| -> u32 { println!("calculating slowly..."); thread::sleep(Duration::from_secs(2)); num }; الشيفرة 2: إضافة توصيف اختياري لأنواع المعاملات والقيمة المُعادة في المغلف تبدو طريقة كتابة المغلفات مشابهة لطريقة كتابة الدوال بعد إضافة توصيف النوع، إذ نعرّف هنا دالةً تجمع 1 إلى المعامل ومغلّفًا بالسلوك ذاته للمقارنة بين الاثنين، ويوضح ذلك كيف أن طريقة كتابة المغلفات مشابهة لطريقة كتابة الدوال باستثناء استخدام الرمز | وكمية الصياغة الاختيارية: fn add_one_v1 (x: u32) -> u32 { x + 1 } let add_one_v2 = |x: u32| -> u32 { x + 1 }; let add_one_v3 = |x| { x + 1 }; let add_one_v4 = |x| x + 1 ; يوضح السطر الأول تعريف دالة، بينما يوضح السطر الثاني تعريف مغلف موصّف بالكامل، ثمّ نزيل في السطر الثالث التوصيف من تعريف المغلف، ونزيل في السطر الرابع الأقواس الاختيارية لأن محتوى المغلف يتألف من تعبير واحد، وجميع التعاريف السابقة تعاريف صالحة الاستخدام تمنحنا السلوك ذاته عند استدعائها. يتطلب السطران add_one_v3 و add_one_v4 تقييم قيمة المغلف حتى يجري تصريفهما، لأن الأنواع يجب أن تُسنتج من خلال استخدامهما وهذا الأمر مشابه لحاجة السطر let v = Vec::new();‎ لتوصيف النوع أو إضافة قيم من نوع ما إلى Vec، بحيث تستطيع رست استنتاج النوع. يستنتج المصرف نوعًا واحدًا ثابتًا لكل من المعاملات في حال تعريف المغلفات، إضافةً إلى النوع المُعاد منها. على سبيل المثال، توضح الشيفرة 3 تعريف مغلف قصير يُعيد القيمة التي يتلقاها مثل معاملٍ له، وهذا المغلف ليس مفيدًا جدًا إلا أن هدفه توضيحي. لاحظ أننا لم نضِف أي توصيف للنوع في التعريف وبالتالي يمكننا استدعاء المغلف باستخدام أي نوع، وهو ما فعلناه في الشيفرة 3، إذ استدعينا المغلّف في المرة الأولى باستخدام النوع String، إلا أننا حصلنا على خطأ عند استدعائنا للمغلف example_closure باستخدام قيمة عدد صحيح integer. اسم الملف: src/main.rs let example_closure = |x| x; let s = example_closure(String::from("hello")); let n = example_closure(5); الشيفرة 3: محاولة استدعاء مغلف يُستنتج نوعه باستخدام نوعين مختلفين يعرض لنا المصرّف الخطأ التالي: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) error[E0308]: mismatched types --> src/main.rs:5:29 | 5 | let n = example_closure(5); | --------------- ^- help: try using a conversion method: `.to_string()` | | | | | expected struct `String`, found integer | arguments to this function are incorrect | note: closure parameter defined here --> src/main.rs:2:28 | 2 | let example_closure = |x| x; | ^ For more information about this error, try `rustc --explain E0308`. error: could not compile `closure-example` due to previous error استنبط المصرّف نوع x المُمرّر بكونه سلسلة نصية String عندما رأى أن أول استخدام للمغلف example_closure كان باستخدام قيمة String، كما استنبط القيمة المعادة بالنوع String. تُقيّد هذه الأنواع في المغلف example_closure من تلك النقطة فصاعدًا وسنحصل على خطأ، إذا حاولنا استخدام نوع مختلف بعد ذلك في المغلف ذاته. الحصول على المراجع أو نقل الملكية يمكن أن تحصل المغلفات على القيم من البيئة بثلاث طرق وهي مرتبطة بالطرق الثلاث التي تستطيع فيها الدالة الحصول على القيم على أنها معاملاتها: الاستعارة الثابتة أو الاستعارة المتغيّرة أو أخذ الملكية، ويحدد المغلف واحدًا من هذه الطرق الثلاث حسب القيم الموجودة في متن الدالة. نعرّف في الشيفرة 4 مغلفًا يحصل على مرجع ثابت لشعاع يدعى list لأنه يحتاج فقط للمراجع الثابتة لطباعة القيمة: اسم الملف: src/main.rs fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {:?}", list); let only_borrows = || println!("From closure: {:?}", list); println!("Before calling closure: {:?}", list); only_borrows(); println!("After calling closure: {:?}", list); } الشيفرة 4: تعريف مغلف واستدعاءه، بحيث يحصل هذا المغلف على مرجع ثابت يوضح هذا المثال أيضًا أنه من الممكن للمتغير أن يرتبط بتعريف مغلف، ويمكننا لاحقًا استدعاء المغلف باستخدام اسم المتغير والقوسين وكأن اسم المتغير يمثل اسم دالة. يمكن الوصول للشعاع list من الشيفرة البرمجية قبل تعريف المغلف وبعد تعريفه، شرط أن يكون قبل استدعاء المغلف وبعد استدعاء المغلف، وذلك لأنه من الممكن وجود عدّة مراجع ثابتة للشعاع list في ذات الوقت، وتُصرَّف الشيفرة البرمجية بنجاح وتُنفَّذ وتطبع التالي: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/closure-example` Before defining closure: [1, 2, 3] Before calling closure: [1, 2, 3] From closure: [1, 2, 3] After calling closure: [1, 2, 3] نعدّل في الشيفرة 5 متن المغلف بحيث نضيف عنصرًا إلى الشعاع list، وبالتالي يحصل المغلف الآن على مرجع متغيّر: اسم الملف: src/main.rs fn main() { let mut list = vec![1, 2, 3]; println!("Before defining closure: {:?}", list); let mut borrows_mutably = || list.push(7); borrows_mutably(); println!("After calling closure: {:?}", list); } الشيفرة 5: تعريف واستدعاء مغلف يحتوي على مرجع متغيّر تُصرَّف الشيفرة البرمجية بنجاح وتُنفَّذ ونحصل على الخرج التالي: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/closure-example` Before defining closure: [1, 2, 3] After calling closure: [1, 2, 3, 7] لاحظ أننا لا نستخدم println!‎ بين تعريف المغلف borrows_mutably واستدعائه، إذ يحصل المغلف على مرجع متغيّر للشعاع list عند تعريفه، ولا نستخدم المغلف مجددًا بعد استدعائه لذا تنتهي الاستعارة المتغيرة عندها. لا يُسمح بطباعة مرجع متغيّر بين تعريف المغلف واستدعائه، إذ لا يُسمح بوجود أي عمليات استعارة أخرى عند وجود استعارة متغيّرة. حاول إضافة استدعاء للماكرو println!‎ لترى رسالة الخطأ التي تظهر لك. إذا أردت إجبار المغلف على أخذ ملكية القيم التي يستخدمها في البيئة على الرغم من أن متن المغلف لا يتطلب أخذ الملكية، فيمكنك استخدام الكلمة المفتاحية move قبل قائمة المعاملات. هذه الطريقة مفيدة خصوصًا عند تمرير مغلف لخيط thread جديد لنقل البيانات، بحيث تُمتلك بواسطة الخيط الجديد. سنناقش الخيوط بالتفصيل وسبب استخدامنا لها لاحقًا، لكن دعنا للوقت الحالي نستكشف عملية إضافة خيط جديد باستخدام مغلّف يحتاج الكلمة المفتاحية move. توضح الشيفرة 6 إصدارًا عن الشيفرة 4، إلا أننا نطبع الشعاع هنا في خيط جديد بدلًا من الخيط الرئيسي: اسم الملف: src/main.rs use std::thread; fn main() { let list = vec![1, 2, 3]; println!("Before defining closure: {:?}", list); thread::spawn(move || println!("From thread: {:?}", list)) .join() .unwrap(); } الشيفرة 6: استخدام move لإجبار خيط المغلف على أخذ ملكية list نُنشئ خيطًا جديدًا وذلك بمنح الخيط مغلف ليعمل به مثل وسيط، ويطبع متن المغلف القائمة. يحصل المغلف في الشيفرة 4 على list باستخدام مرجع ثابت لأنها تُعد أدنى درجات الوصول المطلوبة لطباعة محتويات list، إلا أننا في هذا المثال نحدد أن list يجب أن تُنقل إلى المغلف بإضافة الكلمة المفتاحية move في بداية تعريف المغلّف على الرغم من أن متن المغلف يتطلب فقط مرجعًا ثابتًا. يمكن للخيط الجديد أن ينتهي من التنفيذ قبل انتهاء الخيط الرئيسي من التنفيذ أو أن ينتهي الخيط الرئيسي أولًا، وإذا احتفظ الخيط الرئيسي بملكية list وانتهى من التنفيذ قبل الخيط الجديد وأسقط list فهذا يعني أن المرجع الثابت في الخيط الجديد سيكون غير صالحًا، ولذلك يتطلب المصرف نقل list إلى المغلف في الخيط الجديد بحيث يبقى المرجع صالحًا داخله. جرّب إزالة الكلمة المفتاحية move أو استخدام list في الخيط الرئيسي بعد تعريف المغلف لرؤية خطأ المصرف الذي يظهر لك. نقل القيم خارج المغلفات وسمات Fn تعرّف الشيفرة البرمجية الموجودة داخل مغلف ما الأمر الذي سيحصل للمراجع أو القيم بعد أن يُقيّم المغلف (بالتالي التعريف على الشيء الذي نُقل خارج المغلف إذا كان موجودًا) وذلك بعد أن يحصل المغلف على المرجع أو ملكية قيمة ما من البيئة مكان تعريفه (بالتالي التعريف على الشيء الذي نُقل إلى المغلف إذا كان موجودًا). يمكن لمتن المغلف أن يفعل أيًا من الأشياء التالية: نقل قيمة خارج المغلّف، أو التعديل على قيمة داخل المغلف، أو عدم نقل القيمة وعدم تعديلها، أو عدم الحصول على أي قيمة من البيئة في المقام الأول. تؤثر الطريقة التي يحصل بها المغلف على القيم ويتعامل معها من البيئة على السمات التي يطبقها المغلف والسمات traits هي الطريقة التي تستطيع فيها كل من الدوال والهياكل تحديد نوع المغلفات الممكن استخدامها. تطبّق المغلفات تلقائيًا سمةً أو سمتين أو ثلاث من سمات Fn التالية على نحوٍ تراكمي بحسب تعامل متن المغلف للقيم: السمة FnOnce: تُطبَّق على جميع المغلفات الممكن استدعاؤها مرةً واحدة. تُطبّق جميع المغلفات هذه السمة على الأقل لأنه يمكن لجميع المغلفات أن تُستدعى، وسيطبق المغلف الذي ينقل القيم خارج متنه السمة FnOnce فقط دون أي سمات Fn أخرى لأنه يُمكن استدعاءه مرةً واحدةً فقط. السمة FnMut: تُطبَّق على جميع المغلفات التي لا تنقل القيم خارج متنها، إلا أنها من الممكن أن تعدّل على هذه القيم، ويمكن استدعاء هذه المغلفات أكثر من مرة واحدة. السمة Fn: تُطبَّق على المغلفات التي لا تنقل القيم خارج متنها ولا تعدل على القيم، إضافةً إلى المغلفات التي لا تحصل على أي قيم من البيئة. يمكن لهذه المغلفات أن تُستدعى أكثر من مرة واحدة دون التعديل على بيئتها وهو أمرٌ مهم في حالة استدعاء مغلف عدة مرات على نحوٍ متعاقب. دعنا ننظر إلى تعريف التابع unwrap_or_else على النوع Option<T>‎ الذي استخدمناه في الشيفرة 1: impl<T> Option<T> { pub fn unwrap_or_else<F>(self, f: F) -> T where F: FnOnce() -> T { match self { Some(x) => x, None => f(), } } } تذكّر أن T هو نوع معمم generic type يمثل نوع القيمة في المتغاير Some للنوع Option، وهذا النوع T يمثل أيضًا نوع القيمة المعادة من الدالة unwrap_or_else، إذ ستحصل الشيفرة البرمجية التي تستدعي unwrap_or_else على النوع Option<String>‎ على النوع String على سبيل المثال. لاحظ تاليًا أن الدالة unwrap_or_else تحتوي على معامل نوع معمم إضافي يُدعى F والنوع F هنا هو نوع المعامل ذو الاسم f وهو المغلف الذي نمرره للدالة unwrap_or_else عند استدعائها. حد السمة trait bound المحدد على النوع المعمم E هو FnOnce() -> T، مما يعني أن النوع F يجب أن يكون قابلًا للاستدعاء مرةً واحدة وألّا يأخذ أي وسطاء وأن يعيد قيمةً من النوع T. يوضح استخدام FnOnce في حد السمة القيد: أن unwrap_or_else ستستدعي f مرةً واحدةً على الأكثر. يمكنك من رؤية متن الدالة unwrap_or_else معرفة أن f لن تُستدعى إذا كان المتغاير Some موجودًا في Option، بينما ستُستدعى f مرةً واحدة إذا وُجد المتغاير None في Option. تقبل الدالة unwrap_or_else أنواعًا مختلفة من المغلفات بصورةٍ مرنة، لأن جميع المغلفات تطبّق السمة FnOnce. ملاحظة: يمكن أن تطبق الدوال سمات Fn الثلاث أيضًا. يمكننا استخدام اسم الدالة بدلًا من مغلف عندما نريد شيئًا يطبق واحدةً من سمات Fn إذا كان ما نريد فعله لا يتطلب الحصول على قيمة من البيئة. يمكننا على سبيل المثال، استدعاء unwrap_or_else(Vec::new)‎ على قيمة Option<Vec<T>>‎ للحصول على شعاع فارغ جديد إذا كانت القيمة هي None. دعنا ننظر الآن إلى تابع المكتبة القياسية sort_by_key المعرف على الشرائح slices لرؤية الاختلاف بينه وبين unwrap_or_else وسبب استخدام sort_by_key للسمة FnMut بدلًا من السمة FnOnce لحد السمة. يحصل المغلف على وسيط واحد على هيئة مرجع للعنصر الحالي في الشريحة ويُعيد قيمةً من النوع K يمكن ترتيبها، وهذه الدالة مفيدة عندما تريد ترتيب شرحة بسمة attribute معينة لكل عنصر. لدينا في الشيفرة 7 قائمة من نسخ instances من الهيكل Rectangle ونستخدم sort_by_key لترتيبها حسب سمة width من الأصغر إلى الأكبر: اسم الملف: src/main.rs #[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; list.sort_by_key(|r| r.width); println!("{:#?}", list); } الشيفرة 7: استخدام sort_by_key لترتيب المستطيلات بحسب عرضها نحصل على الخرج التالي مما سبق: $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized + debuginfo] target(s) in 0.41s Running `target/debug/rectangles` [ Rectangle { width: 3, height: 5, }, Rectangle { width: 7, height: 12, }, Rectangle { width: 10, height: 1, }, ] السبب في كون sort_by_key معرفًا ليأخذ مغلفًا يطبق السمة FnMut هو استدعاء الدالة للمغلف عدة مرات: مرةً واحدةً لكل عنصر في الشريحة. لا يحصل المغلف ‎|r| r.width على أي قيمة من البيئة أو يعدل عليها أو ينقلها لذا فهو يحقق شروط حد السمة هذه. توضح الشيفرة 8 مثالًا لمغلف على النقيض، إذ يطبق هذا المغلف السمة FnOnce فقط لأنه ينقل قيمة خارج البيئة، ولن يسمح لنا المصرّف باستخدام هذا المغلف مع sort_by_key: اسم الملف: src/main.rs #[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut sort_operations = vec![]; let value = String::from("by key called"); list.sort_by_key(|r| { sort_operations.push(value); r.width }); println!("{:#?}", list); } الشيفرة 8: محاولة استخدام مغلف يطبق السمة FnOnce فقط مع التابع sort_by_key يمثّل ما سبق طريقةً معقدة (لا تعمل بنجاح) لمحاولة عدّ المرات التي يُستدعى بها التابع sort_by_key عند ترتيب list، وتحاول الشيفرة البرمجية تحقيق ذلك بإضافة value -ألا وهي قيمة من النوع String من بيئة المغلف- إلى الشعاع sort_operations. يحصل المغلف على القيمة value، ثم ينقلها خارج المغلف بنقل ملكيتها إلى الشعاع sort_operations، ويمكن أن يُستدعى هذا المغلف مرةً واحدةً إلا أن محاولة استدعائه للمرة الثانية لن تعمل لأن value لن يكون في البيئة ليُضاف إلى الشعاع sort_operations مجددًا، وبالتالي يطبق هذا المغلف السمة FnOnce فقط، وعندما نحاول تصريف الشيفرة البرمجية السابقة، سنحصل على خطأ مفاده أن value لا يمكن نقلها خارج المغلف لأن المغلف يجب أن يطبّق السمة FnMut: $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure --> src/main.rs:18:30 | 15 | let value = String::from("by key called"); | ----- captured outer variable 16 | 17 | list.sort_by_key(|r| { | --- captured by this `FnMut` closure 18 | sort_operations.push(value); | ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait For more information about this error, try `rustc --explain E0507`. error: could not compile `rectangles` due to previous error يشير الخطأ إلى السطر الذي ننقل فيه القيمة value خارج البيئة داخل متن المغلف، ولتصحيح هذا الخطأ علينا تعديل متن المغلف بحيث لا ينقل القيم خارج البيئة. نحافظ على وجود عدّاد في البيئة ونزيد قيمته داخل المغلف بحيث نستطيع عدّ المرات التي يُستدعى فيها التابع sort_by_key. يعمل المغلف في الشيفرة 9 مع sort_by_key لأنه يحصل فقط على المرجع المتغيّر الخاص بالعداد num_sort_operations ويمكن بالتالي استدعاؤه أكثر من مرة واحدة: اسم الملف: src/main.rs #[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut num_sort_operations = 0; list.sort_by_key(|r| { num_sort_operations += 1; r.width }); println!("{:#?}, sorted in {num_sort_operations} operations", list); } الشيفرة 9: استخدام مغلف يطبق السمة FnMut مع التابع sort_by_key دون الحصول على أخطاء سمات Fn مهمة عند تعريف أو استخدام الدوال أو الأنواع التي تستخدم المغلفات. سنناقش في المقال التالي المكررات iterators، إذ أن العديد من توابع المكررات تأخذ المغلفات مثل وسطاء، لذا تذكّر التفاصيل المتعلقة بالمغلّفات عند قراءة المقالة التالية. ترجمة -وبتصرف- لقسم من الفصل Functional Language Features: Iterators and Closures من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: التعامل مع متغيرات البيئة وطباعة الأخطاء في لغة رست تنظيم الاختبارات Tests في لغة رست Rust السمات Traits في لغة رست Rust تعلم لغة رست Rust: البدايات
  23. بدأنا عملية برمجة أداة سطر الأوامر المشابهة لأداة grep التي تبحث داخل ملف معيّن عن سلسلة نصية محددة في مقال التعامل مع الدخل والخرج أين وضع أساس برنامج سطر الأوامر بلغة رست حيث برمجنا منطق التعامل مع الوسطاء المرّرة في سطر الأوامر، ثم حسناه وطورناه أكثر في مقال إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء حيث عملنا على تجزئة برنامجنا إلى وحدات منفصلة لتسهل عملية اختبار البرنامج، ثم اختبرنا البرنامج عبر كتابة اختبارات له في المقال السابق ، وسنوضّح أخيرًا في هذا المقال كيفية العمل مع متغيرات البيئة environment variables، إضافةً لكيفية طباعة الأخطاء إلى مجرى الأخطاء القياسي، وهما أمران مهمّان في برامج سطر الأوامر. التعامل مع متغيرات البيئة سنحسّن على برنامج "minigrep" باستخدام ميزة إضافية، ألا وهي خيار استخدام البحث بنمط عدم حساسية حالة الحروف case-insensitive (سواءٌ كانت أحرف صغيرة أو كبيرة) بحيث يمكن للمستخدم تفعيل هذا النمط أو تعطيله باستخدام متغيرات البيئة. يمكننا جعل هذه الميزة خيارًا لسطر الأوامر إلا أن المستخدم سيكون بحاجةٍ لكتابة هذا الخيار في سطر الأوامر في كل مرة يريد استخدام البرنامج، وباستخدام متغيرات البيئة نجعل ضبط هذا الخيار لمرة واحدة بحيث تكون كل عمليات البحث غير حساسة لحالة الأحرف في جلسة الطرفية تلك. كتابة اختبار يفشل لدالة search لميزة عدم حساسية حالة الأحرف نُضيف أولًا دالة search_case_insensitive التي تُستدعى عندما يكون لمتغير البيئة قيمةً ما، وسنستمر باتباع عملية التطوير المُقاد بالاختبار هنا، وبالتالي ستكون الخطوة الأولى هي كتابة اختبار يفشل؛ إذ سنضيف اختبارًا جديدًا للدالة الجديدة search_case_insensitive وسنعيد تسمية الاختبار القديم من اسمه السابق one_result إلى case_sensitive لتوضيح الفرق بين الاختبارين كما هو موضح في الشيفرة 20. اسم الملف: src/lib.rs #[cfg(test)] mod tests { use super::*; #[test] fn case_sensitive() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } #[test] fn case_insensitive() { let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } } الشيفرة 20: إضافة اختبار جديد يفشل لدالة عدم حساسية حالة الحرف التي سنضيفها لاحقًا لاحظ أننا أضفنا اختبار contents القديم أيضًا، وأضفنا سطرًا جديدًا للنص ".Duct tape" باستخدام حرف D كبير، والذي يجب ألا يطابق استعلام السلسلة النصية "duct" عندما نبحث في حالة حساسية حالة الأحرف. يساعد تغيير الاختبار القديم بهذه الطريقة في التأكد من أننا لن نعطّل خاصية البحث في حالة حساسية الأحرف (وهي الحالة التي طبقناها أولًا، والتي تعمل بنجاح للوقت الحالي). يجب أن ينجح هذا الاختبار الآن ويجب أن يستمر بالنجاح بينما نعمل على خاصية عدم حساسية حالة الأحرف. يستخدم الاختبار الجديد للبحث بخاصية عدم حساسية حالة الأحرف "rUsT" مثل كلمة بحث، لذلك سنُضيف في الدالة search_case_insensitive الكلمة "rUsT" والتي يجب أن تطابق ":Rust" بحرف R كبير وأن تطابق السطر ".Trust me" أيضًا رغم أن للنتيجتين حالة أحرف مختلفة عن الكلمة التي استخدمناها. هذا هو اختبارنا الذي سيفشل، وستفشل عملية تصريفه لأننا لم نعرّف بعد الدالة search_case_insensitive. ضِف هيكلًا للدالة بحيث تُعيد شعاعًا فارغًا بطريقة مشابهة لما فعلناه في دالة search في الشيفرة 16 حتى نستطيع تصريف الاختبار ورؤية أنه يفشل فعلًا. تنفيذ دالة search_case_insensitive ستكون الدالة search_case_insensitive الموضحة في الشيفرة 21 مماثلة تقريبًا للدالة search، والفارق الوحيد هنا هو أننا سنحوّل حال الأحرف للكلمة التي نبحث عنها إلى أحرف صغيرة (الوسيط query) إضافةً إلى كل سطر line، بحيث تكون حالة الأحرف متماثلة عند المقارنة بينهما بغضّ النظر عن حالة الأحرف الأصلية. اسم الملف: src/lib.rs pub fn search_case_insensitive<'a>( query: &str, contents: &'a str, ) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results } الشيفرة 21: تعريف الدالة search_case_insensitive بحيث تحوّل أحرف الكلمة التي نبحث عنها مع السطر إلى أحرف صغيرة قبل مقارنتهما نحوّل أولًا أحرف السلسلة النصية query إلى أحرف صغيرة ونخزّنها في متغير يحمل الاسم ذاته، ولتحقيق ذلك نستدعي to_lowercase على السلسلة النصية بحيث تكون النتيجة واحدة بغضّ النظر عن حالة الأحرف المدخلة "rust" أو "RUST" أو "Rust" أو "rUsT" وسنعامل السلسلة النصية المدخلة وكأنها "rust" لإهمال حالة الأحرف، سيعمل التابع to_lowercase على محارف يونيكود Unicode الأساسية إلا أن عمله لن يكون صحيحًا مئة بالمئة. إن كنّا نبرمج تطبيقًا واقعيًا فعلينا أن نقوم بالمزيد من العمل بخصوص هذه النقطة، إلا أننا نناقش في هذا القسم متغيرات البيئة وليس يونيكود، لذا لن نتطرق لذلك الآن. لاحظ أن query من النوع String الآن وليس شريحة سلسلة نصية لأن استدعاء التابع to_lowercase يُنشئ بيانات جديدة عوضًا عن استخدام مرجع للبيانات الموجودة مسبقًا. لنفرض بأن الكلمة هي "rUsT" كمثال: لا تحتوي شريحة السلسلة النصية على حرف u أو t صغير لنستخدمه لذا علينا حجز مساحة جديدة لنوع String يحتوي على "rust"، وعندما نمرّر query كوسيط إلى التابع contains فنحن بحاجة لإضافة الرمز & لأن شارة contains معرفة بحيث تأخذ شريحة سلسلة نصية. نضيف من ثمّ استدعاءً للتابع to_lowercase لكل line لتحويل أحرفه إلى أحرف صغيرة، وبذلك نكون حولنا كل من أحرف line وquery إلى أحرف صغيرة وسنجد حالات التطابق بغض النظر عن حالة الأحرف في السلسلتين الأصليتين. دعنا نرى إذا كان تطبيقنا سيجتاز الاختبار: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.33s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 2 tests test tests::case_insensitive ... ok test tests::case_sensitive ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s عظيم، اجتزنا الاختبار. دعنا نستدعي الآن الدالة search_case_insensitive من الدالة run، وسنُضيف أولًا خيار الضبط إلى الهيكل Config للتبديل بين البحث الحساس وغير الحساس لحالة الأحرف، إلا أن إضافة هذا الحقل ستتسبب بأخطاء عند التصريف لأننا لم نسند هذا الحقل إلى أي مكان بعد: اسم الملف: src/lib.rs pub struct Config { pub query: String, pub file_path: String, pub ignore_case: bool, } أضفنا الحقل igonre_case الذي يخزن متحول بولياني Boolean، وسنحتاج الدالة run للتتحقق من قيمة ignore_case لتحديد استدعاء أيّ من دالتي البحث: search أو search_case_insensitive كما هو موضح في الشيفرة 22. لن تُصرَّف هذه الشيفرة بنجاح بعد. اسم الملف: src/lib.rs pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; let results = if config.ignore_case { search_case_insensitive(&config.query, &contents) } else { search(&config.query, &contents) }; for line in results { println!("{line}"); } Ok(()) } الشيفرة 22: استدعاء الدالة search أو الدالة search_case_insensitive بحسب القيمة الموجودة في config.ignore_case أخيرًا، نحن بحاجة إلى فحص متغير البيئة. الدوال الخاصة بالتعامل مع متغيرات البيئة موجودة في الوحدة module env في المكتبة القياسية، لذا نضيف الوحدة إلى النطاق أعلى الملف src/lib.rs. نستخدم الدالة var من الوحدة env لفحص القيمة المضبوطة في متغير البيئة ذو الاسم IGNORE_CASE كما هو موضح في الشيفرة 23. اسم الملف: src/lib.rs use std::env; // --snip-- impl Config { pub fn build(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let file_path = args[2].clone(); let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } } الشيفرة 23: التحقق من القيمة المضبوطة في متغير البيئة ذو الاسم IGNORE_CASE نُنشئ هنا متغيرًا جديدًا يدعى ignore_case ونُسند قيمته باستدعاء الدالة env::var ونمرّر اسم متغير البيئة IGNORE_CASE إليها. تُعيد الدالة env::var قيمةً من النوع Result تحتوي على متغاير variant‏ يدعى Ok يحتوي على قيمة متغير البيئة إذا كان متغير البيئة مضبوطًا إلى قيمة معينة وإلا فهي تعيد قيمة المتغاير Err. نستخدم التابع is_ok على القيمة Result للتحقق فيما إذا كان متغير البيئة مضبوطًا إلى قيمة معينة أم لا؛ فإذا كان مضبوطًا إلي قيمة فهذا يعني أنه علينا استخدام البحث بتجاهل حالة الأحرف؛ وإذا لم يكن مضبوطًا إلى قيمة معينة، فهذا يعني أن is_ok ستُعيد القيمة false وسينفذ البرنامج الدالة التي تجري البحث الحساس لحالة الأحرف. لا نهتم بقيمة متغير البيئة بل نهتم فقط فيما إذا كانت موجودة أو لا، ولذلك فنحن نستخدم التابع is_ok بدلًا من استخدام unwrap أو expect أو أيًا من التوابع الأخرى التي استخدمناها مع Result سابقًا. نمرر القيمة في المتغير ignore_case إلى نسخة Config بحيث يمكن للدالة run أن تقرأ هذه القيمة وتقرّر استدعاء الدالة search_case_insensitive أو search كما طبقنا سابقًا في الشيفرة 22. دعنا نجرّب البرنامج، ولننفذ أولًا البرنامج دون ضبط متغير البيئة وباستخدام الكلمة to، التي يجب أن تمنحنا جميع نتائج المطابقة للكلمة "to" بأحرف صغيرة فقط: $ cargo run -- to poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep to poem.txt` Are you nobody, too? How dreary to be somebody! يبدو أن البرنامج يعمل بنجاح. دعنا نجرّب الآن تنفيذ البرنامج مع ضبط IGNORE_CASE إلى 1 باستخدام الكلمة ذاتها to. $ IGNORE_CASE=1 cargo run -- to poem.txt إذا كنت تستخدم PowerShell، فعليك ضبط متغير البيئة، ثم تنفيذ البرنامج على أنهما أمرين منفصلين: PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt سيجعل ذلك متغير البيئة IGNORE_CASE مستمرًا طوال جلسة الصدفة shell. ويمكن إزالة القيمة عن طريق الأمر Remove-Item: PS> Remove-Item Env:IGNORE_CASE يجب أن نحصل على الأسطر التي تحتوي على الكلمة "to" بغض النظر عن حالة الأحرف: Are you nobody, too? How dreary to be somebody! To tell your name the livelong day To an admiring bog! عظيم، حصلنا على الكلمة "To" ضمن كلمات أخرى. يمكن لبرنامج minigrep الآن البحث عن الكلمات بغض النظر عن حالة الأحرف عن طريق متغير بيئة، ويمكنك الآن التحكم بخيارات البرنامج عن طريق وسطاء سطر الأوامر، أو عن طريق متغيرات البيئة. تسمح بعض البرامج بوسطاء سطر الأوامر ومتغيرات البيئة في ذات الوقت للخيار نفسه، وفي هذه الحالات يقرّر البرنامج أسبقية أحد الخيارين (وسيط سطر الأوامر أو متغير البيئة). تمرّن بنفسك عن طريق التحكم بحساسية حالة الأحرف عن طريق وسيط سطر أوامر أو متغير بيئة في الوقت ذاته، وحدّد أسبقية أحد الخيارين حسب تفضيلك إذا تعارض الخياران مع بعضهما. تحتوي الوحدة std::env على العديد من الخصائص الأخرى المفيدة للتعامل مع متغيرات البيئة، اقرأ توثيق الوحدة للاطّلاع على الخيارات المتاحة. كتابة رسائل الخطأ إلى مجرى الخطأ القياسي بدلا من مجرى الخرج القياسي نكتب رسائل الأخطاء حاليًا إلى الطرفية باستخدام الماكرو println!‎، وفي معظم الطرفيات هناك نوعين من الخرج: خرج قياسي ‏ ‎stdout‎ للمعلومات العامة وخطأ قياسي ‏stderr‎ لرسائل الخطأ، ويساعد التمييز بين النوعين المستخدمين بتوجيه خرج نجاح البرنامج إلى ملف مع المحافظة على ظهور رسائل الخطأ على شاشة الطرفية. الماكرو println!‎ قادرٌ فقط على الطباعة إلى الخرج القياسي، لذا علينا استخدام شيء مختلف لطباعة الأخطاء إلى مجرى الأخطاء القياسي standard error stream. التحقق من مكان كتابة الأخطاء دعنا أولًا نلاحظ كيفية طباعة المحتوى في برنامج minigrep حاليًا إلى الخرج القياسي، متضمنًا ذلك رسائل الأخطاء التي نريد كتابتها إلى مجرى الأخطاء القياسي بدلًا من ذلك، وسنحقّق ذلك بإعادة توجيه مجرى الخرج القياسي إلى ملف والتسبب بخطأ عمدًا، بينما سنُبقي على مجرى الأخطاء القياسي ولن نعيد توجيهه، وبالتالي سيُعرض محتوى مجرى الأخطاء القياسي على الشاشة مباشرةً. من المتوقع لبرامج سطر الأوامر أن ترسل رسائل الخطأ إلى مجرى الأخطاء القياسي بحيث يمكننا رؤية الأخطاء على الشاشة حتى لو كنّا نعيد توجيه مجرى الخرج القياسي إلى ملف ما. لا يسلك برنامجنا حاليًا سلوكًا جيدًا، إذ أننا على وشك رؤية أن رسائل الخطأ تُخزّن في الملف. لتوضيح هذا السلوك سننفذ البرنامج باستخدام < ومسار الملف output.txt وهو الملف الذي نريد إعادة توجيه مجرى الخرج القياسية إليه. لن نمرّر أي وسطاء عند التنفيذ، وهو ما سيتسبب بخطأ: $ cargo run > output.txt يخبر الرمز < الصدفة بكتابة محتويات الخرج القياسي إلى الملف output.txt بدلًا من الشاشة. لم نحصل على أي رسالة خطأ على الرغم من توقعنا لها بالظهور على الشاشة مما يعني أن الرسالة قد كُتبت إلى الملف. إليك محتوى الملف output.txt: Problem parsing arguments: not enough arguments نعم، تُطبع رسالة الخطأ إلى الخرج القياسي كما توقعنا ومن الأفضل لنا طباعة رسائل الخطأ إلى مجرى الأخطاء القياسي بدلًا من ذلك بحيث يحتوي الملف على البيانات الناتجة عن التنفيذ الناجح، دعنا نحقّق ذلك. طباعة الأخطاء إلى مجرى الأخطاء القياسي سنستخدم الشيفرة البرمجية في الشيفرة 24 لتعديل طريقة طباعة الأخطاء. لحسن الحظ، الشيفرة البرمجية المتعلقة بطباعة رسائل الخطأ موجودة في دالة واحدة ألا وهي main بفضل عملية إعادة بناء التعليمات البرمجية التي أنجزناها سابقًا. تقدّم لنا المكتبة القياسية الماكرو eprintln!‎ الذي يطبع إلى مجرى الأخطاء القياسي، لذا دعنا نعدّل من السطرين الذين نستخدم فيهما الماكرو println!‎ ونستخدم eprintln!‎ بدلًا من ذلك. اسم الملف: src/main.rs fn main() { let args: Vec<String> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); if let Err(e) = minigrep::run(config) { eprintln!("Application error: {e}"); process::exit(1); } } الشيفرة 24: كتابة رسائل الخطأ إلى مجرى الأخطاء القياسية بدلًا من مجرى الخرج القياسي باستخدام eprintln!‎ دعنا ننفذ البرنامج مجددًا بالطريقة ذاتها دون وسطاء وبإعادة توجيه الخرج القياسي إلى ملف باستخدام <: $ cargo run > output.txt Problem parsing arguments: not enough arguments نستطيع رؤية الخطأ على الشاشة الآن، ولا يحتوي الملف output.txt أي بيانات وهو السلوك الذي نتوقعه من برامج سطر الأوامر. دعنا ننفذ البرنامج مجددًا باستخدام الوسطاء لتنفيذ البرنامج دون أخطاء، وبتوجيه الخرج القياسي إلى ملف أيضًا كما يلي: $ cargo run -- to poem.txt > output.txt لن نستطيع رؤية أي خرج على الطرفية، وسيحتوي الملف output.txt على نتائجنا: اسم الملف: output.txt Are you nobody, too? How dreary to be somebody! يوضح هذا الأمر أننا نستخدم مجرى الخرج القياسي للخرج في حالة النجاح، بينما نستخدم مجرى الأخطاء القياسي في حالة الفشل. خاتمة المشروع لخّصت جزئية سطر الأوامر من هذه السلسلة بمشروعها الكثير من المفاهيم المهمة التي تعلمناها لحد اللحظة كما أننا تكلمنا عن كيفية إجراء عمليات الدخل والخرج في رست، وذلك باستخدام وسطاء سطر الأوامر والملفات ومتغيرات البيئة والماكرو eprintln!‎ لطباعة الأخطاء، ويجب أن تكون الآن مستعدًا لكتابة تطبيقات سطر الأوامر المختلفة. يجب أن تبقى شيفرتك البرمجية منظمةً جيدًا بمساعدة المفاهيم التي تعلمتها في المقالات السابقة وأن تخزن البيانات بفعالية في هياكل بيانات مناسبة وأن تتعامل مع الأخطاء بصورةٍ مناسبة، إضافةً إلى إجراء الاختبارات. سننظر في المقالات التالية إلى بعض مزايا رست التي تأثرت باللغات الوظيفية، ألا وهي المغلّفات closures والمكررات iterators. ترجمة -وبتصرف- لقسم من الفصل An I/O Project: Building a Command Line Program من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: كتابة برنامج سطر أوامر بلغة رست: اختبار البرنامج ما هو سطر الأوامر ؟ كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج
  24. بدأنا عملية برمجة أداة سطر الأوامر المشابهة لأداة grep التي تبحث داخل ملف معيّن عن سلسلة نصية محددة في مقال كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج وضع أساس برنامج سطر الأوامر بلغة رست حيث برمجنا منطق التعامل مع الوسطاء المرّرة في سطر الأوامر، ثم حسناه وطورناه أكثر في المقال التالي كتابة برنامج سطر أوامر بلغة رست: إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء حيث عملنا على تجزئة برنامجنا إلى وحدات منفصلة لتسهل عملية اختبار البرنامج، ونتطرّق في هذا المقال إلى اختبار البرنامج. تطوير عمل المكتبة باستخدام التطوير المقاد بالاختبار test-driven الآن، وبعد استخراجنا لمعظم منطق البرنامج إلى الملف src/lib.rs، يبقى لدينا منطق الحصول على الوسطاء والتعامل مع الأخطاء في src/main.rs، ومن الأسهل كتابة الاختبارات في هذه الحالة، إذ ستركّز الاختبارات على منطق شيفرتنا البرمجية الأساسية. يمكننا استدعاء الدوال مباشرةً باستخدام مختلف الوسطاء arguments والتحقق من القيمة المعادة دون الحاجة لاستدعاء ملفنا التنفيذي من سطر الأوامر. نُضيف في هذا القسم منطق البحث إلى البرنامج "minigrep" باستخدام التطوير المُقاد بالاختبار test-driven development -أو اختصارًا TDD- باتباع الخطوات التالية: كتابة اختبار يفشل وتنفيذه للتأكد من أنه يفشل فعلًا للسبب الذي تتوقعه. كتابة شيفرة برمجية أو التعديل على شيفرة برمجية موجودة مسبقًا لجعل الاختبار الجديد ينجح. إعادة بناء التعليمات البرمجية المُضافة أو المُعدّلة للتأكد من أن الاختبارات ستنجح دومًا. كرّر الأمر مجدّدًا بدءًا من الخطوة 1. على الرغم من أن التطوير المُقاد بالاختبار هو طريقة من الطرق العديدة الموجودة لكتابة البرمجيات، إلا أنه من الممكن أن يساهم بتصميم الشيفرة البرمجية، إذ تساعد كتابة الاختبارات قبل كتابة الشيفرة البرمجية التي تجعل من الاختبار ناجحًا في المحافظة على اختبار جميع أجزاء الشيفرة البرمجية بسوّية عالية خلال عملية التطوير. سنختبر تطبيق الخاصية التي ستبحث عن السلسلة النصية المُدخلة في محتويات الملف وتعطينا قائمةً من الأسطر تحتوي على حالات التطابق، ثمّ سنضيف هذه الخاصية في دالة تدعى search. كتابة اختبار فاشل دعنا نتخلص من تعليمات println!‎ من الملفين "src/lib.rs" و "src/main.rs" التي كنا نستخدمها لتفقُّد سلوك البرنامج ولن نحتاج إليها بعد الآن، ثم نضيف الوحدة tests في الملف "src/lib.rs" مع دالة اختبار كما فعلنا في مقال سابق. تحدد دالة الاختبار السلوك الذي نريده من الدالة search ألا وهو: ستأخذ الدالة سلسلةً نصيةً محددةً ونصًا تبحث فيه وستُعيد السطور التي تحتوي على تطابق. توضح الشيفرة 15 هذا الاختبار، إلا أنها لن تُصرَّف بنجاح بعد. اسم الملف: src/lib.rs #[cfg(test)] mod tests { use super::*; #[test] fn one_result() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } } الشيفرة 15: إنشاء اختبار فاشل للدالة search التي كنا نتمنى الحصول عليها يبحث هذا الاختبار عن السلسلة النصية "duct"، إذ يتألف النص الذي نبحث فيه من ثلاثة أسطر ويحتوي واحدٌ منها فقط السلسلة النصية "duct" (يخبر الخط المائل العكسي backslash بعد علامتي التنصيص المزدوجتين رست بعدم إضافة محرف سطر جديد في بداية محتوى السلسلة النصية المجردة). نتأكد أن القيمة المُعادة من الدالة search تحتوي فقط على السطر الذي نتوقعه. لا يمكننا تنفيذ هذا الاختبار بعد ورؤيته يفشل لأن الاختبار لا يُصرَّف، والسبب في ذلك هو أن الدالة search غير موجودة بعد. وفقًا لمبادئ التطوير المُقاد بالاختبار، علينا إضافة القليل من الشيفرة البرمجية بحيث يمكننا تصريف الاختبار وتنفيذه بإضافة تعريف الدالة search التي تُعيد شعاعًا vector فارغًا دومًا كما هو موضح في الشيفرة 16، ومن ثم يجب أن يُصرَّف الاختبار ويفشل لعدم مطابقة الشعاع الفارغ للشعاع الذي يحتوي السطر "safe, fast, productive.". اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![] } الشيفرة 16: تعريف الدالة search باستخدام شيفرة برمجية قصيرة بحيث يُصرَّف الاختبار لاحظ أننا بحاجة إلى تعريف دورة حياة lifetime صراحةً تدعى ‎'a في بصمة الدالة search واستخدام دورة الحياة في الوسيط contents والقيمة المُعادة. تذكر أننا ذكرنا في مقال سابق أن معاملات دورة الحياة تحدد أي دورات حياة الوسطاء متصلة بدورة حياة القيمة المُعادة، وفي هذه الحالة فإننا نحدد أن الشعاع المُعاد يجب أن يحتوي على شرائح سلسلة نصية string slices تمثل مرجعًا لشرائح الوسيط contents بدلًا من الوسيط query. بكلمات أخرى، نخبر رست بأن البيانات المُعادة من الدالة search ستعيش طالما تعيش البيانات المُمرّرة إلى الدالة search في الوسيط contents. يجب أن تكون الشرائح المُستخدمة مثل مراجع للبيانات صالحة حتى يكون المرجع صالحًا، إذ سيكون التحقق من الأمان خاطئًا لو افترض المصرف أننا نُنشئ شرائح سلاسل نصية من query بدلًا من contents. نحصل على الخطأ التالي إذا نسينا توصيف دورات الحياة وجرّبنا تصريف هذه الدالة: $ cargo build Compiling minigrep v0.1.0 (file:///projects/minigrep) error[E0106]: missing lifetime specifier --> src/lib.rs:28:51 | 28 | pub fn search(query: &str, contents: &str) -> Vec<&str> { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents` help: consider introducing a named lifetime parameter | 28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> { | ++++ ++ ++ ++ For more information about this error, try `rustc --explain E0106`. error: could not compile `minigrep` due to previous error لا يمكن لرست معرفة أي الوسيطين نحتاج، لذا يجب أن نصرح عن ذلك مباشرةً. نعلم أن contents هو الوسيط الذي يجب أن يُربط مع القيمة المُعادة باستخدام دورة الحياة وذلك لأنه الوسيط الذي يحتوي على كامل محتوى الملف النصي الذي نريد أن نعيد أجزاءً متطابقةً منه. لا تتطلب لغات البرمجة الأخرى ربط الوسطاء للقيمة المعادة في بصمة الدالة، إلا أنك ستعتاد على ذلك مع الممارسة. ننصحك بمقارنة هذا المثال مع مثال موجود في مقال سابق. دعنا ننفذ الاختبار: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 0.97s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test test tests::one_result ... FAILED failures: ---- tests::one_result stdout ---- thread 'main' panicked at 'assertion failed: `(left == right)` left: `["safe, fast, productive."]`, right: `[]`', src/lib.rs:44:9 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::one_result test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` عظيم، فشل الاختبار كما توقعنا. دعنا نجعل الاختبار ينجح الآن. كتابة شيفرة برمجية لاجتياز الاختبار يفشل اختبارنا حاليًا لأننا نُعيد دائمًا شعاعًا فارغًا، وعلى برنامجنا اتباع الخطوات التالية لتصحيح ذلك وتطبيق search: المرور على سطور محتوى الملف. التحقق ما إذا كان السطر يحتوي على السلسلة النصية التي نبحث عنها. إذا كان هذا الأمر محققًا: نضيف السطر إلى قائمة القيم التي سنعيدها. إن لم يكن محققًا: لا نفعل أي شيء. نُعيد قائمة الأسطر التي تحتوي على تطابق مع السلسلة النصية التي نبحث عنها. لنعمل على كل خطوة بالتدريج، بدءًا من المرور على الأسطر على الترتيب. المرور على الأسطر باستخدام التابع lines توفر لنا رست تابعًا مفيدًا للتعامل مع السلاسل النصية سطرًا تلو الآخر بصورةٍ سهلة وهو تابع lines، ويعمل التابع بالشكل الموضح في الشيفرة 17. انتبه إلى أن الشيفرة 17 لن تُصرَّف بنجاح. اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { // أجرِ بعض العمليات على ‫line } } الشيفرة 17: المرور على أسطر الوسيط contents يُعيد التابع lines مكرّرًا iterator، وسنتحدث عن المكررات فيما بعد؛ تذّكر أنك رأيت هذه الطريقة باستعمال المكررات في الشيفرة 5 من فصل سابق عندما استخدمنا حلقة for مع مكرر لتنفيذ شيفرة برمجية على كل عنصر من عناصر التجميعة collection. البحث عن الاستعلام في كل سطر الآن نبحث فيما إذا كان السطر يحتوي على السلسلة النصية المحددة بالاستعلام، وتحتوي السلاسل النصية لحسن الحظ على تابع مفيد يدعى contains يفعل هذا نيابةً عنّا. أضف استدعاءً للتابع contains في الدالة search كما هو موضح في الشيفرة 18. لاحظ أن هذه الشيفرة البرمجية لن تُصرَّف بعد. اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { if line.contains(query) { // do something with line } } } الشيفرة 18: إضافة ميزة البحث عن السلسلة النصية الموجودة في الوسيط query داخل السطر بدأنا ببناء وظيفة البرنامج الأساسية الآن، ولتصريف البرنامج بنجاح نحن بحاجة لإعادة قيمة من متن الدالة كما قلنا أننا سنفعل في بصمة الدالة. تخزين الأسطر المطابقة أخيرًا يجب علينا استخدام طريقة لتخزين الأسطر المُطابقة التي نريد أن نُعيدها من الدالة، ونستخدم لذلك شعاعًا متغيّرًا mutable vector قبل الحلقة for ونستدعي التابع push لتخزين line في الشعاع، ونُعيد هذا الشعاع بعد انتهاء الحلقة for كما هو موضح في الشيفرة 19. اسم الملف: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results } الشيفرة 19: تخزين الأسطر المتطابقة بحيث نستطيع إعادتهم من الدالة الآن يجب أن تُعيد الدالة search فقط الأسطر التي تحتوي على الوسيط query مما يعني أن اختبارنا سينجح. دعنا ننفّذ الاختبار: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.22s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test test tests::one_result ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s نجح الاختبار، لذا فالدالة تعمل. يجب أن نفكّر بفرص إعادة بناء التعليمات البرمجية المحتملة بحلول هذه النقطة داخل الدالة search مع المحافظة على نجاح الاختبار للحفاظ على الهدف من الدالة. ليست الشيفرة البرمجية الموجودة في الدالة search سيئة، لكنها لا تستغلّ خصائص المكرّرات المفيدة، وسنعود لهذه الشيفرة البرمجية في مقالات لاحقة عندما نتحدث عن المكررات بتعمق أكبر لمعرفة التحسينات الممكنة. استخدام الدالة search في الدالة run الآن وبعد عمل الدالة search وتجربتها بنجاح، نحتاج إلى استدعاء search من الدالة run، وتمرير قيمة config.query و contents التي تقرأهما run من الملف إلى الدالة search. تطبع الدالة run كل سطر مُعاد من الدالة search: اسم الملف: src/lib.rs pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; for line in search(&config.query, &contents) { println!("{line}"); } Ok(()) } ما زلنا نستخدم الحلقة for لإعادة كل سطر من search وطباعته. يجب أن يعمل كامل البرنامج الآن. دعنا نجرّبه أولًا بكلمة "الضفدع frog" التي ينبغي أن تُعيد سطرًا واحدًا بالتحديد من قصيدة ايميلي ديكنسون Emily Dickinson: $ cargo run -- frog poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.38s Running `target/debug/minigrep frog poem.txt` How public, like a frog عظيم. دعنا نجرب كلمةً يُفترض أنها موجودة في عدة أسطر مثل "body": $ cargo run -- body poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep body poem.txt` I'm nobody! Who are you? Are you nobody, too? How dreary to be somebody! وأخيرًا، دعنا نتأكد من أننا لن نحصل على أي سطر عندما تكون الكلمة غير موجودة ضمن أي سطر في القصيدة مثل الكلمة "monomorphization": $ cargo run -- monomorphization poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep monomorphization poem.txt` ممتاز، بنينا إصدارنا الخاص المصغّر من أداة البحث الكلاسيكية grep، وتعلمنا الكثير عن هيكلة التطبيقات، كما أننا تعلمنا بعض الأشياء بخصوص دخل وخرج الملفات ودورات الحياة والاختبار والحصول على الوسطاء من سطر الأوامر. سنوضّح في المقال التالي كيفية العمل مع متغيرات البيئة في ختام هذا المشروع، إضافةً لكيفية طباعة الأخطاء إلى مجرى الأخطاء القياسي، وهما أمران مهمّان في برامج سطر الأوامر. ترجمة -وبتصرف- لقسم من الفصل An I/O Project: Building a Command Line Program من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: كتابة برنامج سطر أوامر بلغة رست: إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء ما هو سطر الأوامر ؟ كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج
  25. بدأنا في المقال السابق كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج بناء المشروع وسنكمل العملية في هذا المقال حيث سنصلح أربع مشكلات خاصة بهيكل البرنامج وكيفية تعامله مع الأخطاء المحتملة لتحسين برنامجنا. تتمثل المشكلة الأولى في أن للدالة main مهمتان: المرور على لائحة الوسطاء وقراءة الملفات. سيزداد عدد المهام المتفرقة التي تنجزها الدالة main مع نموّ حجم برنامجنا، وتصبح الدالة التي تحتوي على الكثير من المهام صعبة الفهم والاختبار والتعديل دون المخاطرة بتعطيل بعض خصائصها، ومن الأفضل فصل المهام عن بعضها بعضًا بحيث تكون كل دالة مسؤولةً عن مهمةٍ واحدة. تمتدّ المشكلة إلى مشكلة أخرى ثانية: على الرغم من أن query و file_path تمثلان متغيرات بيئة لبرنامجنا إلا أن متغيرات مثل contents تُستخدم في برنامجنا لتنفيذ المنطق، ومع زيادة حجم الدالة main سيزيد عدد المتغيرات التي سنحتاج إضافتها إلى النطاق مما سيصعّب مهمة متابعة قيمة كل منها، ومن الأفضل تجميع متغيرات الضبط في هيكل واحدة لجعل الهدف منها واضح. المشكلة الثالثة هي أننا استخدمنا expect لطباعة رسالة خطأ عندما تفشل عملية قراءة الملف، إلا أن رسالة الخطأ تقتصر على طباعة "Should have been able to read the file"، وقد تفشل قراءة ملف ما لعدّة أسباب؛ إذ يمكن أن يكون الملف مفقودًا؛ أو أنك لا تمتلك الأذونات المناسبة لفتحه، وحاليًا فنحن نعرض رسالة الخطأ ذاتها بغض النظر عن سبب الخطأ، وهو أمرٌ لن يمنح المستخدم أي معلومات مفيدة. رابعًا، استخدمنا expect بصورةٍ متكررة لنتعامل مع الأخطاء المختلفة وإذا نفّذ المستخدم البرنامج دون تحديد عددٍ كافٍ من الوسطاء فسيحصل على الخطأ "index out of bounds" من رست، وهو خطأ لا يشرح بدقة سبب المشكلة. يُفضَّل هنا أن يكون التعامل مع الأخطاء موجودًا في مكان واحد بحيث يعلم المبرمج الذي يعمل على تطوير البرنامج مستقبلًا المكان الذي يجب التوجه إليه في حال أراد تغيير منطق التعامل مع الأخطاء، كما سيسهّل وجود الشيفرة البرمجية التي تتعامل مع الأخطاء في مكان واحد عملية طباعة رسائل خطأ معبّرة للمستخدم. دعنا نصلح هذه المشاكل الأربع بإعادة بناء التعليمات البرمجية. فصل المهام في المشاريع التنفيذية مشكلة تنظيم المسؤوليات المختلفة بالنسبة للدالة main هي مشكلة شائعة في الكثير من المشاريع التنفيذية binary projects، ونتيجةً لذلك طوّر مجتمع رست توجيهات عامة لفصل المهام المختلفة الموجودة في البرنامج التنفيذي عندما تصبح الدالة main كبيرة، وتتلخص هذه العملية بالخطوات التالية: تجزئة برنامجك إلى "main.rs" و "lib.rs" ونقل منطق البرنامج إلى "lib.rs". يمكن أن يبقى منطق الحصول على الوسطاء من سطر الأوامر في "main.rs" طالما هو قصير. عندما يصبح منطق الحصول على الوسطاء من سطر الأوامر معقّدًا صدِّره من "main.rs" إلى "lib.rs". يجب أن تكون المهام التي يجب أن تبقى في main بعد هذه العملية محصورةً بما يلي: استدعاء منطق الحصول على وسطاء سطر الأوامر باستخدام قيم الوسطاء. إعداد أي ضبط لازم. استدعاء الدالة run في "lib.rs". التعامل مع الخطأ إذا أعادت الدالة run خطأ. يحلّ هذا النمط كل ما يتعلق بفصل المهام، إذ يتعامل "main.rs" بكل شيء يخص تشغيل البرنامج، بينما يتعامل "lib.rs" مع منطق المهة المطروحة. بما أنك لا تستطيع اختبار الدالة main مباشرةً، سيساعدك هذا الهيكل في اختبار كل منطق برنامجك بنقله إلى دوال موجودة في "lib.rs"، وستكون الشيفرة البرمجية المتبقىة في "main.rs" قصيرة وسيكون التحقق من صحتها بالنظر إليها ببساطة كافيًا. دعنا نعيد كتابة التعليمات البرمجية باستخدام هذه الخطوات. استخلاص الشيفرة البرمجية التي تحصل على الوسطاء سنستخرج خاصية الحصول على الوسطاء إلى دالة تستدعيها main لتحضير نقل منطق سطر الأوامر إلى src/lib.rs. توضح الشيفرة 5 بدايةً جديدةً من الدالة main تستدعي دالةً جديدة تدعى parse_config وسنعرّفها حاليًا في src/main.rs. اسم الملف: src/main.rs fn main() { let args: Vec<String> = env::args().collect(); let (query, file_path) = parse_config(&args); // --snip-- } fn parse_config(args: &[String]) -> (&str, &str) { let query = &args[1]; let file_path = &args[2]; (query, file_path) } [الشيفرة 5: استخراج الدالة parse_config من main] ما زلنا نجمع وسطاء سطر الأوامر في شعاع، إلا أننا نمرّر الشعاع كاملًا إلى الدالة parse_config بدلًا من إسناد القيمة في الدليل "1" إلى المتغير query والقيمة في الدليل "2" إلى المتغير file_path داخل الدالة main، إذ تحتوي الدالة parse_config على المنطق الذي يحدّد أي القيمتين سيُخزَّن في أي المتغيّرين ومن ثم تعديل القيم إلى الدالة main، إلا أننا ما زلنا نُنشئ المتغيرين query و file_path في main، لكن لا تحمل main مسؤولية تحديد أي وسطاء سطر الأوامر تنتمي إلى أي المتغيرات. قد تبدو هذه الإضافة مبالغةً شديدةً في برنامجنا البسيط هذا، إلا أننا نعيد بناء التعليمات البرمجية بخطوات صغيرة وتدريجية. نفّذ هذا البرنامج بعد تطبيق التعديلات لتتأكد من أن الحصول على الوسطاء ما زال يعمل. من المحبّذ التحقق من تنفيذ البرنامج بعد كل تعديل بحيث تستطيع معرفة سبب المشكلة فورًا إذا حصلت. تجميع قيم الضبط يمكننا اتخاذ خطوة بسيطة لتحسين الدالة parse_config أكثر، إذ أننا نعيد حاليًا صف tuple على الرغم من أننا نجزّء هذا الصف مباشرةً إلى أجزاء متفرقة من جديد، وهذا يشير إلى أننا لا نطبّق الفكرة صحيحًا. كون config جزءًا من parse_config هو مؤشر آخر لوجود احتمالية تحسين، إذ يشير ذلك أن القيمتين التي نُعيدهما مترابطتان وهما جزء من قيمة ضبط واحدة، إلا أننا لا ننقل هذا المعنى بهيكل البيانات، كما أننا نجمع القيمتين في صف. دعنا نضع القيمتين بدلًا من ذلك في هيكل واحد ونمنح كلًا من حقول الهيكل اسمًا معبرًا. ستكون الشيفرة البرمجية بعد ذلك أسهل فهمًا للمطورين الذين سيعملون على الشيفرة البرمجية مستقبلًا، إذ سيوضّح ذلك كيف ترتبط القيم المختلفة مع بعضها بعضًا وهدف كل واحدة منها. توضح الشيفرة 6 التحسينات التي أجريناها على الدالة parse_config. اسم الملف: src/main.rs fn main() { let args: Vec<String> = env::args().collect(); let config = parse_config(&args); println!("Searching for {}", config.query); println!("In file {}", config.file_path); let contents = fs::read_to_string(config.file_path) .expect("Should have been able to read the file"); // --snip-- } struct Config { query: String, file_path: String, } fn parse_config(args: &[String]) -> Config { let query = args[1].clone(); let file_path = args[2].clone(); Config { query, file_path } } [الشيفرة 6: إعادة بناء التعليمات البرمجية في الدالة parse_config لإعادة نسخة instance من الهيكل Config] أَضفنا هيكلًا يدعى Config وعرّفناه، بحيث يحتوي على حقلين query و file_path. تشير بصمة signature الدالة parse_config الآن أنها تُعيد قيمةً من النوع Config، إلا أننا اعتدنا إعادة شرائح السلسلة النصية string slice التي تمثل مرجعًا للنوع String في args، لذلك نعرّف Config بحيث يحتوي على قيمتين مملوكتين owned من النوع String. المتغير args في main هو المالك لقيم الوسطاء ويسمح للدالة parse_config باستعارتها فقط، مما يعني أننا سنخرق قواعد الاستعارة في رست إذا حاول Config أخذ ملكية القيم من args. هناك عدة طرق نستطيع فيها إدارة بيانات String، إلا أن أسهل الطرق غير فعال وهو باستدعاء التابع clone على القيم، مما سيعطينا نسخةً من البيانات بحيث تستطيع Config امتلاكها وهو أمرٌ يستغرق وقتًا ويشغل ذاكرةً أكبر مقارنةً باستخدام مرجع لبيانات السلسلة النصية، إلا أن نسخ البيانات يجعل من شيفرتنا البرمجية واضحةً لأنه ليس علينا وقتها إدارة دورات حياة المراجع، وفي هذه الحالة تُعد مقايضة الفعالية بالأداء بصورةٍ طفيفة مقابل البساطة أمرًا مقبولًا. حدّثنا main بحيث تضع نسخة من الهيكل Config مُعادة بواسطة الدالة parse_config إلى متغير يدعى config وحدّثنا الشيفرة البرمجية التي استخدمت سابقًا المتغيرات query و file_path بصورةٍ منفصلة، إذ نستخدم الآن الحقول الموجودة في Config بدلًا من ذلك. أصبحت شيفرتنا البرمجية الآن توضح بصورةٍ أفضل أن القيمتين query و file_path مترابطتان وأن الهدف منهما هو ضبط كيفية عمل البرنامج. أي شيفرة برمجية تستخدم هاتين القيمتين ستعثر عليهما في نسخة config في الحقول المسمّاة بحسب الهدف منهما. ملاحظة حول سلبيات استخدام clone: يميل مبرمجو لغة رست لتفادي استخدام clone لتصحيح مشاكل الملكية بسبب الوقت الذي يستغرقه تنفيذها. ستتعلم لاحقًا كيفية استخدام توابع ذات كفاءة في حالات مشابهة لهذه، إلا أن نسخ بعض السلاسل النصية حاليًا أمرٌ مقبول لأنك تنسخ هذه القيم مرةً واحدةً فقط ومسار الملف والكلمة التي تبحث عنها ليستا بالحجم الكبير. من الأفضل وجود برنامج لا يعمل بفعالية مئةً بالمئة من محاولة زيادة فعالية الشيفرة البرمجية بصورةٍ مفرطة في محاولتك الأولى، إذ سيصبح الأمر أسهل بالنسبة لك مع اكتسابك للخبرة في رست، بحيث تستطيع كتابة حلول برمجية أكثر فاعلية ومن المقبول الآن استدعاء clone. إنشاء باني للهيكل Config استخلصنا بحلول هذه اللحظة المنطق المسؤول عن الحصول على قيم وسطاء سطر الأوامر من الدالة main ووضعناه في الدالة parse_config، وساعدنا هذا في رؤية كيفية ارتباط القيمتين query و file_path ببعضهما، ثم أضفنا هيكل Config لتسمية الهدف من القيمتين query و file_path ولنكون قادرين على إعادة أسماء القيم مثل حقول هيكل من الدالة parse_config. إذًا، أصبح الآن الهدف من الدالة parse_config إنشاء نسخ من الهيكل Config، ويمكننا تعديل parse_config من دالة اعتيادية إلى دالة تدعى new مرتبطة بالهيكل Config، وسيؤدي هذا التعديل إلى جعل شيفرتنا البرمجية رسميةً idiomatic أكثر. يمكننا إنشاء نسخ من الأنواع الموجودة في المكتبة القياسية مثل String باستدعاء String::new، وكذلك يمكننا بتعديل الدالة parse_config إلى دالة new مرتبطة مع الهيكل Config استدعاء Config::new للحصول على نسخ من Config. توضح الشيفرة 7 التعديلات التي نحتاج لإجرائها. اسم الملف: src/main.rs fn main() { let args: Vec<String> = env::args().collect(); let config = Config::new(&args); // --snip-- } // --snip-- impl Config { fn new(args: &[String]) -> Config { let query = args[1].clone(); let file_path = args[2].clone(); Config { query, file_path } } } [الشيفرة 7: تغيير الدالة parse_config إلى Config::new] حدّثنا الدالة main التي استدعينا فيها parse_config سابقًا لتستدعي Config::new بدلًا من ذلك، وعدّلنا اسم الدالة parse_config إلى new ونقلناها لتصبح داخل كتلة impl، مما يربط الدالة new مع الهيكل Config. جرّب تصريف الشيفرة البرمجية مجددًا لتتأكد من أنها تعمل دون مشاكل. تحسين التعامل مع الأخطاء سنعمل الآن على تصحيح التعامل مع الأخطاء. تذكّر أن محاولتنا للوصول إلى القيم الموجودة في الشعاع args في الدليل 1 أو الدليل 2 تسببت بهلع البرنامج في حال احتواء الشعاع أقل من 3 عناصر. جرّب تنفيذ البرنامج دون أي وسطاء، من المفترض أن تحصل على رسالة الخطأ التالية: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep` thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace يمثل السطر index out of bounds: the len is 1 but the index is 1 رسالة خطأ موجهة للمبرمجين، ولن تساعد مستخدم البرنامج لفهم الخطأ. دعنا نصلح ذلك الآن. تحسين رسالة الخطأ سنُضيف في الشيفرة 8 اختبارًا في الدالة new يتأكد من أن الشريحة طويلة كفاية قبل محاولة الوصول إلى الدليل 1 و2، وإذا لم كانت الشريحة طويلة كفاية، سيهلع البرنامج وسيعرض رسالة خطأ أفضل من الرسالة التي رأيناها سابقًا. اسم الملف: src/main.rs // --snip-- fn new(args: &[String]) -> Config { if args.len() < 3 { panic!("not enough arguments"); } // --snip-- [الشيفرة 8: إضافة اختبار للتحقق من عدد الوسطاء] الشيفرة البرمجية مشابهة لما فعلناه في مقال سابق عند كتابة الدالة Guess::new، إذ استدعينا الماكرو panic!‎ عندما كان الوسيط value خارج مجال القيم الصالحة، إلا أننا نفحص طول args إذا كان على الأقل بطول 3 بدلًا من فحص مجال القيم، وتتابع بقية الدالة فيما بعد عملها بافتراض أن الشرط محقق. إذا احتوى الشعاع args على أقل من ثلاثة عناصر، سيتحقق الشرط الذي يستدعي الماكرو panic!‎ وبالتالي سيتوقف البرنامج مباشرةً. دعنا نجرّب تنفيذ البرنامج بعد إضافتنا للسطور البرمجية القليلة هذه في دالة new دون أي وسطاء ونرى رسالة الخطأ: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep` thread 'main' panicked at 'not enough arguments', src/main.rs:26:13 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace الخرج هذا أفضل لأنه يدلّنا على الخطأ بوضوح أكبر، إلا أننا نحصل على معلومات زائدة لسنا بحاجة لعرضها للمستخدم في ذات الوقت. لعلّ هذه الطريقة ليست بالطريقة المثلى؛ إذ أن استدعاء panic!‎ ملائم لعرض الخطأ في مرحلة كتابة الشيفرة البرمجية للمبرمج وليس في مرحلة استخدام البرنامج للمستخدم (كما ناقشنا في مقالات سابقة)، نستخدم بدلًا من ذلك طريقة أخرى تعلمناها سابقًا ألا وهي إعادة النوع Result الذي يمثّل قيمة نجاح أو فشل. إعادة النوع Result بدلا من استدعاء panic!‎ يمكننا إعادة قيمة Result التي تحتوي على نسخة من Config في حال النجاح وقيمة تصف الخطأ في حالة الفشل. سنغيّر أيضًا اسم الدالة new إلى build، إذ سيتوقع العديد من المبرمجين أن الدالة new لن تفشل أبدًا. يمكننا استخدام النوع Result للإشارة إلى مشكلة عندما تتواصل الدالة Config::build مع الدالة main، ومن ثم يمكننا التعديل على main بحيث تحوّل المتغاير‏ Err إلى خطأ أوضح للمستخدم دون النص الذي يتضمن thread 'main'‎ و RUST_BACKTRACE وهو ما ينتج عن استدعاء panic!‎. توضح الشيفرة 9 التغييرات التي أجريناها لقيمة الدالة المُعادة -التي تحمل الاسم Config::build الآن- ومتن الدالة الذي يُعيد قيمة Result. لاحظ أن هذه الشيفرة لن تُصرَّف حتى نعدل الدالة main أيضًا، وهو ما سنفعله في الشيفرة التي تليها. اسم الملف: src/main.rs impl Config { fn build(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let file_path = args[2].clone(); Ok(Config { query, file_path }) } } [الشيفرة 9: إعادة قيمة Result من الدالة Config::build] تُعيد الدالة build قيمة Result بنسخة Config في حال النجاح و ‎&'static str في حال الفشل، وستكون قيم الأخطاء سلاسل نصية مجرّدة string literals دومًا بدورة حياة ‎'static. أجرينا تعديلين على محتوى الدالة، فبدلًا من استدعاء panic!‎ عندما لا يمرّر المستخدم عددًا كافيًا من الوسطاء، أصبحنا نُعيد قيمة Err، وغلّفنا قيمة Config المُعادة بمتغاير Ok. تجعل هذه التغييرات من الدالة متوافقة مع نوع بصمتها الجديد. تسمح إعادة القيمة Err من Config::build إلى الدالة main بالتعامل مع القيمة Result المُعادة من الدالة build ومغادرة البرنامج بصورةٍ أفضل في حالة الفشل. استدعاء الدالة Config::build والتعامل مع الأخطاء نحتاج لتعديل الدالة main بحيث تتعامل مع النوع Result المُعاد إليها من الدالة Config::build كي نتعامل مع الأخطاء عند حدوثها وطباعة رسالة مفهومة للمستخدم، وهذا التعديل موضّح في الشيفرة 10. كما أننا سنعدّل البرنامج بحيث نخرج من أداة سطر الأوامر برمز خطأ غير صفري عند استدعاء panic!‎ إلا أننا سنطبق ذلك يدويًا. رمز الخطأ غير الصفري nonzero exit code هو اصطلاح للإشارة إلى العملية التي استدعت البرنامج الذي تسبب بالخروج من برنامجنا برمز الخطأ. اسم الملف: src/main.rs use std::process; fn main() { let args: Vec<String> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { println!("Problem parsing arguments: {err}"); process::exit(1); }); // --snip-- [الشيفرة 10: الخروج برمز خطأ إذا فشل بناء Config] استخدمنا في الشيفرة السابقة تابعًا لم نشرحه بالتفصيل بعد، ألا وهو unwrap_or_else وهو تابع معرَّف في Result<T, E>‎ في المكتبة القياسية. يسمح لنا استخدام التابع unwrap_or_else بتعريف بعض طرق التعامل مع الأخطاء المخصصة التي لا تستخدم الماكرو panic!‎. سلوك التابع مماثل للتابع unwrap إذا كانت قيمة Result هي Ok، إذ أنه يعيد القيمة المغلّفة داخل Ok، إلا أن التابع يستدعي شيفرةً برمجيةً في مغلِّفه closure إذا كانت القيمة Err وهي دالة مجهولة anonymous function نعرّفها ونمرّرها بمثابة وسيط للتابع unwrap_or_else. سنتحدث عن المُغلِّفات بالتفصيل لاحقًا، وكل ما عليك معرفته حاليًا هو أن unwrap_or_else ستمرّر القيمة الداخلية للقيمة Err -وهي في هذه الحالة السلسلة النصية "not enough arguments" التي أضفناها في الشيفرة 9- إلى مغلّفنا ضمن الوسيط err الموجود بين الخطين الشاقوليين (|)، ومن ثم تستطيع الشيفرة البرمجية الموجودة في المغلّف استخدام القيمة الموجودة في err عند تنفيذها. أضفنا سطرuse جديد لإضافة process من المكتبة القياسية إلى النطاق. تُنفَّذ الشيفرة البرمجية الموجودة في المغلّف في حالة الخطأ ضمن سطرين فقط: نطبع قيمة err ومن ثم نستدعي process::exit. توقف الدالة process::exit البرنامج مباشرةً وتُعيد العدد المُمرّر إليها بمثابة رمز حالة خروج. تشابه العملية استخدام panic!‎ في الشيفرة 8، إلا أننا لا نحصل على الخرج الإضافي بعد الآن. دعنا نجرّب الأمر: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.48s Running `target/debug/minigrep` Problem parsing arguments: not enough arguments عظيم، فالرسالة التي نحصل عليها الآن مفهومةً أكثر للمستخدمين. استخراج المنطق من الدالة main الآن وبعد انتهائنا من إعادة بناء التعليمات البرمجية الخاصة بالحصول على الوسطاء، دعنا ننتقل إلى منطق البرنامج، إذ علينا أن نستخرج المنطق إلى دالةٍ نسميها run كما ذكرنا سابقًا، بحيث تحتوي على الشيفرة البرمجية الموجودة في main حاليًا مع استثناء الشيفرة البرمجية المخصصة لضبط البرنامج، أو التعامل مع الأخطاء، وبحلول نهاية المهمة هذه يجب أن تكون الدالة main سهلة الفحص والقراءة ومختصرة، وسنكون قادرين على كتابة الاختبارات لجزء المنطق من البرنامج بصورةٍ منفصلة. توضح الشيفرة 11 الدالة run المُستخلصة، وسنبدأ بإجراء تحسينات صغيرة وتدريجية حاليًا لاستخلاص المنطق إلى الدالة. نبدأ بتعريف الدالة في src/main.rs. اسم الملف: src/main.rs fn main() { // --snip-- println!("Searching for {}", config.query); println!("In file {}", config.file_path); run(config); } fn run(config: Config) { let contents = fs::read_to_string(config.file_path) .expect("Should have been able to read the file"); println!("With text:\n{contents}"); } // --snip-- [الشيفرة 11: استخراج الدالة run التي تحتوي على بقية منطق البرنامج] تحتوي الدالة run الآن على جميع المنطق المتبقي في main بدءًا من قراءة الملف، وتأخذ الدالة run نسخةً من الهيكل Config مثل وسيط. إعادة الأخطاء من الدالة run أصبح بإمكاننا -بعد فصل منطق البرنامج في الدالة run- تحسين التعامل مع الأخطاء كما فعلنا بالدالة Config::build في الشيفرة 9. تُعيد الدالة run القيمة Result<T, E>‎ بعد حصول خطأ بدلًا من السماح للبرنامج بالهلع باستدعاء expect، وسيسمح لنا ذلك بدعم المنطق الخاص بالتعامل مع الأخطاء في main بطريقة سهلة الاستخدام. توضح الشيفرة 12 التغييرات الواجب إجرائها ومحتوى الدالة run. اسم الدالة: src/main.rs use std::error::Error; // --snip-- fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; println!("With text:\n{contents}"); Ok(()) } [الشيفرة 12: تعديل الدالة run بحيث تعيد القيمة Result] أجرينا ثلاثة تعديلات هنا؛ إذ عدّلنا أولًا القيمة المُعادة من الدالة run إلى النوع Result<(), Box<dyn Error>>‎، فقد أعادت هذه الدالة سابقًا نوع الوحدة unit type ()، إلا أننا نُبقي هذا النوع مثل قيمة مُعادة في حالة Ok. استخدمنا كائن السمة Box<dyn Error>‎ لنوع الخطأ (وقد أضفنا std::error::Error إلى النطاق باستخدام التعليمة use في الأعلى). سنغطّي كائنات السمة لاحقًا، ويكفي الآن معرفتك أن Box<dyn Error>‎ تعني أن الدالة ستُعيد نوعًا يطبّق السمة Error، إلا أن تحديد نوع القيمة المُعادة ليس ضروريًا. يمنحنا ذلك مرونة إعادة قيم الخطأ التي قد تكون من أنواع مختلفة في حالات فشل مختلفة، والكلمة المفتاحية dyn هي اختصار للكلمة "ديناميكي dynamic". يتمثّل التعديل الثاني بإزالة استدعاء expect واستبداله بالعامل ?، الذي شرحناه سابقًا هنا؛ فبدلًا من استخدام الماكرو panic!‎ على الخطأ، يُعيد العامل ? قيمة الخطأ من الدالة الحالية للشيفرة البرمجية المستدعية لكي تتعامل معه. ثالثًا، تُعيد الدالة run الآن قيمة Ok في حالة النجاح. صرّحنا عن نوع نجاح الدالة run في بصمتها على النحو التالي: ()، مما يعني أننا بحاجة تغليف قيمة نوع الوحدة في قيمة Ok. قد تبدو طريقة الكتابة Ok(())‎ هذه غريبة قليلًا، إلا أن استخدام () بهذا الشكل هو طريقة اصطلاحية للإشارة إلى أننا نستدعي run من أجل تأثيرها الجانبي فقط، إذ أنها لا تُعيد قيمةً نستخدمها. ستُصرَّف الشيفرة البرمجية السابقة عند تنفيذها، إلا أننا سنحصل على التحذير التالي: $ cargo run the poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) warning: unused `Result` that must be used --> src/main.rs:19:5 | 19 | run(config); | ^^^^^^^^^^^^ | = note: `#[warn(unused_must_use)]` on by default = note: this `Result` may be an `Err` variant, which should be handled warning: `minigrep` (bin "minigrep") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.71s Running `target/debug/minigrep the poem.txt` Searching for the In file poem.txt With text: I'm nobody! Who are you? Are you nobody, too? Then there's a pair of us - don't tell! They'd banish us, you know. How dreary to be somebody! How public, like a frog To tell your name the livelong day To an admiring bog! تخبرنا رست أن شيفرتنا البرمجية تتجاهل قيمة Result، وأن قيمة Result قد تشير إلى حصول خطأ ما، إلا أننا لا نتحقق من حدوث خطأ، ويذكّرنا المصرّف بأنه من الأفضل لنا كتابة شيفرة برمجية للتعامل مع الأخطاء هنا. دعنا نصحّح هذه المشكلة الآن. التعامل مع الأخطاء المُعادة من الدالة run في الدالة main سنتحقق من الأحطاء ونتعامل معها باستخدام طرق مماثلة للطرق التي استخدمناها مع Config::build في الشيفرة 10، مع اختلاف بسيط: اسم الملف: src/main.rs fn main() { // --snip-- println!("Searching for {}", config.query); println!("In file {}", config.file_path); if let Err(e) = run(config) { println!("Application error: {e}"); process::exit(1); } } نستخدم if let بدلًا من unwrap_or_else للتحقق فيما إذا كانت الدالة run تُعيد قيمة Err ونستدعيprocess::exit(1)‎ إذا كانت هذه الحالة محققة. لا تُعيد الدالة run قيمة نحتاج لفك التغليف عنها unwrap باستخدام unwrap بالطريقة ذاتها التي تُعيد فيها الدالة Config::build نسخةً من Config. نهتم فقط بحالة حصول خطأ والتعرف عليه لأن run تُعيد () في حالة النجاح، لذا لا نحتاج من unwrap_or_else أن تُعيد القيمة المفكوك تغليفها، والتي هي ببساطة (). محتوى تعليمات if let ودوال unwrap_or_else مماثلة في الحالتين: إذ نطبع الخطأ، ثم نغادر البرنامج. تجزئة الشيفرة البرمجية إلى وحدة مكتبة مصرفة يبدو مشروع minigrep جيّدًا حتى اللحظة. سنجزّء الآن ملف src/main.rs ونضع جزءًا من الشيفرة البرمجية في ملف "src/lib.rs"، إذ يمكننا بهذه الطريقة اختبار الشيفرة البرمجية مع ملف src/main.rs لا ينجز العديد من المهام. دعنا ننقل الشيفرة البرمجية غير الموجودة في الدالة main من الملف src/main.rs إلى الملف src/lib.rs، والتي تتضمن: تعريف الدالة run. تعليمات use المرتبطة بالشيفرة البرمجية التي سننقلها. تعريف الهيكل Config. تعريف دالة Config::build. يجب أن يحتوي الملف src/lib.rs على البصمات الموضحة في الشيفرة 13 (أهملنا محتوى الدوال لاختصار طول الشيفرة). لاحظ أن الشيفرة البرمجية لا تُصرَّف حتى نعدّل الملف src/main.rs وهو ما نفعله في الشيفرة 14. اسم الملف: src/lib.rs use std::error::Error; use std::fs; pub struct Config { pub query: String, pub file_path: String, } impl Config { pub fn build(args: &[String]) -> Result<Config, &'static str> { // --snip-- } } pub fn run(config: Config) -> Result<(), Box<dyn Error>> { // --snip-- } [الشيفرة 13: نقل Config و run إلى src/lib.rs] استخدمنا الكلمة المفتاحية pub هنا بحرية في كل من الهيكل Config وحقوله وتابعه build وعلى الدالة run. أصبح لدينا وحدة مكتبة مصرَّفة library crate بواجهة برمجية عامة public API يمكننا استخدامها. نحتاج إضافة الشيفرة البرمجية التي نقلناها إلى الملف src/lib.rs إلى نطاق الوحدة الثنائية المصرفة binary crate في الملف src/main.rs كما هو موضح في الشيفرة 14: اسم الملف: src/main.rs use std::env; use std::process; use minigrep::Config; fn main() { // --snip-- if let Err(e) = minigrep::run(config) { // --snip-- } } [الشيفرة 14: استخدام وحدة المكتبة المصرفة minigrep في src/main.rs] أضفنا السطر use minigrep::Config لإضافة النوع Config من وحدة المكتبة المصرّفة إلى نطاق الوحدة الثنائية المصرّفة، وأسبقنا prefix الدالة run باسم الوحدة المصرفة crate. يجب أن تكون وظائف البرنامج مترابطة مع بعضها بعضًا الآن، وأن تعمل بنجاح، لذا نفّذ البرنامج باستخدام cargo run وتأكد من أن كل شيء يعمل كما هو مطلوب. أخيرًا، كان هذا عملًا شاقًا، إلا أننا بدأنا بأساس يضمن لنا النجاح في المستقبل، إذ أصبح التعامل مع الأخطاء الآن سهلًا وقد جعلنا من شيفرتنا البرمجية أكثر معيارية. سنعمل في الملف src/lib.rs بصورةٍ أساسية من الآن وصاعدًا. دعنا نستفيد من المعيارية الجديدة في برنامجنا بتحقيق شيءٍ كان من الممكن أن يكون صعب التحقيق في الشيفرة البرمجية القادمة، إلا أنه أصبح أسهل بالشيفرة البرمجية الجديدة، ألا وهو كتابة الاختبارات. ترجمة -وبتصرف- لقسم من الفصل An I/O Project: Building a Command Line Program من كتاب The Rust Programming Language. اقرأ أيضًا المقال السابق: كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج استخدام الهياكل structs لتنظيم البيانات في لغة رست Rust حل المشكلات وأهميتها في احتراف البرمجة
×
×
  • أضف...