Rust: الأنواع العامة في لغة Rust: البرمجة المعلمة بالأنواع
آخر تحديث: 2026-08-26
تُعرف «الأنواع العامة» بأنها «البرمجة المعلمة على مستوى الأنواع» — أي تمرير الأنواع كمعلمات بحيث يمكن تطبيق نفس الكود على أنواع متعددة دون الحاجة إلى إعادة كتابة الكود لكل نوع على حدة.
إذا كانت الدالة «تُجرد القيم لتصبح معلمات»، فإن الأنواع العامة «تُجرد الأنواع لتصبح معلمات» أيضًا. الأمر يشبه عندما تطلب طعامًا جاهزًا وتقول: «أريد حصة من الأرز»، دون تحديد ما إذا كنت تريده أرزًا مقليًّا أم أرزًا مع طبق آخر — فأنت تقرر ذلك بمجرد وصولك إلى المطعم.
1. ما ستتعلمه
- تعريف الدالة العامة
fn foo<T>(x: T)واستدعاؤها - كيفية استخدام البنية العامة
struct Point<T> - فهم متعمق للتعدادات العامة
Option<T>وResult<T, E> - كيفية كتابة دالة عامة
impl<T> - التوحيد الشكلي — آلية للتوسيع في مرحلة التحويل البرمجي
- استخدام معلمات أنواع متعددة
2. الرسوم التخطيطية المفاهيمية
يوضح الرسم التخطيطي التالي لـ «Mermaid» الآلية التي يتم من خلالها استبدال معلمة النوع العام T بنوع محدد خلال عملية التحويل إلى أحادي الشكل في وقت التحويل البرمجي:
graph LR
A["Generic Functions<br/>fn identity<T>(x: T) -> T"] --> B["Compile-Time Singletonization<br/>Monomorphization"]
B --> C["T → i32<br/>fn identity_i32(x: i32) -> i32"]
B --> D["T → String<br/>fn identity_string(x: String) -> String"]
B --> E["T → f64<br/>fn identity_f64(x: f64) -> f64"]
C --> F["Call identity(42)"]
D --> G["Call identity("hello".to_string())"]
E --> H["Call identity(3.14)"]
3. قصة حاوية متعددة الاستخدامات
(1) المشكلة: كتابة كود متكرر لكل نوع
تقوم «لونا» (Luna) بتطوير مجموعة أدوات، وتحتاج إلى دالة لـ«إيجاد القيمة القصوى».
في البداية، كتبت واحدة لكل نوع:
fn max_i32(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
fn max_f64(a: f64, b: f64) -> f64 {
if a > b { a } else { b }
}
fn max_str(a: &str, b: &str) -> &str {
if a > b { a } else { b }
}
fn main() {
println!("{}", max_i32(3, 7)); // 7
println!("{}", max_f64(2.5, 1.8)); // 2.5
println!("{}", max_str("apple", "banana")); // banana
}
وبصرف النظر عن اختلاف أنواعها، فإن المنطق الذي تعمل به هذه الوظائف الثلاث هو نفسه تمامًا. ولو كان هناك أيضًا أنواع مثل u32 وu64 وchar وما إلى ذلك، لكان عليك نسخ الكود لكل نوع إضافي. وهذا ما يُعرف بـ«البرمجة بالنسخ واللصق» — وهي طريقة ليست أنيقة ولا يسهل صيانتها.
(2) نهج لغة «Rust» في التعامل مع العناصر العامة
fn max<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
fn main() {
println!("{}", max(3, 7)); // 7
println!("{}", max(2.5, 1.8)); // 2.5
println!("{}", max("apple", "banana")); // banana
}
تحل الدالة العامة محل ثلاث دوال لأنواع محددة — ويقوم المُجمِّع تلقائيًّا بإنشاء كود مُخصَّص لكل نوع يتم استخدامه فعليًّا (الأحادية الشكل). فأنت تكتب دالة واحدة، ويقوم المُجمِّع بتوسيعها إلى العديد من الدوال نيابةً عنك.
4. المفاهيم الأساسية
(1) النظام العام
graph TB
A[Generics Generics] --> B[Generic Functions]
A --> C[Generic Structures]
A --> D[Generic Enumerations]
A --> E[Generic Methods]
B --> F["fn identity<T>(x: T) -> T"]
C --> G["struct Point<T> { x: T, y: T }"]
D --> H["Option<T>, Result<T, E>"]
E --> I["impl<T> Point<T> { fn x(&self) -> &T }"]
A --> J[Singleton Monomorphization]
J --> K["At compile time:T → i32, f64, String ..."]
J --> L["Generate separate code for each type"]
(2) الأنواع العامة مقابل الأنواع المحددة مقابل التوزيع الديناميكي
| السمة | النوع المحدد (غير العام) | العام (أحادي الشكل) | التوزيع الديناميكي (السمة الديناميكية) |
|---|---|---|---|
| تكرار الكود | كتابة نسخة واحدة لكل نوع | التوسيع التلقائي بواسطة المُجمِّع | نسخة واحدة من الكود، يتم توزيعها في وقت التشغيل |
| الأداء | الأفضل | الأفضل (بدون عبء الوظائف الافتراضية) | مع عبء الوظائف الافتراضية |
| وقت التجميع | طويل (الكثير من التعليمات البرمجية المكتوبة يدويًّا) | طويل نسبيًّا (العديد من عمليات التوسيع) | قصير |
| حجم الملفات الثنائية | كبير | متوسط (نسخة واحدة من كل نوع) | صغير |
| المرونة | ضعيفة | تُحدد في وقت التحويل البرمجي | تُحدد في وقت التشغيل |
(3) التعدادات العامة الشائعة
| التعداد | التعريف | الغرض |
|---|---|---|
Option<T> |
enum Option<T> { Some(T), None } |
القيم التي قد تكون فارغة |
Result<T, E> |
enum Result<T, E> { Ok(T), Err(E) } |
العمليات التي قد تفشل |
Vec<T> |
struct Vec<T> { ... } |
المصفوفات الديناميكية |
HashMap<K, V> |
struct HashMap<K, V> { ... } |
التعيين بين المفتاح والقيمة |
(4) مقارنة بين طرق القيود العامة
| نوع القيد | الصيغة | حالات الاستخدام | مثال |
|---|---|---|---|
| القيود المضمنة | fn foo<T: Trait>(x: T) |
القيود الفردية البسيطة | fn max<T: PartialOrd>(a: T, b: T) |
| قيود متعددة + | fn foo<T: Trait1 + Trait2>(x: T) |
قيود متعددة | fn print<T: Display + Clone>(x: T) |
| جملة WHERE | fn foo<T>(x: T) where T: Trait |
القيود المعقدة، المعلمات متعددة الأنواع | where T: Display + Clone, U: Debug |
| سمة impl | fn foo(x: impl Trait) |
الترميز المختصر (تسهيلات لغوية) | fn plug(d: &impl USBDevice) |
5. أمثلة عامة
(1) ▶ المثال:دالة عامة — إيجاد القيمة القصوى في مصفوفة (مستوى الصعوبة ⭐)
// ============================================
// Generic Functions:Applies to any comparable type
// ============================================
// PartialOrd Constraint Assurance T Supports comparison operations
fn find_max<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
let mut max = &list[0];
for item in list.iter() {
if item > max {
max = item;
}
}
max
}
fn main() {
let numbers = vec![3, 7, 1, 9, 4];
println!("i32 Maximum value: {}", find_max(&numbers));
let floats = vec![2.5, 1.8, 3.14, 0.99];
println!("f64 Maximum value: {}", find_max(&floats));
let strings = vec!["apple", "banana", "cherry", "date"];
println!("&str Maximum value: {}", find_max(&strings));
// The Same Function,Three Types,Compiler Auto-Expansion
}
الناتج:
i32 Maximum value: 9
f64 Maximum value: 3.14
&str Maximum value: date
find_max<T>هي دالة عامة، وTهي معلمة نوع، و<T: std::cmp::PartialOrd>هي قيد سمة — بمعنى «يجب أن يكون T نوعًا قابلًا للمقارنة». وعند استدعاء الدالة، يستنتج المُترجم تلقائيًّاTبناءً على نوع الوسيطة الفعلي.
(2) ▶ المثال:بنية عامة — نظام إحداثيات النقاط (مستوى الصعوبة ⭐⭐)
// ============================================
// Generic Structures:Point Can store coordinates of any type
// ============================================
#[derive(Debug)]
struct Point<T> {
x: T,
y: T,
}
// Implementation Methods for Generic Structures
impl<T> Point<T> {
// Back x Citation
fn x(&self) -> &T {
&self.x
}
// Back y Citation
fn y(&self) -> &T {
&self.y
}
}
// For Point of a specific type f64, implement additional methods
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let int_point = Point { x: 5, y: 10 };
let float_point = Point { x: 3.0, y: 4.0 };
let string_point = Point {
x: "left",
y: "right",
};
println!("int_point: {:?}", int_point);
println!("float_point x: {}", float_point.x());
println!("string_point: {:?}", string_point);
// Only `Point<f64>` has the distance_from_origin method
println!("Distance from the origin: {:.2}", float_point.distance_from_origin());
// Compilation Error: int_point is Point<i32>, no such method
// println!("{}", int_point.distance_from_origin());
}
الناتج:
int_point: Point { x: 5, y: 10 }
float_point x: 3.0
string_point: Point { x: "left", y: "right" }
Distance from the origin: 5.00
Point<T>هي بنية عامة؛ أماxوyفهما من نفس النوع (كلاهما من نوعT). تُنفذimpl<T> Point<T>طرقًا مشتركة لجميع مثيلاتT. أماimpl Point<f64>فتُنفذ طرقًا محددة لأنواع معينة فقط — وهذه إحدى المزايا الرئيسية للأنواع العامة.
(3) ▶ المثال:التعدادات العامة — استخدام «Option» و«Result» في الممارسة العملية (مستوى الصعوبة ⭐⭐)
// ============================================
// Generic Enumerations: Option<T> and Result<T, E> Usage
// ============================================
// Custom Result Style Enumeration
#[derive(Debug)]
enum MyResult<T, E> {
Success(T),
Failure(E),
}
// Division Function:Back Result Style
fn safe_divide<T>(a: T, b: T) -> MyResult<T, String>
where
T: std::ops::Div<Output = T> + std::cmp::PartialEq + From<u8> + Copy,
{
if b == 0.into() {
MyResult::Failure("Division by zero".to_string())
} else {
MyResult::Success(a / b)
}
}
// Using the standard library Option<T>
fn find_in_vector<T: PartialEq>(vec: &[T], target: &T) -> Option<usize> {
for (i, item) in vec.iter().enumerate() {
if item == target {
return Some(i);
}
}
None
}
// Using the standard library Result<T, E>
fn parse_number(s: &str) -> Result<i32, String> {
s.parse::<i32>().map_err(|e| format!("Parse error: {}", e))
}
fn main() {
// Option Usage
let numbers = vec![10, 20, 30, 40, 50];
match find_in_vector(&numbers, &30) {
Some(index) => println!("Found 30,Index: {}", index),
None => println!("Not found"),
}
match find_in_vector(&numbers, &99) {
Some(index) => println!("Found 99,Index: {}", index),
None => println!("Not found 99"),
}
// Result Usage
match parse_number("42") {
Ok(n) => println!("Parsing Successful: {}", n),
Err(e) => println!("Parsing Failed: {}", e),
}
match parse_number("hello") {
Ok(n) => println!("Parsing Successful: {}", n),
Err(e) => println!("Parsing Failed: {}", e),
}
// Custom MyResult Usage
let result = safe_divide(10.0, 3.0);
println!("Custom Result: {:?}", result);
let result = safe_divide(10.0, 0.0);
println!("Custom Result: {:?}", result);
}
الناتج:
Found 30,Index: 2
Not found 99
Parsing Successful: 42
Parsing Failed: Parse error: invalid digit found in string
Custom Result: Success(3.3333333333333335)
Custom Result: Failure("Division by zero")
يحتوي
Option<T>على معلمة نوع واحدة فقطT(سواء كانت ذات قيمة أم لا)، في حين يحتويResult<T, E>على معلمتي نوع (نوع النجاح ونوع الخطأ). تسمح القوائم العامة بتطبيق هذه الأنواع على أي نوع بيانات — وهذا أحد المبادئ الأساسية لتصميم المكتبة القياسية لـ Rust.
(4) ▶ المثال:معلمات الأنواع المتعددة والتوحيد (مستوى الصعوبة ⭐⭐⭐)
// ============================================
// Multiple Type Parameters + Combining Generic Methods
// ============================================
use std::fmt::Display;
// Generic struct with two type parameters
#[derive(Debug)]
struct Pair<K, V> {
key: K,
value: V,
}
// Implement methods for Pair<K, V>
impl<K, V> Pair<K, V> {
fn new(key: K, value: V) -> Self {
Pair { key, value }
}
}
// Constrained methods: only available when both K and V implement Display
impl<K: Display, V: Display> Pair<K, V> {
fn print(&self) {
println!("Key: {}, Value: {}", self.key, self.value);
}
}
// Generic Methods:Mixing Different Types of Parameters
fn mix_and_match<T, U>(a: T, b: U) -> String
where
T: Display,
U: Display,
{
format!("Mixed: {} and {}", a, b)
}
fn main() {
// Multiple Type Parameters: String and i32
let pair1 = Pair::new("Age".to_string(), 25);
pair1.print();
// Multiple Type Parameters: &str and f64
let pair2 = Pair::new("PI", 3.14159);
pair2.print();
// Different Types of Combinations
let pair3 = Pair::new(100, "HTTP OK");
// pair3.print(); // ❌ Compilation Error: i32 and &str both implement Display, but no error here.
// In fact i32 and &str both implement Display, so you can call it
// This is just to demonstrate the concept of constraint methods.
pair3.print();
// Mixing Different Types
println!("{}", mix_and_match(42, "answer"));
println!("{}", mix_and_match(3.14, 100));
}
الناتج:
Key: Age, Value: 25
Key: PI, Value: 3.14159
Key: 100, Value: HTTP OK
Mixed: 42 and answer
Mixed: 3.14 and 100
تسمح معلمات الأنواع المتعددة (
<K, V>) للبنية (struct) بتخزين بيانات من أنواع مختلفة. وتُستخدم عبارةwhereلتحديد الشروط التي يجب أن تستوفيها معلمات الأنواع. وأثناء عملية التوحيد (monomorphization)، يقوم المُترجم بإنشاء كود منفصل لكل تركيبة، مثلPair<String, i32>وPair<&str, f64>.
(5) ▶ المثال:تمرين شامل — الحاويات والخوارزميات العامة (مستوى الصعوبة ⭐⭐⭐)
// ============================================
// Comprehensive Example:Generic Stack + Generic Search Algorithms
// ============================================
use std::fmt::Display;
struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
fn new() -> Self {
Stack { items: Vec::new() }
}
fn push(&mut self, item: T) {
self.items.push(item);
}
fn pop(&mut self) -> Option<T> {
self.items.pop()
}
fn peek(&self) -> Option<&T> {
self.items.last()
}
fn is_empty(&self) -> bool {
self.items.is_empty()
}
fn len(&self) -> usize {
self.items.len()
}
}
impl<T: Display> Stack<T> {
fn print_all(&self) {
for item in &self.items {
print!("{} ", item);
}
println!();
}
}
fn find_first<T: PartialEq>(items: &[T], target: &T) -> Option<usize> {
items.iter().position(|x| x == target)
}
fn swap_if_greater<T: PartialOrd>(a: &mut T, b: &mut T) {
if *a > *b {
std::mem::swap(a, b);
}
}
fn main() {
let mut int_stack: Stack<i32> = Stack::new();
int_stack.push(10);
int_stack.push(20);
int_stack.push(30);
println!("=== Integer Stack ===");
println!("Stack Contents: ");
int_stack.print_all();
println!("Stack top: {:?}", int_stack.peek());
println!("Pop up: {:?}", int_stack.pop());
println!("Remaining {} element", int_stack.len());
let mut str_stack: Stack<&str> = Stack::new();
str_stack.push("Rust");
str_stack.push("is");
str_stack.push("awesome");
println!("\n=== String Stack ===");
str_stack.print_all();
let nums = vec![10, 20, 30, 40, 50];
println!("\n=== Generic Search ===");
println!("Find 30: Index {:?}", find_first(&nums, &30));
println!("Find 99: Index {:?}", find_first(&nums, &99));
let words = vec!["apple", "banana", "cherry"];
println!("Find 'banana': Index {:?}", find_first(&words, &"banana"));
let mut x = 42;
let mut y = 10;
println!("\n=== Generic Swaps ===");
println!("Before the exchange: x={}, y={}", x, y);
swap_if_greater(&mut x, &mut y);
println!("After the exchange: x={}, y={}", x, y);
}
الناتج:
=== Integer Stack ===
Stack Contents:
10 20 30
Stack top: Some(30)
Pop up: Some(30)
Remaining 2 element
=== String Stack ===
Rust is awesome
=== Generic Search ===
Find 30: Index Some(2)
Find 99: Index None
Find 'banana': Index Some(1)
=== Generic Swaps ===
Before the exchange: x=42, y=10
After the exchange: x=10, y=42
يعمل النوع العام
Stack<T>مع كل من i32 و&str؛ ويضمن القيدimpl<T: Display>أن تكون الطريقةprint_allمتاحة فقط عندما ينفذ النوع واجهة Display؛ استخدم القيدPartialOrdلتنفيذ عامل المقارنة العام.
❓ أسئلة شائعة
Box<dyn Any>؟dyn Any الأنواع في وقت التشغيل (التوزيع الديناميكي). وتوفر «الأنواع العامة» أداءً أفضل لأن المُحوِّل البرمجي يُنشئ كودًا مخصصًا لكل نوع، مما يزيل العبء الإضافي الناجم عن الدوال الافتراضية. أما dyn Any فهو أكثر مرونة، حيث يمكنه التعامل مع أي نوع في وقت التشغيل، لكنه يتسبب في عبء إضافي في وقت التشغيل.dyn Trait بدلاً من ذلك.fn foo<T>(x: T). الطرق العامة: impl<T> MyType<T> { fn bar(&self) }. يمكن للطرق العامة الوصول إلى النوع Self، في حين أن الدوال العامة لا تستطيع ذلك.impl<T> وimpl؟impl<T> هي طريقة التنفيذ لجميع أنواع T، في حين أن impl القياسية هي طريقة التنفيذ لأنواع محددة. impl<T> Point<T> { fn x(&self) } متاحة لجميع أنواع Point. impl Point<f64> { fn distance(&self) } متاح فقط لـ Point<f64>.where الخاصة بحدود السمات وكتابتها مباشرةً بين أقواس زاوية؟where توفر قابلية قراءة أفضل للقيود المعقدة. fn foo<T: Display + Clone, U: Debug>(t: T, u: U) تعادل fn foo<T, U>(t: T, u: U) where T: Display + Clone, U: Debug. يُنصح باستخدام جملة where عند وجود قيود متعددة.📖 ملخص
- الدوال العامة
fn foo<T>(x: T)تتيح تطبيق الدالة نفسها على أنواع متعددة - الهياكل العامة
struct Point<T>تتيح لحقول الهياكل تخزين أي نوع - التعدادات العامة
Option<T>وResult<T, E>هما مثالان بارزان على التصميم العام في المكتبة القياسية لـ Rust. - الطرق العامة
impl<T> Type<T>تُنفِّذ طرقًا لجميع معلمات الأنواع، ويمكنها أيضًا تنفيذ طرق محددة لأنواع معينة - التوحيد الشكلي هو العملية التي يقوم من خلالها المُجمِّع بتوسيع الكود العام ليصبح كودًا مخصصًا لأنواع محددة في وقت التجميع — دون أي عبء إضافي على وقت التشغيل.
- تجعل جمل القيد ذات المعلمات المتعددة من النوعين
<T, U>وwhereالأنواع العامة مرنة وآمنة في آن واحد
📝 تمارين
- الصعوبة ⭐: اكتب دالة عامة
fn echo<T>(x: T) -> Tتأخذ قيمةً ما وتُرجعها كما هي. في الدالةmain، استدعِها بالقيمi32وf64و&str، على التوالي. - الصعوبة ⭐⭐: عرّف بنية عامة
Container<T>تحتوي على حقلvalue: T. يعيد التنفيذfn get(&self) -> &Tمرجعًا إلى القيمة، بينما يقومfn set(&mut self, val: T)بتعديل القيمة. اختبر ذلك في الدالة الرئيسية باستخدامContainer<String>وContainer<i32>على التوالي. - الصعوبة ⭐⭐⭐: اكتب دالة عامة
fn merge_arrays<T>(a: &[T], b: &[T]) -> Vec<T>تقوم بدمج شريحتين وتُرجع كائن Vec جديدًا. يجب أن ينفذ النوع T واجهةClone. بعد ذلك، في الدالة الرئيسية، قم بدمج شريحتين من نوعi32وشريحتين من نوع&str، على التوالي.