الفئات المختومة ومطابقة الأنماط
ما هي الفئات المختومة؟
قدّم Dart 3 الفئات المختومة -- طريقة قوية لتعريف مجموعة مغلقة من الأنواع الفرعية. عندما تُعلّم فئة بـ sealed، يعرف المترجم كل نوع فرعي ممكن وقت الترجمة. هذا يعني أنك تستطيع استخدام تعبيرات switch وسيخبرك المترجم إذا نسيت حالة. الفئات المختومة تجمع بين أمان التعدادات ومرونة التسلسلات الهرمية للفئات.
فكّر في الفئة المختومة كظرف مختوم: بمجرد أن تُعرّف ما بداخله، لا يمكن إضافة شيء آخر من خارج نفس المكتبة. كل فئة فرعية يجب أن تُعرّف في نفس الملف مع الفئة المختومة.
أول فئة مختومة لك
// جميع الأنواع الفرعية يجب أن تكون في نفس هذا الملف
sealed class Shape {}
class Circle extends Shape {
final double radius;
Circle(this.radius);
}
class Rectangle extends Shape {
final double width;
final double height;
Rectangle(this.width, this.height);
}
class Triangle extends Shape {
final double base;
final double height;
Triangle(this.base, this.height);
}
// هذه الدالة شاملة -- المترجم يتحقق من جميع الحالات
double area(Shape shape) {
return switch (shape) {
Circle(radius: var r) => 3.14159 * r * r,
Rectangle(width: var w, height: var h) => w * h,
Triangle(base: var b, height: var h) => 0.5 * b * h,
};
// لا حاجة لـ default! المترجم يعرف جميع الأنواع الفرعية.
}
void main() {
final shapes = [Circle(5), Rectangle(10, 4), Triangle(6, 3)];
for (final s in shapes) {
print('Area: ${area(s).toStringAsFixed(2)}');
}
// Area: 78.54
// Area: 40.00
// Area: 9.00
}
part). إذا حاولت توسيع فئة مختومة من ملف آخر، سيرمي المترجم خطأ.مطابقة الأنماط الشاملة
أكبر فائدة للفئات المختومة هي التحقق الشامل. عندما تُبدّل على نوع مختوم، يتحقق محلل Dart من أنك تعاملت مع كل نوع فرعي ممكن. إذا أضفت نوعاً فرعياً جديداً لاحقاً، كل تعبير switch نسي الحالة الجديدة سيُظهر خطأ وقت الترجمة. هذا يزيل فئة كاملة من الأخطاء.
تعبيرات Switch الشاملة
sealed class AuthState {}
class Authenticated extends AuthState {
final String username;
final String token;
Authenticated(this.username, this.token);
}
class Unauthenticated extends AuthState {}
class AuthLoading extends AuthState {}
class AuthError extends AuthState {
final String message;
AuthError(this.message);
}
// المترجم يُجبرك على معالجة جميع الحالات الأربع
String describeAuth(AuthState state) {
return switch (state) {
Authenticated(username: var user) => 'مرحباً مجدداً، $user!',
Unauthenticated() => 'يرجى تسجيل الدخول.',
AuthLoading() => 'جارٍ التحميل...',
AuthError(message: var msg) => 'خطأ: $msg',
};
}
// إذا أضفت نوعاً فرعياً جديداً لاحقاً:
// class AuthExpired extends AuthState {}
// المترجم يحذر فوراً:
// "النوع 'AuthState' غير مطابق بشكل شامل -- AuthExpired مفقود"
default. مع تعداد + default، إضافة قيمة جديدة تمر بصمت إلى default. مع الفئات المختومة، نسيان حالة هو خطأ وقت الترجمة، وليس خطأ وقت التشغيل.تعبيرات Switch مع التفكيك
تعبيرات switch في Dart 3 أقوى من جمل switch التقليدية. يمكنك تفكيك خصائص الكائن مباشرة في النمط، واستخدام شروط الحماية مع when، وإرجاع القيم مباشرة.
مطابقة الأنماط المتقدمة
sealed class ApiResponse {}
class Success extends ApiResponse {
final Map<String, dynamic> data;
final int statusCode;
Success(this.data, this.statusCode);
}
class Failure extends ApiResponse {
final String error;
final int statusCode;
Failure(this.error, this.statusCode);
}
class Loading extends ApiResponse {}
// التفكيك + شروط الحماية
String handleResponse(ApiResponse response) {
return switch (response) {
Success(data: var d, statusCode: 200) => 'موافق: ${d.length} حقول',
Success(data: var d, statusCode: 201) => 'تم الإنشاء: ${d.length} حقول',
Success(statusCode: var code) => 'نجاح برمز $code',
Failure(error: var e, statusCode: 404) => 'غير موجود: $e',
Failure(error: var e, statusCode: 500) => 'خطأ الخادم: $e',
Failure(error: var e, statusCode: var c) when c >= 400 => 'خطأ العميل $c: $e',
Failure(error: var e, statusCode: var c) => 'خطأ $c: $e',
Loading() => 'يرجى الانتظار...',
};
}
void main() {
final responses = [
Success({'name': 'Dart'}, 200),
Failure('المستخدم غير موجود', 404),
Loading(),
Failure('طلب خاطئ', 422),
];
for (final r in responses) {
print(handleResponse(r));
}
// موافق: 1 حقول
// غير موجود: المستخدم غير موجود
// يرجى الانتظار...
// خطأ العميل 422: طلب خاطئ
}
المختومة مقابل المجردة مقابل التعداد
اختيار الأداة المناسبة يعتمد على احتياجاتك. إليك مقارنة:
متى تستخدم كل نوع
// ENUM -- مجموعة بسيطة وثابتة من القيم بدون بيانات
enum Color { red, green, blue }
// استخدم عندما: القيم مجرد تسميات ولا حاجة لبيانات إضافية
// ABSTRACT CLASS -- تسلسل هرمي مفتوح، أي شخص يمكنه التوسيع
abstract class Animal {
String get sound;
}
// استخدم عندما: تريد أن تضيف مكتبات خارجية أنواعاً فرعية
// SEALED CLASS -- تسلسل هرمي مغلق، مطابقة شاملة
sealed class Result<T> {}
class Ok<T> extends Result<T> {
final T value;
Ok(this.value);
}
class Err<T> extends Result<T> {
final String error;
Err(this.error);
}
// استخدم عندما: تعرف جميع الأنواع الفرعية الممكنة مسبقاً
// وتريد أماناً وقت الترجمة
abstract class أو abstract interface class للتسلسلات الهرمية المفتوحة.نمط نوع النتيجة
أحد أكثر الاستخدامات العملية للفئات المختومة هو نوع النتيجة، الذي يحل محل الاستثناءات للأخطاء المتوقعة. بدلاً من الرمي والالتقاط، تُعيد إما نجاحاً أو فشلاً، والمترجم يُجبر المستدعين على معالجة كلتا الحالتين.
نوع النتيجة مع الفئات المختومة
sealed class Result<T> {
const Result();
}
class Success<T> extends Result<T> {
final T value;
const Success(this.value);
}
class Failure<T> extends Result<T> {
final String message;
final Exception? exception;
const Failure(this.message, [this.exception]);
}
// خدمة تُعيد Result بدلاً من الرمي
class UserRepository {
final Map<int, String> _users = {1: 'Alice', 2: 'Bob'};
Result<String> findUser(int id) {
if (id <= 0) {
return Failure('معرف غير صالح: يجب أن يكون موجباً');
}
final name = _users[id];
if (name == null) {
return Failure('المستخدم #$id غير موجود');
}
return Success(name);
}
}
void main() {
final repo = UserRepository();
// المترجم يضمن أنك تعالج كلاً من Success و Failure
for (final id in [1, 3, -1]) {
final result = repo.findUser(id);
final message = switch (result) {
Success(value: var name) => 'وُجد: $name',
Failure(message: var msg) => 'خطأ: $msg',
};
print(message);
}
// وُجد: Alice
// خطأ: المستخدم #3 غير موجود
// خطأ: معرف غير صالح: يجب أن يكون موجباً
}
fpdart و dartz توفر أنواع Result/Either جاهزة، لكن فهم كيفية بنائها بنفسك مع الفئات المختومة يمنحك تحكماً كاملاً.آلات الحالة مع الفئات المختومة
الفئات المختومة مثالية لنمذجة آلات الحالة حيث يمكن للكائن أن يكون فقط في واحدة من مجموعة معروفة من الحالات. هذا مفيد بشكل خاص في Flutter لإدارة حالات واجهة المستخدم مثل التحميل والخطأ وشاشات البيانات المحملة.
آلة حالة واجهة المستخدم
// كل حالة ممكنة لشاشة
sealed class PageState<T> {}
class Initial<T> extends PageState<T> {}
class Loading<T> extends PageState<T> {
final double? progress;
Loading([this.progress]);
}
class Loaded<T> extends PageState<T> {
final T data;
final DateTime loadedAt;
Loaded(this.data) : loadedAt = DateTime.now();
}
class Error<T> extends PageState<T> {
final String message;
final bool canRetry;
Error(this.message, {this.canRetry = true});
}
class Empty<T> extends PageState<T> {
final String hint;
Empty(this.hint);
}
// محاكاة بناء شجرة واجهة المستخدم بناءً على الحالة
String buildUI(PageState<List<String>> state) {
return switch (state) {
Initial() => '[شاشة الترحيب]',
Loading(progress: null) => '[مؤشر التحميل]',
Loading(progress: var p) => '[التقدم: ${(p! * 100).toInt()}%]',
Loaded(data: var items) when items.isEmpty
=> '[فارغ: لم يتم العثور على عناصر]',
Loaded(data: var items) => '[قائمة: ${items.length} عناصر]',
Error(message: var m, canRetry: true)
=> '[خطأ: $m] [زر إعادة المحاولة]',
Error(message: var m, canRetry: false)
=> '[خطأ: $m] [زر العودة]',
Empty(hint: var h) => '[حالة فارغة: $h]',
};
}
void main() {
final states = [
Initial<List<String>>(),
Loading<List<String>>(0.65),
Loaded(['Dart', 'Flutter', 'Firebase']),
Error<List<String>>('انتهت مهلة الشبكة', canRetry: true),
Empty<List<String>>('جرّب بحثاً مختلفاً'),
];
for (final s in states) {
print(buildUI(s));
}
// [شاشة الترحيب]
// [التقدم: 65%]
// [قائمة: 3 عناصر]
// [خطأ: انتهت مهلة الشبكة] [زر إعادة المحاولة]
// [حالة فارغة: جرّب بحثاً مختلفاً]
}
Loading قد يحتوي على قيمة تقدم، Loaded يحتوي على البيانات، و Error يحتوي على الرسالة. واجهة المستخدم تُبدّل على الحالة وتعرض العنصر المناسب.