الأنواع العامة Generics في Dart
منذ درس القوائم وأنت تكتب List<String> و Map<String, int>، وفي الدرس الثامن كتبت Future<String>. ما سر هذه الأقواس المدببة؟ إنها الأنواع العامة (Generics): طريقة لكتابة كود واحد يعمل مع أنواع كثيرة، دون أن تخسر حماية Dart من أخطاء الأنواع. في هذا الدرس ستفهمها، وتكتب كلاساتك ودوالك العامة.
لماذا نحتاج Generics؟
Section titled “لماذا نحتاج Generics؟”تخيّل صناديق التخزين في البيت. لو كانت كلها بلا ملصقات، فستفتح صندوقاً تبحث عن الأحذية فتجد الأطباق. الحل ملصق على كل صندوق: «أحذية فقط». النوع بين القوسين هو هذا الملصق:
void main() { List<int> scores = [90, 75, 88]; scores.add(60);
var total = scores[0] + scores[1]; print(total); print(scores.runtimeType);}165List<int>لأن القائمة معلَّمة بأنها List<int>:
- لو كتبت
scores.add('ممتاز')فسيرفض المحرر فوراً برسالةThe argument type 'String' can't be assigned to the parameter type 'int'. - وعندما تقرأ عنصراً، تعرف Dart أنه
int، فتسمح لك بالجمع مباشرة دون أي تحويل.
والجميل أن كلاس List مكتوب مرة واحدة فقط، ويعمل مع int و String وأي كلاس تكتبه أنت.
كلاس عام من صنعك
Section titled “كلاس عام من صنعك”لنكتب صندوقاً يحفظ قيمة واحدة من أي نوع. نكتب بعد اسم الكلاس حرفاً بين قوسين مدببين، وجرت العادة أن يكون 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);}106Box<String>في Box<int> كل T داخل الكلاس صارت int: الخاصية value من نوع int، والتابع replace لا يقبل إلا int. وفي السطر الثاني لم نكتب النوع، فاستنتجت Dart من القيمة أنه Box<String>.
مثال حقيقي: رد الخادم
Section titled “مثال حقيقي: رد الخادم”في التطبيقات، أي طلب من الخادم إما ينجح ومعه بيانات، وإما يفشل ومعه رسالة خطأ. نوع البيانات يختلف من طلب لآخر، لكن شكل الرد واحد. هذا مكان مثالي لكلاس عام:
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> لمنتج. ولاحظ أن النوع العام نفسه يمكن أن يكون نوعاً عاماً آخر.
الدوال العامة
Section titled “الدوال العامة”الدالة أيضاً يمكن أن تكون عامة: نكتب <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 وسمحت بالجمع عليه.
تقييد النوع: extends
Section titled “تقييد النوع: extends”أحياناً لا تريد «أي نوع»، بل «أي نوع رقمي» مثلاً. نقيّد 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));}92.5T extends num تعني أن T يجب أن يكون int أو double. وبفضل هذا القيد سمحت Dart باستخدام > داخل الدالة، لأنها تضمن أن القيم أرقام. ولو كتبت larger('أ', 'ب') فسيرفضها المحرر.
جرّب بنفسك
Section titled “جرّب بنفسك”اكتب كلاساً عاماً اسمه Pair يحفظ قيمتين قد يختلف نوعاهما: first و second. ستحتاج حرفين: Pair<A, B>. أضف له تابعاً swap يعيد زوجاً جديداً من نوع Pair<B, A> بترتيب معكوس. جرّبه بالزوج ('العمر', 15):
العمر 1515 العمرأخطاء شائعة
Section titled “أخطاء شائعة”- قائمة بلا نوع:
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>.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ماذا تعني T في class Box<T>؟
T معامل نوع: يُستبدل بالنوع الحقيقي مثل int أو String عند إنشاء الكائن.
سؤال
ما معنى T extends num؟
القيد extends num يحصر T في الأنواع الرقمية، فيسمح باستخدام عمليات الأرقام عليه.
الخلاصة
Section titled “الخلاصة”- النوع بين القوسين المدببين يحدد ما يتعامل معه الكلاس أو الدالة.
- Generics تجمع بين إعادة استخدام الكود وأمان الأنواع.
class Box<T>كلاس عام، وT pickLast<T>(...)دالة عامة.T extends numيقيّد النوع ويفتح لك توابع ذلك النوع.- تجنّب
dynamic، وحدّد نوع القوائم الفارغة دائماً.