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

مساواة الكائنات و Hashcode

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

فهم المساواة في Dart

في Dart، هناك طريقتان للتحقق مما إذا كان كائنان “نفس الشيء”: معامل == ودالة identical(). فهم الفرق أمر حاسم لكتابة كود صحيح وخالٍ من الأخطاء -- خاصة عند استخدام الكائنات كمفاتيح في Map أو عناصر في Set.

  • == (معامل المساواة) -- يتحقق مما إذا كان كائنان متساويين منطقياً (نفس القيمة). افتراضياً يتحقق من هوية المرجع، لكن يمكنك تجاوزه.
  • identical(a, b) -- يتحقق مما إذا كان متغيران يشيران إلى نفس الكائن بالضبط في الذاكرة. لا يمكن تجاوز هذا.

سلوك المساواة الافتراضي

class Point {
  final int x;
  final int y;
  Point(this.x, this.y);
}

void main() {
  final p1 = Point(3, 4);
  final p2 = Point(3, 4);
  final p3 = p1;

  // == الافتراضي يتحقق من هوية المرجع (مثل identical)
  print(p1 == p2);          // false -- كائنات مختلفة في الذاكرة
  print(p1 == p3);          // true  -- نفس المرجع
  print(identical(p1, p2)); // false -- كائنات مختلفة
  print(identical(p1, p3)); // true  -- نفس الكائن

  // النصوص والأرقام لها مساواة قيمة مدمجة
  print(42 == 42);           // true
  print('hello' == 'hello'); // true
}
رؤية رئيسية: بدون تجاوز ==، كائنان بنفس قيم الحقول يعتبران غير متساويين لأنهما يحتلان مواقع ذاكرة مختلفة. هذا تقريباً لا يكون أبداً ما تريده لكائنات القيمة مثل النقاط والألوان والمال والإحداثيات.

تجاوز == و hashCode

لتعريف مساواة القيمة لفئتك، يجب تجاوز كل من operator == و hashCode. هذان عقد -- يجب تجاوزهما معاً دائماً.

تجاوز المساواة بشكل صحيح

class Point {
  final int x;
  final int y;

  const Point(this.x, this.y);

  @override
  bool operator ==(Object other) {
    // 1. التحقق إذا نفس المرجع (مسار سريع)
    if (identical(this, other)) return true;

    // 2. التحقق من النوع وقيم الحقول
    return other is Point && other.x == x && other.y == y;
  }

  @override
  int get hashCode => Object.hash(x, y);

  @override
  String toString() => 'Point($x, $y)';
}

void main() {
  final p1 = Point(3, 4);
  final p2 = Point(3, 4);
  final p3 = Point(5, 6);

  print(p1 == p2);  // true  -- نفس القيم!
  print(p1 == p3);  // false -- قيم مختلفة
  print(identical(p1, p2));  // false -- لا يزالان كائنات مختلفة في الذاكرة
}
تحذير: معامل == يجب أن يكون نوعه Object، وليس نوع فئتك المحدد. كتابة bool operator ==(Point other) ستسبب خطأ تجميع لأن التوقيع يجب أن يطابق Object.operator ==.

العقد بين == و hashCode

هناك قاعدة غير قابلة للكسر في Dart (ومعظم لغات البرمجة):

  • إذا كان a == b صحيحاً، فيجب أن يكون a.hashCode == b.hashCode صحيحاً أيضاً.
  • العكس غير مطلوب -- كائنان غير متساويين يمكن أن يكون لهما نفس رمز التجزئة (يسمى تصادم).

كسر هذا العقد يسبب أخطاء كارثية مع Set و Map و HashMap و HashSet -- ستبدو كائناتك وكأنها “تختفي” من المجموعات.

ما يحدث عندما تكسر العقد

// سيء: يتجاوز == لكن ليس hashCode
class BrokenPoint {
  final int x;
  final int y;
  BrokenPoint(this.x, this.y);

  @override
  bool operator ==(Object other) {
    return other is BrokenPoint && other.x == x && other.y == y;
  }

  // hashCode غير متجاوز -- يستخدم الافتراضي (المبني على الهوية)
}

void main() {
  final p1 = BrokenPoint(3, 4);
  final p2 = BrokenPoint(3, 4);

  print(p1 == p2);  // true -- هما متساويان

  // لكن في Set...
  final set = {p1};
  print(set.contains(p2));  // false!! المجموعة لا تجد p2
                            // لأن p2.hashCode != p1.hashCode

  // نفس المشكلة مع مفاتيح Map
  final map = {p1: 'origin'};
  print(map[p2]);  // null!! لا يمكن إيجاد المفتاح
}
كيف تعمل Set و Map: عندما تضيف كائناً إلى Set أو تستخدمه كمفتاح Map، يتحقق Dart أولاً من hashCode لإيجاد “الحاوية” الصحيحة، ثم يستخدم == لتأكيد التطابق. إذا أعطى hashCode قيماً مختلفة لكائنات متساوية، يبحث Dart في الحاوية الخاطئة ولا يجد التطابق أبداً.

استخدام Object.hash() و Object.hashAll()

يوفر Dart 2.14+ طرق مساعدة مريحة لتوليد رموز تجزئة صحيحة من عدة حقول:

طرق توليد رمز التجزئة

class Student {
  final String name;
  final int age;
  final String email;
  final List<String> courses;

  const Student(this.name, this.age, this.email, this.courses);

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    if (other is! Student) return false;
    if (other.name != name || other.age != age || other.email != email) {
      return false;
    }
    // مقارنة عميقة للقائمة
    if (other.courses.length != courses.length) return false;
    for (int i = 0; i < courses.length; i++) {
      if (other.courses[i] != courses[i]) return false;
    }
    return true;
  }

  // Object.hash() لعدد ثابت من الحقول
  @override
  int get hashCode => Object.hash(name, age, email, Object.hashAll(courses));

  @override
  String toString() => 'Student($name, age: $age)';
}

void main() {
  final s1 = Student('Alice', 20, 'alice@email.com', ['Math', 'CS']);
  final s2 = Student('Alice', 20, 'alice@email.com', ['Math', 'CS']);

  print(s1 == s2);                // true
  print(s1.hashCode == s2.hashCode);  // true

  // يعمل بشكل صحيح في Set و Map
  final students = {s1};
  print(students.contains(s2));   // true -- وجد!

  final grades = {s1: 'A+'};
  print(grades[s2]);              // A+ -- وجد!
}
نصيحة: استخدم Object.hash(field1, field2, ...) لعدد ثابت من الحقول (حتى 20). استخدم Object.hashAll(iterable) عند تجزئة مجموعة. يمكنك تداخلها كما هو موضح -- Object.hash(name, Object.hashAll(list)).

الكلمة المفتاحية covariant

أحياناً تريد أن يقبل تجاوز == نوعاً أكثر تحديداً من Object. الكلمة المفتاحية covariant تخبر Dart بتخفيف فحص النوع، مما يسمح للمعامل بأن يكون نوعاً فرعياً من النوع المُعلن.

استخدام covariant في المساواة

class Animal {
  final String name;
  final int age;

  Animal(this.name, this.age);

  @override
  bool operator ==(covariant Animal other) {
    return name == other.name && age == other.age;
  }

  @override
  int get hashCode => Object.hash(name, age);
}

class Dog extends Animal {
  final String breed;

  Dog(super.name, super.age, this.breed);

  @override
  bool operator ==(covariant Dog other) {
    return super == other && breed == other.breed;
  }

  @override
  int get hashCode => Object.hash(name, age, breed);
}

void main() {
  final d1 = Dog('Rex', 5, 'Labrador');
  final d2 = Dog('Rex', 5, 'Labrador');
  final d3 = Dog('Rex', 5, 'Poodle');

  print(d1 == d2);  // true  -- نفس الاسم والعمر والسلالة
  print(d1 == d3);  // false -- سلالة مختلفة
}
تحذير: استخدام covariant يعطل فحص أمان النوع. إذا قارنت Dog مع Animal ليس Dog، ستحصل على TypeError وقت التشغيل بدلاً من خطأ وقت التجميع. استخدمها فقط عندما تكون واثقاً من الأنواع المقارنة.

مثال عملي: كائنات القيمة

كائنات القيمة هي كائنات تعتمد هويتها على بياناتها وليس مرجعها. الأمثلة الشائعة تشمل العناوين ونطاقات التاريخ والإحداثيات. إليك نمط كائن قيمة واقعي كامل.

كائن قيمة العنوان

class Address {
  final String street;
  final String city;
  final String state;
  final String zipCode;
  final String country;

  const Address({
    required this.street,
    required this.city,
    required this.state,
    required this.zipCode,
    required this.country,
  });

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is Address &&
        other.street == street &&
        other.city == city &&
        other.state == state &&
        other.zipCode == zipCode &&
        other.country == country;
  }

  @override
  int get hashCode => Object.hash(street, city, state, zipCode, country);

  // copyWith للتحديثات غير القابلة للتغيير
  Address copyWith({
    String? street,
    String? city,
    String? state,
    String? zipCode,
    String? country,
  }) {
    return Address(
      street: street ?? this.street,
      city: city ?? this.city,
      state: state ?? this.state,
      zipCode: zipCode ?? this.zipCode,
      country: country ?? this.country,
    );
  }

  @override
  String toString() => '$street, $city, $state $zipCode, $country';
}

class DateRange {
  final DateTime start;
  final DateTime end;

  DateRange(this.start, this.end) {
    if (end.isBefore(start)) {
      throw ArgumentError('تاريخ الانتهاء يجب أن يكون بعد تاريخ البداية');
    }
  }

  bool get isActive {
    final now = DateTime.now();
    return now.isAfter(start) && now.isBefore(end);
  }

  Duration get duration => end.difference(start);

  bool overlaps(DateRange other) {
    return start.isBefore(other.end) && end.isAfter(other.start);
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is DateRange && other.start == start && other.end == end;
  }

  @override
  int get hashCode => Object.hash(start, end);

  @override
  String toString() => 'DateRange($start - $end)';
}

void main() {
  // مساواة العنوان
  final home1 = Address(
    street: '123 Main St',
    city: 'Springfield',
    state: 'IL',
    zipCode: '62701',
    country: 'US',
  );
  final home2 = Address(
    street: '123 Main St',
    city: 'Springfield',
    state: 'IL',
    zipCode: '62701',
    country: 'US',
  );

  print(home1 == home2);  // true -- نفس البيانات = نفس العنوان
  print({home1, home2}.length);  // 1 -- المجموعة تزيل التكرار

  // استخدام DateRange
  final q1 = DateRange(DateTime(2024, 1, 1), DateTime(2024, 3, 31));
  final q2 = DateRange(DateTime(2024, 1, 1), DateTime(2024, 3, 31));
  print(q1 == q2);  // true
}

مساواة الكيان مقابل مساواة القيمة

ليس كل الكائنات يجب أن تستخدم مساواة القيمة. الكيانات هي كائنات محددة بمعرف فريد (مثل مستخدم أو طلب)، بينما كائنات القيمة محددة ببياناتها. اختيار النهج الخاطئ يسبب أخطاء دقيقة.

مساواة الكيان مقابل كائن القيمة

// كيان: محدد بمعرف فريد
class User {
  final String id;
  String name;
  String email;

  User({required this.id, required this.name, required this.email});

  // مستخدمان متساويان إذا كان لهما نفس المعرف
  // (حتى لو تغير الاسم/البريد)
  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is User && other.id == id;
  }

  @override
  int get hashCode => id.hashCode;
}

// كائن قيمة: محدد بكل بياناته
class Color {
  final int r, g, b;
  const Color(this.r, this.g, this.b);

  // لونان متساويان إذا تطابقت جميع المكونات
  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is Color && other.r == r && other.g == g && other.b == b;
  }

  @override
  int get hashCode => Object.hash(r, g, b);
}

void main() {
  // كيان: نفس المعرف = نفس الكيان (بغض النظر عن الحقول الأخرى)
  final u1 = User(id: 'abc', name: 'Alice', email: 'old@email.com');
  final u2 = User(id: 'abc', name: 'Alice Smith', email: 'new@email.com');
  print(u1 == u2);  // true -- نفس المعرف

  // كائن قيمة: نفس البيانات = نفس القيمة
  final c1 = Color(255, 0, 0);
  final c2 = Color(255, 0, 0);
  print(c1 == c2);  // true -- نفس RGB
}
دليل القرار: اسأل نفسك: “إذا غيرت حقلاً، هل لا يزال نفس الشيء؟” إذا غيّر مستخدم بريده الإلكتروني، لا يزال نفس المستخدم (كيان). إذا غيرت قيمة الأحمر في لون، يصبح لوناً مختلفاً (كائن قيمة). استخدم مساواة المعرف للكيانات ومساواة الحقول لكائنات القيمة.
ملخص: تنفيذ المساواة ورمز التجزئة بشكل صحيح ضروري لكائنات Dart التي ستُستخدم في المجموعات. تجاوز == و hashCode معاً دائماً. استخدم Object.hash() لتوليد تجزئة مريح. اختر مساواة الكيان (بالمعرف) أو مساواة القيمة (بالحقول) بناءً على طبيعة كائنك. الكلمة المفتاحية covariant يمكن أن تبسط مساواة الفئات الفرعية لكن تضحي بأمان النوع.