أساسيات برمجة Dart

المجموعات: القوائم

40 دقيقة الدرس 9 من 13

مقدمة في القوائم في Dart

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

ملاحظة: في Dart، جميع القوائم هي كائنات من الفئة List<T>، حيث T هو نوع العناصر التي تحتويها القائمة. لا يوجد في Dart نوع منفصل يسمى "مصفوفة" — القوائم هي المجموعة المرتبة الأساسية.

إنشاء القوائم

هناك عدة طرق لإنشاء القوائم في Dart. دعونا نستكشف كل طريقة.

القوائم الحرفية (القابلة للنمو)

الطريقة الأكثر شيوعًا لإنشاء قائمة هي استخدام صيغة الأقواس المربعة. بشكل افتراضي، هذه القوائم قابلة للنمو، مما يعني أنه يمكنك إضافة أو إزالة عناصر بعد الإنشاء.

إنشاء قوائم قابلة للنمو

void main() {
  // يتم استنتاج النوع كـ List<int>
  var numbers = [1, 2, 3, 4, 5];
  print(numbers); // [1, 2, 3, 4, 5]

  // تحديد النوع صراحة
  List<String> fruits = ['Apple', 'Banana', 'Cherry'];
  print(fruits); // [Apple, Banana, Cherry]

  // قائمة فارغة مع تحديد النوع
  List<double> prices = [];
  prices.add(9.99);
  prices.add(14.50);
  print(prices); // [9.99, 14.5]

  // أنواع مختلطة باستخدام dynamic (غير مستحسن)
  List<dynamic> mixed = [1, 'hello', true, 3.14];
  print(mixed); // [1, hello, true, 3.14]
}

القوائم ثابتة الطول

يمكنك إنشاء قائمة بحجم ثابت باستخدام List.filled(). القوائم ثابتة الطول لا يمكن أن تنمو أو تتقلص — يمكنك فقط تغيير القيم في المواضع الموجودة.

القوائم ثابتة الطول

void main() {
  // إنشاء قائمة ثابتة من 5 عناصر، جميعها مُهيأة بـ 0
  var fixedList = List<int>.filled(5, 0);
  print(fixedList); // [0, 0, 0, 0, 0]

  // تعديل العناصر حسب الفهرس
  fixedList[0] = 10;
  fixedList[1] = 20;
  fixedList[4] = 50;
  print(fixedList); // [10, 20, 0, 0, 50]

  // fixedList.add(60); // خطأ! لا يمكن الإضافة إلى قائمة ثابتة الطول
  // fixedList.removeAt(0); // خطأ! لا يمكن الإزالة من قائمة ثابتة الطول

  // قائمة ثابتة من النصوص
  var names = List<String>.filled(3, '');
  names[0] = 'Ahmed';
  names[1] = 'Sara';
  names[2] = 'Omar';
  print(names); // [Ahmed, Sara, Omar]
}
تحذير: المُنشئ القديم List(n) لإنشاء قوائم ثابتة الطول مهمل في Dart 2. استخدم دائمًا List.filled(length, defaultValue) أو List.generate() بدلاً من ذلك.

الوصول إلى العناصر عبر الفهرس

يتم الوصول إلى عناصر القائمة باستخدام الفهرسة المبنية على الصفر. العنصر الأول في الفهرس 0، والثاني في الفهرس 1، وهكذا.

الوصول عبر الفهرس

void main() {
  var colors = ['Red', 'Green', 'Blue', 'Yellow', 'Purple'];

  print(colors[0]);   // Red (العنصر الأول)
  print(colors[2]);   // Blue (العنصر الثالث)
  print(colors[4]);   // Purple (العنصر الأخير)

  // الوصول إلى العنصر الأول والأخير
  print(colors.first); // Red
  print(colors.last);  // Purple

  // تعديل عنصر
  colors[1] = 'Lime';
  print(colors); // [Red, Lime, Blue, Yellow, Purple]

  // الحصول على الطول
  print(colors.length); // 5

  // الوصول إلى العنصر الأخير باستخدام الطول
  print(colors[colors.length - 1]); // Purple
}
تحذير: الوصول إلى فهرس خارج النطاق (مثل colors[10] في قائمة من 5 عناصر) سيطرح RangeError في وقت التشغيل. تحقق دائمًا من طول القائمة قبل الوصول عبر الفهرس إذا لم تكن متأكدًا.

إضافة العناصر

تدعم القوائم القابلة للنمو عدة طرق لإضافة عناصر جديدة.

إضافة عناصر إلى القائمة

void main() {
  var languages = ['Dart', 'Python'];

  // add() - إضافة عنصر واحد في النهاية
  languages.add('JavaScript');
  print(languages); // [Dart, Python, JavaScript]

  // addAll() - إضافة جميع العناصر من مجموعة أخرى
  languages.addAll(['Go', 'Rust']);
  print(languages); // [Dart, Python, JavaScript, Go, Rust]

  // insert() - إدراج في فهرس محدد
  languages.insert(1, 'Java');
  print(languages); // [Dart, Java, Python, JavaScript, Go, Rust]

  // insertAll() - إدراج عناصر متعددة في فهرس محدد
  languages.insertAll(3, ['C++', 'Swift']);
  print(languages);
  // [Dart, Java, Python, C++, Swift, JavaScript, Go, Rust]
}

إزالة العناصر

هناك طرق متعددة لإزالة العناصر من قائمة قابلة للنمو.

إزالة العناصر من القائمة

void main() {
  var items = ['Apple', 'Banana', 'Cherry', 'Date', 'Elderberry'];

  // remove() - إزالة أول ظهور لقيمة
  items.remove('Cherry');
  print(items); // [Apple, Banana, Date, Elderberry]

  // removeAt() - إزالة العنصر في فهرس محدد
  var removed = items.removeAt(0);
  print(removed); // Apple
  print(items);   // [Banana, Date, Elderberry]

  // removeLast() - إزالة وإرجاع العنصر الأخير
  var last = items.removeLast();
  print(last);  // Elderberry
  print(items); // [Banana, Date]

  // removeRange() - إزالة مجموعة من العناصر
  var numbers = [1, 2, 3, 4, 5, 6, 7];
  numbers.removeRange(2, 5); // إزالة الفهرس من 2 إلى 4
  print(numbers); // [1, 2, 6, 7]

  // removeWhere() - إزالة العناصر المطابقة لشرط
  var scores = [85, 42, 91, 37, 78, 55];
  scores.removeWhere((score) => score < 50);
  print(scores); // [85, 91, 78, 55]

  // clear() - إزالة جميع العناصر
  scores.clear();
  print(scores); // []
}

طرق القائمة الأساسية

تأتي قوائم Dart مع مجموعة واسعة من الطرق. إليك أهمها التي ستستخدمها بانتظام.

البحث والتحقق

طرق البحث

void main() {
  var fruits = ['Apple', 'Banana', 'Cherry', 'Apple', 'Date'];

  // contains() - التحقق من وجود عنصر
  print(fruits.contains('Banana')); // true
  print(fruits.contains('Mango'));  // false

  // indexOf() - إيجاد أول فهرس لعنصر
  print(fruits.indexOf('Apple'));   // 0
  print(fruits.indexOf('Mango'));   // -1 (غير موجود)

  // lastIndexOf() - إيجاد آخر فهرس لعنصر
  print(fruits.lastIndexOf('Apple')); // 3

  // isEmpty و isNotEmpty
  print(fruits.isEmpty);    // false
  print(fruits.isNotEmpty); // true

  // length
  print(fruits.length); // 5

  // any() - التحقق مما إذا كان أي عنصر يطابق شرطًا
  print(fruits.any((f) => f.startsWith('C'))); // true

  // every() - التحقق مما إذا كانت جميع العناصر تطابق شرطًا
  print(fruits.every((f) => f.length > 2)); // true
}

الترتيب

ترتيب القوائم

void main() {
  // sort() - الترتيب في المكان (يعدل القائمة الأصلية)
  var numbers = [5, 2, 8, 1, 9, 3];
  numbers.sort();
  print(numbers); // [1, 2, 3, 5, 8, 9]

  // ترتيب النصوص أبجديًا
  var names = ['Charlie', 'Alice', 'Bob', 'David'];
  names.sort();
  print(names); // [Alice, Bob, Charlie, David]

  // ترتيب مخصص مع مقارن
  var words = ['banana', 'apple', 'cherry', 'date'];
  words.sort((a, b) => a.length.compareTo(b.length));
  print(words); // [date, apple, banana, cherry]

  // ترتيب تنازلي
  var scores = [75, 92, 88, 64, 95];
  scores.sort((a, b) => b.compareTo(a));
  print(scores); // [95, 92, 88, 75, 64]
}

العكس والقائمة الفرعية

العكس والقائمة الفرعية

void main() {
  var letters = ['A', 'B', 'C', 'D', 'E'];

  // reversed - يرجع Iterable (وليس List)
  var reversedLetters = letters.reversed;
  print(reversedLetters); // (E, D, C, B, A)

  // تحويل Iterable المعكوس إلى List
  var reversedList = letters.reversed.toList();
  print(reversedList); // [E, D, C, B, A]

  // sublist() - استخراج جزء من القائمة
  var subset1 = letters.sublist(1, 4); // من الفهرس 1 إلى 3
  print(subset1); // [B, C, D]

  var subset2 = letters.sublist(2); // من الفهرس 2 إلى النهاية
  print(subset2); // [C, D, E]

  // ملاحظة: sublist ينشئ قائمة جديدة (وليس عرضًا)
  subset1[0] = 'Z';
  print(letters); // [A, B, C, D, E] (لم تتغير)
}

التكرار عبر القوائم

يوفر Dart عدة طرق للتكرار عبر عناصر القائمة.

طرق التكرار

void main() {
  var cities = ['Riyadh', 'Dubai', 'Cairo', 'Istanbul'];

  // 1. حلقة for-in (الأكثر شيوعًا)
  for (var city in cities) {
    print(city);
  }

  // 2. حلقة for التقليدية (عندما تحتاج الفهرس)
  for (int i = 0; i < cities.length; i++) {
    print('$i: ${cities[i]}');
  }

  // 3. طريقة forEach
  cities.forEach((city) {
    print('City: $city');
  });

  // 4. forEach مع المرجع المباشر
  cities.forEach(print);

  // 5. استخدام asMap() للحصول على الفهرس والقيمة
  cities.asMap().forEach((index, city) {
    print('$index: $city');
  });

  // 6. استخدام indexed (Dart 3+)
  for (var (index, city) in cities.indexed) {
    print('$index: $city');
  }
}
نصيحة: استخدم for-in عندما تحتاج القيم فقط. استخدم حلقة for التقليدية عندما تحتاج الفهرس. استخدم forEach للعمليات البسيطة ذات السطر الواحد. تجنب forEach عندما تحتاج استخدام break أو return — فهي لا تدعمهما.

عامل النشر (...)

يسمح عامل النشر ... بفك جميع عناصر قائمة داخل قائمة أخرى. وهو مفيد جدًا لدمج القوائم.

عامل النشر

void main() {
  var first = [1, 2, 3];
  var second = [4, 5, 6];

  // دمج قائمتين
  var combined = [...first, ...second];
  print(combined); // [1, 2, 3, 4, 5, 6]

  // إضافة عناصر قبل وبين وبعد
  var full = [0, ...first, 99, ...second, 100];
  print(full); // [0, 1, 2, 3, 99, 4, 5, 6, 100]

  // عامل النشر الآمن من القيم الفارغة (...?)
  List<int>? maybeNull;
  var safe = [1, 2, ...?maybeNull, 3];
  print(safe); // [1, 2, 3]

  // مفيد لتضمين القوائم بشكل مشروط
  maybeNull = [10, 20];
  var withValues = [1, 2, ...?maybeNull, 3];
  print(withValues); // [1, 2, 10, 20, 3]
}

المجموعة الشرطية والمجموعة التكرارية

يدعم Dart العناصر الشرطية والحلقات مباشرة داخل القوائم الحرفية. تسمى هذه collection if و collection for، وتجعل بناء القوائم معبرًا جدًا.

Collection if

العناصر الشرطية في القوائم

void main() {
  bool isLoggedIn = true;
  bool isAdmin = false;

  var menu = [
    'Home',
    'About',
    if (isLoggedIn) 'Profile',
    if (isLoggedIn) 'Settings',
    if (isAdmin) 'Admin Panel',
    'Contact',
  ];

  print(menu); // [Home, About, Profile, Settings, Contact]

  // collection if-else
  var greeting = [
    if (isLoggedIn) 'Welcome back!' else 'Please log in',
  ];
  print(greeting); // [Welcome back!]
}

Collection for

عناصر الحلقة في القوائم

void main() {
  // توليد قائمة من الأرقام المربعة
  var squares = [
    for (int i = 1; i <= 5; i++) i * i,
  ];
  print(squares); // [1, 4, 9, 16, 25]

  // تحويل قائمة إلى أخرى
  var names = ['alice', 'bob', 'charlie'];
  var uppercased = [
    for (var name in names) name.toUpperCase(),
  ];
  print(uppercased); // [ALICE, BOB, CHARLIE]

  // دمج collection if و for
  var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
  var evenSquares = [
    for (var n in numbers)
      if (n.isEven) n * n,
  ];
  print(evenSquares); // [4, 16, 36, 64, 100]
}
نصيحة: يتم حل collection if و collection for في وقت بناء القائمة. فهي ليست مجرد تجميل بناء الجمل للحلقات — إنها تتكامل مباشرة في بناء القائمة الحرفية وهي فعالة جدًا.

القوائم غير القابلة للتعديل

أحيانًا تريد قائمة لا يمكن تعديلها بعد الإنشاء. يوفر Dart عدة طرق لإنشاء قوائم غير قابلة للتعديل (للقراءة فقط).

إنشاء قوائم غير قابلة للتعديل

void main() {
  // استخدام كلمة const - ثابت وقت الترجمة
  const colors = ['Red', 'Green', 'Blue'];
  // colors.add('Yellow'); // خطأ! لا يمكن تعديل قائمة const
  // colors[0] = 'Pink';   // خطأ! لا يمكن تعديل قائمة const

  // قائمة const مُسندة إلى متغير var
  var moreColors = const ['Cyan', 'Magenta', 'Yellow'];
  // moreColors.add('Black'); // خطأ في وقت التشغيل!

  // لكن يمكنك إعادة إسناد المتغير نفسه
  moreColors = ['Black', 'White']; // حسنًا - قائمة قابلة للنمو جديدة
  moreColors.add('Gray');            // حسنًا - هذه القائمة قابلة للنمو

  // List.unmodifiable() - غير قابل للتعديل في وقت التشغيل
  var original = [1, 2, 3, 4, 5];
  var unmodifiable = List<int>.unmodifiable(original);
  // unmodifiable.add(6);   // خطأ! عملية غير مدعومة
  // unmodifiable[0] = 99;  // خطأ! عملية غير مدعومة

  // القائمة الأصلية لا تزال قابلة للتعديل
  original.add(6);
  print(original);     // [1, 2, 3, 4, 5, 6]
  print(unmodifiable); // [1, 2, 3, 4, 5] (لقطة في وقت الإنشاء)
}
ملاحظة: قوائم const هي ثوابت وقت الترجمة وغير قابلة للتعديل بشكل عميق. List.unmodifiable() ينشئ عرضًا غير قابل للتعديل في وقت التشغيل. استخدم const عندما تكون القيم معروفة في وقت الترجمة، و List.unmodifiable() عندما تريد تجميد قائمة تم بناؤها ديناميكيًا.

List.generate()

المُنشئ List.generate() ينشئ قائمة بطول معين مع قيم محسوبة بواسطة دالة. وهو مفيد جدًا لتهيئة القوائم بأنماط.

توليد القوائم

void main() {
  // توليد قائمة من 5 عناصر: الفهرس * 2
  var doubles = List<int>.generate(5, (index) => index * 2);
  print(doubles); // [0, 2, 4, 6, 8]

  // توليد جدول الضرب للعدد 7
  var table = List<String>.generate(
    10,
    (i) => '7 x ${i + 1} = ${7 * (i + 1)}',
  );
  for (var row in table) {
    print(row);
  }
  // 7 x 1 = 7
  // 7 x 2 = 14
  // ... وهكذا

  // توليد قائمة من القوائم الفارغة (هيكل ثنائي الأبعاد)
  var grid = List<List<int>>.generate(3, (_) => []);
  grid[0].addAll([1, 2, 3]);
  grid[1].addAll([4, 5, 6]);
  grid[2].addAll([7, 8, 9]);
  print(grid); // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

  // قائمة مولدة ثابتة الطول
  var fixed = List<int>.generate(5, (i) => i * 10, growable: false);
  print(fixed); // [0, 10, 20, 30, 40]
  // fixed.add(50); // خطأ! لا يمكن الإضافة إلى قائمة ثابتة الطول
}

أنماط شائعة: التصفية والتحويل والاختزال

تدعم قوائم Dart عمليات وظيفية قوية تتيح لك تحويل البيانات بدون كتابة حلقات صريحة.

التصفية باستخدام where()

تصفية القوائم

void main() {
  var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

  // where() يرجع Iterable كسول - حوّله إلى List بـ toList()
  var evens = numbers.where((n) => n.isEven).toList();
  print(evens); // [2, 4, 6, 8, 10]

  var greaterThan5 = numbers.where((n) => n > 5).toList();
  print(greaterThan5); // [6, 7, 8, 9, 10]

  // firstWhere() - إيجاد أول عنصر مطابق
  var firstEven = numbers.firstWhere((n) => n.isEven);
  print(firstEven); // 2

  // firstWhere مع orElse للأمان
  var bigNumber = numbers.firstWhere(
    (n) => n > 100,
    orElse: () => -1,
  );
  print(bigNumber); // -1
}

التحويل باستخدام map()

تحويل القوائم

void main() {
  var numbers = [1, 2, 3, 4, 5];

  // map() يحوّل كل عنصر
  var doubled = numbers.map((n) => n * 2).toList();
  print(doubled); // [2, 4, 6, 8, 10]

  var names = ['alice', 'bob', 'charlie'];
  var capitalized = names.map((name) {
    return name[0].toUpperCase() + name.substring(1);
  }).toList();
  print(capitalized); // [Alice, Bob, Charlie]

  // سلسلة where و map
  var students = [
    {'name': 'Ahmed', 'grade': 92},
    {'name': 'Sara', 'grade': 45},
    {'name': 'Omar', 'grade': 88},
    {'name': 'Layla', 'grade': 37},
  ];

  var passingNames = students
      .where((s) => (s['grade'] as int) >= 50)
      .map((s) => s['name'])
      .toList();
  print(passingNames); // [Ahmed, Omar]
}

الاختزال باستخدام reduce() و fold()

اختزال القوائم إلى قيمة واحدة

void main() {
  var numbers = [10, 20, 30, 40, 50];

  // reduce() - يدمج جميع العناصر في قيمة واحدة
  var sum = numbers.reduce((a, b) => a + b);
  print(sum); // 150

  var max = numbers.reduce((a, b) => a > b ? a : b);
  print(max); // 50

  // fold() - مثل reduce لكن مع قيمة ابتدائية
  // أكثر أمانًا للقوائم الفارغة لأن reduce يطرح خطأ على القوائم الفارغة
  var total = numbers.fold<int>(0, (sum, n) => sum + n);
  print(total); // 150

  // fold مع نوع إرجاع مختلف
  var words = ['Hello', 'Dart', 'World'];
  var sentence = words.fold<String>('', (result, word) {
    return result.isEmpty ? word : '$result $word';
  });
  print(sentence); // Hello Dart World

  // join() - طريقة أبسط لدمج النصوص
  var joined = words.join(' ');
  print(joined); // Hello Dart World

  // حساب المتوسط
  var scores = [85, 90, 78, 92, 88];
  var average = scores.fold<double>(0, (sum, s) => sum + s) / scores.length;
  print(average); // 86.6
}
نصيحة: فضّل fold() على reduce() عندما قد تكون القائمة فارغة. reduce() يطرح StateError على قائمة فارغة، بينما fold() يرجع ببساطة القيمة الابتدائية. استخدم أيضًا fold() عندما يختلف نوع الإرجاع عن نوع العنصر.

حيل مفيدة للقوائم

عمليات عملية على القوائم

void main() {
  // إزالة التكرارات باستخدام toSet()
  var withDuplicates = [1, 2, 3, 2, 4, 1, 5, 3];
  var unique = withDuplicates.toSet().toList();
  print(unique); // [1, 2, 3, 4, 5]

  // تسطيح قائمة من القوائم باستخدام expand()
  var nested = [[1, 2], [3, 4], [5, 6]];
  var flat = nested.expand((list) => list).toList();
  print(flat); // [1, 2, 3, 4, 5, 6]

  // take و skip
  var items = ['a', 'b', 'c', 'd', 'e', 'f'];
  print(items.take(3).toList());  // [a, b, c]
  print(items.skip(3).toList());  // [d, e, f]

  // تبديل عنصرين
  var list = [10, 20, 30, 40];
  var temp = list[0];
  list[0] = list[3];
  list[3] = temp;
  print(list); // [40, 20, 30, 10]

  // نسخ قائمة (نسخ سطحي)
  var original = [1, 2, 3];
  var copy = [...original]; // أو List.from(original) أو .toList()
  copy.add(4);
  print(original); // [1, 2, 3]
  print(copy);     // [1, 2, 3, 4]

  // التحقق من تساوي القوائم (عنصر بعنصر)
  // import 'package:collection/collection.dart';
  // ListEquality().equals([1, 2], [1, 2]); // true
}

تمرين تطبيقي

أنشئ برنامج Dart يدير قائمة تسوق. يجب أن يقوم برنامجك بما يلي:

  1. إنشاء قائمة من 5 مواد بقالة.
  2. إضافة عنصرين آخرين في نهاية القائمة.
  3. إدراج عنصر واحد في البداية (الفهرس 0).
  4. إزالة العنصر الثالث من القائمة.
  5. ترتيب القائمة أبجديًا.
  6. استخدام map() لإنشاء قائمة جديدة حيث يكون كل عنصر مسبوقًا برقم (مثل "1. Milk"، "2. Eggs").
  7. طباعة القائمة المرقمة النهائية.
  8. استخدام where() لتصفية العناصر التي تبدأ بحرف معين وطباعة النتيجة.

حاول استخدام collection if لإضافة عنصر "Discount Coupon" بشكل مشروط فقط إذا كانت القائمة تحتوي على أكثر من 5 عناصر.