المقالات والمعرفة

اختيار المجموعات في Dart: من نموذج البيانات إلى خط معالجة قابل للاختبار

دليل عملي لاختيار List وSet وMap، وفهم Iterable الكسول، وبناء تحويلات واضحة يمكن التحقق منها بدل الاعتماد على وصفات عامة.

نطاق المقالة

هذه المقالة لا تحاول حفظ واجهات List وSet وMap. الهدف هو تحويل متطلبات المجال إلى اختيار يمكن تفسيره واختباره. الأمثلة تستهدف Dart 3، وقد شُغّل البرنامج الكامل أدناه باستخدام Dart SDK 3.12.1.

المتطلبات المسبقة:

  • تثبيت Dart SDK؛
  • معرفة الأنواع العامة مثل List<String>؛
  • حفظ المثال في ملف ثم تشغيله بالأمر dart run example.dart.

ابدأ بالعقد لا باسم البنية

تدعم Dart القوائم والمجموعات والخرائط دعمًا مباشرًا. القائمة ترتيب من العناصر، والمجموعة تمنع التكرار، والخريطة تربط كل مفتاح بقيمة. هذا الوصف البسيط يقود إلى أسئلة عملية:

السؤالالاختيار الأوليالسبب
هل ترتيب الإدخال والتكرار جزء من المعنى؟List<T>تحتفظ بالعناصر وفق ترتيب يمكن الوصول إليه بالفهرس.
هل المطلوب عضوية فريدة فقط؟Set<T>يمثل كل عنصر مرة واحدة وفق مساواة النوع.
هل نصل إلى قيمة بواسطة مفتاح فريد؟Map<K, V>يفصل هوية العنصر، أي المفتاح، عن البيانات المرتبطة به.

لا يكفي أن تكون البنية «أسرع» في حالة عامة. إذا كان ترتيب السجلات مطلوبًا في المخرجات مثلًا، فإن تحويلها مبكرًا إلى بنية لا يمثّل عقدها ترتيبًا واضحًا يضيف اعتمادًا خفيًا. اختر أولًا ما يحفظ معنى البيانات، ثم قِس الأداء على حجم حقيقي.

Iterable خط معالجة، وليس بالضرورة نتيجة جاهزة

تتعامل عمليات مثل where وmap مع Iterable. يمكن تركيبها قبل تحويل النتيجة إلى قائمة. الفائدة ليست تقليل عدد الأسطر، بل فصل المراحل:

1. التحقق من البيانات؛

2. التصفية؛

3. التحويل؛

4. التجميع أو إنتاج بنية نهائية.

ينبغي استدعاء toList() عندما يحتاج المستهلك إلى لقطة فعلية قابلة للفهرسة أو عندما لا نريد إعادة حساب السلسلة. توصي إرشادات Effective Dart باستخدام toList() لنسخ iterable مع الحفاظ على نوع عناصره، وعدم استخدام List.from() إلا عندما يكون تغيير النوع مقصودًا.

مثال قابل للتنفيذ: تلخيص طلبات مكتملة

يفصل المثال بين قائمة الطلبات الأصلية، ومجموعة المعرّفات المقبولة، وخريطة الإجماليات حسب العميل:

typedef Order = ({String id, String customer, int amount, bool completed});

Map<String, int> totalsByCustomer(Iterable<Order> orders) {
  final acceptedIds = <String>{};
  final totals = <String, int>{};

  for (final order in orders.where((order) => order.completed)) {
    if (order.amount < 0) {
      throw ArgumentError.value(order.amount, 'amount', 'must not be negative');
    }

    if (!acceptedIds.add(order.id)) {
      continue;
    }

    totals.update(
      order.customer,
      (current) => current + order.amount,
      ifAbsent: () => order.amount,
    );
  }

  return Map.unmodifiable(totals);
}

void main() {
  final orders = <Order>[
    (id: 'o-1', customer: 'A', amount: 40, completed: true),
    (id: 'o-2', customer: 'B', amount: 15, completed: false),
    (id: 'o-1', customer: 'A', amount: 40, completed: true),
    (id: 'o-3', customer: 'A', amount: 10, completed: true),
  ];

  final result = totalsByCustomer(orders);

  assert(result.length == 1);
  assert(result['A'] == 50);
  print(result); // {A: 50}
}

تؤدي كل بنية دورًا مختلفًا:

  • List<Order> تحفظ عينة الإدخال بما فيها الطلب المكرر وغير المكتمل؛
  • Set<String> يجعل قرار قبول المعرّف المكرر صريحًا؛
  • Map<String, int> يعبّر عن التجميع بحسب العميل؛
  • Map.unmodifiable يمنع المستهلك من تعديل النتيجة بعد عودتها.

حالات حدية يجب اختبارها

المثال البسيط لا يغطي جميع سياسات النظام. قبل استخدامه في إنتاج حقيقي، أضف اختبارات لما يلي:

  • إدخال فارغ يعيد خريطة فارغة؛
  • كل الطلبات غير مكتملة؛
  • تكرار المعرّف لعميل آخر: هل يُرفض أم يعد خطأ بيانات؟
  • مبلغ سالب، وهو مرفوض هنا بـ ArgumentError؛
  • مجموع يتجاوز نطاقًا يفرضه النظام، حتى لو كان نوع int نفسه يدعم القيمة؛
  • الحاجة إلى ترتيب ثابت في التقرير النهائي.

إذا كانت هوية التكرار مركبة، فلا تستخدم نصًا موصولًا مثل '$customer:$id' دون عقد واضح؛ استخدم record أو نوع قيمة يطبق المساواة المقصودة.

أخطاء شائعة

إنشاء مجموعة فارغة بلا نوع

التعبير {} ينشئ Map<dynamic, dynamic>، لا Set. اكتب <String>{} عندما تريد مجموعة نصوص فارغة.

فحص الفراغ بواسطة length

استخدم isEmpty وisNotEmpty. هذا أوضح، ولا يفرض على كل Iterable حساب طول لا تحتاجه.

استخدام forEach عندما تحتاج تحكمًا في التدفق

تفضّل إرشادات Dart حلقة for-in على Iterable.forEach() مع دالة حرفية. الحلقة أوضح عندما نحتاج continue أو break أو معالجة استثناء في مستوى محدد.

إخفاء مشكلة النوع بواسطة cast()

يفضل إنشاء المجموعة بالنوع الصحيح أو استخدام whereType<T>() عند تصفية عناصر مختلطة. قد يؤجل cast() الخطأ إلى لحظة الوصول إلى عنصر بدل كشفه قرب مصدر البيانات.

كيف تتحقق بدل التخمين؟

1. اكتب عقدًا قصيرًا: هل الترتيب والتكرار والمفتاح جزء من المعنى؟

2. أنشئ مثالًا صغيرًا يتضمن حالة نجاح وفشل وحدًا طرفيًا.

3. شغّل dart analyze وdart run.

4. اكتب benchmark فقط إذا كان الحجم أو الكمون مشكلة مقاسة؛ لا تستنتج تعقيدًا عمليًا من اسم البنية وحده.

5. حوّل النتيجة إلى بنية غير قابلة للتعديل إذا كان ذلك جزءًا من واجهة الدالة.

القيود

لا تقارن هذه المقالة هياكل dart:collection المتخصصة، ولا تقيس الذاكرة أو الزمن، ولا تدّعي أن بنية واحدة أفضل لكل حالة. القياسات تعتمد على حجم البيانات ونمط الوصول ونسخة المنصة.

المراجع

سجل التحديث

  • 2026-10-09: اعتُمد النشر من صاحب الموقع، وأُعيد تحليل وتشغيل البرنامج على Dart 3.12.1 مع خمس حالات حدية ناجحة. هذا تحقق برمجي للمثال، وليس مراجعة لغوية مستقلة أو اختبارًا لكامل مخطوط الكتاب.
  • 2026-10-04: إعادة بناء المقالة كدليل تطبيقي، وإضافة برنامج شُغّل على Dart 3.12.1، وحالات حدية، وقيود، ومراجع أصلية. ما تزال المراجعة التقنية واللغوية مطلوبة قبل النشر.

ساعد في تحسين هذا المحتوى

هل كان المحتوى مفيدًا؟
اقتراح تصحيح أو تعديل

مساهمتك خاصة بالمراجعة والتحسين وتعالج عبر Google Firebase وFirestore. مدة الاحتفاظ المقترحة 12 شهرًا من الإنشاء، والحذف الآلي غير مفعّل في المعاينة. يمكنك طلب حذف بيانات الاتصال عبر نموذج التواصل. اقرأ سياسة الخصوصية وشروط الاستخدام.