التعامل مع الأخطاء (Exceptions) في Dart لمنع انهيار التطبيق


التعامل مع الأخطاء (Exceptions) في Dart لمنع انهيار التطبيق

ماذا سنتعلم اليوم؟

سنتعلم كيفية التعامل مع الأخطاء غير المتوقعة (Exceptions) في تطبيقات Dart لضمان استقرارها ومنع توقفها المفاجئ، مع التركيز على أفضل الممارسات.

الخطوة 1: فهم الأساسيات - Try-Catch

في Dart، تُستخدم كتل try و catch لالتقاط الأخطاء (Exceptions) ومعالجتها بطريقة منظمة. يتم وضع الكود الذي قد يتسبب في حدوث خطأ داخل كتلة try، وفي حال حدوث خطأ، يتم "التقاطه" بواسطة كتلة catch.

ملاحظة تقنية: الـ Exceptions هي أحداث تحدث أثناء تشغيل البرنامج تشير إلى حالة غير طبيعية يمكن للبرنامج التعامل معها، على عكس الـ Errors التي تمثل مشكلات أكثر خطورة ولا يُتوقع من البرنامج التعامل معها.

مثال: محاولة تحويل نص غير صالح إلى رقم

void main() {
  print('بدء تنفيذ البرنامج...');
  try {
    // محاولة تحويل نص لا يمكن تحويله إلى عدد صحيح
    int result = int.parse('ليس رقماً');
    print('النتيجة: $result');
  } on FormatException catch (e) {
    // التقاط خطأ FormatException على وجه التحديد
    print('حدث خطأ في التنسيق: ${e.message}');
  } catch (e) {
    // التقاط أي نوع آخر من الأخطاء لم يتم تحديده
    print('حدث خطأ غير متوقع: $e');
  }
  print('انتهاء تنفيذ البرنامج.');
}

الخطوة 2: التعامل مع أنواع مختلفة من الأخطاء

يمكننا استخدام عدة كتل on لتقاط أنواع مختلفة من الـ Exceptions، أو استخدام catch عام لالتقاط أي خطأ لم يتم تحديده مسبقاً. يمكن الوصول إلى كائن الـ Exception نفسه (عادةً ما يُرمز له بـ e) للحصول على تفاصيل إضافية.

مثال: التعامل مع أخطاء القسمة على صفر وأخطاء التنسيق

void performOperation(int a, int b) {
  try {
    // محاولة إجراء عملية قد تؤدي إلى أخطاء
    double divisionResult = a / b;
    print('نتيجة القسمة: $divisionResult');

    // محاولة تحويل نص قد يكون غير صالح
    String textNumber = '123x';
    int parsedNumber = int.parse(textNumber);
    print('الرقم المحول: $parsedNumber');

  } on IntegerDivisionByZeroException catch (e) {
    // التقاط خطأ القسمة على صفر
    print('خطأ: لا يمكن القسمة على صفر! ${e.runtimeType}');
  } on FormatException catch (e) {
    // التقاط خطأ تنسيق البيانات
    print('خطأ في التنسيق: ${e.message}');
  } catch (e, s) {
    // التقاط أي خطأ آخر والحصول على الـ Stack Trace (s)
    print('حدث خطأ عام غير متوقع: $e');
    print('تتبع الخطأ (Stack Trace): $s');
  } finally {
    // هذا الجزء من الكود سيتم تنفيذه دائماً، سواء حدث خطأ أم لا
    print('تم الانتهاء من محاولة العملية.');
  }
}

void main() {
  print('--- محاولة آمنة ---');
  performOperation(10, 2);
  print('\n--- محاولة القسمة على صفر ---');
  performOperation(10, 0);
  print('\n--- محاولة تحويل نص غير صالح ---');
  // لتشغيل هذا السيناريو، يجب أن نعدل الدالة performOperation لتمرير النص غير الصالح مباشرة
  // أو نستدعيها بطريقة مختلفة، ولكن الكود أعلاه يوضح كيف سيتم التقاط FormatException
}

الخطوة 3: استخدام Finally لضمان التنفيذ

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

مثال: إغلاق مورد بعد العملية

void processFile(String fileName) {
  var fileHandle;
  try {
    // افتراض فتح ملف هنا
    fileHandle = 'مقبض الملف لـ $fileName';
    print('تم فتح $fileHandle');

    if (fileName == 'خطأ.txt') {
      // محاكاة خطأ أثناء معالجة الملف
      throw Exception('فشل في قراءة الملف');
    }
    print('تمت معالجة الملف بنجاح.');
  } catch (e) {
    print('حدث خطأ أثناء معالجة الملف: $e');
  } finally {
    // ضمان إغلاق مقبض الملف دائماً
    if (fileHandle != null) {
      print('تم إغلاق $fileHandle');
    }
  }
}

void main() {
  print('--- معالجة ملف صحيح ---');
  processFile('بيانات.txt');
  print('\n--- معالجة ملف خاطئ ---');
  processFile('خطأ.txt');
}

الخطوة 4: إنشاء استثناءات مخصصة (Custom Exceptions)

في بعض الحالات، قد تحتاج إلى تعريف أنواع خاصة بك من الـ Exceptions لتمثيل حالات خطأ محددة في منطق تطبيقك. يمكنك القيام بذلك عن طريق إنشاء فئة جديدة ترث من Exception أو Error.

مثال: استثناء لعدم كفاية الرصيد

// تعريف استثناء مخصص
class InsufficientFundsException implements Exception {
  final String message;
  InsufficientFundsException([this.message = 'رصيد غير كافٍ لإجراء العملية.']);

  @override
  String toString() => 'InsufficientFundsException: $message';
}

void withdraw(double amount, double balance) {
  if (amount > balance) {
    // رمي الاستثناء المخصص إذا كان الرصيد غير كافٍ
    throw InsufficientFundsException('محاولة سحب $amount من رصيد $balance');
  }
  print('تم سحب $amount بنجاح. الرصيد المتبقي: ${balance - amount}');
}

void main() {
  double accountBalance = 100.0;

  print('--- محاولة سحب مبلغ مقبول ---');
  try {
    withdraw(50.0, accountBalance);
  } catch (e) {
    print('خطأ: $e');
  }

  print('\n--- محاولة سحب مبلغ أكبر من الرصيد ---');
  try {
    withdraw(150.0, accountBalance);
  } on InsufficientFundsException catch (e) {
    // التقاط الاستثناء المخصص
    print('خطأ في العملية: ${e.message}');
  } catch (e) {
    print('حدث خطأ عام: $e');
  }
}

الكود النهائي الكامل

هذا الكود يجمع الأمثلة التي ناقشناها ليوضح كيفية التعامل مع الـ Exceptions في سيناريو واحد.

// تعريف استثناء مخصص
class InsufficientFundsException implements Exception {
  final String message;
  InsufficientFundsException([this.message = 'رصيد غير كافٍ لإجراء العملية.']);

  @override
  String toString() => 'InsufficientFundsException: $message';
}

// دالة تقوم بعملية قد تسبب أخطاء
void performComplexOperation(int valueA, int valueB, String textInput, double balance) {
  print('بدء العملية المعقدة...');
  var resourceHandle; // مورد افتراضي لغرض التوضيح

  try {
    // 1. محاولة فتح مورد
    resourceHandle = 'مقبض المورد_ID_${DateTime.now().millisecondsSinceEpoch}';
    print('تم فتح المورد: $resourceHandle');

    // 2. محاولة القسمة على صفر
    if (valueB == 0) {
      throw IntegerDivisionByZeroException();
    }
    double divisionResult = valueA / valueB;
    print('نتيجة القسمة: $divisionResult');

    // 3. محاولة تحويل نص غير صالح
    int parsedNumber = int.parse(textInput);
    print('الرقم المحول من النص: $parsedNumber');

    // 4. محاولة سحب من رصيد (باستخدام استثناء مخصص)
    double withdrawAmount = 75.0;
    if (withdrawAmount > balance) {
      throw InsufficientFundsException('محاولة سحب $withdrawAmount من رصيد $balance');
    }
    print('تم سحب $withdrawAmount بنجاح. الرصيد المتبقي: ${balance - withdrawAmount}');

  } on IntegerDivisionByZeroException catch (e) {
    print('خطأ (القسمة على صفر): ${e.runtimeType}');
  } on FormatException catch (e) {
    print('خطأ (تنسيق البيانات): ${e.message}');
  } on InsufficientFundsException catch (e) {
    print('خطأ (رصيد غير كافٍ): ${e.message}');
  } catch (e, s) {
    // التقاط أي خطأ آخر لم يتم تحديده
    print('خطأ عام غير متوقع: $e');
    print('تتبع الخطأ: $s');
  } finally {
    // ضمان إغلاق المورد دائماً
    if (resourceHandle != null) {
      print('تم إغلاق المورد: $resourceHandle');
    }
    print('انتهت محاولة العملية المعقدة.');
  }
}

void main() {
  print('----- سيناريو 1: كل شيء على ما يرام -----');
  performComplexOperation(10, 2, '123', 200.0);

  print('\n----- سيناريو 2: القسمة على صفر -----');
  performComplexOperation(10, 0, '456', 200.0);

  print('\n----- سيناريو 3: تنسيق نص خاطئ -----');
  performComplexOperation(10, 2, 'abc', 200.0);

  print('\n----- سيناريو 4: رصيد غير كافٍ -----');
  performComplexOperation(10, 2, '789', 50.0);

  print('\n----- سيناريو 5: خطأ غير معروف (محاكاة) -----');
  // لتشغيل خطأ غير معروف، سنعدل مؤقتًا لرمي خطأ عشوائي
  // لن يتم تضمين هذا في الكود النهائي لتبسيطه، ولكن يمكن تخيله
  // مثلاً: throw ArgumentError('خطأ غير متوقع');
  try {
    print('بدء محاكاة خطأ غير معروف...');
    throw ArgumentError('قيمة غير صالحة تم تمريرها.');
  } catch (e, s) {
    print('تم التقاط خطأ غير معروف: $e');
    print('تتبع الخطأ: $s');
  } finally {
    print('انتهت محاكاة الخطأ غير المعروف.');
  }
}

النتيجة المتوقعة

عند تشغيل الكود النهائي، ستلاحظ أن البرنامج لا يتوقف عن العمل (لا ينهار) حتى عندما تحدث أخطاء. بدلاً من ذلك، يقوم بطباعة رسائل توضح طبيعة الخطأ الذي حدث، ثم يواصل تنفيذ بقية البرنامج. سيتم تنفيذ كتلة finally في كل مرة لضمان إغلاق الموارد الافتراضية. على سبيل المثال، ستكون المخرجات كالتالي (مع اختلافات طفيفة في أرقام الـ IDs وتتبع الخطأ):

----- سيناريو 1: كل شيء على ما يرام -----
بدء العملية المعقدة...
تم فتح المورد: مقبض المورد_ID_xxxxxxxxxxxxx
نتيجة القسمة: 5.0
الرقم المحول من النص: 123
تم سحب 75.0 بنجاح. الرصيد المتبقي: 125.0
تم إغلاق المورد: مقبض المورد_ID_xxxxxxxxxxxxx
انتهت محاولة العملية المعقدة.

----- سيناريو 2: القسمة على صفر -----
بدء العملية المعقدة...
تم فتح المورد: مقبض المورد_ID_xxxxxxxxxxxxx
خطأ (القسمة على صفر): IntegerDivisionByZeroException
تم إغلاق المورد: مقبض المورد_ID_xxxxxxxxxxxxx
انتهت محاولة العملية المعقدة.

----- سيناريو 3: تنسيق نص خاطئ -----
بدء العملية المعقدة...
تم فتح المورد: مقبض المورد_ID_xxxxxxxxxxxxx
نتيجة القسمة: 5.0
خطأ (تنسيق البيانات): Invalid radix-10 number (at character 1)
abc
^
تم إغلاق المورد: مقبض المورد_ID_xxxxxxxxxxxxx
انتهت محاولة العملية المعقدة.

----- سيناريو 4: رصيد غير كافٍ -----
بدء العملية المعقدة...
تم فتح المورد: مقبض المورد_ID_xxxxxxxxxxxxx
نتيجة القسمة: 5.0
الرقم المحول من النص: 789
خطأ (رصيد غير كافٍ): محاولة سحب 75.0 من رصيد 50.0
تم إغلاق المورد: مقبض المورد_ID_xxxxxxxxxxxxx
انتهت محاولة العملية المعقدة.

----- سيناريو 5: خطأ غير معروف (محاكاة) -----
بدء محاكاة خطأ غير معروف...
تم التقاط خطأ غير معروف: Invalid argument(s): قيمة غير صالحة تم تمريرها.
تتبع الخطأ: #0      main.<anonymous closure> (file:///.../main.dart:xx:xx)
#1      _RootZone.runUnaryGuarded (dart:async/zone.dart:1584:10)
#2      _FutureListener.handleValue (dart:async/future_impl.dart:140:18)
#3      Future._propagateToListeners.handleValueCallback (dart:async/future_impl.dart:575:45)
#4      Future._propagateToListeners (dart:async/future_impl.dart:604:32)
#5      Future._completeWithValue (dart:async/future_impl.dart:429:5)
#6      _AsyncAwaitCompleter.complete (dart:async-patch/async_patch.dart:41:15)
#7      _AsyncAwaitCompleer.sync await (dart:async-patch/async_patch.dart:47:11)
#8      main (file:///.../main.dart:xx:xx)
#9      _runMain (dart:cli/run_lib.dart:25:27)
#10     _startIsolate.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:308:19)
#11     _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:174:12)
انتهت محاكاة الخطأ غير المعروف.

يوضح هذا بوضوح كيف تمنع معالجة الـ Exceptions انهيار التطبيق، وتوفر معلومات مفيدة لتصحيح الأخطاء، وتضمن نظافة الموارد.