تخطَّ إلى المحتوى

الأنواع العامة Generics في Dart

الدرس 12 من 14 · Dart

منذ درس القوائم وأنت تكتب List<String> و Map<String, int>، وفي الدرس الثامن كتبت Future<String>. ما سر هذه الأقواس المدببة؟ إنها الأنواع العامة (Generics): طريقة لكتابة كود واحد يعمل مع أنواع كثيرة، دون أن تخسر حماية Dart من أخطاء الأنواع. في هذا الدرس ستفهمها، وتكتب كلاساتك ودوالك العامة.

تخيّل صناديق التخزين في البيت. لو كانت كلها بلا ملصقات، فستفتح صندوقاً تبحث عن الأحذية فتجد الأطباق. الحل ملصق على كل صندوق: «أحذية فقط». النوع بين القوسين هو هذا الملصق:

void main() {
List<int> scores = [90, 75, 88];
scores.add(60);
var total = scores[0] + scores[1];
print(total);
print(scores.runtimeType);
}
الناتج
165
List<int>

لأن القائمة معلَّمة بأنها List<int>:

  • لو كتبت scores.add('ممتاز') فسيرفض المحرر فوراً برسالة The argument type 'String' can't be assigned to the parameter type 'int'.
  • وعندما تقرأ عنصراً، تعرف Dart أنه int، فتسمح لك بالجمع مباشرة دون أي تحويل.

والجميل أن كلاس List مكتوب مرة واحدة فقط، ويعمل مع int و String وأي كلاس تكتبه أنت.

لنكتب صندوقاً يحفظ قيمة واحدة من أي نوع. نكتب بعد اسم الكلاس حرفاً بين قوسين مدببين، وجرت العادة أن يكون T (أول حرف من Type). هذا الحرف مكان فارغ يُملأ بالنوع الحقيقي عند الاستخدام:

class Box<T> {
T value;
Box(this.value);
void replace(T newValue) {
value = newValue;
}
}
void main() {
var intBox = Box<int>(5);
var textBox = Box('مرحباً');
intBox.replace(9);
print(intBox.value + 1);
print(textBox.value.length);
print(textBox.runtimeType);
}
الناتج
10
6
Box<String>

في Box<int> كل T داخل الكلاس صارت int: الخاصية value من نوع int، والتابع replace لا يقبل إلا int. وفي السطر الثاني لم نكتب النوع، فاستنتجت Dart من القيمة أنه Box<String>.

في التطبيقات، أي طلب من الخادم إما ينجح ومعه بيانات، وإما يفشل ومعه رسالة خطأ. نوع البيانات يختلف من طلب لآخر، لكن شكل الرد واحد. هذا مكان مثالي لكلاس عام:

class ApiResponse<T> {
final T? data;
final String? error;
ApiResponse.success(this.data) : error = null;
ApiResponse.failure(this.error) : data = null;
bool get isSuccess => error == null;
}
void main() {
var users = ApiResponse<List<String>>.success(['سارة', 'عمر']);
var price = ApiResponse<double>.failure('لا يوجد اتصال');
if (users.isSuccess) {
print('عدد المستخدمين: ${users.data!.length}');
}
if (!price.isSuccess) {
print('خطأ: ${price.error}');
}
}
الناتج
عدد المستخدمين: 2
خطأ: لا يوجد اتصال

كلاس واحد يخدم كل طلبات التطبيق: ApiResponse<List<String>> لقائمة أسماء، و ApiResponse<double> لسعر، وغداً ApiResponse<Product> لمنتج. ولاحظ أن النوع العام نفسه يمكن أن يكون نوعاً عاماً آخر.

الدالة أيضاً يمكن أن تكون عامة: نكتب <T> بعد اسمها مباشرة.

T pickLast<T>(List<T> items) {
return items[items.length - 1];
}
void main() {
var number = pickLast([1, 2, 3]);
var letter = pickLast(['أ', 'ب']);
print(number + 10);
print(letter);
}
الناتج
13
ب

الدالة تقول: «أعطني قائمة من نوع ما، وسأعيد لك عنصراً من النوع نفسه». لذلك عرفت Dart أن number من نوع int وسمحت بالجمع عليه.

أحياناً لا تريد «أي نوع»، بل «أي نوع رقمي» مثلاً. نقيّد T بالكلمة extends:

T larger<T extends num>(T a, T b) => a > b ? a : b;
void main() {
print(larger(4, 9));
print(larger(2.5, 1.5));
}
الناتج
9
2.5

T extends num تعني أن T يجب أن يكون int أو double. وبفضل هذا القيد سمحت Dart باستخدام > داخل الدالة، لأنها تضمن أن القيم أرقام. ولو كتبت larger('أ', 'ب') فسيرفضها المحرر.

اكتب كلاساً عاماً اسمه Pair يحفظ قيمتين قد يختلف نوعاهما: first و second. ستحتاج حرفين: Pair<A, B>. أضف له تابعاً swap يعيد زوجاً جديداً من نوع Pair<B, A> بترتيب معكوس. جرّبه بالزوج ('العمر', 15):

الناتج
العمر 15
15 العمر
  • قائمة بلا نوع: var items = []; نوعها List<dynamic>، فتقبل أي شيء وتفقد الحماية. اكتب var items = <String>[];.
  • نسيان <T> بعد اسم الكلاس: استخدام T داخل الكلاس دون إعلانها في رأسه خطأ، لأن Dart لا تعرف ما هي T.
  • استخدام تابع غير مضمون: داخل Box<T> لا تستطيع كتابة value.length، لأن T قد يكون أي نوع. قيّد النوع بـ extends أولاً.
  • الخلط بين List<int> و List<num>: لا تستطيع إضافة 2.5 إلى List<int>. إذا أردت أرقاماً صحيحة وعشرية معاً فاستخدم List<num>.

سؤال

ماذا تعني T في class Box<T>؟

سؤال

ما معنى T extends num؟

  • النوع بين القوسين المدببين يحدد ما يتعامل معه الكلاس أو الدالة.
  • Generics تجمع بين إعادة استخدام الكود وأمان الأنواع.
  • class Box<T> كلاس عام، و T pickLast<T>(...) دالة عامة.
  • T extends num يقيّد النوع ويفتح لك توابع ذلك النوع.
  • تجنّب dynamic، وحدّد نوع القوائم الفارغة دائماً.