Dart: التعدادات وطرق الامتداد في Dart — تعدادات محسّنة
آخر تحديث: 2026-08-26
التعدادات تمنح الحالات المتناهية أسماء، والامتدادات تمنح الأنواع القديمة قدرات جديدة — كلتاهما أداتان قويتان لتعزيز الكود دون تعديل مصدره.
1. ما ستتعلمه
- التعدادات المحسنة: الخصائص، المُنشئات، الطرق
- التعدادات مع
switch - طرق الامتداد: التعريف والاستخدام
- الامتدادات والخصوصية، حل تعارضات التسمية
- سيناريو بوب: enum
OrderStatus+ امتداد String (تنسيق المبالغ بالدولار الأمريكي)
2. قصة مطور حقيقية
(1) نقطة الألم: استخدام السلاسل النصية لمحاكاة الحالات يؤدي إلى أخطاء إملائية
استخدمت أليس سلاسل نصية لتمثيل حالات الطلبات في كودها: 'pending'، 'shipped'، 'delivered'. لم يتم اكتشاف خطأ إملائي 'shiped' من قبل المترجم، مما تسبب في بقاء الطلب عالقاً في حالة "لم يتم الشحن"، وأدى إلى 200 شكوى من العملاء. كما كانت تكتب بشكل متكرر فحوصات مثل if (status == 'pending' || status == 'processing')، التي كان من السهل إغفالها.
(2) حل التعدادات
تمنح التعدادات المحسنة في Dart كل حالة اسماً آمن النوع، إلى جانب الخصائص والطرق المرفقة. تعابير switch تضمن الشمولية؛ إغفال حالة ما يسبب خطأ ترجمة.
enum OrderStatus {
pending(label: 'Awaiting Processing', isFinal: false),
shipped(label: 'In Transit', isFinal: false),
delivered(label: 'Completed', isFinal: true),
cancelled(label: 'Cancelled', isFinal: true);
final String label;
final bool isFinal;
const OrderStatus({required this.label, required this.isFinal});
}
// switch شامل - المترجم يفحص جميع الحالات
String handle(OrderStatus status) => switch (status) {
OrderStatus.pending => 'Queue for processing',
OrderStatus.shipped => 'Track shipment',
OrderStatus.delivered => 'Send survey',
OrderStatus.cancelled => 'Process refund',
};
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
(3) الفوائد
- يتم اكتشاف الأخطاء الإملائية في وقت الترجمة بدلاً من وقت التشغيل، مما يقلل الأخطاء المتعلقة بالحالة بنسبة 90%
- switch الشامل يضمن عدم إغفال أي حالة
- طرق الامتداد تتيح إضافة طرق تجارية إلى String و num دون إنشاء فئات فرعية
3. التعدادات المحسنة
(1) التعدادات الأساسية
▶ مثال
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: تعداد بسيط
enum OutputFormat {
json,
csv,
html,
}
void main() {
final format = OutputFormat.json;
// قيم التعداد
print(format.name); // json
print(format.index); // 0
print(OutputFormat.values); // [OutputFormat.json, OutputFormat.csv, OutputFormat.html]
// التحليل من سلسلة نصية
final parsed = OutputFormat.values.byName('csv');
print(parsed); // OutputFormat.csv
}
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
(2) التعدادات المحسنة
▶ مثال
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: تعداد محسن مع خصائص
enum OrderStatus {
pending(label: 'Awaiting Processing', isFinal: false, priority: 1),
processing(label: 'Being Processed', isFinal: false, priority: 2),
shipped(label: 'In Transit', isFinal: false, priority: 3),
delivered(label: 'Completed', isFinal: true, priority: 0),
cancelled(label: 'Cancelled', isFinal: true, priority: 0);
final String label;
final bool isFinal;
final int priority;
const OrderStatus({required this.label, required this.isFinal, required this.priority});
bool get isActive => !isFinal;
String get displayName => '${name.toUpperCase()} - $label';
}
void main() {
final status = OrderStatus.shipped;
print(status.label); // In Transit
print(status.isFinal); // false
print(status.isActive); // true
print(status.displayName); // SHIPPED - In Transit
}
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
▶ مثال
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: تعداد محسن مع طرق
enum TaxCategory {
standard(rate: 0.08, label: 'Standard Rate'),
reduced(rate: 0.05, label: 'Reduced Rate'),
zero(rate: 0.0, label: 'Zero Rate'),
exempt(rate: 0.0, label: 'Tax Exempt');
final double rate;
final String label;
const TaxCategory({required this.rate, required this.label});
double calculate(double amount) => amount * rate;
double applyTo(double amount) => amount * (1 + rate);
String formatRate() => '${(rate * 100).toStringAsFixed(1)}%';
}
void main() {
final tax = TaxCategory.standard;
print(tax.calculate(1500.0)); // 120.0
print(tax.applyTo(1500.0)); // 1620.0
print(tax.formatRate()); // 8.0%
// جميع الفئات
for (final cat in TaxCategory.values) {
print('${cat.label}: ${cat.formatRate()}');
}
}
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
4. التعدادات مع Switch
▶ مثال
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: switch شامل
enum DataSourceType {
api,
file,
database,
}
String describeSource(DataSourceType type) => switch (type) {
DataSourceType.api => 'REST API endpoint',
DataSourceType.file => 'Local file system',
DataSourceType.database => 'SQL database connection',
};
// مع فحص الشمولية - المترجم يفرض جميع الحالات
bool canRetry(DataSourceType type) => switch (type) {
DataSourceType.api => true, // API يمكن إعادة المحاولة
DataSourceType.file => false, // أخطاء الملفات تحتاج إصلاحاً يدوياً
DataSourceType.database => true, // DB يمكن إعادة المحاولة مع التراجع
};
void main() {
print(describeSource(DataSourceType.api)); // REST API endpoint
print(canRetry(DataSourceType.file)); // false
}
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
| الميزة | if-else | جملة switch | تعبير switch |
|---|---|---|---|
| فحص الشمولية | لا | لا | نعم (للتعدادات) |
| ضمان وقت الترجمة | لا | لا | نعم |
| عند إضافة قيمة تعداد جديدة | قد تُفقد | قد تُفقد | خطأ ترجمة |
5. طرق الامتداد
(1) الامتدادات الأساسية
▶ مثال
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: امتداد String
extension StringCurrency on String {
String toUSD() => '\$$this USD';
String toEUR() => '€${this} EUR';
String truncate(int maxLength) =>
length <= maxLength ? this : '${substring(0, maxLength)}...';
String get capitalized =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}
void main() {
print('1500.00'.toUSD()); // $1500.00 USD
print('1200.00'.toEUR()); // €1200.00 EUR
print('Very long product name'.truncate(10)); // Very long...
print('electronics'.capitalized); // Electronics
}
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
▶ مثال
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: امتداد num (تنسيق المبالغ)
extension NumFormatting on num {
String toUSD() => '\$${toStringAsFixed(2)} USD';
String toCompact() {
if (this >= 1000000) return '\$${(this / 1000000).toStringAsFixed(1)}M USD';
if (this >= 1000) return '\$${(this / 1000).toStringAsFixed(1)}K USD';
return toUSD();
}
double get asK => this / 1000;
double get asM => this / 1000000;
bool isBetween(num from, num to) => from <= this && this <= to;
}
void main() {
print(1500.0.toUSD()); // $1500.00 USD
print(1500000.0.toCompact()); // $1.5M USD
print(5000.asK); // 5.0
print(1500.0.isBetween(1000, 2000)); // true
}
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
▶ مثال
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: امتداد List
extension ListStats on List<double> {
double get sum => fold(0, (a, b) => a + b);
double get average => isEmpty ? 0 : sum / length;
double get median {
final sorted = [...this]..sort();
final mid = length ~/ 2;
return length.isEven
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
}
}
void main() {
final amounts = [1500.0, 3200.0, 890.0, 50.0];
print('Sum: ${amounts.sum.toUSD()}'); // Sum: $5640.00 USD
print('Average: ${amounts.average.toUSD()}'); // Average: $1410.00 USD
print('Median: ${amounts.median.toUSD()}'); // Median: $1195.00 USD
}
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
6. الامتدادات والخصوصية وتعارضات التسمية
(1) حل تعارضات التسمية
▶ مثال
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: حل التعارض عبر مساحات الأسماء
extension MathExtras on num {
int get squared => (this * this).toInt();
}
extension StringExtras on String {
String get reversed => split('').reversed.join('');
}
// إذا كان امتدادان لهما نفس اسم الطريقة
extension DoubleExtras on double {
String toMoney() => '\$${toStringAsFixed(2)}';
}
extension IntExtras on int {
String toMoney() => '\$${this}.00';
}
void main() {
// استدعاء مباشر - المترجم يحل حسب النوع
print(5.squared); // 25
print('hello'.reversed); // olleh
// حل صريح عند الغموض
print(DoubleExtras(1500.5).toMoney()); // $1500.50
print(IntExtras(1500).toMoney()); // $1500.00
}
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
| سيناريو التعارض | الحل |
|---|---|
| امتدادان يحددان نفس اسم الطريقة | استدعاء صريح عبر ExtensionName(obj).method() |
| طريقة امتداد وطريقة فئة لهما نفس الاسم | طريقة الفئة لها الأولوية، طريقة الامتداد تكون مخفية |
| امتدادان في ملفات مختلفة | أولوية الامتدادات المستوردة تعتمد على ترتيب الاستيراد |
7. سيناريو بوب: enum OrderStatus + امتداد String
▶ مثال
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: تعداد وامتداد عملي لخط أنابيب البيانات
// enum حالة الطلب مع منطق تجاري
enum OrderStatus {
pending(label: 'Awaiting Processing', isFinal: false),
processing(label: 'Being Processed', isFinal: false),
shipped(label: 'In Transit', isFinal: false),
delivered(label: 'Completed', isFinal: true),
cancelled(label: 'Cancelled', isFinal: true),
refunded(label: 'Refunded', isFinal: true);
final String label;
final bool isFinal;
const OrderStatus({required this.label, required this.isFinal});
bool get isActive => !isFinal;
bool get canCancel => this == pending || this == processing;
bool get canRefund => this == delivered;
}
// امتداد String لتنسيق خط أنابيب البيانات
extension DataPipelineString on String {
String get asOrderId => 'ORD-$this';
String toUSD() => '\$$this USD';
String toCategoryLabel => split('_').map((w) => w.capitalizeFirst).join(' ');
}
extension StringCap on String {
String get capitalizeFirst =>
isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}
// امتداد num لتنسيق الإيرادات
extension RevenueFormatting on num {
String toRevenue() => '\$${toStringAsFixed(2)} USD';
String toCompactRevenue() {
if (this >= 1000000) return '\$${(this / 1000000).toStringAsFixed(1)}M USD';
if (this >= 1000) return '\$${(this / 1000).toStringAsFixed(1)}K USD';
return toRevenue();
}
}
void main() {
// استخدام التعداد
final status = OrderStatus.shipped;
print('Status: ${status.label}'); // In Transit
print('Active: ${status.isActive}'); // true
print('Can cancel: ${status.canCancel}'); // false
// امتدادات String
print('001'.asOrderId); // ORD-001
print('1500.00'.toUSD()); // $1500.00 USD
// تنسيق الإيرادات
print(1500000.toCompactRevenue()); // $1.5M USD
print(52500.75.toRevenue()); // $52500.75 USD
// switch شامل على التعداد
for (final s in OrderStatus.values) {
final action = switch (s) {
OrderStatus.pending => 'Queue for processing',
OrderStatus.processing => 'Monitor progress',
OrderStatus.shipped => 'Track delivery',
OrderStatus.delivered => 'Send confirmation',
OrderStatus.cancelled => 'Process cancellation',
OrderStatus.refunded => 'Update records',
};
print(' ${s.name}: $action');
}
}
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
8. المثال الكامل: آلة حالة طلبات خط أنابيب البيانات
// ============================================
// آلة حالة طلبات خط أنابيب البيانات
// تعدادات محسنة + امتدادات في العمل
// ============================================
enum OrderStatus {
pending(label: 'Awaiting Processing', isFinal: false, color: 'yellow'),
processing(label: 'Being Processed', isFinal: false, color: 'blue'),
shipped(label: 'In Transit', isFinal: false, color: 'orange'),
delivered(label: 'Completed', isFinal: true, color: 'green'),
cancelled(label: 'Cancelled', isFinal: true, color: 'red'),
refunded(label: 'Refunded', isFinal: true, color: 'gray');
final String label;
final bool isFinal;
final String color;
const OrderStatus({
required this.label,
required this.isFinal,
required this.color,
});
bool get isActive => !isFinal;
bool get canTransition => !isFinal;
List<OrderStatus> get allowedTransitions => switch (this) {
pending => [processing, cancelled],
processing => [shipped, cancelled],
shipped => [delivered],
delivered => [refunded],
cancelled => [],
refunded => [],
};
bool canTransitionTo(OrderStatus target) =>
allowedTransitions.contains(target);
}
extension NumRevenue on num {
String toUSD() => '\$${toStringAsFixed(2)} USD';
}
class Order {
final String id;
final double amount;
OrderStatus status;
Order({required this.id, required this.amount, this.status = OrderStatus.pending});
bool transitionTo(OrderStatus newStatus) {
if (!status.canTransitionTo(newStatus)) {
print(' Cannot transition from ${status.name} to ${newStatus.name}');
return false;
}
print(' $id: ${status.name} → ${newStatus.name}');
status = newStatus;
return true;
}
String get summary => '$id: ${status.label} (${amount.toUSD()})';
}
void main() {
final order = Order(id: 'ORD-001', amount: 1500.0);
print('=== Order State Machine ===');
print('Initial: ${order.summary}');
// انتقالات صحيحة
order.transitionTo(OrderStatus.processing); // OK
order.transitionTo(OrderStatus.shipped); // OK
order.transitionTo(OrderStatus.delivered); // OK
// انتقال غير صحيح
order.transitionTo(OrderStatus.cancelled); // Cannot: delivered → cancelled
// استرداد صحيح
order.transitionTo(OrderStatus.refunded); // OK
print('\nFinal: ${order.summary}');
// طباعة جميع الحالات والانتقالات
print('\n=== State Transition Table ===');
for (final status in OrderStatus.values) {
final targets = status.allowedTransitions.map((t) => t.name).join(', ');
print(' ${status.name.padRight(12)} → ${targets.isEmpty ? '(final)' : targets}');
}
// إحصائيات الحالات
print('\n=== Status Properties ===');
final activeCount = OrderStatus.values.where((s) => s.isActive).length;
final finalCount = OrderStatus.values.where((s) => s.isFinal).length;
print('Active states: $activeCount');
print('Final states: $finalCount');
print('Total states: ${OrderStatus.values.length}');
}
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
الإخراج:
=== Order State Machine ===
Initial: ORD-001: Awaiting Processing ($1500.00 USD)
ORD-001: pending → processing
ORD-001: processing → shipped
ORD-001: shipped → delivered
Cannot transition from delivered to cancelled
ORD-001: delivered → refunded
Final: ORD-001: Refunded ($1500.00 USD)
=== State Transition Table ===
pending → processing, cancelled
processing → shipped, cancelled
shipped → delivered
delivered → refunded
cancelled → (final)
refunded → (final)
=== Status Properties ===
Active states: 3
Final states: 3
Total states: 6
❓ أسئلة شائعة
س: ما الفرق بين التعداد المحسن والتعداد العادي؟ ج: التعدادات المحسنة يمكن أن تحتوي على خصائص ومُنشئات وطرق. التعدادات العادية لديها فقط
nameوindex. Dart 2.17+ يوصي باستخدام التعدادات المحسنة في كل مكان.
س: هل يمكن للتعدادات تنفيذ واجهات؟ ج: نعم. يمكن للتعدادات تنفيذ واجهات، مثل
enum Status implements Comparable<Status>. ومع ذلك، لا يمكنها تمديد فئات أخرى (التعدادات ترث ضمنياً من Enum).
س: هل يمكن لطرق الامتداد الوصول إلى الأعضاء الخاصة؟ ج: لا. تُعرَّف طرق الامتداد خارج الفئة ولا يمكنها الوصول إلا إلى الأعضاء العامة. هذا هو الفرق الجوهري بين الامتدادات وطرق الفئة.
س: هل يتم توزيع طرق الامتداد بشكل ثابت أم ديناميكي؟ ج: يتم التوزيع بشكل ثابت. يحدد المترجم أي طريقة امتداد ستُستدعى بناءً على النوع المُعلن للمتغير في وقت الترجمة. نوع وقت التشغيل غير مهم. هذا هو الفرق الأساسي عن طرق الفئة، التي يتم توزيعها ديناميكياً.
س: هل يمكن للامتدادات إضافة خصائص؟ ج: يمكنها إضافة خصائص محسوبة (getters) ولكن لا يمكنها إضافة متغيرات مثيل (خصائص مخزنة). لا تعدل الامتدادات تخطيط الذاكرة للكائن.
س: ماذا يحدث إذا كان امتدادان يحددان طريقة بنفس الاسم؟ ج: إذا كان بإمكان المترجم التمييز بناءً على نوع المستقبل، فإنه يختار الصحيح تلقائياً. إذا لم يستطع التمييز (غموض)، يجب عليك التحديد صراحة باستخدام
ExtensionName(obj).method().
س: هل هناك فرق في الأداء بين
valuesوbyNameللتعدادات؟ ج:valuesتُرجع قائمة مخزنة مؤقتاً، O(1).byNameتتكرر علىvaluesللبحث عن تطابق، O(n). للبحث المتكرر، فكر في إنشاء ذاكرة مخبأة Map خاصة بك.
📖 ملخص
- التعدادات المحسنة تسمح لقيم التعداد بأن يكون لها خصائص ومُنشئات وطرق، مما يجعلها أكثر أماناً من ثوابت السلسلة النصية البسيطة.
- تعابير switch مع التعدادات تضمن الشمولية المضمونة من المترجم؛ لن يتم إغفال إضافة قيمة تعداد جديدة.
- طرق الامتداد تضيف وظائف إلى الأنواع الموجودة دون تعديل الكود المصدر أو تغيير تخطيط الذاكرة.
- يتم توزيع طرق الامتداد بشكل ثابت ولا يمكنها الوصول إلا إلى الأعضاء العامة. تتطلب تعارضات التسمية حلاً صريحاً.
- يستخدم خط أنابيب البيانات enum
OrderStatusلتعريف آلة الحالة، وامتدادات String/num لتنسيق المبالغ.
📝 تمارين
- أساسي (صعوبة ⭐): عرّف
OutputFormatكتعداد محسن يحتوي على القيمjsonوcsvوhtml، لكل منها خاصيةfileExtension(مثل.json) وخاصيةmimeType(مثلapplication/json). - متوسط (صعوبة ⭐⭐): أضف طرق امتداد إلى
String:toOrderId(تنسيق كـ ORD-XXX)،isValidEmail(التحقق من تنسيق البريد الإلكتروني)،truncateWithEllipsis(int max)(اقتطاع وإضافة علامة الحذف)، واختبرها. - تحدي (صعوبة ⭐⭐⭐): استخدم تعداداً محسناً لتنفيذ آلة حالة سير عمل كاملة (مسودة ← مراجعة ← موافق عليه ← منشور). يجب أن تحدد كل الحالة أهداف الانتقال المسموحة. نفّذ طريقة
transitionTo()وتحقق من رفض الانتقالات غير القانونية.