فئة Object ونظام الأنواع
تسلسل فئة 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
}
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
}
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('ليس مستطيلاً'); // يطبع هذا
}
}
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.