
لماذا ينبغي عليك تعلم لغة البرمجة Rust
يحب المطوّرون تطوير التطبيقات باستخدام لغة Rust، فما الذي يجعل Rust مميزة إلى هذا الحد؟ يستعرض هذا المقال أبرز مزايا هذه اللغة الشبيهة بلغة C ويوضح لماذا يجب أن تكون Rust ضمن قائمة اللغات التالية التي تتعلمها.
Table Of Content
- تاريخ لغة Rust
- مفاهيم أساسية في Rust
- إعادة استخدام الكود عبر الوحدات (Modules)
- فحوصات الأمان لكود أنظف
- معالجة الأخطاء بشكل أفضل
- أخطاء لا يمكن استردادها
- الأخطاء القابلة للاسترداد
- دعم التزامن والخيوط (Concurrency and Threads)
- دعم أنواع البيانات المعقدة (المجموعات)
- تثبيت لغة Rust وأدواتها
- اعتبارات Windows
تاريخ لغة Rust
لنبدأ أولًا بنبذة تاريخية سريعة. تعتبر Rust لغة حديثة نسبيًا مقارنة بسابقاتها (وأهمها لغة C التي سبقتها بـ38 عامًا)، لكن أصولها البرمجية هي ما يجعلها متعددة الأنماط. تُصنف Rust ضمن اللغات الشبيهة بـ C، لكن الميزات الإضافية التي توفرها تمنحها مزايا على اللغات الأقدم.
Rust متأثرة بشكل كبير بلغة Cyclone (وهي لهجة آمنة من لغة C وتنتمي للبرمجة الأمرية)، مع بعض الجوانب من البرمجة الكائنية من لغة ++C. كما أنها تضم خصائص وظيفية (functional) مأخوذة من لغات مثل Haskell وOCaml. والنتيجة هي لغة شبيهة بـ C تدعم البرمجة متعددة الأنماط (الأمرية، والوظيفية، والكائنية).

مفاهيم أساسية في Rust
تضم لغة Rust العديد من الميزات التي تجعلها مفيدة، لكن احتياجات المطورين تختلف. أستعرض هنا خمسة من أهم المفاهيم التي تجعل من Rust لغة تستحق التعلم، وأعرض هذه الأفكار عبر أمثلة من كود Rust.
للبدء، لنلقِ نظرة على برنامج “Hello World” الكلاسيكي الذي يطبع الرسالة للمستخدم:
fn main()
{
println!("Hello World.");
}
يعرّف هذا البرنامج البسيط، على غرار لغة C، دالة رئيسية main تعد نقطة البداية المعيّنة لأي برنامج (وكل برنامج يجب أن يحتوي عليها). تُعرَّف الدالة بالكلمة المفتاحية fn، تليها مجموعة اختيارية من المعاملات بين قوسين (). تحدد الأقواس المعقوفة ({}) جسم الدالة؛ في هذا المثال تتكوّن الدالة من استدعاء للماكرو println! الذي يطبع نصًا منسقًا على الشاشة (stdout) حسب ما يتم تمريره كسلسلة نصية.
تتضمن Rust مجموعة متنوعة من الميزات التي تجعل تعلمها استثمارًا مجديًا. ستجد مفاهيم مثل الوحدات (modules) لإعادة الاستخدام، أمان الذاكرة وضماناته (العمليات الآمنة مقابل غير الآمنة)، ميزات معالجة الأخطاء (التي لا يمكن استردادها وتلك التي يمكن استردادها)، دعم التزامن، وأنواع بيانات معقدة (تُسمى collections).
إعادة استخدام الكود عبر الوحدات (Modules)
تتيح لك لغة Rust تنظيم الكود بطريقة تشجّع على إعادة استخدامه. يتم هذا التنظيم من خلال الوحدات (modules)، والتي يمكن أن تحتوي على دوال وهياكل ووحدات أخرى، ويمكنك جعلها عامة (public) بحيث يراها مستخدمو الوحدة، أو خاصة (private) بحيث تُستخدم داخل الوحدة فقط ولا يمكن لمستخدمي الوحدة الوصول إليها مباشرة. تعمل الوحدة كحزمة من الكود يمكن للآخرين استخدامها.
تستخدم ثلاث كلمات مفتاحية لإنشاء الوحدات، واستخدامها، وتعديل ظهور العناصر فيها:
- الكلمة المفتاحية mod لإنشاء وحدة جديدة.
- الكلمة المفتاحية use لاستخدام الوحدة (أي إظهار التعريفات ضمن النطاق لاستخدامها).
- الكلمة المفتاحية pub لجعل عناصر الوحدة عامة (وإلا فهي خاصة افتراضيًا).
الكود التالي مثال بسيط على استخدام الوحدات. يبدأ بإنشاء وحدة جديدة باسم bits تحتوي على ثلاث دوال. الدالة الأولى pos هي دالة خاصة تأخذ متغيرًا من نوع u32 وتعيد u32 (كما هو مبيّن بالسهم ->)، وهي ببساطة تحسب 1 منزاحة لليسار بعدد البتات المحدد. لا حاجة لاستخدام كلمة return هنا. تستدعي هذه القيمة دالتان عامتان (لاحظ الكلمة المفتاحية pub): decimal و hex. تستدعي هاتان الدالتان الدالة الخاصة pos وتطبَع قيمة البت بالصيغة العشرية أو الست عشرية (لاحظ استخدام 😡 للدلالة على النظام الست عشري). في النهاية، هناك دالة main تستدعي الدالتين العامتين من وحدة bits، مع عرض ناتج التنفيذ كتعليقات في نهاية الكود.
mod bits {
fn pos(bit: u32) -> u32 {
1 << bit
}
pub fn decimal(bit: u32) {
println!("Bits decimal {}", pos(bit));
}
pub fn hex(bit: u32) {
println!("Bits decimal 0x{:x}", pos(bit));
}
}
fn main( ) {
bits::decimal(8);
bits::hex(8);
}
// Bits decimal 256
// Bits decimal 0x100
تمكّنك الوحدات من تجميع الوظائف إما بشكل عام أو خاص، ويمكنك أيضًا ربط الدوال بالكائنات باستخدام الكلمة المفتاحية impl.
فحوصات الأمان لكود أنظف
يفرض مترجم Rust ضمانات أمان الذاكرة وفحوصات أخرى تجعل اللغة آمنة (على عكس لغة C التي يمكن أن تكون غير آمنة). في Rust، لن تقلق أبدًا بشأن المؤشرات المعلقة أو استخدام كائن بعد تحريره. هذه أمور متجذرة في جوهر اللغة. ومع ذلك، في مجالات مثل تطوير الأنظمة المدمجة، من المهم أحيانًا تنفيذ أمور مثل وضع بنية بيانات في عنوان معين يمثل مجموعة من مسجلات العتاد.
تتضمن Rust الكلمة المفتاحية unsafe والتي تتيح لك تعطيل الفحوصات التي عادة ما تؤدي إلى خطأ في الترجمة. في المثال التالي، تتيح الكلمة unsafe إعلان كتلة كود غير آمنة. في هذا المثال، أعرّف متغيرًا غير قابل للتغيير باسم a، ثم مؤشرًا لهذا المتغير يسمى rawp. بعد ذلك، ولتحويل rawp إلى القيمة الفعلية (وفي هذه الحالة ستطبع 1 على الشاشة)، أستخدم الكلمة unsafe للسماح بهذه العملية التي كانت ستمنع أثناء الترجمة.
fn main() {
let a = 1;
let rawp = &a as *const i32;
unsafe {
println!("rawp is {}", *rawp);
}
}
يمكنك تطبيق الكلمة unsafe على الدوال بالكامل أو على أجزاء من الكود ضمن دالة. وتُستخدم غالبًا عند كتابة روابط لدوال غير مكتوبة بلغة Rust. هذه الميزة تجعل Rust مفيدة في تطوير أنظمة التشغيل أو البرمجة المدمجة (Bare-metal).
معالجة الأخطاء بشكل أفضل
الأخطاء تحدث مهما كانت لغة البرمجة المستخدمة. في Rust، تنقسم الأخطاء إلى نوعين: أخطاء لا يمكن استردادها (النوع السيء) وأخطاء يمكن استردادها (الأقل سوءًا).
أخطاء لا يمكن استردادها
دالة panic! في Rust مشابهة للماكرو assert في C. فهي تطبع رسالة للمساعدة في تصحيح الأخطاء (وتوقف التنفيذ قبل حدوث مشاكل كارثية أكبر). المثال التالي يوضح استخدام panic! مع عرض ناتج التنفيذ كتعليقات.
fn main() {
panic!("Bad things happening.");
}
// thread 'main' panicked at 'Bad things happening.', panic.rs:2:4
// note: Run with RUST_BACKTRACE=1 for a backtrace.
من خلال الناتج، ترى أن بيئة تشغيل Rust تحدد بدقة مكان وقوع المشكلة (السطر 2) وتعرض الرسالة التي أُعطيت (والتي يمكن أن تحمل معلومات أوضح). كما هو موضح في الرسالة، يمكنك توليد تتبع تكدسي (stack backtrace) بتشغيل البرنامج مع متغير بيئة خاص باسم RUST_BACKTRACE. يمكنك أيضًا استدعاء panic! داخليًا استنادًا إلى أخطاء يمكن كشفها (مثل محاولة الوصول إلى فهرس غير صحيح في متجه بيانات). Ask ChatGPT
الأخطاء القابلة للاسترداد
التعامل مع الأخطاء القابلة للاسترداد جزء أساسي في البرمجة، وتوفّر Rust ميزة رائعة لفحص الأخطاء. لنأخذ هذا المثال في سياق التعامل مع الملفات. تعيد الدالة File::open
نوعًا يُسمى Result، حيث يمثل T وE معلمات نوعية عامة (في هذا السياق، يمثلان std::fs::File
و std::io::Error)
. إذًا، عند استدعاء File::open
، إذا لم يحدث خطأ (أي كانت النتيجة Ok)، فسيكون T هو نوع الإرجاع (std::fs::File
). أما إذا حدث خطأ، فسيكون E هو نوع الخطأ الذي حدث (باستخدام النوع std::io::Error
). (لاحظ أن المتغير f يستخدم شرطة سفلية () لتجنب تحذير المتغير غير المستخدم من المترجم).
أستخدم بعد ذلك ميزة خاصة في Rust تُدعى match، وهي شبيهة بتعليمة switch في لغة C لكنها أكثر قوة. في هذا السياق، أستخدم match لمقارنة _f بالقيم المحتملة (Ok وErr). في حالة Ok أعيد الملف للتخصيص، وفي حالة Err أستخدم panic!.
use std::fs::File;
fn main() {
let _f = File::open("file.txt");
let _f = match _f {
Ok(file) => file,
Err(why) => panic!("Error opening the file {:?}", why),
};
}
// thread 'main' panicked at 'Error opening the file Error { repr: Os
// { code: 2, message: "No such file or directory" } }', recover.rs:8:23
// note: Run with RUST_BACKTRACE=1 for a backtrace.
يتم تبسيط التعامل مع الأخطاء القابلة للاسترداد في Rust باستخدام المعدّد Result، ويزداد التبسيط عند استخدام match. لاحظ أيضًا في هذا المثال عدم وجود دالة File::close:
حيث يتم إغلاق الملف تلقائيًا عند نهاية نطاق المتغير _f.
دعم التزامن والخيوط (Concurrency and Threads)
عادة ما يأتي التزامن بمشاكل مثل سباقات البيانات والتوقفات المتبادلة (deadlocks). توفر Rust وسائل لإنشاء خيوط (threads) باستخدام نظام التشغيل الأصلي، لكنها تحاول أيضًا الحد من سلبيات التزامن. تتيح Rust تمرير الرسائل بين الخيوط للتواصل (عبر send و recv وكذلك الإقفال عبر mutexes). كما تسمح لك Rust بإعارة قيمة thread ما، بحيث تمنحها الملكية وتنقل نطاقها وملكيتها إلى خيط جديد. وهكذا تضمن Rust أمان الذاكرة مع التزامن، دون سباقات بيانات.
إليك مثالًا بسيطًا لإنشاء خيوط في Rust مع بعض عناصر المعالجة على المتجهات (vectors)، واستخدام بعض المفاهيم السابقة مثل match. يبدأ الكود باستيراد مساحة الأسماء thread وDuration. ثم أُعرّف دالة جديدة my_thread، تمثل الخيط الذي سننشئه لاحقًا. في هذا الخيط، نطبع فقط معرف الخيط، ثم نجعله ينام لفترة قصيرة للسماح للجدولة بتشغيل خيط آخر.
الدالة الرئيسية main هي جوهر المثال. أبدأ بإنشاء متجه فارغ وقابل للتعديل لتخزين القيم من نفس النوع. ثم أنشئ 10 خيوط باستخدام دالة spawn وأضيف نتيجة كل عملية إلى المتجه (سنشرح ذلك لاحقًا). هذا المثال منفصل عن الخيط الحالي، بحيث يمكن للخيط الجديد الاستمرار حتى بعد خروج الخيط الأب. بعد طباعة رسالة من الخيط الرئيسي، أدور عبر المتجه من النوع JoinHandle وأنتظر انتهاء كل خيط فرعي. لكل JoinHandle في المتجه، أستدعي الدالة join التي تنتظر خروج الخيط قبل المتابعة. إذا أعادت الدالة join خطأ، أعرض ذلك الخطأ باستخدام match.
use std::thread;
use std::time::Duration;
fn my_thread() {
println!("Thread {:?} is running", std::thread::current().id());
thread::sleep(Duration::from_millis(1));
}
fn main() {
let mut v = vec![];
for _i in 1..10 {
v.push(thread::spawn(|| { my_thread(); }));
}
println!("main() waiting.");
for child in v {
match child.join() {
Ok(_) => (),
Err(why) => println!("Join failure {:?}", why),
};
}
}
عند التنفيذ، ستلاحظ أن الخيط الرئيسي استمر في العمل حتى بدأ الانضمام (join). ثم نفذت الخيوط الفرعية وخرجت في أوقات مختلفة، مما يبرز الطبيعة غير المتزامنة للخيوط.
دعم أنواع البيانات المعقدة (المجموعات)
تتضمن مكتبة Rust القياسية العديد من هياكل البيانات الشائعة والمفيدة، من بينها أربعة أنواع رئيسية: السلاسل (sequences)، والخرائط (maps)، والمجموعات (sets)، ونوع متفرّق.
بالنسبة للسلاسل، يمكنك استخدام نوع المتجه (Vec) الذي استخدمته في مثال الخيوط. يوفّر هذا النوع مصفوفة يمكن تغيير حجمها ديناميكيًا، وهو مفيد لتجميع البيانات لمعالجتها لاحقًا. بنية VecDeque مشابهة لـ Vec، لكن يمكنك إضافة العناصر من الطرفين. LinkedList مشابه أيضًا، لكنه يسمح بتقسيم ودمج القوائم.
أما الخرائط، فهناك هياكل HashMap و BTreeMap. تُستخدم HashMap لإنشاء أزواج مفتاح-قيمة، ويمكنك الرجوع للعناصر من خلال المفتاح. أما BTreeMap فمشابه لـ HashMap، لكنه يرتب المفاتيح ويمكنك بسهولة المرور على جميع الإدخالات.
وبالنسبة للمجموعات، هناك HashSet و BTreeSet (وهما يتبعان نفس ترتيب الخرائط). هذه الهياكل مفيدة عندما لا تحتاج لقيم، بل مجرد المفاتيح.
وأخيرًا، هناك النوع المتفرّق وهو حاليًا BinaryHeap، وهو بنية بيانات تُستخدم لتنفيذ طابور أولويات عبر الكومة الثنائية (binary heap).
تثبيت لغة Rust وأدواتها
إحدى أسهل طرق تثبيت Rust هي استخدام أمر curl مع سكربت التثبيت. فقط نفذ السطر التالي من سطر أوامر لينكس:
curl -sSf https://static.rust-lang.org/rustup.sh | sh
يقوم هذا السطر بنقل سكربت rustup من موقع rust-lang.org، ثم يمرر السكربت إلى الصدفة لتنفيذه. بعد انتهاء التثبيت، يمكنك تنفيذ الأمر rustc -v لعرض إصدار Rust الذي تم تثبيته. بعد تثبيت Rust، يمكنك صيانتها باستخدام أداة rustup، والتي يمكن استخدامها أيضًا لتحديث تثبيت Rust.
المُصرّف الخاص بلغة Rust يُدعى rustc. في الأمثلة هنا، يتم بناء البرنامج ببساطة بهذا الأمر: rustc threads.rs
حيث يقوم مصرّف rust بإنشاء ملف تنفيذي أصلي باسم threads. يمكنك تصحيح برامج Rust باستخدام أي من rust-lldb أو rust-gdb.
ربما لاحظت أن برامج Rust التي عُرضت هنا لها أسلوب خاص في كتابة الكود. يمكنك تعلم هذا الأسلوب من خلال أداة تنسيق الكود التلقائي rustfmt. هذه الأداة، عند تنفيذها على اسم ملف مصدر، ستعيد ترتيب وتنسيق الكود تلقائيًا وفق أسلوب موحد ومعتمد.
وأخيرًا، رغم أن Rust صارمة فيما تقبله من الكود، يمكنك استخدام برنامج rust-clippy لتحليل الكود بشكل أعمق واكتشاف نقاط الممارسة السيئة. فكر في rust-clippy كأنه أداة lint في لغة C.
اعتبارات Windows
في نظام ويندوز، تتطلب Rust أيضًا وجود أدوات بناء ++C الخاصة ببرنامج Visual Studio 2013 أو أحدث. أسهل طريقة للحصول على هذه الأدوات هي تثبيت Microsoft Visual C++ Build Tools 2017، والذي يوفر فقط أدوات بناء ++C. أو بدلاً من ذلك، يمكنك تثبيت Visual Studio 2017 أو 2015 أو 2013، وخلال التثبيت اختر أدوات ++C.
في منتصف فبراير 2018، أصدرت فريق Rust الإصدار 1.24. يتضمن هذا الإصدار الترجمة التزايدية (incremental compilation)، وتنسيق الكود التلقائي بواسطة rustfmt، وتحسينات جديدة، وتثبيت مكتبات. يمكنك معرفة المزيد عن Rust وتطورها من خلال مدونة Rust، وتحميلها من موقع لغة Rust. هناك ستجد الكثير من الميزات الأخرى مثل مطابقة الأنماط (pattern matching)، والعدادات (iterators)، والوظائف المغلقة (closures)، والمؤشرات الذكية (smart pointers).