التعدادات والثوابت
لماذا الثوابت والتعدادات مهمة؟
في كل تطبيق، هناك قيم لا يجب أن تتغير أبداً -- معدل الضريبة، الحد الأقصى لمحاولات تسجيل الدخول، أو مجموعة من الحالات المحددة مسبقاً مثل "نشط" و"غير نشط" و"محظور". يمنحك Dart أدوات قوية لتمثيل هذه القيم الثابتة: الثوابت (const و final) والتعدادات (enums). استخدامها بشكل صحيح يجعل كودك أكثر أماناً وقابلية للقراءة وأسهل في الصيانة.
الكلمة المفتاحية const -- ثوابت وقت الترجمة
متغير const هو ثابت وقت الترجمة. يجب أن تكون قيمته معروفة وثابتة قبل أن يعمل البرنامج حتى. يستبدل مترجم Dart كل إشارة إلى const بقيمتها الفعلية أثناء الترجمة.
الاستخدام الأساسي لـ const
void main() {
const double pi = 3.14159;
const int maxAttempts = 5;
const String appName = 'MyDartApp';
print(pi); // 3.14159
print(maxAttempts); // 5
print(appName); // MyDartApp
// خطأ: لا يمكن تغيير متغير const
// pi = 3.14; // خطأ وقت الترجمة!
}
const يجب أن تكون قابلة للتحديد وقت الترجمة. لا يمكنك تعيين نتيجة استدعاء دالة، أو إدخال مستخدم، أو أي شيء يعتمد على وقت التشغيل لمتغير const.ما الذي يمكن أن يكون const؟
void main() {
// هذه قيم const صالحة:
const a = 42; // رقم حرفي
const b = 'hello'; // نص حرفي
const c = true; // قيمة منطقية حرفية
const d = 3 * 7; // عملية حسابية وقت الترجمة
const e = 'Hello, $b world'; // استيفاء نص وقت الترجمة (إذا كان b ثابت)
// هذه ليست صالحة كـ const:
// const f = DateTime.now(); // خطأ: قيمة وقت التشغيل
// const g = int.parse('42'); // خطأ: استدعاء دالة وقت التشغيل
}
الكلمة المفتاحية final -- ثوابت وقت التشغيل
يمكن تعيين متغير final مرة واحدة فقط، لكن يمكن تحديد قيمته في وقت التشغيل. بمجرد تعيينه، لا يمكن تغييره. فكر في final على أنه "عيّن مرة واحدة، اقرأ إلى الأبد".
الاستخدام الأساسي لـ final
void main() {
final DateTime now = DateTime.now(); // يُعيّن وقت التشغيل
final String greeting = 'Hello at ${now.hour}:${now.minute}';
print(now); // 2026-03-12 14:30:00.000 (مثال)
print(greeting); // Hello at 14:30
// خطأ: لا يمكن إعادة تعيين متغير final
// now = DateTime.now(); // خطأ وقت الترجمة!
}
final مقابل var -- الفرق
void main() {
var name = 'Alice';
name = 'Bob'; // مسموح: var يمكن إعادة تعيينه
final city = 'Riyadh';
// city = 'Dubai'; // خطأ: final لا يمكن إعادة تعيينه
// final لا يزال يسمح بتعديل الكائن نفسه
final List<int> numbers = [1, 2, 3];
numbers.add(4); // مسموح: تعديل محتويات القائمة
print(numbers); // [1, 2, 3, 4]
// numbers = [5, 6]; // خطأ: لا يمكن إعادة تعيين المتغير
}
final ليست غير قابلة للتغيير -- لا يزال بإمكانك إضافة أو إزالة أو تغيير العناصر. المتغير فقط لا يمكن أن يشير إلى قائمة مختلفة. إذا أردت مجموعة غير قابلة للتغيير حقاً، استخدم const.const مقابل final -- الفروق الرئيسية
فهم متى تستخدم const مقابل final هو أحد أهم القرارات في برمجة Dart.
مقارنة جنباً إلى جنب
void main() {
// const: ثابت وقت الترجمة (القيمة يجب أن تكون معروفة وقت الترجمة)
const double taxRate = 0.15;
// final: ثابت وقت التشغيل (يُعيّن مرة واحدة، يمكن استخدام بيانات وقت التشغيل)
final DateTime startTime = DateTime.now();
// مجموعات const غير قابلة للتغيير بعمق
const List<String> planets = ['Mercury', 'Venus', 'Earth'];
// planets.add('Mars'); // خطأ: لا يمكن تعديل قائمة const
// مجموعات final يمكن تعديلها داخلياً
final List<String> fruits = ['Apple', 'Banana'];
fruits.add('Cherry'); // مسموح
print(fruits); // [Apple, Banana, Cherry]
}
// جدول ملخص:
// الميزة | const | final
// ----------------|--------------------|------------------
// متى يُعيّن؟ | وقت الترجمة | وقت التشغيل (مرة واحدة)
// قابل لإعادة التعيين؟ | لا | لا
// القيمة معروفة في | وقت الترجمة | وقت التشغيل مسموح
// المجموعات | غير قابلة للتغيير بعمق | محتويات قابلة للتعديل
// حالة الاستخدام | قيم حرفية ثابتة | تُحسب مرة واحدة
const كلما أمكن. إنها تتيح تحسينات المترجم وتضمن عدم القابلية للتغيير. استخدم final عندما تعتمد القيمة على شيء متاح فقط وقت التشغيل (مثل الوقت الحالي، أو إدخال المستخدم، أو استجابات API).static const -- ثوابت على مستوى الفئة
عندما تعرف ثوابت داخل فئة، استخدم static const لجعلها قابلة للوصول دون إنشاء نسخة. هذا هو النمط المعياري لتنظيم الثوابت المرتبطة.
static const في الفئات
class AppConfig {
static const String appName = 'MyDartApp';
static const String version = '2.1.0';
static const int maxRetries = 3;
static const Duration timeout = Duration(seconds: 30);
}
class HttpStatus {
static const int ok = 200;
static const int created = 201;
static const int badRequest = 400;
static const int unauthorized = 401;
static const int notFound = 404;
static const int serverError = 500;
}
class MathConstants {
static const double pi = 3.14159265358979;
static const double e = 2.71828182845905;
static const double goldenRatio = 1.61803398874989;
}
void main() {
// الوصول دون إنشاء نسخة
print(AppConfig.appName); // MyDartApp
print(HttpStatus.notFound); // 404
print(MathConstants.goldenRatio); // 1.61803398874989
if (statusCode == HttpStatus.unauthorized) {
print('يرجى تسجيل الدخول مرة أخرى.');
}
}
const فقط (بدون static) في المستوى الأعلى لفئة لثوابت على مستوى النسخة. ثوابت مستوى الفئة يجب أن تكون static const.التعدادات الأساسية
التعداد (enum) يعرّف مجموعة ثابتة من القيم المسماة. بدلاً من استخدام نصوص أو أعداد صحيحة لتمثيل أشياء مثل الحالة أو اللون أو الاتجاه، تمنحك التعدادات قيماً آمنة النوع، ومكتملة تلقائياً، وموثقة ذاتياً.
تعريف واستخدام التعدادات
// عرّف التعدادات خارج الدوال/الفئات
enum Direction { north, south, east, west }
enum Season { spring, summer, autumn, winter }
enum Priority { low, medium, high, critical }
void main() {
Direction heading = Direction.north;
Season current = Season.summer;
Priority taskPriority = Priority.high;
print(heading); // Direction.north
print(current); // Season.summer
print(taskPriority); // Priority.high
}
قيم التعداد والفهرس
كل تعداد يأتي مع خصائص مدمجة: index يعطي الموضع بدءاً من الصفر، وname يعطي اسم النص، وvalues يعطي قائمة بجميع أعضاء التعداد.
خصائص التعداد المدمجة
enum Color { red, green, blue, yellow }
void main() {
// .index -- الموضع بدءاً من الصفر
print(Color.red.index); // 0
print(Color.green.index); // 1
print(Color.blue.index); // 2
// .name -- اسم النص للقيمة
print(Color.red.name); // red
print(Color.yellow.name); // yellow
// .values -- قائمة بجميع أعضاء التعداد
print(Color.values); // [Color.red, Color.green, Color.blue, Color.yellow]
print(Color.values.length); // 4
// التكرار على جميع القيم
for (var color in Color.values) {
print('${color.name} is at index ${color.index}');
}
// red is at index 0
// green is at index 1
// blue is at index 2
// yellow is at index 3
}
استخدام التعدادات مع Switch
التعدادات وswitch زوج طبيعي. محلل Dart يحذرك إذا نسيت التعامل مع أي قيمة تعداد، مما يمنع الأخطاء الناتجة عن حالات مفقودة.
التعدادات مع عبارات Switch
enum TrafficLight { red, yellow, green }
String getAction(TrafficLight light) {
switch (light) {
case TrafficLight.red:
return 'Stop';
case TrafficLight.yellow:
return 'Slow down';
case TrafficLight.green:
return 'Go';
}
}
// تعبير switch في Dart 3+ (أكثر إيجازاً)
String getActionV2(TrafficLight light) => switch (light) {
TrafficLight.red => 'Stop',
TrafficLight.yellow => 'Slow down',
TrafficLight.green => 'Go',
};
void main() {
print(getAction(TrafficLight.red)); // Stop
print(getActionV2(TrafficLight.green)); // Go
}
default. بدونها، سيحذرك محلل Dart إذا أضفت قيمة تعداد جديدة لكن نسيت التعامل معها. حالة default تلتقط القيم الجديدة بصمت، مما يخفي أخطاء محتملة.التحويل بين النص والتعداد
غالباً ما تحتاج إلى التحويل بين التعدادات والنصوص، خاصة عند قراءة البيانات من واجهات API أو قواعد البيانات.
من نص إلى تعداد والعكس
enum Status { active, inactive, banned }
void main() {
// من تعداد إلى نص
String statusStr = Status.active.name;
print(statusStr); // active
// من نص إلى تعداد (Dart 2.15+)
Status status = Status.values.byName('inactive');
print(status); // Status.inactive
// تحويل آمن مع try-catch
try {
Status s = Status.values.byName('unknown');
} catch (e) {
print('قيمة حالة غير صالحة: $e');
// ArgumentError: No enum value with name "unknown"
}
// تحويل آمن مع firstWhere وقيمة افتراضية
String input = 'deleted';
Status safeStatus = Status.values.firstWhere(
(s) => s.name == input,
orElse: () => Status.inactive, // قيمة افتراضية
);
print(safeStatus); // Status.inactive
}
التعدادات المحسّنة (Dart 3+)
قدّم Dart 3 التعدادات المحسّنة التي يمكن أن تحتوي على حقول ومنشئات وطرق وحتى تنفيذ واجهات. هذا يجعل التعدادات أقوى بكثير -- كل قيمة يمكن أن تحمل بيانات وسلوكاً.
تعداد محسّن مع حقول وطرق
enum Planet {
mercury(diameter: 4879, distanceFromSun: 57.9),
venus(diameter: 12104, distanceFromSun: 108.2),
earth(diameter: 12756, distanceFromSun: 149.6),
mars(diameter: 6792, distanceFromSun: 227.9);
// الحقول
final double diameter; // كم
final double distanceFromSun; // مليون كم
// المنشئ (يجب أن يكون const)
const Planet({required this.diameter, required this.distanceFromSun});
// الطرق
String get description =>
'$name: ${diameter}km diameter, ${distanceFromSun}M km from Sun';
bool get isInnerPlanet => distanceFromSun < 200;
}
void main() {
print(Planet.earth.diameter); // 12756
print(Planet.earth.distanceFromSun); // 149.6
print(Planet.earth.description);
// earth: 12756km diameter, 149.6M km from Sun
print(Planet.mars.isInnerPlanet); // false
// تصفية الكواكب
var innerPlanets = Planet.values.where((p) => p.isInnerPlanet);
for (var p in innerPlanets) {
print(p.name); // mercury, venus, earth
}
}
تعداد محسّن -- أدوار المستخدمين
enum UserRole {
admin(level: 3, label: 'Administrator'),
editor(level: 2, label: 'Editor'),
viewer(level: 1, label: 'Viewer'),
guest(level: 0, label: 'Guest');
final int level;
final String label;
const UserRole({required this.level, required this.label});
bool canEdit() => level >= 2;
bool canDelete() => level >= 3;
bool hasHigherPrivilegeThan(UserRole other) => level > other.level;
}
void main() {
UserRole currentUser = UserRole.editor;
print(currentUser.label); // Editor
print(currentUser.canEdit()); // true
print(currentUser.canDelete()); // false
print(currentUser.hasHigherPrivilegeThan(UserRole.viewer)); // true
// الاستخدام في المنطق الشرطي
if (currentUser.canEdit()) {
print('يمكنك تعديل المحتوى.');
}
if (!currentUser.canDelete()) {
print('لا يمكنك حذف المحتوى.');
}
}
تعداد محسّن ينفذ واجهة
// التعدادات يمكن أن تنفذ واجهات
abstract class Describable {
String get description;
}
enum AppTheme implements Describable {
light(primaryColor: '#FFFFFF', textColor: '#000000'),
dark(primaryColor: '#1E1E1E', textColor: '#FFFFFF'),
ocean(primaryColor: '#006994', textColor: '#E0F7FA');
final String primaryColor;
final String textColor;
const AppTheme({required this.primaryColor, required this.textColor});
@override
String get description =>
'$name theme (primary: $primaryColor, text: $textColor)';
bool get isDark => name == 'dark';
}
void main() {
AppTheme theme = AppTheme.ocean;
print(theme.description);
// ocean theme (primary: #006994, text: #E0F7FA)
print(theme.isDark); // false
}
const. جميع الحقول يجب أن تكون final. لا يمكنك أن يكون لديك حالة قابلة للتغيير داخل قيمة تعداد.متى تستخدم التعدادات مقابل الثوابت
كل من التعدادات والثوابت تمثل قيماً ثابتة، لكنها تخدم أغراضاً مختلفة. إليك كيفية الاختيار:
اختيار الأداة المناسبة
// استخدم التعدادات عندما يكون لديك مجموعة ثابتة من الخيارات المرتبطة:
enum OrderStatus { pending, processing, shipped, delivered, cancelled }
enum PaymentMethod { cash, creditCard, bankTransfer, wallet }
enum Difficulty { easy, medium, hard }
// استخدم الثوابت عندما يكون لديك قيم ثابتة مستقلة:
class ApiConfig {
static const String baseUrl = 'https://api.example.com';
static const int timeout = 30;
static const String apiKey = 'abc123';
}
// استخدم الثوابت للقيم الرقمية/النصية التي لا تشكل مجموعة:
const double taxRate = 0.15;
const int maxUploadSizeMB = 10;
const String defaultLanguage = 'en';
// استخدم التعدادات المحسّنة عندما يحمل كل خيار بيانات:
enum Currency {
usd(symbol: '\$', name: 'US Dollar'),
eur(symbol: '\u20AC', name: 'Euro'),
sar(symbol: '\uFDFC', name: 'Saudi Riyal');
final String symbol;
final String name;
const Currency({required this.symbol, required this.name});
String format(double amount) => '$symbol${amount.toStringAsFixed(2)}';
}
void main() {
// التعداد: آمن النوع، مكتمل تلقائياً، switch شامل
OrderStatus status = OrderStatus.shipped;
// الثابت: قيمة ثابتة بسيطة
print(ApiConfig.baseUrl);
// تعداد محسّن: بيانات + سلوك
print(Currency.usd.format(29.99)); // \$29.99
print(Currency.sar.format(112.5)); // ﷼112.50
}
مثال عملي: نظام رموز الحالة
لنبني مثالاً كاملاً يجمع بين الثوابت والتعدادات المحسّنة لنمذجة نظام استجابة HTTP.
نظام استجابة HTTP كامل
enum HttpMethod { get, post, put, patch, delete }
enum ResponseStatus {
success(code: 200, message: 'OK'),
created(code: 201, message: 'Created'),
badRequest(code: 400, message: 'Bad Request'),
unauthorized(code: 401, message: 'Unauthorized'),
forbidden(code: 403, message: 'Forbidden'),
notFound(code: 404, message: 'Not Found'),
serverError(code: 500, message: 'Internal Server Error');
final int code;
final String message;
const ResponseStatus({required this.code, required this.message});
bool get isSuccess => code >= 200 && code < 300;
bool get isClientError => code >= 400 && code < 500;
bool get isServerError => code >= 500;
static ResponseStatus fromCode(int code) {
return ResponseStatus.values.firstWhere(
(s) => s.code == code,
orElse: () => ResponseStatus.serverError,
);
}
}
void main() {
var status = ResponseStatus.notFound;
print('${status.code}: ${status.message}'); // 404: Not Found
print('Is success: ${status.isSuccess}'); // false
print('Is client error: ${status.isClientError}'); // true
// البحث بالرمز
var found = ResponseStatus.fromCode(201);
print(found); // ResponseStatus.created
print(found.message); // Created
// الاستخدام في معالجة الطلبات
void handleResponse(ResponseStatus status) {
if (status.isSuccess) {
print('نجح الطلب!');
} else if (status.isClientError) {
print('خطأ العميل: ${status.message}');
} else if (status.isServerError) {
print('خطأ الخادم -- يرجى المحاولة مرة أخرى لاحقاً.');
}
}
handleResponse(ResponseStatus.success); // نجح الطلب!
handleResponse(ResponseStatus.notFound); // خطأ العميل: Not Found
}
تمرين عملي
أنشئ تعداداً محسّناً يسمى TaskPriority بالقيم low و medium و high و urgent. يجب أن يحتوي كل منها على:
- حقل
int level(من 1 إلى 4) - حقل
String labelباسم قابل للقراءة - حقل
Duration responseTimeيمثل وقت الاستجابة المتوقع - طريقة
bool shouldNotify()تُرجع true لـ high و urgent - طريقة
String summary()تُرجع نصاً منسقاً
ثم اكتب دالة main() تنشئ قائمة مهام بأولويات مختلفة، وتُصفّي للعثور على المهام التي يجب أن تطلق إشعارات، وتطبع ملخصاً لكل منها.