Generics في Rust: لماذا هي ضرورية وكيفية استخدامها عملياً

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

Generics في Rust: لماذا هي ضرورية وكيفية استخدامها عملياً



البرمجة العامة (Generics) هي إحدى الإمكانيات الرئيسية في Rust، والتي تتيح كتابة كود مرن وقابل لإعادة الاستخدام وآمن. في هذه المقالة، سنشرح لماذا نحتاج إلى Generics، وكيف تعمل تحت الغطاء، وكيفية تطبيقها في المشاريع الحقيقية. ستكون المادة مفيدة لكل من المبتدئين الذين يتعلمون Rust وللمطورين ذوي الخبرة الذين يرغبون في تنظيم معرفتهم.



لماذا نحتاج إلى Generics في Rust؟



تخيل أنك بحاجة إلى كتابة دالة تجد أكبر عنصر في مصفوفة. بدون Generics، سيتعين عليك إنشاء إصدارات منفصلة لكل نوع بيانات: i32، f64، String وهكذا. هذا يؤدي إلى تكرار الكود وتعقيد الصيانة.



تحل Generics هذه المشكلة من خلال السماح بتمييز الأنواع. بدلاً من كتابة:



fn find_max_i32(list: &[i32]) -> i32 { ... }fn find_max_f64(list: &[f64]) -> f64 { ... }


تكتب دالة واحدة عامة:



fn find_max<T: PartialOrd>(list: &[T]) -> &T { ... }


المزايا الرئيسية لـ Generics:



  • إعادة استخدام الكود — دالة أو هيكل واحد يعمل مع أنواع مختلفة
  • أمان الأنواع في وقت الترجمة — يتم اكتشاف جميع أخطاء الأنواع قبل تشغيل البرنامج
  • الأداء — يستخدم Rust التجسيد الأحادي (monomorphization)، مما ينشئ كوداً محسناً لكل نوع محدد
  • قابلية القراءة — تكرار أقل، يصبح الكود أكثر إيجازاً


كيف تعمل Generics: التجسيد الأحادي (Monomorphization)



يقوم Rust بترجمة الكود العام بطريقة خاصة — من خلال التجسيد الأحادي. وهذا يعني أن المترجم ينشئ نسخة منفصلة من الدالة أو الهيكل لكل نوع محدد يُستخدم في البرنامج.



مثال:



fn print_type<T>(val: T) {    println!("القيمة: {}", val);}

fn main() { print_type(42); // T = i32 print_type(3.14); // T = f64 print_type("مرحباً"); // T = &str}


بعد الترجمة، سينشئ Rust ثلاثة إصدارات من print_type:



  • print_type_i32
  • print_type_f64
  • print_type_str


وهذا يضمن أن الكود العام لا يقل أداءً عن الكود المتخصص. "العيب" الوحيد هو زيادة حجم الملف الثنائي، لكن هذا نادراً ما يمثل مشكلة عملياً.



استخدام Generics مع الدوال والهياكل



Generics في الدوال



أبسط طريقة لاستخدام Generics هي تمييز الدالة:



fn swap<T>(a: &mut T, b: &mut T) {    let temp = std::mem::replace(a, unsafe { std::mem::zeroed() });    *a = std::mem::replace(b, temp);    *b = temp;}

fn main() { let mut x = 10; let mut y = 20; swap(&mut x, &mut y); println!("x = {}, y = {}", x, y); // x = 20, y = 10}


لاحظ: نحن لا نحدد النوع — المترجم يستنتجه تلقائياً بناءً على الوسائط.



Generics في الهياكل



يمكن أن تكون الهياكل أيضاً عامة. وهذا مفيد بشكل خاص لحاويات البيانات:



struct Point<T> {    x: T,    y: T,}

impl<T> Point<T> { fn new(x: T, y: T) -> Self { Point { x, y } }

fn distance_from_origin(&self) -> f64 where T: Into<f64> + Copy, { let x: f64 = self.x.into(); let y: f64 = self.y.into(); (x * x + y * y).sqrt() }}

fn main() { let int_point = Point::new(3, 4); let float_point = Point::new(3.0, 4.0); // distance_from_origin تعمل فقط للأنواع التي تطبق Into<f6

المدونات

توصيات الكتب