البرمجة كائنية التوجه في Dart

فئة Object ونظام الأنواع

50 دقيقة الدرس 16 من 24

تسلسل فئة Object

في Dart، كل فئة ترث ضمنياً من Object. هذا يعني أن كل قيمة تنشئها -- سواء كانت String أو int أو List أو فئتك المخصصة -- هي نسخة من Object. فئة Object تقع في أعلى تسلسل الفئات وتوفر عدة طرق أساسية يرثها كل كائن.

مع أمان null السليم في Dart، تسلسل الأنواع لديه فعلياً جذران:

  • Object -- جذر جميع الأنواع غير القابلة للإلغاء. كل قيمة غير null هي Object.
  • Object? -- جذر جميع الأنواع بما فيها القابلة للإلغاء. null هو Object? لكن ليس Object.
  • Null -- نوع null، نوع فرعي لكل نوع قابل للإلغاء.
  • Never -- النوع السفلي، نوع فرعي لكل نوع. لا توجد قيمة من نوع Never.

تسلسل الأنواع

void main() {
  // كل شيء هو Object
  Object a = 42;
  Object b = 'hello';
  Object c = [1, 2, 3];
  Object d = true;

  print(a is Object);  // true
  print(b is Object);  // true
  print(c is Object);  // true

  // null هو Object? لكن ليس Object
  Object? nullable = null;
  print(nullable is Object?);  // true
  print(nullable is Object);   // false -- null ليس Object

  // كل نوع هو نوع فرعي من Object?
  print(42 is Object?);      // true
  print('hi' is Object?);   // true
  print(null is Object?);    // true
}
تمييز رئيسي: في Dart 3.x مع أمان null، Object تعني “أي قيمة غير null” و Object? تعني “أي قيمة بما فيها null.” عندما ترى معامل دالة من نوع Object، تعلم أنه لن يكون null أبداً.

الطرق الموروثة من Object

فئة Object توفر ثلاث طرق رئيسية يرثها كل فئة ويمكن تجاوزها:

toString() و operator == و hashCode

class Product {
  final String name;
  final double price;
  final String category;

  const Product(this.name, this.price, this.category);

  // تجاوز toString() لتمثيل نصي ذي معنى
  @override
  String toString() => 'Product($name, \$$price, $category)';

  // تجاوز == لمساواة القيمة
  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is Product &&
        other.name == name &&
        other.price == price &&
        other.category == category;
  }

  // تجاوز hashCode (يجب أن يتطابق مع ==)
  @override
  int get hashCode => Object.hash(name, price, category);
}

void main() {
  final p = Product('Laptop', 999.99, 'Electronics');

  // toString() يُستدعى بواسطة print() والاستيفاء النصي
  print(p);                     // Product(Laptop, $999.99, Electronics)
  print('اشتريت: $p');          // اشتريت: Product(Laptop, $999.99, Electronics)
  print(p.toString());          // Product(Laptop, $999.99, Electronics)

  // بدون التجاوز سترى: Instance of 'Product'
}

خاصية runtimeType

كل كائن لديه خاصية runtimeType تُرجع نوعه الفعلي وقت التشغيل. هذا مفيد للتصحيح والتسجيل، لكن عموماً لا يجب استخدامه لفحص الأنواع في كود الإنتاج.

استخدام runtimeType

class Animal {
  final String name;
  Animal(this.name);
}

class Dog extends Animal {
  Dog(super.name);
}

class Cat extends Animal {
  Cat(super.name);
}

void main() {
  Animal a = Dog('Rex');
  Animal b = Cat('Whiskers');

  print(a.runtimeType);  // Dog
  print(b.runtimeType);  // Cat

  // Type هو نوع وقت التجميع، runtimeType هو النوع الفعلي
  print(a.runtimeType == Dog);  // true
  print(a.runtimeType == Animal);  // false -- إنه Dog وليس فقط Animal

  // مفيد للتصحيح
  void debugPrint(Object obj) {
    print('[${obj.runtimeType}] $obj');
  }

  debugPrint(42);          // [int] 42
  debugPrint('hello');     // [String] hello
  debugPrint(a);           // [Dog] Instance of 'Dog'
  debugPrint([1, 2, 3]);   // [List<int>] [1, 2, 3]
}
تحذير: لا تستخدم runtimeType لفحص الأنواع في كود الإنتاج. استخدم is بدلاً من ذلك. خاصية runtimeType يمكن تجاوزها، وقد لا تعمل جيداً مع الأنواع العامة، ولا تأخذ في الاعتبار علاقات الأنواع الفرعية. هي أداة تصحيح بشكل أساسي.

تجاوز noSuchMethod()

عندما تستدعي طريقة غير موجودة على كائن، عادةً يعطيك Dart خطأ وقت التجميع. لكن إذا تجاوزت فئة noSuchMethod()، يمكنها التعامل مع استدعاءات الطرق غير المعرّفة وقت التشغيل. يُستخدم هذا بشكل أساسي مع أنواع dynamic.

noSuchMethod() للوكلاء الديناميكيين

class DynamicLogger {
  final String prefix;

  DynamicLogger(this.prefix);

  @override
  dynamic noSuchMethod(Invocation invocation) {
    final methodName = invocation.memberName.toString();
    final args = invocation.positionalArguments;
    final named = invocation.namedArguments;

    print('[$prefix] استُدعي: $methodName');
    if (args.isNotEmpty) print('  المعاملات: $args');
    if (named.isNotEmpty) print('  المسماة: $named');

    return null;
  }
}

// أكثر عملية: محاكاة للاختبار
abstract class Database {
  Future<Map<String, dynamic>> findById(String id);
  Future<List<Map<String, dynamic>>> findAll();
  Future<void> insert(Map<String, dynamic> data);
}

class MockDatabase implements Database {
  final List<Map<String, dynamic>> _data = [];
  final List<String> callLog = [];

  @override
  dynamic noSuchMethod(Invocation invocation) {
    callLog.add(invocation.memberName.toString());
    // إرجاع قيم افتراضية مناسبة
    if (invocation.memberName == #findById) {
      return Future.value(<String, dynamic>{});
    }
    if (invocation.memberName == #findAll) {
      return Future.value(_data);
    }
    return Future.value(null);
  }
}

void main() {
  // استخدام ديناميكي
  dynamic logger = DynamicLogger('APP');
  logger.start();           // [APP] استُدعي: Symbol("start")
  logger.process('data');   // [APP] استُدعي: Symbol("process"), المعاملات: [data]

  // محاكاة قاعدة البيانات
  final db = MockDatabase();
  // db.findAll() و db.findById('123') إلخ تُعالج بواسطة noSuchMethod
}
نصيحة: في Dart الحديث، noSuchMethod() نادراً ما يكون مطلوباً لأن نظام الأنواع يلتقط معظم الأخطاء وقت التجميع. يُستخدم بشكل رئيسي لـ: (1) تنفيذ كائنات محاكاة للاختبار، (2) بناء أنماط الوكيل/المغلف، و (3) التعامل مع هياكل بيانات ديناميكية شبيهة بـ JSON.

فحص الأنواع مع is و is!

معامل is يتحقق مما إذا كان كائن نسخة من نوع محدد (بما فيها أنواعه الفرعية). معامل is! هو نفيه. يقوم Dart أيضاً بـ ترقية النوع -- بعد فحص is، يُعامل المتغير تلقائياً كالنوع المفحوص ضمن ذلك النطاق.

فحص وترقية الأنواع

abstract class Shape {
  double get area;
}

class Circle extends Shape {
  final double radius;
  Circle(this.radius);

  @override
  double get area => 3.14159 * radius * radius;

  double get circumference => 2 * 3.14159 * radius;
}

class Rectangle extends Shape {
  final double width;
  final double height;
  Rectangle(this.width, this.height);

  @override
  double get area => width * height;

  double get diagonal => (width * width + height * height).sqrt();
}

class Triangle extends Shape {
  final double base;
  final double height;
  Triangle(this.base, this.height);

  @override
  double get area => 0.5 * base * height;
}

void describeShape(Shape shape) {
  print('المساحة: ${shape.area.toStringAsFixed(2)}');

  // ترقية النوع: بعد فحص 'is'، shape يُحول تلقائياً
  if (shape is Circle) {
    // shape يُعامل الآن كـ Circle -- لا حاجة للتحويل!
    print('المحيط: ${shape.circumference.toStringAsFixed(2)}');
  } else if (shape is Rectangle) {
    // shape يُعامل الآن كـ Rectangle
    print('القطر: ${shape.diagonal.toStringAsFixed(2)}');
    print('الأبعاد: ${shape.width} x ${shape.height}');
  } else if (shape is Triangle) {
    print('القاعدة: ${shape.base}، الارتفاع: ${shape.height}');
  }
}

void main() {
  final shapes = <Shape>[
    Circle(5),
    Rectangle(10, 4),
    Triangle(6, 8),
  ];

  for (final shape in shapes) {
    print('--- ${shape.runtimeType} ---');
    describeShape(shape);
  }

  // معامل is! (ليس is)
  final obj = Circle(3);
  if (obj is! Rectangle) {
    print('ليس مستطيلاً');  // يطبع هذا
  }
}
ترقية النوع: ترقية النوع في Dart ذكية -- بعد فحص is في عبارة if، يمكنك الوصول لأعضاء الفئة الفرعية المحددة بدون تحويل صريح. يعمل هذا مع if و while والتعبيرات الشرطية والمعاملات المنطقية. يعمل أيضاً مع فحوصات null: if (x != null) يرقي x من T? إلى T.

تحويل الأنواع مع as

الكلمة المفتاحية as تقوم بتحويل نوع صريح. على عكس is (الآمن)، as سيرمي TypeError وقت التشغيل إذا كان التحويل غير صالح.

التحويل الآمن مقابل غير الآمن

void main() {
  Object value = 'Hello, Dart!';

  // آمن: استخدام 'is' أولاً ثم ترقية النوع
  if (value is String) {
    print(value.toUpperCase());  // HELLO, DART!
  }

  // غير آمن: تحويل مباشر بـ 'as' -- يرمي إذا النوع خاطئ
  String text = value as String;  // يعمل لأنه فعلاً String
  print(text.length);  // 12

  // هذا سيرمي TypeError وقت التشغيل:
  // int number = value as int;  // TypeError: String ليس int

  // نمط آمن: try-catch مع 'as'
  try {
    final number = value as int;
    print(number);
  } on TypeError {
    print('لا يمكن التحويل إلى int');
  }

  // التحويل في المجموعات
  List<Object> mixed = [1, 'two', 3.0, true];

  // تصفية وتحويل بأمان
  final strings = mixed.whereType<String>().toList();
  print(strings);  // [two]

  final numbers = mixed.whereType<int>().toList();
  print(numbers);  // [1]
}
أفضل ممارسة: فضّل دائماً is مع ترقية النوع على تحويل as. نهج is آمن ولن يرمي أبداً. استخدم as فقط عندما تكون متأكداً تماماً من النوع، مثل بعد فحص is سابق في نطاق مختلف أو في الاختبارات.

dynamic مقابل Object مقابل Object?

هذه الأنواع الثلاثة غالباً ما يُخلط بينها، لكنها تخدم أغراضاً مختلفة جداً:

فهم الاختلافات

void main() {
  // Object: يقبل أي قيمة غير null، لكن يقيد الوصول لطرق Object
  Object obj = 'hello';
  // obj.toUpperCase();  // خطأ: Object ليس لديه toUpperCase
  print(obj.toString());  // حسناً: toString() موجود على Object
  print(obj.hashCode);    // حسناً: hashCode موجود على Object

  // Object?: يقبل أي قيمة بما فيها null
  Object? nullable = null;
  nullable = 'hello';
  nullable = 42;
  // nullable.toUpperCase();  // خطأ: نفس القيود كـ Object

  // dynamic: يقبل أي قيمة، يسمح بأي استدعاء طريقة (بدون فحوصات وقت التجميع)
  dynamic dyn = 'hello';
  print(dyn.toUpperCase());  // حسناً وقت التجميع، حسناً وقت التشغيل: HELLO
  dyn = 42;
  // print(dyn.toUpperCase());  // حسناً وقت التجميع، ينهار وقت التشغيل!
}

// مقارنة أمان الأنواع
void processObject(Object value) {
  // المترجم يضمن أنك تستخدم فقط طرق Object
  print(value.toString());
  // value.customMethod();  // خطأ تجميع -- يُلتقط قبل التشغيل
}

void processDynamic(dynamic value) {
  // المترجم يسمح بأي شيء -- الأخطاء تحدث وقت التشغيل
  print(value.toString());
  // value.customMethod();  // لا خطأ تجميع! ينهار وقت التشغيل إذا الطريقة مفقودة
}

// متى تستخدم كل واحد:
// Object  -- عندما تحتاج لقبول أي قيمة غير null لكن تريد أمان الأنواع
// Object? -- عندما تحتاج لقبول أي قيمة بما فيها null
// dynamic -- عند العمل مع JSON أو التكامل أو أنواع مجهولة حقاً
//            (استخدم باعتدال -- تفقد كل أمان الأنواع)
قاعدة عامة: الافتراضي هو Object أو Object? عندما تحتاج نوعاً عاماً. استخدم dynamic فقط عندما تحتاج فعلاً لاستدعاء طرق تعسفية (مثل معالجة بيانات JSON). كلما كان النوع أكثر صرامة، كلما ساعدك المترجم في التقاط الأخطاء.

مثال عملي: حاوية آمنة النوع

لنبني حاوية آمنة النوع تستخدم نظام الأنواع بفعالية -- لتوضيح فحص الأنواع والتحويل وفئة Object في سيناريو واقعي.

TypedBox: حاوية آمنة النوع

class TypedBox<T extends Object> {
  T _value;

  TypedBox(this._value);

  T get value => _value;

  set value(T newValue) {
    _value = newValue;
  }

  // التحقق مما إذا كانت القيمة المخزنة من نوع محدد
  bool containsType<U>() => _value is U;

  // تحويل آمن: يُرجع null إذا النوع لا يتطابق
  U? getAs<U>() {
    final v = _value;
    if (v is U) return v;
    return null;
  }

  // تحويل القيمة مع أمان النوع
  TypedBox<U> map<U extends Object>(U Function(T) transform) {
    return TypedBox<U>(transform(_value));
  }

  @override
  String toString() => 'TypedBox<${T}>($_value)';
}

// سجل يخزن أنواعاً مختلفة بأمان
class TypeRegistry {
  final Map<Type, Object> _entries = {};

  void register<T extends Object>(T value) {
    _entries[T] = value;
  }

  T? get<T extends Object>() {
    final value = _entries[T];
    if (value is T) return value;
    return null;
  }

  bool has<T>() => _entries.containsKey(T);

  List<Type> get registeredTypes => _entries.keys.toList();
}

void main() {
  // استخدام TypedBox
  final box = TypedBox<String>('Hello');
  print(box.value);              // Hello
  print(box.containsType<String>());  // true
  print(box.containsType<int>());     // false

  // تحويل آمن
  final asString = box.getAs<String>();
  final asInt = box.getAs<int>();
  print(asString);  // Hello
  print(asInt);     // null (آمن!)

  // تحويل
  final lengthBox = box.map<int>((s) => s.length);
  print(lengthBox);  // TypedBox<int>(5)

  // استخدام TypeRegistry
  final registry = TypeRegistry();
  registry.register<String>('App Config');
  registry.register<int>(42);
  registry.register<List<String>>(['a', 'b']);

  print(registry.get<String>());  // App Config
  print(registry.get<int>());     // 42
  print(registry.get<double>());  // null (غير مسجل)
  print(registry.registeredTypes); // [String, int, List<String>]
}

مطابقة الأنماط مع الأنواع (Dart 3.x)

قدم Dart 3 مطابقة أنماط قوية تجعل فحص الأنواع أكثر أناقة. يمكنك دمج فحوصات الأنواع مع تفكيك البنية في تعبيرات switch وعبارات if-case.

مطابقة الأنماط الحديثة المبنية على الأنواع

sealed class Result<T> {}

class Success<T> extends Result<T> {
  final T value;
  Success(this.value);
}

class Failure<T> extends Result<T> {
  final String error;
  final int code;
  Failure(this.error, this.code);
}

class Loading<T> extends Result<T> {}

// مطابقة الأنماط مع تعبير switch
String describeResult(Result<String> result) {
  return switch (result) {
    Success(value: final v) => 'حصلنا على: $v',
    Failure(error: final e, code: final c) => 'خطأ $c: $e',
    Loading() => 'جاري التحميل...',
  };
}

// أنماط الأنواع في if-case
void processValue(Object value) {
  if (value case int n when n > 0) {
    print('عدد صحيح موجب: $n');
  } else if (value case String s when s.isNotEmpty) {
    print('نص غير فارغ: $s');
  } else if (value case List<int> numbers when numbers.length > 2) {
    print('قائمة أعداد طويلة: $numbers');
  } else {
    print('آخر: $value');
  }
}

void main() {
  print(describeResult(Success('تم تحميل البيانات')));  // حصلنا على: تم تحميل البيانات
  print(describeResult(Failure('غير موجود', 404)));  // خطأ 404: غير موجود
  print(describeResult(Loading()));  // جاري التحميل...

  processValue(42);          // عدد صحيح موجب: 42
  processValue('hello');     // نص غير فارغ: hello
  processValue([1, 2, 3]);   // قائمة أعداد طويلة: [1, 2, 3]
  processValue(-5);          // آخر: -5
}
ملخص: فئة Object هي جذر تسلسل أنواع Dart، وتوفر toString() و == و hashCode و runtimeType و noSuchMethod(). استخدم is لفحص الأنواع الآمن مع ترقية تلقائية، و as للتحويل الصريح (غير الآمن)، وفضّل Object على dynamic لأمان الأنواع. مطابقة أنماط Dart 3.x تجعل المنطق المبني على الأنواع أنظف مع تعبيرات switch وعبارات if-case.