Dart: التعدادات وطرق الامتداد في Dart — تعدادات محسّنة

آخر تحديث: 2026-08-26

التعدادات تمنح الحالات المتناهية أسماء، والامتدادات تمنح الأنواع القديمة قدرات جديدة — كلتاهما أداتان قويتان لتعزيز الكود دون تعديل مصدره.

1. ما ستتعلمه


2. قصة مطور حقيقية

(1) نقطة الألم: استخدام السلاسل النصية لمحاكاة الحالات يؤدي إلى أخطاء إملائية

استخدمت أليس سلاسل نصية لتمثيل حالات الطلبات في كودها: 'pending'، 'shipped'، 'delivered'. لم يتم اكتشاف خطأ إملائي 'shiped' من قبل المترجم، مما تسبب في بقاء الطلب عالقاً في حالة "لم يتم الشحن"، وأدى إلى 200 شكوى من العملاء. كما كانت تكتب بشكل متكرر فحوصات مثل if (status == 'pending' || status == 'processing')، التي كان من السهل إغفالها.

(2) حل التعدادات

تمنح التعدادات المحسنة في Dart كل حالة اسماً آمن النوع، إلى جانب الخصائص والطرق المرفقة. تعابير switch تضمن الشمولية؛ إغفال حالة ما يسبب خطأ ترجمة.

DART
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',
};
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

(3) الفوائد


3. التعدادات المحسنة

(1) التعدادات الأساسية

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: تعداد بسيط

DART
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
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

(2) التعدادات المحسنة

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: تعداد محسن مع خصائص

DART
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
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: تعداد محسن مع طرق

DART
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()}');
  }
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

4. التعدادات مع Switch

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: switch شامل

DART
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
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
الميزة if-else جملة switch تعبير switch
فحص الشمولية لا لا نعم (للتعدادات)
ضمان وقت الترجمة لا لا نعم
عند إضافة قيمة تعداد جديدة قد تُفقد قد تُفقد خطأ ترجمة

5. طرق الامتداد

(1) الامتدادات الأساسية

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: امتداد String

DART
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
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: امتداد num (تنسيق المبالغ)

DART
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
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: امتداد List

DART
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
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

6. الامتدادات والخصوصية وتعارضات التسمية

(1) حل تعارضات التسمية

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: حل التعارض عبر مساحات الأسماء

DART
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
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
سيناريو التعارض الحل
امتدادان يحددان نفس اسم الطريقة استدعاء صريح عبر ExtensionName(obj).method()
طريقة امتداد وطريقة فئة لهما نفس الاسم طريقة الفئة لها الأولوية، طريقة الامتداد تكون مخفية
امتدادان في ملفات مختلفة أولوية الامتدادات المستوردة تعتمد على ترتيب الاستيراد

7. سيناريو بوب: enum OrderStatus + امتداد String

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: تعداد وامتداد عملي لخط أنابيب البيانات

DART
// 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');
  }
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

8. المثال الكامل: آلة حالة طلبات خط أنابيب البيانات

DART
// ============================================
// آلة حالة طلبات خط أنابيب البيانات
// تعدادات محسنة + امتدادات في العمل
// ============================================

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}');
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل محلياً في DartPad أو عبر `dart run`. جميع أمثلة دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

الإخراج:

TEXT 📖 للعرض فقط
=== 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 خاصة بك.


📖 ملخص


📝 تمارين

  1. أساسي (صعوبة ⭐): عرّف OutputFormat كتعداد محسن يحتوي على القيم json و csv و html، لكل منها خاصية fileExtension (مثل .json) وخاصية mimeType (مثل application/json).
  2. متوسط (صعوبة ⭐⭐): أضف طرق امتداد إلى String: toOrderId (تنسيق كـ ORD-XXX)، isValidEmail (التحقق من تنسيق البريد الإلكتروني)، truncateWithEllipsis(int max) (اقتطاع وإضافة علامة الحذف)، واختبرها.
  3. تحدي (صعوبة ⭐⭐⭐): استخدم تعداداً محسناً لتنفيذ آلة حالة سير عمل كاملة (مسودة ← مراجعة ← موافق عليه ← منشور). يجب أن تحدد كل الحالة أهداف الانتقال المسموحة. نفّذ طريقة transitionTo() وتحقق من رفض الانتقالات غير القانونية.

← الدرس السابق | الدرس التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%