المجموعات والعناصر القابلة للتكرار المتقدمة
Iterable مقابل List مقابل Set مقابل Map
تسلسل المجموعات في Dart مبني على واجهة Iterable. فهم الفروقات بين أنواع المجموعات الأساسية ضروري لكتابة كود فعّال ومعبّر.
نظرة عامة على أنواع المجموعات
void main() {
// Iterable — الواجهة الأساسية، تُقيّم بشكل كسول
Iterable<int> lazyRange = Iterable.generate(5, (i) => i * 10);
print(lazyRange); // (0, 10, 20, 30, 40)
// List — مرتبة، مفهرسة، تسمح بالتكرارات
List<int> numbers = [1, 2, 3, 2, 1];
print(numbers[2]); // 3 (وصول بالفهرس)
// Set — غير مرتبة (مرتبة حسب الإدراج في Dart)، بدون تكرارات
Set<int> unique = {1, 2, 3, 2, 1};
print(unique); // {1, 2, 3}
// Map — أزواج مفتاح-قيمة، المفاتيح فريدة
Map<String, int> ages = {'Alice': 30, 'Bob': 25};
print(ages['Alice']); // 30
}
List و Set كلاهما يُنفّذ Iterable، لذا أي طريقة تعمل على Iterable تعمل على كليهما. Map لا يُنفّذ Iterable مباشرة، لكن map.keys و map.values و map.entries كلها Iterable.التقييم الكسول مع Iterables
أحد أهم ميزات Iterable هو التقييم الكسول. كثير من العمليات مثل map و where و expand و take تُرجع iterables كسولة تحسب القيم فقط عند التكرار.
التقييم الكسول مقابل الفوري
void main() {
final numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// كسول: لا يحدث شيء حتى نكرر
final lazyResult = numbers
.where((n) {
print(' Checking $n');
return n.isEven;
})
.map((n) {
print(' Mapping $n');
return n * n;
});
print('--- أخذ أول 2 ---');
// يعالج العناصر فقط حتى يجد تطابقين
final first2 = lazyResult.take(2).toList();
print(first2); // [4, 16]
// المخرجات تُظهر أنه فحص 1,2,3,4 فقط — ليس القائمة بأكملها!
print('\n--- فوري: toList() يفرض التقييم الكامل ---');
final eagerResult = numbers.where((n) => n.isEven).map((n) => n * n).toList();
print(eagerResult); // [4, 16, 36, 64, 100]
}
.toList() حتى تحتاج فعلاً الوصول العشوائي أو التكرار عدة مرات. يمكن أن يوفر هذا حساباً كبيراً على مجموعات البيانات الكبيرة.طريقة expand
طريقة expand تحوّل كل عنصر إلى صفر أو أكثر من العناصر، وتُسطّح النتيجة في iterable واحد. هي مكافئ Dart لـ flatMap في لغات أخرى.
استخدام expand (flatMap)
void main() {
final sentences = ['Hello world', 'Dart is great', 'Flutter rocks'];
// تقسيم كل جملة إلى كلمات وتسطيح
final words = sentences.expand((s) => s.split(' '));
print(words.toList());
// [Hello, world, Dart, is, great, Flutter, rocks]
// مضاعفة كل عنصر
final doubled = [1, 2, 3].expand((n) => [n, n]);
print(doubled.toList()); // [1, 1, 2, 2, 3, 3]
// تصفية وتحويل في خطوة واحدة
final matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];
final evenNumbers = matrix.expand((row) => row.where((n) => n.isEven));
print(evenNumbers.toList()); // [2, 4, 6, 8]
}
fold و reduce
fold و reduce طريقتان للتجميع تدمجان جميع العناصر في قيمة واحدة. الفرق الرئيسي هو أن fold تأخذ قيمة أولية، بينما reduce تستخدم العنصر الأول كنقطة بداية.
fold مقابل reduce
void main() {
final numbers = [1, 2, 3, 4, 5];
// fold: تبدأ بقيمة أولية
final sum = numbers.fold<int>(0, (acc, n) => acc + n);
print('المجموع: $sum'); // المجموع: 15
final product = numbers.fold<int>(1, (acc, n) => acc * n);
print('الجداء: $product'); // الجداء: 120
// reduce: تستخدم العنصر الأول كقيمة أولية
final max = numbers.reduce((a, b) => a > b ? a : b);
print('الأكبر: $max'); // الأكبر: 5
// fold يمكنها تغيير النوع؛ reduce لا تستطيع
final csv = numbers.fold<String>('', (acc, n) =>
acc.isEmpty ? '$n' : '$acc, $n');
print('CSV: $csv'); // CSV: 1, 2, 3, 4, 5
// reduce على قائمة فارغة تطرح StateError!
// [].reduce((a, b) => a + b); // خطأ: لا يوجد عنصر
// fold على قائمة فارغة تُرجع القيمة الأولية
final emptySum = <int>[].fold<int>(0, (acc, n) => acc + n);
print('مجموع فارغ: $emptySum'); // مجموع فارغ: 0
}
reduce على مجموعة قد تكون فارغة — تطرح StateError. فضّل دائماً fold عندما يكون هناك أي احتمال أن تكون المجموعة فارغة، لأنها تُرجع القيمة الأولية بأمان.whereType وتصفية الأنواع
طريقة whereType<T>() تُصفّي العناصر حسب نوعها في وقت التشغيل، وتُرجع فقط العناصر التي هي مثيلات لـ T. هذا تصفية وتحويل نوع في خطوة واحدة.
التصفية حسب النوع
void main() {
final mixed = [1, 'hello', 2.5, true, 42, 'world', 3.14, false];
// الحصول على الأعداد الصحيحة فقط
final ints = mixed.whereType<int>();
print(ints.toList()); // [1, 42]
// الحصول على النصوص فقط
final strings = mixed.whereType<String>();
print(strings.toList()); // [hello, world]
// الحصول على الأرقام فقط (int و double)
final nums = mixed.whereType<num>();
print(nums.toList()); // [1, 2.5, 42, 3.14]
// هذا أنظف بكثير من:
final intsOldWay = mixed.where((e) => e is int).cast<int>();
}
followedBy والربط
طريقة followedBy تربط iterable بشكل كسول دون إنشاء قائمة جديدة. هذا فعّال للذاكرة مع المجموعات الكبيرة.
الربط الكسول مع followedBy
void main() {
final first = [1, 2, 3];
final second = [4, 5, 6];
final third = [7, 8, 9];
// ربط كسول — لا تُنشأ قائمة جديدة
final combined = first.followedBy(second).followedBy(third);
print(combined.toList()); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// مقارنة مع عامل النشر (ينشئ قائمة جديدة فوراً)
final eager = [...first, ...second, ...third];
print(eager); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// followedBy أفضل عندما تحتاج التكرار مرة واحدة فقط
// النشر أفضل عندما تحتاج List ملموسة
}
عامل النشر و collection if/for
حرفيات المجموعات في Dart تدعم عامل النشر (... و ...?)، وcollection if، وcollection for لبناء المجموعات بشكل تصريحي.
النشر و collection if و collection for
void main() {
final base = [1, 2, 3];
List<int>? maybeNull;
final extra = [7, 8, 9];
// عامل النشر
final combined = [...base, 4, 5, 6, ...extra];
print(combined); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// النشر الآمن من null
final safe = [...base, ...?maybeNull, ...extra];
print(safe); // [1, 2, 3, 7, 8, 9]
// collection if
bool isAdmin = true;
final menu = [
'Home',
'Profile',
if (isAdmin) 'Admin Panel',
'Settings',
];
print(menu); // [Home, Profile, Admin Panel, Settings]
// collection for
final squares = [
for (var i = 1; i <= 5; i++) i * i,
];
print(squares); // [1, 4, 9, 16, 25]
// الجمع بين الثلاثة
final dashboard = [
'Overview',
if (isAdmin) ...['Users', 'Roles', 'Logs'],
for (var section in ['Reports', 'Analytics'])
section.toUpperCase(),
];
print(dashboard);
// [Overview, Users, Roles, Logs, REPORTS, ANALYTICS]
}
المجموعات غير القابلة للتعديل
يوفر Dart عدة طرق لإنشاء مجموعات لا يمكن تعديلها بعد الإنشاء. هذا مهم لعدم التغيير وأمان واجهة البرمجة.
إنشاء مجموعات غير قابلة للتعديل
void main() {
// الطريقة 1: List.unmodifiable (تنشئ نسخة غير قابلة للتعديل)
final source = [3, 1, 4, 1, 5];
final frozen = List.unmodifiable(source);
// frozen.add(9); // خطأ: عملية غير مدعومة
// frozen[0] = 0; // خطأ: عملية غير مدعومة
source.add(9); // OK — المصدر لا يزال قابلاً للتعديل
print(frozen); // [3, 1, 4, 1, 5] (غير متأثرة بتغييرات المصدر)
// الطريقة 2: UnmodifiableListView (تنشئ عرضاً غير قابل للتعديل)
import 'dart:collection';
final view = UnmodifiableListView(source);
// view.add(1); // خطأ
source.add(2);
print(view); // [3, 1, 4, 1, 5, 9, 2] (تعكس تغييرات المصدر!)
// الطريقة 3: حرفيات const (غير قابلة للتغيير وقت الترجمة)
const immutable = [1, 2, 3];
// immutable.add(4); // خطأ
// للخرائط
final frozenMap = Map.unmodifiable({'a': 1, 'b': 2});
// frozenMap['c'] = 3; // خطأ
// للمجموعات
final frozenSet = Set.unmodifiable({1, 2, 3});
// frozenSet.add(4); // خطأ
}
List.unmodifiable تنشئ نسخة مجمّدة في الزمن. UnmodifiableListView تنشئ عرضاً يعكس التغييرات على الأصل لكن يمنع التعديلات من خلال العرض. اختر بناءً على ما إذا كنت تحتاج العزل عن المصدر أو فقط الوصول للقراءة.SplayTreeMap و SplayTreeSet
للمجموعات التي تحتاج الحفاظ على ترتيب مُرتّب، يوفر Dart SplayTreeMap و SplayTreeSet من dart:collection. تستخدم شجرة ثنائية ذاتية التوازن داخلياً.
المجموعات المُرتّبة مع SplayTree
import 'dart:collection';
void main() {
// SplayTreeSet — مُرتّبة دائماً
final sortedNumbers = SplayTreeSet<int>();
sortedNumbers.addAll([5, 2, 8, 1, 9, 3]);
print(sortedNumbers); // {1, 2, 3, 5, 8, 9}
// مُقارن مخصص للترتيب العكسي
final descending = SplayTreeSet<int>((a, b) => b.compareTo(a));
descending.addAll([5, 2, 8, 1, 9, 3]);
print(descending); // {9, 8, 5, 3, 2, 1}
// SplayTreeMap — المفاتيح مُرتّبة دائماً
final sortedMap = SplayTreeMap<String, int>();
sortedMap['banana'] = 2;
sortedMap['apple'] = 5;
sortedMap['cherry'] = 3;
print(sortedMap); // {apple: 5, banana: 2, cherry: 3}
// ترتيب مخصص للكائنات المعقدة
final byLength = SplayTreeSet<String>(
(a, b) => a.length != b.length
? a.length.compareTo(b.length)
: a.compareTo(b),
);
byLength.addAll(['fig', 'apple', 'kiwi', 'banana', 'date']);
print(byLength); // {fig, date, kiwi, apple, banana}
}
Queue من dart:collection
Queue هي مجموعة مُحسّنة لإضافة وإزالة العناصر من كلا الطرفين. مثالية لعمليات FIFO (الأول يدخل الأول يخرج).
استخدام Queue
import 'dart:collection';
void main() {
final queue = Queue<String>();
// إضافة في النهاية (enqueue)
queue.add('First');
queue.add('Second');
queue.add('Third');
print(queue); // {First, Second, Third}
// إضافة في البداية
queue.addFirst('Zero');
print(queue); // {Zero, First, Second, Third}
// إزالة من البداية (dequeue) — O(1)
final first = queue.removeFirst();
print('أُزيل: $first'); // أُزيل: Zero
// إزالة من النهاية — O(1)
final last = queue.removeLast();
print('أُزيل: $last'); // أُزيل: Third
print(queue); // {First, Second}
// استعراض بدون إزالة
print('الأول: ${queue.first}'); // الأول: First
print('الأخير: ${queue.last}'); // الأخير: Second
}
مثال عملي: تجميع البيانات
تجميع البيانات عملية شائعة جداً. إليك كيفية بناء دالة groupBy عامة باستخدام fold.
تجميع البيانات مع fold
/// يُجمّع العناصر حسب مفتاح مُستخرج من كل عنصر.
Map<K, List<V>> groupBy<K, V>(Iterable<V> items, K Function(V) keyFn) {
return items.fold<Map<K, List<V>>>({}, (map, item) {
final key = keyFn(item);
(map[key] ??= []).add(item);
return map;
});
}
void main() {
final people = [
{'name': 'Alice', 'dept': 'Engineering'},
{'name': 'Bob', 'dept': 'Marketing'},
{'name': 'Charlie', 'dept': 'Engineering'},
{'name': 'Diana', 'dept': 'Marketing'},
{'name': 'Eve', 'dept': 'Design'},
];
final byDept = groupBy(people, (p) => p['dept']!);
byDept.forEach((dept, members) {
print('$dept: ${members.map((m) => m['name']).join(', ')}');
});
// Engineering: Alice, Charlie
// Marketing: Bob, Diana
// Design: Eve
}
مثال عملي: سلاسل التحويل
تسلسل الطرق على المجموعات ينشئ أنابيب تحويل بيانات قوية وقابلة للقراءة.
أنبوب تحويل البيانات
class Product {
final String name;
final String category;
final double price;
final int stock;
Product(this.name, this.category, this.price, this.stock);
}
void main() {
final products = [
Product('Laptop', 'Electronics', 999.99, 50),
Product('Phone', 'Electronics', 699.99, 200),
Product('Shirt', 'Clothing', 29.99, 500),
Product('Headphones', 'Electronics', 149.99, 0),
Product('Jeans', 'Clothing', 59.99, 150),
Product('Tablet', 'Electronics', 449.99, 75),
];
// أنبوب: إلكترونيات متوفرة، مُرتّبة بالسعر، مُنسّقة
final result = products
.where((p) => p.category == 'Electronics')
.where((p) => p.stock > 0)
.toList()
..sort((a, b) => a.price.compareTo(b.price))
..forEach((p) => print('${p.name}: \$${p.price} (${p.stock} متوفر)'));
// القيمة الإجمالية للإلكترونيات المتوفرة
final totalValue = products
.where((p) => p.category == 'Electronics' && p.stock > 0)
.fold<double>(0.0, (sum, p) => sum + p.price * p.stock);
print('إجمالي قيمة المخزون: \$${totalValue.toStringAsFixed(2)}');
}
مثال عملي: عدّ التكرارات
عدّ تكرار العناصر مهمة شائعة تجمع بين الخرائط و fold بأناقة.
عدّاد التكرارات
/// يعدّ تكرارات كل عنصر.
Map<T, int> frequency<T>(Iterable<T> items) {
return items.fold<Map<T, int>>({}, (counts, item) {
counts[item] = (counts[item] ?? 0) + 1;
return counts;
});
}
/// يُرجع أعلى N عناصر تكراراً.
List<MapEntry<T, int>> topN<T>(Map<T, int> freq, int n) {
return (freq.entries.toList()..sort((a, b) => b.value.compareTo(a.value)))
.take(n)
.toList();
}
void main() {
final words = 'the cat sat on the mat the cat ate the rat'.split(' ');
final wordFreq = frequency(words);
print(wordFreq);
// {the: 4, cat: 2, sat: 1, on: 1, mat: 1, ate: 1, rat: 1}
final top3 = topN(wordFreq, 3);
for (final entry in top3) {
print('${entry.key}: ${entry.value} مرات');
}
// the: 4 مرات
// cat: 2 مرات
// sat: 1 مرات
}
counts[item] = (counts[item] ?? 0) + 1 هو الأسلوب المعتاد في Dart. العامل الآمن من null ?? يعالج حالة عدم وجود المفتاح بعد في الخريطة، مع القيمة الافتراضية 0 قبل الزيادة.الملخص
مكتبة مجموعات Dart توفر أدوات غنية وقابلة للتركيب لمعالجة البيانات. النقاط الرئيسية:
Iterableهو الأساس الكسول؛ListوSetوطرقهما تُرجع iterables كسولة افتراضياً.expandهو flatMap في Dart؛foldأكثر أماناً منreduceللمجموعات الفارغة.whereType<T>()تُصفّي وتحوّل النوع في خطوة واحدة.- النشر (
...) و collection if و collection for تجعل بناء المجموعات التصريحي سهلاً. SplayTreeMap/SplayTreeSetتحافظ على ترتيب العناصر؛Queueتُحسّن الإضافة/الإزالة من كلا الطرفين.- اجمع بين
foldوwhereوmapوexpandلأنابيب تحويل بيانات قوية.