ميزات Dart المتقدمة

المجموعات والعناصر القابلة للتكرار المتقدمة

50 دقيقة الدرس 10 من 16

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]
}
نصيحة: استخدم iterables الكسولة عندما تحتاج فقط مجموعة فرعية من النتائج. تجنب استدعاء .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 لأنابيب تحويل بيانات قوية.