تحميل المعاملات الزائد
ما هو تحميل المعاملات الزائد؟
تحميل المعاملات الزائد يسمح لك بتعريف سلوك مخصص للمعاملات القياسية مثل + و - و * و == و < و > و [] و []= عند استخدامها مع فئاتك الخاصة. بدلاً من استدعاء طريقة مثل vector.add(other)، يمكنك كتابة الصيغة الأكثر طبيعية vector + other.
في Dart، المعاملات هي في الواقع طرق بأسماء خاصة. عندما تكتب a + b، تستدعي Dart a.+(b) في الخلفية. من خلال تجاوز هذه الطرق الخاصة في فئتك، تتحكم فيما يحدث عند استخدام معامل مع كائناتك.
الكلمة المفتاحية 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)
}
+ على فئتك لا يشعر وكأنه “جمع” أو “دمج”، استخدم طريقة مسماة بدلاً من ذلك. دلالات المعاملات المربكة تجعل الكود أصعب في الفهم. قاعدة جيدة: إذا كان عليك شرح ما يفعله المعامل، استخدم اسم طريقة.operator. يجعل فئات مثل المتجهات والمال والمصفوفات والألوان أكثر طبيعية في الاستخدام. أرجع دائماً نسخاً جديدة غير قابلة للتغيير، وتحقق من المدخلات، وحمّل المعاملات فقط عندما يكون المعنى واضحاً وبديهياً.