المجموعات والعناصر القابلة للتكرار في OOP
فهم العناصر القابلة للتكرار في Dart
في قلب نظام المجموعات في Dart تقع واجهة Iterable<E>. كل List و Set و Map.values ينفذ Iterable. عندما تستخدم حلقة for-in، فأنت تستهلك Iterable. فهم هذه الواجهة يتيح لك إنشاء مجموعات مخصصة تتكامل بسلاسة مع ميزات لغة Dart -- حلقات for-in و map و where و fold والمزيد.
في هذا الدرس، ستنفذ مُكررات خاصة بك، وتبني مجموعات مخصصة عامة مثل Stack و Queue، وتنشئ مصادر بيانات مُقسّمة لصفحات، وتتعلم عن المجموعات غير القابلة للتعديل لتصميم API آمن.
واجهتا Iterable و Iterator
نظام التكرار في Dart يستخدم واجهتين متعاونتين:
Iterable<E>-- كائن يمكنه إنتاجIterator. لديه خاصية واحدة مطلوبة:Iterator<E> get iterator.Iterator<E>-- كائن يمشي عبر العناصر واحداً تلو الآخر. لديه عضوان:bool moveNext()وE get current.
كيف يعمل Iterable و Iterator
void main() {
List<String> fruits = ['apple', 'banana', 'cherry'];
// خلف الكواليس، for-in يفعل هذا:
Iterator<String> it = fruits.iterator;
while (it.moveNext()) {
print(it.current);
}
// وهو مكافئ لـ:
for (String fruit in fruits) {
print(fruit);
}
// طرق Iterable تعمل على أي Iterable
Iterable<String> upperFruits = fruits.map((f) => f.toUpperCase());
Iterable<String> longFruits = fruits.where((f) => f.length > 5);
print(upperFruits.toList()); // [APPLE, BANANA, CHERRY]
print(longFruits.toList()); // [banana, cherry]
}
.map() أو .where()، لا يحدث حساب فوري. يتم تطبيق التحويل فقط عند استهلاك العنصر القابل للتكرار (مثلاً مع toList() أو for-in أو first). هذا مهم للأداء مع مجموعات البيانات الكبيرة.تنفيذ مُكرر مخصص
لجعل أي فئة قابلة للتكرار، تنفذ Iterable<E> وتوفر Iterator<E> مخصصاً. لنبدأ بمُكرر نطاق بسيط يولد أرقاماً.
مُكرر نطاق مخصص
class NumberRange extends Iterable<int> {
final int start;
final int end;
final int step;
const NumberRange(this.start, this.end, {this.step = 1});
@override
Iterator<int> get iterator => _NumberRangeIterator(start, end, step);
}
class _NumberRangeIterator implements Iterator<int> {
final int _end;
final int _step;
int _current;
bool _started = false;
_NumberRangeIterator(int start, this._end, this._step)
: _current = start - _step;
@override
int get current => _current;
@override
bool moveNext() {
if (!_started) {
_current += _step;
_started = true;
} else {
_current += _step;
}
return _current <= _end;
}
}
void main() {
// يعمل مع for-in
for (int n in NumberRange(1, 10, step: 2)) {
print(n); // 1, 3, 5, 7, 9
}
// يعمل مع جميع طرق Iterable
final range = NumberRange(1, 20);
final evenSquares = range
.where((n) => n.isEven)
.map((n) => n * n)
.toList();
print(evenSquares); // [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]
// كسول -- لا يولد جميع الأرقام
print(NumberRange(1, 1000000).first); // 1 (فوري)
print(NumberRange(1, 1000000).take(5).toList()); // [1, 2, 3, 4, 5]
}
بناء مكدس عام (Stack)
Stack هو هيكل بيانات آخر-داخل-أول-خارج (LIFO). بجعله عاماً وقابلاً للتكرار، يتكامل بشكل طبيعي مع نظام المجموعات في Dart.
مجموعة مكدس عامة
class Stack<E> extends Iterable<E> {
final List<E> _items = [];
// دفع عنصر إلى الأعلى
void push(E item) => _items.add(item);
// إخراج العنصر العلوي (يرمي خطأ إذا فارغ)
E pop() {
if (_items.isEmpty) {
throw StateError('لا يمكن الإخراج من مكدس فارغ');
}
return _items.removeLast();
}
// إلقاء نظرة على الأعلى بدون إزالة
E get peek {
if (_items.isEmpty) {
throw StateError('لا يمكن النظر في مكدس فارغ');
}
return _items.last;
}
// مسح جميع العناصر
void clear() => _items.clear();
@override
bool get isEmpty => _items.isEmpty;
@override
bool get isNotEmpty => _items.isNotEmpty;
@override
int get length => _items.length;
// التكرار من الأعلى إلى الأسفل (ترتيب عكسي)
@override
Iterator<E> get iterator => _items.reversed.iterator;
@override
String toString() => 'Stack(أعلى->أسفل): ${_items.reversed.toList()}';
}
void main() {
final stack = Stack<int>();
stack.push(10);
stack.push(20);
stack.push(30);
print(stack); // Stack(أعلى->أسفل): [30, 20, 10]
print(stack.peek); // 30
print(stack.pop()); // 30
print(stack.length); // 2
// يعمل مع for-in (يتكرر من الأعلى إلى الأسفل)
for (int item in stack) {
print(item); // 20 ثم 10
}
// يعمل مع طرق Iterable
print(stack.where((n) => n > 15).toList()); // [20]
print(stack.contains(10)); // true
}
بناء طابور عام (Queue)
Queue هو هيكل بيانات أول-داخل-أول-خارج (FIFO) -- مثالي لجدولة المهام ومعالجة الرسائل وعمليات التنقل بالعرض أولاً.
مجموعة طابور عامة
class SimpleQueue<E> extends Iterable<E> {
final List<E> _items = [];
// إضافة إلى الخلف
void enqueue(E item) => _items.add(item);
// إضافة عناصر متعددة
void enqueueAll(Iterable<E> items) => _items.addAll(items);
// إزالة من الأمام
E dequeue() {
if (_items.isEmpty) {
throw StateError('لا يمكن الإزالة من طابور فارغ');
}
return _items.removeAt(0);
}
// إلقاء نظرة على الأمام
E get front {
if (_items.isEmpty) {
throw StateError('الطابور فارغ');
}
return _items.first;
}
void clear() => _items.clear();
@override
bool get isEmpty => _items.isEmpty;
@override
int get length => _items.length;
@override
Iterator<E> get iterator => _items.iterator;
@override
String toString() => 'Queue(أمام->خلف): $_items';
}
// طابور أولويات باستخدام Comparable
class PriorityQueue<E extends Comparable<E>> extends Iterable<E> {
final List<E> _items = [];
void enqueue(E item) {
_items.add(item);
_items.sort(); // الحفاظ على الترتيب الطبيعي
}
E dequeue() {
if (_items.isEmpty) {
throw StateError('طابور الأولويات فارغ');
}
return _items.removeAt(0); // إزالة الأعلى أولوية (الأصغر)
}
E get front => _items.first;
@override
bool get isEmpty => _items.isEmpty;
@override
int get length => _items.length;
@override
Iterator<E> get iterator => _items.iterator;
}
void main() {
// طابور بسيط
final queue = SimpleQueue<String>();
queue.enqueue('المهمة أ');
queue.enqueue('المهمة ب');
queue.enqueue('المهمة ج');
print(queue.dequeue()); // المهمة أ (أول داخل، أول خارج)
// طابور أولويات
final pq = PriorityQueue<int>();
pq.enqueue(30);
pq.enqueue(10);
pq.enqueue(20);
print(pq.dequeue()); // 10 (الأصغر أولاً)
print(pq.dequeue()); // 20
}
PriorityQueue<E extends Comparable<E>> معامل نوع محدود. هذا يضمن في وقت الترجمة أن الأنواع القابلة للمقارنة فقط يمكن استخدامها، لذا الترتيب يعمل دائماً.مجموعة مُقسّمة لصفحات
حاجة شائعة في العالم الحقيقي هي التنقل بين صفحات مجموعات بيانات كبيرة. لنبنِ مجموعة كسولة مُقسّمة لصفحات تحمّل الصفحات عند الطلب.
مصدر بيانات مُقسّم لصفحات
class Page<T> {
final List<T> items;
final int pageNumber;
final int totalPages;
final int totalItems;
const Page({
required this.items,
required this.pageNumber,
required this.totalPages,
required this.totalItems,
});
bool get hasNext => pageNumber < totalPages;
bool get hasPrevious => pageNumber > 1;
}
// مصدر بيانات مجرد مُقسّم لصفحات
abstract class PaginatedSource<T> extends Iterable<T> {
final int pageSize;
PaginatedSource({this.pageSize = 10});
// الفئات الفرعية تنفذ هذا لجلب صفحة
Page<T> fetchPage(int pageNumber);
@override
Iterator<T> get iterator => _PaginatedIterator(this);
}
class _PaginatedIterator<T> implements Iterator<T> {
final PaginatedSource<T> _source;
Page<T>? _currentPage;
int _indexInPage = -1;
bool _done = false;
_PaginatedIterator(this._source);
@override
T get current => _currentPage!.items[_indexInPage];
@override
bool moveNext() {
if (_done) return false;
// تحميل الصفحة الأولى
if (_currentPage == null) {
_currentPage = _source.fetchPage(1);
if (_currentPage!.items.isEmpty) {
_done = true;
return false;
}
_indexInPage = 0;
return true;
}
// التنقل ضمن الصفحة الحالية
_indexInPage++;
if (_indexInPage < _currentPage!.items.length) {
return true;
}
// تحميل الصفحة التالية
if (_currentPage!.hasNext) {
_currentPage = _source.fetchPage(_currentPage!.pageNumber + 1);
_indexInPage = 0;
return _currentPage!.items.isNotEmpty;
}
_done = true;
return false;
}
}
// تنفيذ ملموس
class UserPaginatedSource extends PaginatedSource<String> {
final List<String> _allUsers;
UserPaginatedSource(this._allUsers, {super.pageSize});
@override
Page<String> fetchPage(int pageNumber) {
final start = (pageNumber - 1) * pageSize;
final end = start + pageSize;
final items = _allUsers.sublist(
start.clamp(0, _allUsers.length),
end.clamp(0, _allUsers.length),
);
final totalPages = (_allUsers.length / pageSize).ceil();
print(' [تحميل الصفحة $pageNumber من $totalPages]');
return Page(
items: items,
pageNumber: pageNumber,
totalPages: totalPages,
totalItems: _allUsers.length,
);
}
}
void main() {
final allUsers = List.generate(25, (i) => 'مستخدم_${i + 1}');
final source = UserPaginatedSource(allUsers, pageSize: 10);
// كسول -- يحمّل الصفحات عند الحاجة فقط
print('أول 5 مستخدمين:');
for (String user in source.take(5)) {
print(' $user');
}
// الخرج: [تحميل الصفحة 1 من 3]، ثم مستخدم_1 حتى مستخدم_5
print('\nجميع المستخدمين (يحمّل جميع الصفحات):');
print(' الإجمالي: ${source.length}');
// يحمّل جميع الصفحات الـ 3 للعدّ
}
المجموعات غير القابلة للتعديل
عند كشف المجموعات عبر API، غالباً ما تريد منع الكود الخارجي من تعديل الحالة الداخلية. يوفر Dart UnmodifiableListView و UnmodifiableMapView، ويمكنك بناء أغلفتك الخاصة غير القابلة للتعديل.
مجموعات غير قابلة للتعديل لـ APIs آمنة
import 'dart:collection';
class StudentRegistry {
final List<String> _students = [];
// إرجاع عرض غير قابل للتعديل -- المستدعون لا يمكنهم الإضافة/الإزالة
UnmodifiableListView<String> get students =>
UnmodifiableListView(_students);
void addStudent(String name) {
if (name.trim().isEmpty) {
throw ArgumentError('اسم الطالب لا يمكن أن يكون فارغاً');
}
_students.add(name);
}
bool removeStudent(String name) => _students.remove(name);
}
// مجموعة مخصصة غير قابلة للتعديل
class ReadOnlyMap<K, V> {
final Map<K, V> _data;
const ReadOnlyMap(this._data);
V? operator [](K key) => _data[key];
bool containsKey(K key) => _data.containsKey(key);
int get length => _data.length;
Iterable<K> get keys => _data.keys;
Iterable<V> get values => _data.values;
// لا يوجد طرق إضافة أو إزالة أو تحديث!
}
void main() {
final registry = StudentRegistry();
registry.addStudent('أليس');
registry.addStudent('بوب');
// يمكن قراءة القائمة
final students = registry.students;
print(students); // [أليس, بوب]
// لا يمكن التعديل -- يرمي UnsupportedError
try {
students.add('تشارلي');
} on UnsupportedError {
print('لا يمكن تعديل قائمة غير قابلة للتعديل!');
}
// ReadOnlyMap
final config = ReadOnlyMap({'host': 'localhost', 'port': '8080'});
print(config['host']); // localhost
// config['host'] = 'x'; // خطأ ترجمة -- لا يوجد عامل []=
}
List.unmodifiable() ينشئ قائمة جديدة (نسخة)، بينما UnmodifiableListView() يغلّف الأصلية بدون نسخ. إذا تغيرت الأصلية، يعكس العرض تلك التغييرات. اختر بناءً على ما إذا كنت تريد لقطة أو عرض حي للقراءة فقط.مثال عملي: مجموعة مُرشّحة قابلة للمراقبة
لنبنِ مجموعة تجمع عدة مفاهيم: الأنواع العامة والتكرار والتصفية ونمط المراقب لإشعار المستمعين عند تغيير العناصر.
مجموعة مُرشّحة قابلة للمراقبة
typedef CollectionCallback<E> = void Function(E item, String action);
class ObservableList<E> extends Iterable<E> {
final List<E> _items = [];
final List<CollectionCallback<E>> _listeners = [];
// الاشتراك في التغييرات
void addListener(CollectionCallback<E> callback) {
_listeners.add(callback);
}
void removeListener(CollectionCallback<E> callback) {
_listeners.remove(callback);
}
void _notify(E item, String action) {
for (var listener in _listeners) {
listener(item, action);
}
}
// العمليات المُعدّلة تُشعر المستمعين
void add(E item) {
_items.add(item);
_notify(item, 'added');
}
bool remove(E item) {
final removed = _items.remove(item);
if (removed) _notify(item, 'removed');
return removed;
}
// إنشاء عرض مُرشّح
FilteredView<E> where_(bool Function(E) test) {
return FilteredView(this, test);
}
@override
Iterator<E> get iterator => _items.iterator;
@override
int get length => _items.length;
}
class FilteredView<E> extends Iterable<E> {
final ObservableList<E> _source;
final bool Function(E) _test;
FilteredView(this._source, this._test);
// يعكس دائماً الحالة الحالية للمصدر
@override
Iterator<E> get iterator =>
_source._items.where(_test).iterator;
}
class Task {
final String title;
final bool completed;
final String priority;
const Task(this.title, {this.completed = false, this.priority = 'medium'});
@override
String toString() => '$title [${completed ? "منجز" : priority}]';
}
void main() {
final tasks = ObservableList<Task>();
// الاستماع للتغييرات
tasks.addListener((task, action) {
print(' حدث: "$action" - ${task.title}');
});
// إنشاء عروض مُرشّحة
final activeTasks = tasks.where_((t) => !t.completed);
final highPriority = tasks.where_((t) => t.priority == 'high');
tasks.add(Task('كتابة التوثيق', priority: 'high'));
tasks.add(Task('إصلاح العلة', priority: 'high'));
tasks.add(Task('مراجعة الكود', priority: 'low'));
tasks.add(Task('النشر', completed: true));
print('\nالمهام النشطة: ${activeTasks.length}'); // 3
print('أولوية عالية: ${highPriority.length}'); // 2
// العروض تتحدث تلقائياً
tasks.remove(Task('إصلاح العلة', priority: 'high'));
print('أولوية عالية بعد الإزالة: ${highPriority.length}');
}
Iterable<E> (أو نفّذه) حتى تعمل مجموعتك بشكل طبيعي مع حلقات for-in في Dart وعوامل الانتشار وجميع طرق المجموعات المدمجة مثل map و where و fold و any و every.