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

تحميل المعاملات الزائد

45 دقيقة الدرس 13 من 24

ما هو تحميل المعاملات الزائد؟

تحميل المعاملات الزائد يسمح لك بتعريف سلوك مخصص للمعاملات القياسية مثل + و - و * و == و < و > و [] و []= عند استخدامها مع فئاتك الخاصة. بدلاً من استدعاء طريقة مثل vector.add(other)، يمكنك كتابة الصيغة الأكثر طبيعية vector + other.

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

مفهوم رئيسي: تحميل المعاملات الزائد لا ينشئ معاملات جديدة -- بل يعيد تعريف سلوك معاملات Dart الموجودة لأنواعك المخصصة. هذا يجعل كودك أكثر تعبيراً وبديهية.

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

لتحميل معامل زائد، تعرّف طريقة باستخدام الكلمة المفتاحية operator متبوعة برمز المعامل. تأخذ الطريقة المعامل الأيمن كمعامل لها (للمعاملات الثنائية) أو بدون معاملات (للمعاملات الأحادية مثل - المستخدم كنفي).

الصيغة الأساسية

class MyClass {
  // معامل ثنائي: a + b
  MyClass operator +(MyClass other) {
    // إرجاع MyClass جديد يجمع this و other
  }

  // معامل أحادي: -a
  MyClass operator -() {
    // إرجاع نسخة منفية
  }

  // معامل المقارنة: a == b
  @override
  bool operator ==(Object other) {
    // إرجاع true إذا متساويان
  }

  // معامل الفهرس: a[index]
  int operator [](int index) {
    // إرجاع العنصر في الفهرس
  }

  // تعيين الفهرس: a[index] = value
  void operator []=(int index, int value) {
    // تعيين العنصر في الفهرس
  }
}

المعاملات القابلة للتجاوز في Dart

يسمح Dart بتجاوز المعاملات التالية:

  • الحسابية: + و - و * و / و ~/ (القسمة الصحيحة) و % (باقي القسمة) و - الأحادي
  • المقارنة: == و < و > و <= و >=
  • البتية: & و | و ^ و ~ (NOT البتي) و << و >>
  • الفهرس: [] و []=
تحذير: لا يمكنك تجاوز = (التعيين) أو && أو || أو ! أو is أو is! أو as أو ?? أو ?. -- هذه مدمجة في اللغة ولا يمكن تخصيصها.

مثال عملي: فئة Vector2D

لنبني فئة متجه ثنائي الأبعاد تدعم العمليات الحسابية. هذا أحد أكثر الاستخدامات شيوعاً لتحميل المعاملات الزائد في تطوير الألعاب وبرمجة الرسوميات.

Vector2D مع تحميل المعاملات الزائد

class Vector2D {
  final double x;
  final double y;

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

  // جمع المتجهات: v1 + v2
  Vector2D operator +(Vector2D other) {
    return Vector2D(x + other.x, y + other.y);
  }

  // طرح المتجهات: v1 - v2
  Vector2D operator -(Vector2D other) {
    return Vector2D(x - other.x, y - other.y);
  }

  // الضرب بعدد: v * scalar
  Vector2D operator *(double scalar) {
    return Vector2D(x * scalar, y * scalar);
  }

  // القسمة على عدد: v / scalar
  Vector2D operator /(double scalar) {
    if (scalar == 0) throw ArgumentError('لا يمكن القسمة على صفر');
    return Vector2D(x / scalar, y / scalar);
  }

  // النفي: -v
  Vector2D operator -() {
    return Vector2D(-x, -y);
  }

  // المساواة: v1 == v2
  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is Vector2D && other.x == x && other.y == y;
  }

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

  // حجم (طول) المتجه
  double get magnitude => (x * x + y * y).sqrt();

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

void main() {
  final v1 = Vector2D(3, 4);
  final v2 = Vector2D(1, 2);

  print(v1 + v2);    // Vector2D(4.0, 6.0)
  print(v1 - v2);    // Vector2D(2.0, 2.0)
  print(v1 * 2);     // Vector2D(6.0, 8.0)
  print(v1 / 2);     // Vector2D(1.5, 2.0)
  print(-v1);        // Vector2D(-3.0, -4.0)

  // تسلسل المعاملات
  final result = (v1 + v2) * 3;
  print(result);     // Vector2D(12.0, 18.0)
}
نصيحة: لاحظ أن كل معامل يُرجع نسخة جديدة من Vector2D بدلاً من تعديل النسخة الموجودة. هذا يجعل الفئة غير قابلة للتغيير، وهو أفضل ممارسة لكائنات القيمة. الكائنات غير القابلة للتغيير أكثر أماناً في الكود المتزامن وأسهل في الفهم.

مثال عملي: فئة Money

في التطبيقات المالية، تمثيل المال كفئة مع تحميل المعاملات الزائد يمنع أخطاء النقطة العائمة الشائعة ويفرض سلامة العملة.

فئة Money مع حسابات آمنة

class Money {
  final int _cents; // التخزين كسنتات لتجنب مشاكل النقطة العائمة
  final String currency;

  const Money(this._cents, this.currency);

  // مصنع من الدولارات
  factory Money.fromDollars(double dollars, String currency) {
    return Money((dollars * 100).round(), currency);
  }

  double get dollars => _cents / 100;

  // الجمع: m1 + m2
  Money operator +(Money other) {
    _checkCurrency(other);
    return Money(_cents + other._cents, currency);
  }

  // الطرح: m1 - m2
  Money operator -(Money other) {
    _checkCurrency(other);
    return Money(_cents - other._cents, currency);
  }

  // الضرب بعدد: money * quantity
  Money operator *(int multiplier) {
    return Money(_cents * multiplier, currency);
  }

  // معاملات المقارنة
  bool operator <(Money other) {
    _checkCurrency(other);
    return _cents < other._cents;
  }

  bool operator >(Money other) {
    _checkCurrency(other);
    return _cents > other._cents;
  }

  bool operator <=(Money other) {
    _checkCurrency(other);
    return _cents <= other._cents;
  }

  bool operator >=(Money other) {
    _checkCurrency(other);
    return _cents >= other._cents;
  }

  // النفي: -money
  Money operator -() {
    return Money(-_cents, currency);
  }

  void _checkCurrency(Money other) {
    if (currency != other.currency) {
      throw ArgumentError(
        'لا يمكن إجراء عمليات على عملات مختلفة: $currency مقابل ${other.currency}',
      );
    }
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is Money && other._cents == _cents && other.currency == currency;
  }

  @override
  int get hashCode => Object.hash(_cents, currency);

  @override
  String toString() => '${currency} ${dollars.toStringAsFixed(2)}';
}

void main() {
  final price = Money.fromDollars(29.99, 'USD');
  final tax = Money.fromDollars(2.40, 'USD');
  final total = price + tax;

  print(total);            // USD 32.39
  print(price * 3);        // USD 89.97
  print(price > tax);      // true
  print(price + tax);      // USD 32.39

  // عدم تطابق العملة يرمي خطأ
  final euro = Money.fromDollars(25.00, 'EUR');
  // price + euro;  // خطأ: لا يمكن إجراء عمليات على عملات مختلفة
}
ملاحظة تصميمية: طريقة _checkCurrency تضمن أنك لا تخلط أبداً بين USD و EUR في الحسابات. هذا مثال رائع على كيف يمكن لتحميل المعاملات الزائد فرض قواعد العمل في وقت التجميع بدلاً من الاعتماد على انضباط المطور.

معاملات الفهرس: [] و []=

معاملات الفهرس تجعل كائناتك تتصرف مثل القوائم أو الخرائط. هذا مفيد لفئات المصفوفات والمجموعات المخصصة وكائنات التكوين.

فئة Matrix مع معاملات الفهرس

class Matrix {
  final List<List<double>> _data;
  final int rows;
  final int cols;

  Matrix(this.rows, this.cols)
      : _data = List.generate(rows, (_) => List.filled(cols, 0.0));

  Matrix.fromData(this._data)
      : rows = _data.length,
        cols = _data.isEmpty ? 0 : _data[0].length;

  // الوصول لصف: matrix[row]
  List<double> operator [](int row) {
    if (row < 0 || row >= rows) {
      throw RangeError('الصف $row خارج النطاق [0, $rows)');
    }
    return _data[row];
  }

  // جمع المصفوفات: m1 + m2
  Matrix operator +(Matrix other) {
    if (rows != other.rows || cols != other.cols) {
      throw ArgumentError('يجب أن تكون المصفوفات بنفس الأبعاد');
    }
    final result = Matrix(rows, cols);
    for (int i = 0; i < rows; i++) {
      for (int j = 0; j < cols; j++) {
        result[i][j] = _data[i][j] + other._data[i][j];
      }
    }
    return result;
  }

  // الضرب بعدد: matrix * scalar
  Matrix operator *(double scalar) {
    final result = Matrix(rows, cols);
    for (int i = 0; i < rows; i++) {
      for (int j = 0; j < cols; j++) {
        result[i][j] = _data[i][j] * scalar;
      }
    }
    return result;
  }

  @override
  String toString() {
    return _data.map((row) => row.map((v) => v.toStringAsFixed(1)).join('\t')).join('\n');
  }
}

void main() {
  final m1 = Matrix.fromData([
    [1, 2, 3],
    [4, 5, 6],
  ]);

  final m2 = Matrix.fromData([
    [7, 8, 9],
    [10, 11, 12],
  ]);

  // الوصول للعناصر
  print(m1[0][1]);  // 2.0 -- الصف 0، العمود 1

  // تعيين العناصر
  m1[0][2] = 99;
  print(m1[0][2]);  // 99.0

  // جمع المصفوفات
  final sum = m1 + m2;
  print(sum);
  // 108.0  10.0  12.0
  // 14.0   16.0  18.0

  // الضرب بعدد
  final scaled = m2 * 2;
  print(scaled);
  // 14.0  16.0  18.0
  // 20.0  22.0  24.0
}

معاملات المقارنة للترتيب

تحميل معاملات المقارنة مثل < و > يجعل كائناتك قابلة للترتيب وقابلة للاستخدام مع أدوات الترتيب والمقارنة المدمجة في Dart.

فئة Temperature مع المقارنات

class Temperature implements Comparable<Temperature> {
  final double celsius;

  const Temperature(this.celsius);

  factory Temperature.fromFahrenheit(double f) {
    return Temperature((f - 32) * 5 / 9);
  }

  double get fahrenheit => celsius * 9 / 5 + 32;

  // معاملات المقارنة
  bool operator <(Temperature other) => celsius < other.celsius;
  bool operator >(Temperature other) => celsius > other.celsius;
  bool operator <=(Temperature other) => celsius <= other.celsius;
  bool operator >=(Temperature other) => celsius >= other.celsius;

  // الحساب: جمع/طرح فروقات الحرارة
  Temperature operator +(Temperature other) => Temperature(celsius + other.celsius);
  Temperature operator -(Temperature other) => Temperature(celsius - other.celsius);

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

  @override
  int get hashCode => celsius.hashCode;

  // مطلوب بواسطة واجهة Comparable
  @override
  int compareTo(Temperature other) => celsius.compareTo(other.celsius);

  @override
  String toString() => '${celsius.toStringAsFixed(1)}°C';
}

void main() {
  final temps = [
    Temperature(100),
    Temperature(0),
    Temperature(37),
    Temperature.fromFahrenheit(212),
  ];

  // الترتيب يعمل لأننا نفذنا Comparable و <
  temps.sort();
  print(temps);  // [0.0°C, 37.0°C, 100.0°C, 100.0°C]

  final boiling = Temperature(100);
  final freezing = Temperature(0);
  print(boiling > freezing);   // true
  print(boiling == Temperature(100));  // true
}
أفضل ممارسة: عند تحميل معاملات المقارنة، نفّذ أيضاً واجهة Comparable. هذا يجعل فئتك تعمل مع List.sort() و SplayTreeSet وأدوات المكتبة القياسية الأخرى التي تعتمد على compareTo().

مثال واقعي: مزج الألوان

إليك مثال عملي يوضح تحميل المعاملات الزائد في سياق شبيه بـ Flutter -- فئة ألوان تدعم المزج والتعديل من خلال المعاملات.

فئة Color مع تحميل المعاملات الزائد

class AppColor {
  final int r, g, b;
  final double opacity;

  const AppColor(this.r, this.g, this.b, [this.opacity = 1.0])
      : assert(r >= 0 && r <= 255),
        assert(g >= 0 && g <= 255),
        assert(b >= 0 && b <= 255),
        assert(opacity >= 0.0 && opacity <= 1.0);

  // مزج لونين: color1 + color2
  AppColor operator +(AppColor other) {
    return AppColor(
      ((r + other.r) / 2).round().clamp(0, 255),
      ((g + other.g) / 2).round().clamp(0, 255),
      ((b + other.b) / 2).round().clamp(0, 255),
      ((opacity + other.opacity) / 2).clamp(0.0, 1.0),
    );
  }

  // تعتيم: color * factor (0.0 إلى 1.0)
  AppColor operator *(double factor) {
    return AppColor(
      (r * factor).round().clamp(0, 255),
      (g * factor).round().clamp(0, 255),
      (b * factor).round().clamp(0, 255),
      opacity,
    );
  }

  // عكس: ~color
  AppColor operator ~() {
    return AppColor(255 - r, 255 - g, 255 - b, opacity);
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is AppColor &&
        other.r == r &&
        other.g == g &&
        other.b == b &&
        other.opacity == opacity;
  }

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

  @override
  String toString() => 'AppColor($r, $g, $b, ${opacity.toStringAsFixed(2)})';
}

void main() {
  final red = AppColor(255, 0, 0);
  final blue = AppColor(0, 0, 255);

  // مزج الألوان
  final purple = red + blue;
  print(purple);       // AppColor(128, 0, 128, 1.00)

  // تعتيم
  final darkRed = red * 0.5;
  print(darkRed);      // AppColor(128, 0, 0, 1.00)

  // عكس
  final invertedRed = ~red;
  print(invertedRed);  // AppColor(0, 255, 255, 1.00)
}
مهم: حمّل المعاملات فقط عندما يكون المعنى بديهياً. إذا كان + على فئتك لا يشعر وكأنه “جمع” أو “دمج”، استخدم طريقة مسماة بدلاً من ذلك. دلالات المعاملات المربكة تجعل الكود أصعب في الفهم. قاعدة جيدة: إذا كان عليك شرح ما يفعله المعامل، استخدم اسم طريقة.
ملخص: تحميل المعاملات الزائد في Dart يتيح لك تعريف سلوك مخصص للمعاملات القياسية باستخدام الكلمة المفتاحية operator. يجعل فئات مثل المتجهات والمال والمصفوفات والألوان أكثر طبيعية في الاستخدام. أرجع دائماً نسخاً جديدة غير قابلة للتغيير، وتحقق من المدخلات، وحمّل المعاملات فقط عندما يكون المعنى واضحاً وبديهياً.