أساسيات الأنواع المعممة
لماذا الأنواع المعممة؟
تخيل أنك تبني فئة Box تحتفظ بعنصر. بدون الأنواع المعممة لديك خياران سيئان: (1) اجعلها تحتفظ بـ dynamic مما يفقد كل أمان النوع أو (2) أنشئ فئات منفصلة مثل IntBox و StringBox و UserBox -- مكرراً الكود بلا نهاية. الأنواع المعممة تحل هذا بالسماح لك بكتابة Box<T> واحد يعمل مع أي نوع مع الحفاظ على أمان النوع الكامل. الـ T هو معامل نوع -- عنصر نائب يُستبدل بنوع حقيقي عند استخدام الفئة.
أنت تستخدم الأنواع المعممة كل يوم بالفعل: List<int> و Map<String, dynamic> و Future<String> -- هذه كلها أنواع معممة. الآن ستتعلم إنشاء أنواعك الخاصة.
المشكلة بدون الأنواع المعممة
// سيئ: استخدام dynamic -- لا أمان للنوع
class DynamicBox {
dynamic content;
DynamicBox(this.content);
}
void main() {
var box = DynamicBox(42);
// المُجمّع يسمح بهذا -- لكنه سينهار وقت التشغيل!
String text = box.content; // خطأ وقت التشغيل: int ليس String
}
// سيئ: فئات منفصلة لكل نوع -- تكرار الكود
class IntBox {
int content;
IntBox(this.content);
}
class StringBox {
String content;
StringBox(this.content);
}
// ماذا عن UserBox و ProductBox و OrderBox...؟
// هذا لا يتوسع!
الحل: الأنواع المعممة
// جيد: فئة واحدة تعمل لجميع الأنواع مع أمان نوع كامل
class Box<T> {
T content;
Box(this.content);
T open() {
print('Opening box containing: $content');
return content;
}
}
void main() {
// Dart يستنتج النوع من المعامل
var intBox = Box(42); // Box<int>
var strBox = Box('Hello'); // Box<String>
// أو حدد صراحة
Box<double> dblBox = Box(3.14);
int number = intBox.open(); // آمن النوع: يُرجع int
String text = strBox.open(); // آمن النوع: يُرجع String
// خطأ وقت التجميع -- يُلتقط قبل حتى تشغيل الكود!
// String wrong = intBox.open(); // خطأ: int لا يمكن تعيينه لـ String
}
T هو مجرد اصطلاح لـ “Type” (نوع). يمكنك استخدام أي اسم: E للعنصر و K و V للمفتاح/القيمة و R لنوع الإرجاع. الاصطلاحات المعيارية هي: T (Type نوع) و E (Element عنصر) و K (Key مفتاح) و V (Value قيمة) و S (State حالة) و R (Result نتيجة).الدوال المعممة
يمكنك أيضاً جعل دوال فردية معممة دون إنشاء فئة معممة. هذا مفيد لدوال المنفعة التي تعمل مع أي نوع.
الدوال المعممة
// دالة معممة -- T يُعلن بعد اسم الدالة
T firstElement<T>(List<T> items) {
if (items.isEmpty) {
throw StateError('List is empty');
}
return items.first;
}
// دالة معممة تحوّل قيمة
R transform<T, R>(T value, R Function(T) transformer) {
return transformer(value);
}
// دالة تبديل معممة
(T, T) swap<T>(T a, T b) => (b, a);
void main() {
// النوع يُستنتج من المعامل
int first = firstElement([10, 20, 30]); // T يُستنتج كـ int
String word = firstElement(['a', 'b', 'c']); // T يُستنتج كـ String
// معامل نوع صريح
double d = firstElement<double>([1.1, 2.2]);
// تحويل: int إلى String
String result = transform(42, (n) => 'Number: $n');
print(result); // Number: 42
// تحويل: String إلى int
int length = transform('Hello', (s) => s.length);
print(length); // 5
// تبديل
var (a, b) = swap(1, 2);
print('$a, $b'); // 2, 1
}
الفئات المعممة
الفئات المعممة هي الاستخدام الأكثر شيوعاً للأنواع المعممة. تُعلن معامل نوع واحد أو أكثر بعد اسم الفئة ثم تستخدم تلك الأنواع في جميع أنحاء الفئة للحقول ومعاملات الطرق وأنواع الإرجاع.
بناء مكدس معمم
class Stack<T> {
final List<T> _items = [];
void push(T item) => _items.add(item);
T pop() {
if (_items.isEmpty) {
throw StateError('Stack is empty');
}
return _items.removeLast();
}
T get peek {
if (_items.isEmpty) {
throw StateError('Stack is empty');
}
return _items.last;
}
bool get isEmpty => _items.isEmpty;
bool get isNotEmpty => _items.isNotEmpty;
int get length => _items.length;
@override
String toString() => 'Stack(${_items.join(', ')})';
}
void main() {
// مكدس من الأعداد الصحيحة
var intStack = Stack<int>();
intStack.push(1);
intStack.push(2);
intStack.push(3);
print(intStack); // Stack(1, 2, 3)
print(intStack.pop()); // 3
print(intStack.peek); // 2
// مكدس من السلاسل النصية -- نفس الفئة ونوع مختلف
var strStack = Stack<String>();
strStack.push('hello');
strStack.push('world');
print(strStack.pop()); // world
// أمان النوع: لا يمكن دفع نوع خاطئ
// intStack.push('text'); // خطأ تجميع!
}
قيود النوع مع extends
أحياناً تحتاج تقييد الأنواع التي يمكن استخدامها مع النوع المعمم. مثلاً دالة sort يجب أن تعمل فقط مع أنواع Comparable. تستخدم extends لتعيين حد أعلى لمعامل النوع.
الأنواع المعممة المحدودة
// T يجب أن يكون نوعاً فرعياً من Comparable<T>
T findMin<T extends Comparable<T>>(List<T> items) {
if (items.isEmpty) throw StateError('Empty list');
T smallest = items.first;
for (var item in items.skip(1)) {
if (item.compareTo(smallest) < 0) {
smallest = item;
}
}
return smallest;
}
// T يجب أن يمتد num -- لتتمكن من عمليات الرياضيات
class Statistics<T extends num> {
final List<T> data;
Statistics(this.data) {
if (data.isEmpty) throw ArgumentError('Data cannot be empty');
}
double get mean => data.reduce((a, b) => (a + b) as T) / data.length;
T get max => data.reduce((a, b) => a > b ? a : b);
T get min => data.reduce((a, b) => a < b ? a : b);
@override
String toString() => 'Stats(mean: ${mean.toStringAsFixed(2)}, min: $min, max: $max)';
}
void main() {
// يعمل مع int (int يمتد Comparable<int>)
print(findMin([5, 2, 8, 1, 9])); // 1
// يعمل مع String (String يمتد Comparable<String>)
print(findMin(['banana', 'apple', 'cherry'])); // apple
// لن يُجمّع مع نوع غير Comparable:
// findMin([Object(), Object()]); // خطأ!
var intStats = Statistics([10, 20, 30, 40, 50]);
print(intStats); // Stats(mean: 30.00, min: 10, max: 50)
var dblStats = Statistics([1.5, 2.7, 3.14, 0.99]);
print(dblStats); // Stats(mean: 2.08, min: 0.99, max: 3.14)
// لن يُجمّع:
// Statistics<String>(['a', 'b']); // خطأ: String لا يمتد num
}
T افتراضياً Object? مما يعني أنه يمكن أن يكون أي شيء بما في ذلك null. إذا كتبت T extends Object تستبعد الأنواع القابلة للـ null. إذا كتبت T extends num تقيّد للأنواع العددية. أضف دائماً قيوداً عندما يفترض كودك قدرات معينة للنوع (مثل المقارنة أو العمليات الحسابية).نمط Result<T>
أحد أكثر الاستخدامات العملية للأنواع المعممة هو نمط Result -- طريقة آمنة النوع لتمثيل النجاح أو الفشل دون استخدام الاستثناءات للأخطاء المتوقعة. يُستخدم هذا النمط بكثرة في تطبيقات Flutter الإنتاجية.
بناء نوع Result<T>
// فئة مختومة بنوعين فرعيين: Success و Failure
sealed class Result<T> {
const Result();
// مُنشئات مصنع للراحة
factory Result.success(T value) = Success<T>;
factory Result.failure(String message, [Exception? exception]) =
Failure<T>;
// مساعد مطابقة الأنماط
R when<R>({
required R Function(T value) success,
required R Function(String message, Exception? exception) failure,
}) => switch (this) {
Success(:final value) => success(value),
Failure(:final message, :final exception) => failure(message, exception),
};
}
class Success<T> extends Result<T> {
final T value;
const Success(this.value);
}
class Failure<T> extends Result<T> {
final String message;
final Exception? exception;
const Failure(this.message, [this.exception]);
}
// الاستخدام في مستودع
class UserRepository {
Future<Result<String>> fetchUserName(int id) async {
try {
// محاكاة استدعاء API
if (id <= 0) {
return Result.failure('Invalid user ID');
}
await Future.delayed(Duration(milliseconds: 100));
return Result.success('User_$id');
} on Exception catch (e) {
return Result.failure('Network error', e);
}
}
}
void main() async {
var repo = UserRepository();
// حالة النجاح
var result = await repo.fetchUserName(42);
var message = result.when(
success: (name) => 'Welcome, $name!',
failure: (msg, _) => 'Error: $msg',
);
print(message); // Welcome, User_42!
// حالة الفشل
var bad = await repo.fetchUserName(-1);
print(bad.when(
success: (name) => 'Got: $name',
failure: (msg, _) => 'Failed: $msg',
)); // Failed: Invalid user ID
// مطابقة الأنماط مع switch
switch (result) {
case Success(:final value):
print('Success: $value');
case Failure(:final message):
print('Failure: $message');
}
}
Result<T> هو أحد أكثر الأنماط قيمة في Dart. استخدمه للعمليات التي يمكن أن تفشل بطرق متوقعة (استدعاءات API وقراءة الملفات والتحقق). احتفظ بالاستثناءات للمواقف غير المتوقعة حقاً (أخطاء البرمجة ونفاد الذاكرة). هذا يجعل معالجة الأخطاء صريحة وقابلة للاختبار ومستحيلة التجاهل بالصدفة.مثال عملي: نمط المستودع المعمم
لنبني نمطاً واقعياً يُستخدم في كل تطبيق Flutter تقريباً -- مستودع معمم يوفر عمليات CRUD لأي نوع نموذج.
واقعي: مستودع معمم
// فئة أساسية يجب أن تمتدها جميع النماذج
abstract class Model {
final String id;
final DateTime createdAt;
Model({required this.id, required this.createdAt});
Map<String, dynamic> toJson();
}
// مستودع معمم يعمل مع أي نوع فرعي من Model
class Repository<T extends Model> {
final Map<String, T> _store = {};
final String name;
Repository(this.name);
// إنشاء
void add(T item) {
_store[item.id] = item;
print('[$name] Added: ${item.id}');
}
// قراءة
T? findById(String id) => _store[id];
List<T> findAll() => _store.values.toList();
List<T> findWhere(bool Function(T) predicate) =>
_store.values.where(predicate).toList();
// حذف
bool remove(String id) {
var removed = _store.remove(id) != null;
if (removed) print('[$name] Removed: $id');
return removed;
}
int get count => _store.length;
}
// نماذج ملموسة
class User extends Model {
final String name;
final String email;
User({required super.id, required this.name, required this.email})
: super(createdAt: DateTime.now());
@override
Map<String, dynamic> toJson() => {
'id': id, 'name': name, 'email': email,
};
@override
String toString() => 'User($name, $email)';
}
class Product extends Model {
final String title;
final double price;
Product({required super.id, required this.title, required this.price})
: super(createdAt: DateTime.now());
@override
Map<String, dynamic> toJson() => {
'id': id, 'title': title, 'price': price,
};
@override
String toString() => 'Product($title, \$$price)';
}
void main() {
// نفس فئة Repository وأنواع مختلفة
var users = Repository<User>('Users');
var products = Repository<Product>('Products');
users.add(User(id: 'u1', name: 'Alice', email: 'alice@test.com'));
users.add(User(id: 'u2', name: 'Bob', email: 'bob@test.com'));
products.add(Product(id: 'p1', title: 'Laptop', price: 999.99));
products.add(Product(id: 'p2', title: 'Phone', price: 699.99));
// استعلامات آمنة النوع
User? alice = users.findById('u1');
print(alice); // User(Alice, alice@test.com)
// تصفية المنتجات تحت 800$
var affordable = products.findWhere((p) => p.price < 800);
print(affordable); // [Product(Phone, $699.99)]
// لا يمكن خلط الأنواع -- خطأ تجميع:
// users.add(Product(...)); // خطأ: Product ليس User
}
UserRepository و ProductRepository و OrderRepository بمنطق CRUD متطابق. مع Repository<T extends Model> تكتب المنطق مرة واحدة ويعمل بأمان مع أي نوع نموذج.