المجموعات: الخرائط والمجموعات
مقدمة إلى الخرائط والمجموعات
في الدرس السابق استكشفنا القوائم (Lists)، المجموعة المرتبة في Dart. الآن ننتقل إلى نوعين مهمين بنفس القدر: الخرائط (Maps) والمجموعات (Sets). تخزن الخرائط البيانات كأزواج مفتاح-قيمة، وهي مثالية للبحث والقواميس. تخزن المجموعات قيماً فريدة غير مرتبة، مثالية لاختبارات العضوية وإزالة التكرارات. مع القوائم تغطي هذه الأنواع الثلاثة تقريباً كل احتياج لهياكل البيانات في Dart.
الخرائط: أزواج مفتاح-قيمة
تربط Map المفاتيح بالقيم. كل مفتاح فريد ويرتبط بقيمة واحدة بالضبط. فكر فيها كقاموس حقيقي: تبحث عن كلمة (المفتاح) وتحصل على تعريفها (القيمة).
إنشاء الخرائط
هناك عدة طرق لإنشاء خريطة في Dart:
صيغة الخريطة الحرفية
void main() {
// خريطة حرفية مع تحديد النوع
Map<String, int> ages = {
'أحمد': 25,
'سارة': 30,
'علي': 22,
};
// خريطة حرفية مع استدلال النوع
var capitals = {
'السعودية': 'الرياض',
'مصر': 'القاهرة',
'الأردن': 'عمّان',
};
// خريطة فارغة مع أنواع صريحة
Map<String, double> prices = {};
// خريطة فارغة باستخدام المُنشئ
var scores = Map<String, int>();
print(ages); // {أحمد: 25, سارة: 30, علي: 22}
print(capitals); // {السعودية: الرياض, مصر: القاهرة, الأردن: عمّان}
}
var emptyMap = {}; يستدل Dart النوع كـ Map<dynamic, dynamic>. حدد النوع دائماً صراحة للخرائط الفارغة: Map<String, int> emptyMap = {}; أو var emptyMap = <String, int>{};.صيغة نوع Map<K, V>
النوع العام Map<K, V> يحدد نوع المفتاح K ونوع القيمة V. هذا يضمن أمان الأنواع في وقت الترجمة.
خرائط محددة الأنواع
Map<String, int> wordCount = {}; // مفاتيح String، قيم int
Map<int, String> idToName = {}; // مفاتيح int، قيم String
Map<String, List<String>> groups = {}; // مفاتيح String، قيم List
Map<String, Map<String, int>> nested = {}; // خرائط متداخلة
الوصول إلى إدخالات الخريطة وتعديلها
تستخدم صيغة الأقواس لقراءة وإضافة وتحديث وإزالة الإدخالات في الخريطة:
الوصول إلى قيم الخريطة
void main() {
var user = {
'name': 'إدريس',
'email': 'edrees@example.com',
'age': '28',
};
// قراءة قيمة
print(user['name']); // إدريس
print(user['email']); // edrees@example.com
// الوصول إلى مفتاح غير موجود يُرجع null
print(user['phone']); // null
// إضافة إدخال جديد
user['phone'] = '+966555555555';
print(user['phone']); // +966555555555
// تحديث إدخال موجود
user['age'] = '29';
print(user['age']); // 29
// إزالة إدخال
user.remove('email');
print(user); // {name: إدريس, age: 29, phone: +966555555555}
}
null وليس خطأ. هذا يعني أن نوع الإرجاع لـ map[key] دائماً قابل للـ null (V?). تحقق دائماً من null أو استخدم عامل ! فقط عندما تكون متأكداً أن المفتاح موجود.خصائص الخريطة الأساسية
خصائص الخريطة
void main() {
var fruits = {
'تفاح': 3,
'موز': 5,
'برتقال': 2,
};
print(fruits.length); // 3
print(fruits.isEmpty); // false
print(fruits.isNotEmpty); // true
print(fruits.keys); // (تفاح, موز, برتقال)
print(fruits.values); // (3, 5, 2)
print(fruits.entries); // (MapEntry(تفاح: 3), MapEntry(موز: 5), MapEntry(برتقال: 2))
}
دوال الخريطة المهمة
توفر فئة Map في Dart مجموعة غنية من الدوال للاستعلام وتحويل البيانات:
containsKey و containsValue
void main() {
var inventory = {'لابتوب': 10, 'هاتف': 25, 'تابلت': 8};
print(inventory.containsKey('لابتوب')); // true
print(inventory.containsKey('كمبيوتر')); // false
print(inventory.containsValue(25)); // true
print(inventory.containsValue(100)); // false
}
putIfAbsent
void main() {
var settings = {'theme': 'dark', 'language': 'ar'};
// يضيف فقط إذا لم يكن المفتاح موجوداً
settings.putIfAbsent('theme', () => 'light');
print(settings['theme']); // dark (لم يتغير، المفتاح موجود)
settings.putIfAbsent('fontSize', () => '14');
print(settings['fontSize']); // 14 (أُضيف لأن المفتاح كان غائباً)
print(settings); // {theme: dark, language: ar, fontSize: 14}
}
دالة update
void main() {
var stock = {'تفاح': 10, 'موز': 5};
// تحديث مفتاح موجود
stock.update('تفاح', (value) => value + 5);
print(stock['تفاح']); // 15
// تحديث مع ifAbsent -- يضيف المفتاح إذا لم يكن موجوداً
stock.update('عنب', (value) => value + 1, ifAbsent: () => 20);
print(stock['عنب']); // 20
print(stock); // {تفاح: 15, موز: 5, عنب: 20}
}
forEach على الخرائط
void main() {
var prices = {'قهوة': 15.0, 'شاي': 10.0, 'عصير': 12.5};
prices.forEach((key, value) {
print('$key يكلف $value ريال');
});
// قهوة يكلف 15.0 ريال
// شاي يكلف 10.0 ريال
// عصير يكلف 12.5 ريال
}
دالة map -- تحويل الخريطة
void main() {
var prices = {'قهوة': 15.0, 'شاي': 10.0, 'عصير': 12.5};
// تطبيق ضريبة 15% على جميع الأسعار
var withVat = prices.map((key, value) {
return MapEntry(key, value * 1.15);
});
print(withVat);
// {قهوة: 17.25, شاي: 11.5, عصير: 14.375}
}
دوال خريطة مفيدة أخرى
void main() {
var scores = {'أحمد': 85, 'سارة': 92, 'علي': 78, 'فاطمة': 95};
// addAll -- دمج خريطة أخرى
scores.addAll({'عمر': 88, 'نور': 91});
print(scores.length); // 6
// removeWhere -- إزالة الإدخالات المطابقة لشرط
scores.removeWhere((key, value) => value < 80);
print(scores); // {أحمد: 85, سارة: 92, فاطمة: 95, عمر: 88, نور: 91}
// clear -- إزالة جميع الإدخالات
scores.clear();
print(scores); // {}
}
التكرار عبر الخرائط
هناك عدة طرق للتكرار عبر خريطة:
طرق تكرار مختلفة
void main() {
var countries = {'SA': 'السعودية', 'EG': 'مصر', 'JO': 'الأردن'};
// 1. دالة forEach
countries.forEach((code, name) {
print('$code => $name');
});
// 2. for-in مع entries
for (var entry in countries.entries) {
print('${entry.key} => ${entry.value}');
}
// 3. التكرار على المفاتيح فقط
for (var code in countries.keys) {
print('الرمز: $code');
}
// 4. التكرار على القيم فقط
for (var name in countries.values) {
print('الدولة: $name');
}
}
الخرائط المتداخلة
يمكن أن تحتوي الخرائط على خرائط أخرى كقيم، وهذا شائع عند العمل مع بيانات منظمة مثل JSON:
مثال على الخرائط المتداخلة
void main() {
Map<String, Map<String, dynamic>> users = {
'user1': {
'name': 'أحمد',
'age': 25,
'address': {
'city': 'الرياض',
'country': 'السعودية',
},
},
'user2': {
'name': 'سارة',
'age': 30,
'address': {
'city': 'القاهرة',
'country': 'مصر',
},
},
};
// الوصول إلى القيم المتداخلة
print(users['user1']!['name']); // أحمد
var address = users['user1']!['address'] as Map;
print(address['city']); // الرياض
}
Map<String, dynamic> المتداخلة بعمق.المجموعات: مجموعات فريدة غير مرتبة
Set هي مجموعة من القيم الفريدة بدون ترتيب مضمون. على عكس القوائم، لا تحتوي المجموعة أبداً على عناصر مكررة. هذا يجعل المجموعات مثالية لتتبع العناصر الفريدة واختبارات العضوية وعمليات المجموعات الرياضية.
إنشاء المجموعات
إنشاء المجموعات
void main() {
// مجموعة حرفية مع تحديد النوع
Set<String> colors = {'أحمر', 'أخضر', 'أزرق'};
// مجموعة مع استدلال النوع
var numbers = {1, 2, 3, 4, 5};
// مجموعة فارغة (يجب تحديد النوع)
Set<int> emptySet = {};
var anotherEmpty = <String>{};
// مجموعة من قائمة (تزيل التكرارات)
var listWithDupes = [1, 2, 2, 3, 3, 3, 4];
var uniqueNumbers = listWithDupes.toSet();
print(uniqueNumbers); // {1, 2, 3, 4}
// مُنشئ Set.from
var fromList = Set<int>.from([10, 20, 20, 30]);
print(fromList); // {10, 20, 30}
}
var x = {}; ينشئ خريطة فارغة وليس مجموعة. لإنشاء مجموعة فارغة استخدم var x = <Type>{}; أو Set<Type> x = {};.إضافة وإزالة العناصر
تعديلات المجموعة
void main() {
var tags = <String>{'dart', 'flutter'};
// add -- يُرجع true إذا تمت إضافة العنصر (جديد)
bool added = tags.add('firebase');
print(added); // true
print(tags); // {dart, flutter, firebase}
// إضافة عنصر مكرر لا تفعل شيئاً
added = tags.add('dart');
print(added); // false (موجود بالفعل)
print(tags); // {dart, flutter, firebase}
// addAll -- إضافة عناصر متعددة
tags.addAll({'android', 'ios', 'dart'});
print(tags); // {dart, flutter, firebase, android, ios}
// remove -- يُرجع true إذا تمت الإزالة
tags.remove('android');
print(tags); // {dart, flutter, firebase, ios}
// removeWhere
tags.removeWhere((tag) => tag.length > 5);
print(tags); // {dart, ios}
}
عضوية المجموعة وخصائصها
التحقق من العضوية
void main() {
var permissions = {'read', 'write', 'execute'};
print(permissions.contains('read')); // true
print(permissions.contains('delete')); // false
print(permissions.length); // 3
print(permissions.isEmpty); // false
print(permissions.isNotEmpty); // true
// containsAll -- تحقق من وجود جميع العناصر
print(permissions.containsAll({'read', 'write'})); // true
print(permissions.containsAll({'read', 'admin'})); // false
}
contains() على المجموعة تعمل بزمن ثابت O(1)، بينما على القائمة تعمل بزمن خطي O(n). إذا كنت تتحقق كثيراً من وجود قيمة في مجموعة كبيرة، حوّلها إلى Set للحصول على أداء أفضل بكثير.عمليات المجموعات: الاتحاد والتقاطع والفرق
تدعم المجموعات عمليات المجموعات الرياضية الكلاسيكية، وهي مفيدة جداً لمقارنة ودمج البيانات:
عمليات المجموعات
void main() {
var frontend = {'HTML', 'CSS', 'JavaScript', 'Dart'};
var backend = {'PHP', 'Python', 'Dart', 'JavaScript'};
// الاتحاد -- جميع العناصر من كلتا المجموعتين (بدون تكرار)
var allLanguages = frontend.union(backend);
print(allLanguages);
// {HTML, CSS, JavaScript, Dart, PHP, Python}
// التقاطع -- العناصر الموجودة في كلتا المجموعتين
var shared = frontend.intersection(backend);
print(shared); // {JavaScript, Dart}
// الفرق -- العناصر في المجموعة الأولى وليست في الثانية
var frontendOnly = frontend.difference(backend);
print(frontendOnly); // {HTML, CSS}
var backendOnly = backend.difference(frontend);
print(backendOnly); // {PHP, Python}
}
إزالة التكرارات من القائمة
أحد أكثر الاستخدامات شيوعاً للمجموعات هو إزالة التكرارات من القائمة:
نمط إزالة التكرارات
void main() {
var votes = ['أحمر', 'أزرق', 'أحمر', 'أخضر', 'أزرق', 'أحمر', 'أخضر'];
// تحويل إلى Set لإزالة التكرارات ثم العودة إلى List
var uniqueVotes = votes.toSet().toList();
print(uniqueVotes); // [أحمر, أزرق, أخضر]
// عد المصوتين الفريدين
var voterIds = [101, 102, 101, 103, 102, 104];
var uniqueVoters = voterIds.toSet();
print('إجمالي الأصوات: ${voterIds.length}'); // 6
print('المصوتون الفريدون: ${uniqueVoters.length}'); // 4
}
أمثلة عملية
عدّاد الكلمات باستخدام الخرائط
عدّاد تكرار الكلمات
void main() {
var text = 'the cat sat on the mat the cat';
var words = text.split(' ');
Map<String, int> wordCount = {};
for (var word in words) {
wordCount.update(word, (count) => count + 1, ifAbsent: () => 1);
}
print(wordCount);
// {the: 3, cat: 2, sat: 1, on: 1, mat: 1}
// إيجاد الكلمة الأكثر تكراراً
var mostFrequent = wordCount.entries
.reduce((a, b) => a.value > b.value ? a : b);
print('الأكثر تكراراً: "${mostFrequent.key}" (${mostFrequent.value} مرات)');
// الأكثر تكراراً: "the" (3 مرات)
}
تخزين الإعدادات باستخدام الخرائط
إعدادات التطبيق
void main() {
Map<String, dynamic> config = {
'appName': 'تطبيقي',
'version': '2.1.0',
'maxRetries': 3,
'debugMode': false,
'supportedLocales': ['en', 'ar', 'fr'],
'api': {
'baseUrl': 'https://api.example.com',
'timeout': 30,
},
};
// وصول آمن مع فحص null
var appName = config['appName'] ?? 'غير معروف';
print(appName); // تطبيقي
// تحديث إعداد
config['debugMode'] = true;
// إضافة إعداد جديد فقط إذا لم يكن موجوداً
config.putIfAbsent('cacheEnabled', () => true);
print(config['cacheEnabled']); // true
}
تتبع العناصر الفريدة باستخدام المجموعات
زوار الصفحة الفريدون
void main() {
// محاكاة زيارات الصفحة (بعض المستخدمين يزورون عدة مرات)
var pageVisits = [
{'userId': 'u1', 'page': '/home'},
{'userId': 'u2', 'page': '/home'},
{'userId': 'u1', 'page': '/about'},
{'userId': 'u3', 'page': '/home'},
{'userId': 'u1', 'page': '/home'},
{'userId': 'u2', 'page': '/about'},
];
// الزوار الفريدون لكل صفحة
Map<String, Set<String>> uniqueVisitors = {};
for (var visit in pageVisits) {
var page = visit['page']!;
var user = visit['userId']!;
uniqueVisitors.putIfAbsent(page, () => <String>{});
uniqueVisitors[page]!.add(user);
}
uniqueVisitors.forEach((page, visitors) {
print('$page: ${visitors.length} زائر فريد $visitors');
});
// /home: 3 زائر فريد {u1, u2, u3}
// /about: 2 زائر فريد {u1, u2}
}
متى تستخدم Map مقابل Set مقابل List
اختيار نوع المجموعة الصحيح هو قرار تصميمي مهم:
- List -- استخدمها عندما يهم الترتيب، والتكرارات مسموحة، وتصل للعناصر بالفهرس. مثال: قائمة رسائل الدردشة، قائمة مهام مرتبة.
- Map -- استخدمها عندما تحتاج للبحث عن قيم بمفتاح فريد. مثال: ملفات المستخدمين بالمعرف، إعدادات التطبيق، عد تكرار الكلمات.
- Set -- استخدمها عندما تحتاج قيماً فريدة مع فحص عضوية سريع. مثال: تتبع العناصر التي شاهدها المستخدم، جمع الوسوم الفريدة، الصلاحيات.
Map<String, Set<String>> يربط المفاتيح بمجموعات من القيم الفريدة. List<Map<String, dynamic>> هي أساساً قائمة سجلات (مشابهة لنتائج قاعدة البيانات). اختر التوليفة التي تمثل بياناتك بأفضل شكل.ملخص
- الخرائط تخزن أزواج مفتاح-قيمة بصيغة
Map<K, V> - الوصول لقيم الخريطة بصيغة الأقواس:
map[key] - دوال الخريطة الرئيسية:
containsKey،containsValue،putIfAbsent،update،forEach،map،addAll،removeWhere - المجموعات تخزن قيماً فريدة بصيغة
Set<T> - دوال المجموعة الرئيسية:
add،remove،contains،union،intersection،difference - حوّل القائمة إلى مجموعة بـ
.toSet()لإزالة التكرارات - استخدم الخرائط للبحث، والمجموعات للتفرد، والقوائم للتسلسلات المرتبة
تمرين عملي
افتح DartPad وابنِ برنامج إدارة مخزون صغير. أنشئ Map<String, int> لمخزون المنتجات (مثلاً 'لابتوب': 10، 'هاتف': 25). اكتب دوال لـ: (1) إضافة مخزون باستخدام update مع ifAbsent، (2) إزالة منتج، (3) عرض جميع المنتجات التي مخزونها أقل من 5. ثم أنشئ Set<String> من التصنيفات. أضف تصنيفات، تحقق من العضوية، واحسب اتحاد مجموعتي تصنيفات. اطبع نتائج كل عملية.