التعدادات والتعدادات المحسّنة
ما هي التعدادات؟
التعداد (enum اختصاراً لـ enumeration) هو نوع خاص يمثل مجموعة ثابتة من القيم الثابتة. بدلاً من استخدام سلاسل نصية أو أعداد صحيحة خام لتمثيل حالات مثل "active" و "inactive" أو "pending"، تمنحك التعدادات ثوابت مسماة آمنة النوع ومكتملة تلقائياً ومُتحقق منها وقت التجميع. إذا أخطأت في كتابة سلسلة نصية تحصل على خطأ وقت التشغيل؛ إذا أخطأت في كتابة قيمة تعداد تحصل على خطأ وقت التجميع.
تطورت تعدادات Dart بشكل كبير. في Dart 2 كانت التعدادات قوائم بسيطة من الثوابت. بدءاً من Dart 2.17 (ومُحسّنة في Dart 3) أصبحت التعدادات تعدادات محسّنة يمكن أن تحتوي على حقول وطرق ومُنشئات وحتى تنفيذ واجهات -- مما يجعلها واحدة من أقوى الميزات في اللغة.
تعدادك الأول
// تعداد بسيط -- مجرد ثوابت مسماة
enum Direction {
north,
south,
east,
west,
}
void main() {
Direction heading = Direction.north;
print(heading); // Direction.north
print(heading.name); // north (الاسم النصي)
print(heading.index); // 0 (الموقع من الصفر)
// جميع القيم كقائمة
print(Direction.values); // [Direction.north, Direction.south, Direction.east, Direction.west]
// التكرار على جميع قيم التعداد
for (var d in Direction.values) {
print('${d.name} is at index ${d.index}');
}
// north is at index 0
// south is at index 1
// east is at index 2
// west is at index 3
}
.name (الاسم النصي للثابت) و .index (موقعه بدءاً من الصفر حسب ترتيب التعريف). القائمة الثابتة .values تحتوي على جميع الثوابت بترتيب التعريف.استخدام التعدادات في عبارات Switch
تتألق التعدادات مع عبارات switch لأن المُجمّع يضمن أنك تتعامل مع كل حالة. إذا أضفت قيمة تعداد جديدة لاحقاً يخبرك المُجمّع بكل switch يحتاج تحديثاً -- وهذا يمنع الأخطاء.
التبديل الشامل
enum Season {
spring,
summer,
autumn,
winter,
}
String describeWeather(Season season) {
// تعبير switch في Dart 3 -- شامل بشكل افتراضي
return switch (season) {
Season.spring => 'Mild and rainy',
Season.summer => 'Hot and sunny',
Season.autumn => 'Cool and windy',
Season.winter => 'Cold and snowy',
};
// إذا أزلت حالة واحدة يعطي Dart خطأ تجميع:
// "The type Season is not exhaustively matched"
}
void main() {
print(describeWeather(Season.summer)); // Hot and sunny
// التحليل من سلسلة نصية
Season? parsed = Season.values.where(
(s) => s.name == 'winter'
).firstOrNull;
print(parsed); // Season.winter
}
التعدادات المحسّنة: الحقول والمُنشئات والطرق
بدءاً من Dart 2.17 يمكن للتعدادات أن تحتوي على حقول ومُنشئات وطرق -- تماماً مثل الفئات. هذا يتيح لك إرفاق بيانات ذات معنى بكل قيمة تعداد بدلاً من الحفاظ على تعيين منفصل. كل قيمة تعداد تستدعي مُنشئاً والحقول تكون تلقائياً final.
تعداد محسّن بحقول وطرق
enum HttpStatus {
ok(200, 'OK'),
created(201, 'Created'),
badRequest(400, 'Bad Request'),
unauthorized(401, 'Unauthorized'),
forbidden(403, 'Forbidden'),
notFound(404, 'Not Found'),
serverError(500, 'Internal Server Error');
// الحقول -- يجب أن تكون final
final int code;
final String message;
// المُنشئ -- يجب أن يكون const
const HttpStatus(this.code, this.message);
// الطرق
bool get isSuccess => code >= 200 && code < 300;
bool get isClientError => code >= 400 && code < 500;
bool get isServerError => code >= 500;
// toString مخصص
@override
String toString() => 'HTTP $code: $message';
// طريقة ثابتة للبحث بالكود
static HttpStatus? fromCode(int code) {
return HttpStatus.values.where(
(s) => s.code == code,
).firstOrNull;
}
}
void main() {
var status = HttpStatus.notFound;
print(status); // HTTP 404: Not Found
print(status.code); // 404
print(status.message); // Not Found
print(status.isClientError); // true
print(status.isSuccess); // false
// البحث بالكود
var found = HttpStatus.fromCode(200);
print(found); // HTTP 200: OK
// الاستخدام في المنطق الشرطي
var response = HttpStatus.ok;
if (response.isSuccess) {
print('Request succeeded!');
}
}
final وجميع المُنشئات يجب أن تكون const. التعدادات غير قابلة للتغيير بالتصميم -- لا يمكنك تغيير حالتها بعد الإنشاء. إذا كنت تحتاج حالة قابلة للتغيير استخدم فئة بدلاً من ذلك.التعدادات التي تنفذ واجهات
يمكن للتعدادات المحسّنة تنفيذ واجهة واحدة أو أكثر مما يجعلها تتناسب مع الكود متعدد الأشكال. هذا مفيد للغاية عندما تريد أن تفي قيم التعداد بعقد تنفذه فئات أخرى أيضاً.
تعداد ينفذ واجهة
// عقد يجب أن يفيه أي شيء "قابل للوصف"
abstract class Describable {
String get label;
String describe();
}
// عقد للأشياء التي يمكن تسلسلها
abstract class Serializable {
Map<String, dynamic> toJson();
}
enum PaymentMethod implements Describable, Serializable {
creditCard('Credit Card', 2.5, true),
debitCard('Debit Card', 0.5, true),
bankTransfer('Bank Transfer', 0.0, false),
cash('Cash', 0.0, false),
crypto('Cryptocurrency', 1.0, false);
final String label;
final double feePercent;
final bool supportsRefund;
const PaymentMethod(this.label, this.feePercent, this.supportsRefund);
// تنفيذ Describable
@override
String describe() => '$label (fee: ${feePercent}%)';
// تنفيذ Serializable
@override
Map<String, dynamic> toJson() => {
'method': name,
'label': label,
'fee': feePercent,
'refundable': supportsRefund,
};
// طرق إضافية
double calculateFee(double amount) => amount * feePercent / 100;
}
void main() {
var method = PaymentMethod.creditCard;
// يعمل كـ Describable
Describable d = method;
print(d.describe()); // Credit Card (fee: 2.5%)
// يعمل كـ Serializable
Serializable s = method;
print(s.toJson()); // {method: creditCard, label: Credit Card, fee: 2.5, refundable: true}
// حساب الرسوم على 100$
print(method.calculateFee(100)); // 2.5
// تصفية الطرق التي تدعم الاسترداد
var refundable = PaymentMethod.values
.where((m) => m.supportsRefund)
.toList();
print(refundable); // [PaymentMethod.creditCard, PaymentMethod.debitCard]
}
متى تستخدم التعدادات مقابل الفئات
الاختيار بين التعدادات والفئات قرار شائع. إليك إرشاداً واضحاً:
استخدم التعدادات عندما:
- مجموعة القيم ثابتة ومعروفة وقت التجميع (لن تتغير وقت التشغيل)
- كل قيمة هي ثابت ببيانات غير قابلة للتغيير
- تريد تحقق switch الشامل
- أمثلة: الاتجاهات وأيام الأسبوع وطرق HTTP وحالات التطبيق والأدوار
استخدم الفئات عندما:
- القيم تُنشأ ديناميكياً وقت التشغيل
- تحتاج حالة قابلة للتغيير
- مجموعة النُسخ غير ثابتة (مثل الفئات التي ينشئها المستخدم)
- تحتاج تسلسلات وراثة (التعدادات لا تستطيع وراثة تعدادات أخرى)
مقارنة التعداد مع الفئة
// جيد: مجموعة ثابتة من الأدوار -- استخدم تعداد
enum UserRole {
admin('Administrator', ['read', 'write', 'delete', 'manage']),
editor('Editor', ['read', 'write']),
viewer('Viewer', ['read']);
final String displayName;
final List<String> permissions;
const UserRole(this.displayName, this.permissions);
bool hasPermission(String permission) =>
permissions.contains(permission);
}
// سيئ كتعداد: فئات ديناميكية ينشئها المستخدمون -- استخدم فئة
class Category {
final String id;
final String name;
String? description; // قابل للتغيير -- التعدادات لا تستطيع ذلك
Category({required this.id, required this.name, this.description});
}
void main() {
// تعداد: معروف وقت التجميع وشامل
var role = UserRole.editor;
print(role.hasPermission('write')); // true
print(role.hasPermission('delete')); // false
// فئة: تُنشأ ديناميكياً
var cat = Category(id: '1', name: 'Flutter');
cat.description = 'Flutter development tips'; // قابل للتغيير
}
مثال عملي: إدارة حالة التطبيق
لنبني مثالاً واقعياً قد تستخدمه في تطبيق Flutter -- إدارة حالات اتصال التطبيق مع سلوك غني مرتبط بكل حالة.
واقعي: تعداد حالة الاتصال
enum ConnectionState {
disconnected(
'Disconnected',
'Not connected to the server',
false,
Duration(seconds: 5),
),
connecting(
'Connecting',
'Establishing connection...',
false,
Duration(seconds: 10),
),
connected(
'Connected',
'Successfully connected',
true,
Duration(seconds: 30),
),
reconnecting(
'Reconnecting',
'Lost connection, attempting to reconnect...',
false,
Duration(seconds: 15),
),
error(
'Error',
'Connection failed',
false,
Duration(seconds: 60),
);
final String displayName;
final String description;
final bool canSendData;
final Duration retryAfter;
const ConnectionState(
this.displayName,
this.description,
this.canSendData,
this.retryAfter,
);
// قواعد انتقال الحالة
List<ConnectionState> get allowedTransitions => switch (this) {
disconnected => [connecting],
connecting => [connected, error],
connected => [disconnected, reconnecting],
reconnecting => [connected, error, disconnected],
error => [reconnecting, disconnected],
};
bool canTransitionTo(ConnectionState next) =>
allowedTransitions.contains(next);
// التسلسل / إلغاء التسلسل
static ConnectionState fromString(String name) =>
ConnectionState.values.firstWhere(
(s) => s.name == name,
orElse: () => ConnectionState.disconnected,
);
}
void main() {
var state = ConnectionState.disconnected;
// تحقق من الانتقالات المسموحة
print(state.canTransitionTo(ConnectionState.connecting)); // true
print(state.canTransitionTo(ConnectionState.connected)); // false
// محاكاة آلة الحالة
state = ConnectionState.connecting;
print('${state.displayName}: ${state.description}');
// Connecting: Establishing connection...
if (!state.canSendData) {
print('Waiting... retry after ${state.retryAfter.inSeconds}s');
}
state = ConnectionState.connected;
print('Can send data: ${state.canSendData}'); // true
// إلغاء التسلسل من سلسلة نصية مخزنة
var restored = ConnectionState.fromString('error');
print(restored.displayName); // Error
}