معالجة الأخطاء والاستثناءات
ما هي الاستثناءات؟
الاستثناء هو حدث يعطل التدفق الطبيعي لبرنامجك. عندما يحدث خطأ ما -- مثل القسمة على صفر أو الوصول إلى فهرس غير صالح أو محاولة فتح ملف غير موجود -- ينشئ Dart كائن استثناء يصف المشكلة. إذا لم تتعامل مع (تلتقط) ذلك الاستثناء سيتعطل برنامجك.
فكر في الاستثناءات كأجراس إنذار. عندما يحدث شيء غير متوقع ينطلق الإنذار. يمكنك إما ترك الإنذار يوقف برنامجك بالكامل أو يمكنك اعتراضه والتعامل مع المشكلة والمتابعة.
Try و Catch و Finally
أهم هيكل لمعالجة الأخطاء في Dart هو كتلة try-catch-finally. يتيح لك محاولة تنفيذ كود قد يكون خطيراً والتقاط أي مشاكل وتشغيل كود تنظيف اختيارياً بغض النظر عن حدوث خطأ أم لا.
Try-Catch الأساسي
void main() {
try {
int result = 10 ~/ 0; // قسمة عدد صحيح على صفر
print(result);
} catch (e) {
print('حدث خطأ: $e');
}
print('البرنامج يستمر في العمل!');
}
// المخرجات:
// حدث خطأ: IntegerDivisionByZeroException
// البرنامج يستمر في العمل!
بدون try-catch كان البرنامج سيتعطل عند سطر القسمة. معه نتعامل مع الخطأ بأناقة ونستمر في التنفيذ.
كتلة finally
كتلة finally تُنفذ دائماً سواء تم طرح استثناء أم لا. هذا مثالي لمهام التنظيف مثل إغلاق الملفات واتصالات قواعد البيانات أو تحرير الموارد.
Try-Catch-Finally
void main() {
try {
print('فتح المورد...');
var result = int.parse('not_a_number');
print('النتيجة: $result');
} catch (e) {
print('خطأ: $e');
} finally {
print('تنظيف: إغلاق المورد.');
}
}
// المخرجات:
// فتح المورد...
// خطأ: FormatException: not_a_number
// تنظيف: إغلاق المورد.
finally تعمل حتى لو استخدمت return داخل try أو catch. مضمون تنفيذها مما يجعلها المكان المثالي لكود التنظيف.التقاط أنواع استثناءات محددة
بدلاً من التقاط جميع الاستثناءات بـ catch عام يمكنك استهداف أنواع استثناءات محددة باستخدام كلمة on. هذا يتيح لك الاستجابة بشكل مختلف لأنواع مختلفة من الأخطاء.
التقاط أنواع محددة مع on
void main() {
try {
var value = int.parse('hello');
} on FormatException {
print('هذا ليس تنسيق رقم صالح!');
} on RangeError {
print('القيمة خارج النطاق!');
} catch (e) {
print('حدث خطأ آخر: $e');
}
}
// المخرجات:
// هذا ليس تنسيق رقم صالح!
كلمة on مقابل كلمة catch
يمنحك Dart آليتين يمكن استخدامهما بشكل منفصل أو معاً:
on ExceptionType-- يطابق نوع استثناء محدد لكن لا يمنحك الوصول إلى كائن الاستثناء.catch (e)-- يلتقط أي استثناء ويمنحك كائن الاستثناءe.on ExceptionType catch (e)-- يطابق نوعاً محدداً ويمنحك الكائن أيضاً.
الجمع بين on و catch
void main() {
try {
List<int> numbers = [1, 2, 3];
print(numbers[10]); // فهرس خارج النطاق
} on RangeError catch (e) {
print('تم التقاط RangeError!');
print('الرسالة: ${e.message}');
}
}
// المخرجات:
// تم التقاط RangeError!
// الرسالة: Index out of range
الوصول إلى تتبع المكدس
يمكنك التقاط تتبع المكدس كمعامل ثانٍ في كتلة catch. يوضح لك تتبع المكدس بالضبط أين حدث الخطأ في كودك.
الالتقاط مع تتبع المكدس
void main() {
try {
riskyFunction();
} catch (e, stackTrace) {
print('خطأ: $e');
print('تتبع المكدس:\n$stackTrace');
}
}
void riskyFunction() {
throw Exception('حدث خطأ في riskyFunction');
}
فئة Exception
فئة Exception في Dart هي الفئة الأساسية لجميع الاستثناءات التي يجب أن تلتقطها البرامج. بعض الاستثناءات المدمجة الشائعة تشمل:
FormatException-- يُطرح عندما لا يمكن تحليل نص (مثلint.parse('abc'))IOException-- يُطرح لإخفاقات الإدخال/الإخراجHttpException-- يُطرح للأخطاء المتعلقة بـ HTTPIntegerDivisionByZeroException-- يُطرح عند قسمة عدد صحيح على صفرStateError-- يُطرح عندما يكون الكائن في حالة غير صالحةArgumentError-- يُطرح عندما تتلقى دالة وسيطاً غير صالحRangeError-- يُطرح عندما يكون الفهرس خارج الحدود
الاستثناءات المدمجة الشائعة
void main() {
// FormatException
try {
int.parse('abc');
} on FormatException catch (e) {
print('خطأ تنسيق: $e');
}
// RangeError
try {
var list = [1, 2, 3];
print(list[5]);
} on RangeError catch (e) {
print('خطأ نطاق: $e');
}
// StateError
try {
var emptyList = <int>[];
emptyList.first; // لا يوجد عنصر
} on StateError catch (e) {
print('خطأ حالة: $e');
}
}
طرح الاستثناءات
يمكنك طرح استثناءاتك الخاصة باستخدام كلمة throw. يمكنك طرح أي كائن في Dart لكن من أفضل الممارسات طرح كائنات تنفذ Exception أو Error.
طرح استثناء
double divide(double a, double b) {
if (b == 0) {
throw ArgumentError('لا يمكن القسمة على صفر');
}
return a / b;
}
void main() {
try {
var result = divide(10, 0);
print(result);
} on ArgumentError catch (e) {
print('خطأ: ${e.message}');
}
}
// المخرجات:
// خطأ: لا يمكن القسمة على صفر
طرح Exception مع رسالة
void validateAge(int age) {
if (age < 0) {
throw Exception('العمر لا يمكن أن يكون سالباً: $age');
}
if (age > 150) {
throw Exception('العمر غير واقعي: $age');
}
print('عمر صالح: $age');
}
void main() {
try {
validateAge(-5);
} catch (e) {
print(e); // Exception: العمر لا يمكن أن يكون سالباً: -5
}
}
إنشاء استثناءات مخصصة
للتطبيقات المعقدة يجب عليك إنشاء فئات استثناءات خاصة بك. هذا يجعل معالجة الأخطاء أكثر دقة ومعنى.
فئات استثناءات مخصصة
class InsufficientFundsException implements Exception {
final double amount;
final double balance;
InsufficientFundsException(this.amount, this.balance);
@override
String toString() =>
'InsufficientFundsException: حاول سحب \$$amount '
'لكن \$$balance فقط متاح.';
}
class AccountLockedException implements Exception {
final String reason;
AccountLockedException(this.reason);
@override
String toString() => 'AccountLockedException: $reason';
}
class BankAccount {
double balance;
bool isLocked;
BankAccount(this.balance, {this.isLocked = false});
void withdraw(double amount) {
if (isLocked) {
throw AccountLockedException('الحساب مقفل لأسباب أمنية.');
}
if (amount > balance) {
throw InsufficientFundsException(amount, balance);
}
balance -= amount;
print('تم سحب \$$amount. الرصيد الجديد: \$$balance');
}
}
void main() {
var account = BankAccount(100.0);
try {
account.withdraw(150.0);
} on InsufficientFundsException catch (e) {
print(e);
} on AccountLockedException catch (e) {
print(e);
}
}
// المخرجات:
// InsufficientFundsException: حاول سحب $150.0
// لكن $100.0 فقط متاح.
implements Exception (وليس extends) للاستثناءات المخصصة. أعد تعريف toString() لتوفير رسائل خطأ ذات معنى. أضف حقول بيانات ذات صلة حتى يتمكن الملتقط من فهم ما حدث.Error مقابل Exception
يفرق Dart بين Error و Exception. فهم الفرق أمر حاسم للتعامل السليم مع الأخطاء:
- Exception -- يمثل حالات يجب أن يتوقعها البرنامج ويلتقطها. أمثلة: فشل الشبكة ومدخلات المستخدم غير الصالحة وعدم العثور على ملف. هذه قابلة للاسترداد.
- Error -- يمثل أخطاء برمجية أو حالات لا يجب أن تحدث. أمثلة: الوصول إلى مؤشر فارغ وتجاوز المكدس وفشل التأكيدات. هذه عموماً ليست مصممة للالتقاط.
Error مقابل Exception
void main() {
// هذا Exception -- قابل للاسترداد ويجب التقاطه
try {
int.parse('hello');
} on FormatException {
print('تم المعالجة: تنسيق إدخال سيئ');
}
// هذا Error -- عادةً خطأ برمجي
// يمكنك التقاطه لكن عموماً لا يجب ذلك
try {
List<int> empty = [];
print(empty[0]); // RangeError (فئة فرعية من Error)
} on RangeError {
print('تم التقاط RangeError -- لكن أصلح الكود بدلاً من ذلك!');
}
}
Error في كود الإنتاج إلا إذا كان لديك سبب وجيه جداً. أخطاء مثل StackOverflowError و OutOfMemoryError و TypeError تشير إلى أخطاء في كودك يجب إصلاحها وليس إخفاؤها خلف كتل catch.إعادة طرح الاستثناءات
أحياناً تريد التقاط استثناء لتسجيله أو إجراء معالجة جزئية ثم تمريره لشخص آخر للتعامل معه. استخدم كلمة rethrow لإعادة طرح الاستثناء الأصلي مع تتبع المكدس الأصلي.
استخدام rethrow
void processData(String data) {
try {
var number = int.parse(data);
print('تمت المعالجة: $number');
} catch (e) {
print('تسجيل الخطأ: $e');
rethrow; // تمرير الاستثناء إلى المستدعي
}
}
void main() {
try {
processData('not_a_number');
} catch (e) {
print('Main التقط: $e');
}
}
// المخرجات:
// تسجيل الخطأ: FormatException: not_a_number
// Main التقط: FormatException: not_a_number
rethrow بدلاً من throw e. كلمة rethrow تحافظ على تتبع المكدس الأصلي بينما throw e تنشئ واحداً جديداً مما يجعل تصحيح الأخطاء أصعب.عبارات Assert (وضع التطوير)
عبارة assert هي أداة تطوير تتحقق مما إذا كان الشرط صحيحاً. إذا كان الشرط خاطئاً تطرح AssertionError. التأكيدات تنشط فقط في وضع التصحيح وتُزال تماماً في بنيات الإنتاج.
استخدام assert
void setTemperature(double celsius) {
// هذا الفحص يعمل فقط في وضع التصحيح
assert(celsius >= -273.15, 'الحرارة لا يمكن أن تكون أقل من الصفر المطلق!');
print('تم ضبط الحرارة على $celsius°C');
}
void main() {
setTemperature(25.0); // يعمل بشكل جيد
setTemperature(-300.0); // AssertionError في وضع التصحيح!
}
// في وضع التصحيح:
// تم ضبط الحرارة على 25.0°C
// AssertionError: الحرارة لا يمكن أن تكون أقل من الصفر المطلق!
Assert مع المنشئات
class Rectangle {
final double width;
final double height;
Rectangle(this.width, this.height)
: assert(width > 0, 'العرض يجب أن يكون موجباً'),
assert(height > 0, 'الارتفاع يجب أن يكون موجباً');
double get area => width * height;
}
void main() {
var rect = Rectangle(5, 10);
print('المساحة: ${rect.area}'); // المساحة: 50.0
// في وضع التصحيح هذا سيطرح AssertionError:
// var invalid = Rectangle(-1, 10);
}
assert لالتقاط أخطاء البرمجة أثناء التطوير. للشروط التي يجب فحصها في الإنتاج (مثل مدخلات المستخدم) استخدم عبارات if العادية واطرح استثناءات بدلاً من ذلك.أنماط معالجة الأخطاء العملية
دعنا نلقي نظرة على بعض الأنماط الواقعية التي ستواجهها كثيراً في تطوير Dart و Flutter.
النمط 1: تحليل مدخلات المستخدم بأمان
التحليل الآمن مع tryParse
void main() {
String userInput = 'abc';
// سيئ: يطرح استثناء
// int number = int.parse(userInput);
// جيد: يعيد null إذا فشل التحليل
int? number = int.tryParse(userInput);
if (number != null) {
print('تم التحليل: $number');
} else {
print('رقم غير صالح: $userInput');
}
}
// المخرجات:
// رقم غير صالح: abc
النمط 2: معالجة أخطاء العمليات المتعددة
معالجة الأخطاء في العمليات المتسلسلة
class ApiException implements Exception {
final int statusCode;
final String message;
ApiException(this.statusCode, this.message);
@override
String toString() => 'ApiException [$statusCode]: $message';
}
class UserService {
Map<String, dynamic> fetchUser(int id) {
if (id < 0) {
throw ArgumentError('معرف المستخدم يجب أن يكون موجباً');
}
if (id == 0) {
throw ApiException(404, 'المستخدم غير موجود');
}
return {'id': id, 'name': 'مستخدم $id'};
}
void deleteUser(int id) {
if (id == 1) {
throw ApiException(403, 'لا يمكن حذف المستخدم المسؤول');
}
print('تم حذف المستخدم $id بنجاح');
}
}
void main() {
var service = UserService();
// معالجة أنواع أخطاء مختلفة
for (var id in [-1, 0, 1, 42]) {
try {
var user = service.fetchUser(id);
print('وُجد: ${user['name']}');
service.deleteUser(id);
} on ArgumentError catch (e) {
print('وسيط غير صالح: ${e.message}');
} on ApiException catch (e) {
print('خطأ API: $e');
} catch (e) {
print('خطأ غير متوقع: $e');
}
print('---');
}
}
النمط 3: نمط نوع النتيجة (تجنب الاستثناءات)
استخدام فئة Result
class Result<T> {
final T? value;
final String? error;
final bool isSuccess;
Result.success(this.value)
: error = null,
isSuccess = true;
Result.failure(this.error)
: value = null,
isSuccess = false;
}
Result<int> safeDivide(int a, int b) {
if (b == 0) {
return Result.failure('لا يمكن القسمة على صفر');
}
return Result.success(a ~/ b);
}
void main() {
var result1 = safeDivide(10, 3);
if (result1.isSuccess) {
print('النتيجة: ${result1.value}'); // النتيجة: 3
}
var result2 = safeDivide(10, 0);
if (!result2.isSuccess) {
print('خطأ: ${result2.error}'); // خطأ: لا يمكن القسمة على صفر
}
}
متى لا تستخدم الاستثناءات
الاستثناءات قوية لكن استخدامها بشكل غير صحيح يمكن أن يجعل كودك أصعب في القراءة وأبطأ في التنفيذ. إليك الحالات التي يجب تجنبها:
- الحالات المتوقعة: إذا كانت الحالة طبيعية ومتوقعة (مثل عدم إدخال المستخدم لنص) استخدم فحص
ifبدلاً من التقاط استثناء. - التحكم في التدفق: لا تستخدم الاستثناءات أبداً للتحكم في التدفق الطبيعي لبرنامجك. يجب أن تشير فقط إلى حالات استثنائية حقيقية.
- الكود الحساس للأداء: طرح والتقاط الاستثناءات له تكلفة أداء. في الحلقات الضيقة استخدم القيم المرجعة أو فحوصات null بدلاً من ذلك.
- عندما يتوفر tryParse: استخدم
int.tryParse()وdouble.tryParse()والطرق المشابهة بدلاً من التقاطFormatException.
استخدام جيد مقابل سيئ للاستثناءات
// سيئ: استخدام الاستثناءات للتدفق العادي
bool isValidEmailBad(String email) {
try {
if (!email.contains('@')) throw FormatException();
return true;
} on FormatException {
return false;
}
}
// جيد: منطق شرطي بسيط
bool isValidEmailGood(String email) {
return email.contains('@') && email.contains('.');
}
// سيئ: التقاط استثناءات لـ null المتوقع
String getUserNameBad(Map<String, dynamic> data) {
try {
return data['name'] as String;
} catch (e) {
return 'غير معروف';
}
}
// جيد: فحص null-aware
String getUserNameGood(Map<String, dynamic> data) {
return (data['name'] as String?) ?? 'غير معروف';
}
catch (e) عام في المستوى الأعلى لتطبيقك يمكن أن يخفي أخطاء حقيقية. كن محدداً قدر الإمكان في عبارات catch واستخدم التقاطاً عاماً فقط كملاذ أخير للأخطاء غير المتوقعة.ملخص
إليك مرجعاً سريعاً لما غطيناه:
try-catch-finally-- الهيكل الأساسي لمعالجة الاستثناءاتon-- يلتقط نوع استثناء محددcatch (e, stackTrace)-- يلتقط كائن الاستثناء وتتبع المكدسthrow-- يطرح استثناءrethrow-- يعيد طرح الاستثناء الحالي مع الحفاظ على تتبع المكدس- الاستثناءات المخصصة -- نفذ
Exceptionلأنواع أخطاء ذات معنى ErrorمقابلException-- الأخطاء هي عيوب برمجية والاستثناءات حالات قابلة للاستردادassert-- فحوصات شروط للتصحيح فقط تُزال في الإنتاج- استخدم
tryParseوفحوصات null بدلاً من الاستثناءات عندما يكون ذلك ممكناً - لا تستخدم الاستثناءات أبداً للتحكم في التدفق العادي
تمرين عملي
افتح DartPad وابنِ مدقق تسجيل مستخدم بسيط. أنشئ استثناءات مخصصة: InvalidEmailException و WeakPasswordException و UsernameTakenException. اكتب دالة registerUser() تتحقق من البريد الإلكتروني (يجب أن يحتوي على @) وكلمة المرور (يجب أن تكون 8 أحرف على الأقل) واسم المستخدم (لا يمكن أن يكون "admin" أو "root"). استخدم try-catch مع عبارات on محددة للتعامل مع كل نوع استثناء بشكل مختلف وطباعة رسائل خطأ مفهومة للمستخدم. أضف عبارات assert للتحقق من أن أياً من المدخلات ليست نصوصاً فارغة.