أساسيات برمجة Dart

الدوال والمعاملات

40 دقيقة الدرس 8 من 13

الدوال والمعاملات في Dart

الدوال هي اللبنات الأساسية لأي تطبيق Dart. فهي تتيح لك تنظيم الكود في أجزاء قابلة لإعادة الاستخدام ومعيارية تؤدي مهامًا محددة. في هذا الدرس، سنستكشف كل شيء من تعريفات الدوال الأساسية إلى المفاهيم المتقدمة مثل الإغلاقات والدوال عالية المستوى.

ملاحظة: في Dart، حتى دالة main() هي دالة من المستوى الأعلى. Dart هي لغة كائنية التوجه حقيقية، لكن الدوال يمكن أن توجد خارج الفئات كدوال من المستوى الأعلى.

تعريف الدوال

يتم تعريف الدالة في Dart بتحديد نوع الإرجاع، واسم الدالة، والمعاملات بين أقواس، وجسم الدالة بين أقواس معقوفة.

// دالة أساسية مع نوع إرجاع
String greet(String name) {
  return 'Hello, $name!';
}

// دالة بدون قيمة إرجاع (void)
void printMessage(String message) {
  print(message);
}

// استدعاء الدوال
void main() {
  String greeting = greet('Alice');
  print(greeting); // Hello, Alice!

  printMessage('Welcome to Dart!'); // Welcome to Dart!
}
نصيحة: على الرغم من أن Dart يمكنها استنتاج أنواع الإرجاع، إلا أنه يُعتبر من أفضل الممارسات التصريح دائمًا بنوع الإرجاع بشكل صريح من أجل الوضوح وسهولة الصيانة.

أنواع الإرجاع

كل دالة في Dart لها نوع إرجاع. إذا لم يتم تحديد نوع الإرجاع، فإن Dart تعاملها ضمنيًا على أنها dynamic. تشمل أنواع الإرجاع الشائعة void و String و int و double و bool و List و Map والأنواع المخصصة.

// ترجع عددًا صحيحًا
int add(int a, int b) {
  return a + b;
}

// ترجع قيمة منطقية
bool isEven(int number) {
  return number % 2 == 0;
}

// ترجع قائمة
List<String> getNames() {
  return ['Alice', 'Bob', 'Charlie'];
}

// ترجع خريطة
Map<String, int> getScores() {
  return {'Alice': 95, 'Bob': 87, 'Charlie': 92};
}

void main() {
  print(add(3, 5));        // 8
  print(isEven(4));        // true
  print(getNames());       // [Alice, Bob, Charlie]
  print(getScores());      // {Alice: 95, Bob: 87, Charlie: 92}
}

المعاملات المطلوبة

بشكل افتراضي، جميع المعاملات الموضعية في Dart مطلوبة. لا يمكن استدعاء الدالة بدون توفير قيم لكل معامل مطلوب.

// كلا المعاملين مطلوبان
double calculateArea(double width, double height) {
  return width * height;
}

void main() {
  double area = calculateArea(5.0, 3.0);
  print('Area: $area'); // Area: 15.0

  // calculateArea(5.0); // خطأ: عدد قليل جدًا من المعاملات الموضعية
}

المعاملات الموضعية الاختيارية

يتم وضع المعاملات الموضعية الاختيارية بين أقواس مربعة []. يمكن حذفها عند استدعاء الدالة، وفي هذه الحالة تكون قيمتها الافتراضية null (أو قيمة افتراضية محددة).

String buildGreeting(String name, [String? title, String? suffix]) {
  String result = 'Hello, ';
  if (title != null) {
    result += '$title ';
  }
  result += name;
  if (suffix != null) {
    result += ' $suffix';
  }
  return result;
}

void main() {
  print(buildGreeting('Smith'));                    // Hello, Smith
  print(buildGreeting('Smith', 'Dr.'));             // Hello, Dr. Smith
  print(buildGreeting('Smith', 'Dr.', 'PhD'));      // Hello, Dr. Smith PhD
}

المعاملات المسماة الاختيارية

يتم وضع المعاملات المسماة بين أقواس معقوفة {}. وهي اختيارية بشكل افتراضي ويتم الإشارة إليها بالاسم عند استدعاء الدالة. وهذا يحسن قابلية قراءة الكود بشكل كبير.

void createUser({
  required String name,
  required String email,
  int age = 0,
  String role = 'user',
}) {
  print('Name: $name');
  print('Email: $email');
  print('Age: $age');
  print('Role: $role');
}

void main() {
  createUser(
    name: 'Alice',
    email: 'alice@example.com',
  );
  // Name: Alice
  // Email: alice@example.com
  // Age: 0
  // Role: user

  createUser(
    name: 'Bob',
    email: 'bob@example.com',
    age: 30,
    role: 'admin',
  );
  // Name: Bob
  // Email: bob@example.com
  // Age: 30
  // Role: admin
}
تحذير: لا يمكنك الجمع بين المعاملات الموضعية الاختيارية [] والمعاملات المسماة الاختيارية {} في نفس الدالة. يجب عليك اختيار أحدهما.

القيم الافتراضية

يمكن أن تحتوي كل من المعاملات الموضعية والمسماة الاختيارية على قيم افتراضية. إذا لم يقدم المستدعي قيمة، يتم استخدام القيمة الافتراضية.

// قيم افتراضية مع معاملات موضعية اختيارية
String repeat(String text, [int times = 1, String separator = ' ']) {
  return List.filled(times, text).join(separator);
}

// قيم افتراضية مع معاملات مسماة
double calculatePrice({
  required double basePrice,
  double taxRate = 0.10,
  double discount = 0.0,
}) {
  double taxed = basePrice * (1 + taxRate);
  return taxed - discount;
}

void main() {
  print(repeat('Hi'));              // Hi
  print(repeat('Hi', 3));           // Hi Hi Hi
  print(repeat('Hi', 3, '-'));      // Hi-Hi-Hi

  print(calculatePrice(basePrice: 100.0));                     // 110.0
  print(calculatePrice(basePrice: 100.0, discount: 10.0));     // 100.0
  print(calculatePrice(basePrice: 100.0, taxRate: 0.20));      // 120.0
}

الدوال السهمية (=>)

بالنسبة للدوال التي تحتوي على تعبير واحد، توفر Dart صيغة مختصرة باستخدام السهم السميك (=>). يتم تقييم التعبير وإرجاعه تلقائيًا.

// دالة تقليدية
int addTraditional(int a, int b) {
  return a + b;
}

// دالة سهمية مكافئة
int addArrow(int a, int b) => a + b;

// دوال سهمية مع أنواع إرجاع مختلفة
bool isAdult(int age) => age >= 18;
String capitalize(String s) => s[0].toUpperCase() + s.substring(1);
double circleArea(double radius) => 3.14159 * radius * radius;

// دالة سهمية ترجع void (تنفذ إجراء)
void sayHello(String name) => print('Hello, $name!');

void main() {
  print(addArrow(3, 5));         // 8
  print(isAdult(20));            // true
  print(capitalize('dart'));     // Dart
  print(circleArea(5.0));        // 78.53975
  sayHello('World');             // Hello, World!
}
نصيحة: الدوال السهمية مثالية للمُحصّلات البسيطة وعمليات الاستدعاء العكسي والدوال المساعدة القصيرة. تجنب استخدامها للمنطق المعقد الذي يتطلب عبارات متعددة.

الدوال المجهولة (الإغلاقات)

الدوال المجهولة (وتسمى أيضًا دوال لامدا أو الإغلاقات) هي دوال بدون اسم. تُستخدم عادةً كوسائط لدوال أخرى، خاصةً مع عمليات المجموعات.

void main() {
  // دالة مجهولة مُعيّنة لمتغير
  var multiply = (int a, int b) {
    return a * b;
  };
  print(multiply(4, 5)); // 20

  // دالة مجهولة سهمية
  var square = (int n) => n * n;
  print(square(6)); // 36

  // دوال مجهولة مع عمليات القوائم
  var numbers = [1, 2, 3, 4, 5];

  // forEach مع دالة مجهولة
  numbers.forEach((number) {
    print('Number: $number');
  });

  // map مع دالة مجهولة سهمية
  var doubled = numbers.map((n) => n * 2).toList();
  print(doubled); // [2, 4, 6, 8, 10]

  // where (تصفية) مع دالة مجهولة
  var evens = numbers.where((n) => n % 2 == 0).toList();
  print(evens); // [2, 4]

  // reduce مع دالة مجهولة
  var sum = numbers.reduce((a, b) => a + b);
  print(sum); // 15
}

الإغلاقات والتقاط المتغيرات

الإغلاق هو دالة تلتقط المتغيرات من نطاقها المحيط. يحتفظ الإغلاق بالوصول إلى هذه المتغيرات حتى بعد عودة الدالة المحيطة.

// دالة ترجع إغلاقًا
Function makeCounter() {
  int count = 0;
  return () {
    count++;
    return count;
  };
}

// إغلاق يلتقط مُضاعِف
Function makeMultiplier(int factor) {
  return (int number) => number * factor;
}

void main() {
  var counter = makeCounter();
  print(counter()); // 1
  print(counter()); // 2
  print(counter()); // 3

  var doubler = makeMultiplier(2);
  var tripler = makeMultiplier(3);
  print(doubler(5));  // 10
  print(tripler(5));  // 15
}
ملاحظة: الإغلاقات قوية لأنها "تتذكر" البيئة التي أُنشئت فيها. وهذا يجعلها مفيدة لعمليات الاستدعاء العكسي ومعالجات الأحداث وإنشاء مصانع الدوال.

الدوال عالية المستوى

الدالة عالية المستوى هي دالة تأخذ دوالًا أخرى كمعاملات أو ترجع دالة. تدعم Dart الدوال عالية المستوى بشكل كامل، مما يجعلها لغة رائعة للبرمجة بأسلوب وظيفي.

// دالة عالية المستوى تأخذ دالة كمعامل
int applyOperation(int a, int b, int Function(int, int) operation) {
  return operation(a, b);
}

// دالة عالية المستوى ترجع دالة
Function createGreeter(String greeting) {
  return (String name) => '$greeting, $name!';
}

// استخدام typedef لأنواع الدوال
typedef MathOperation = int Function(int, int);

int calculate(int a, int b, MathOperation op) {
  return op(a, b);
}

void main() {
  // تمرير دوال كوسائط
  print(applyOperation(10, 5, (a, b) => a + b)); // 15
  print(applyOperation(10, 5, (a, b) => a - b)); // 5
  print(applyOperation(10, 5, (a, b) => a * b)); // 50

  // استخدام دالة مُرجعة
  var helloGreeter = createGreeter('Hello');
  var hiGreeter = createGreeter('Hi');
  print(helloGreeter('Alice')); // Hello, Alice!
  print(hiGreeter('Bob'));      // Hi, Bob!

  // استخدام typedef
  MathOperation add = (a, b) => a + b;
  MathOperation multiply = (a, b) => a * b;
  print(calculate(6, 3, add));      // 9
  print(calculate(6, 3, multiply)); // 18
}

أساسيات التكرار (Recursion)

التكرار هو عندما تستدعي الدالة نفسها. كل دالة تكرارية تحتاج إلى حالة أساسية (شرط يوقف التكرار) وحالة تكرارية (حيث تستدعي الدالة نفسها بمشكلة أصغر).

// المضروب باستخدام التكرار
int factorial(int n) {
  if (n <= 1) return 1;     // الحالة الأساسية
  return n * factorial(n - 1); // الحالة التكرارية
}

// متتالية فيبوناتشي باستخدام التكرار
int fibonacci(int n) {
  if (n <= 0) return 0;     // الحالة الأساسية
  if (n == 1) return 1;      // الحالة الأساسية
  return fibonacci(n - 1) + fibonacci(n - 2); // الحالة التكرارية
}

// مجموع قائمة باستخدام التكرار
int sumList(List<int> numbers) {
  if (numbers.isEmpty) return 0;
  return numbers.first + sumList(numbers.sublist(1));
}

void main() {
  print(factorial(5));    // 120 (5 * 4 * 3 * 2 * 1)
  print(fibonacci(7));    // 13 (0, 1, 1, 2, 3, 5, 8, 13)
  print(sumList([1, 2, 3, 4, 5])); // 15
}
تحذير: يمكن أن تتسبب الدوال التكرارية في تجاوز سعة المكدس إذا لم يتم الوصول إلى الحالة الأساسية أبدًا أو إذا كان الإدخال كبيرًا جدًا. تأكد دائمًا من أن التكرار ينتهي. للمدخلات الكبيرة، فكر في استخدام التكرار الحلقي بدلاً من ذلك.

النطاق (Scope)

النطاق يحدد أين يمكن الوصول إلى المتغيرات. تستخدم Dart النطاق المعجمي، مما يعني أن المتغير يكون متاحًا داخل كتلة الكود حيث تم تعريفه، بما في ذلك أي كتل متداخلة.

// نطاق المستوى الأعلى (عام)
String globalVar = 'I am global';

void outerFunction() {
  // نطاق محلي
  String outerVar = 'I am in outerFunction';

  void innerFunction() {
    // نطاق محلي متداخل
    String innerVar = 'I am in innerFunction';

    // يمكن الوصول إلى جميع النطاقات الخارجية
    print(globalVar);  // I am global
    print(outerVar);   // I am in outerFunction
    print(innerVar);   // I am in innerFunction
  }

  innerFunction();

  print(globalVar);  // I am global
  print(outerVar);   // I am in outerFunction
  // print(innerVar); // خطأ: innerVar غير متاح هنا
}

void main() {
  outerFunction();
  print(globalVar);  // I am global
  // print(outerVar); // خطأ: outerVar غير متاح هنا

  // نطاق الكتلة مع if/for
  for (int i = 0; i < 3; i++) {
    var loopVar = 'Iteration $i';
    print(loopVar);
  }
  // print(loopVar); // خطأ: loopVar غير متاح خارج الحلقة
}
نصيحة: حافظ على متغيراتك في أضيق نطاق ممكن. هذا يمنع تعارض الأسماء، ويجعل الكود أسهل في الفهم، ويساعد جامع القمامة على تحرير الذاكرة في وقت أقرب.

تجميع كل شيء معًا

إليك مثال شامل يجمع بين مفاهيم الدوال المتعددة في سيناريو عملي.

// typedef للوضوح
typedef Validator = bool Function(String);

// دالة عالية المستوى تتحقق من صحة البيانات
List<String> filterValid(List<String> items, Validator validator) {
  return items.where(validator).toList();
}

// دوال ترجع مُتحققات (إغلاقات)
Validator minLength(int min) {
  return (String value) => value.length >= min;
}

Validator containsChar(String char) {
  return (String value) => value.contains(char);
}

// معاملات مسماة مع قيم افتراضية
String formatList(
  List<String> items, {
  String separator = ', ',
  String prefix = '',
  String suffix = '',
}) {
  return prefix + items.join(separator) + suffix;
}

void main() {
  var emails = [
    'alice@example.com',
    'bob',
    'charlie@test.com',
    'd@e',
    'frank@example.com',
  ];

  // استخدام الإغلاقات كمُتحققات
  var validEmails = filterValid(emails, (email) {
    return minLength(5)(email) && containsChar('@')(email);
  });

  print(formatList(
    validEmails,
    prefix: 'Valid emails: [',
    suffix: ']',
  ));
  // Valid emails: [alice@example.com, charlie@test.com, frank@example.com]
}

تمرين تطبيقي

أنشئ الدوال التالية:

  1. دالة power تأخذ base (مطلوب) ومعامل مسمى اختياري exponent بقيمة افتراضية 2. يجب أن تستخدم التكرار لحساب النتيجة.
  2. دالة عالية المستوى applyToAll تأخذ List<int> ودالة، ثم ترجع قائمة جديدة مع تطبيق الدالة على كل عنصر.
  3. دالة createRangeChecker تأخذ قيم min و max وترجع إغلاقًا يتحقق مما إذا كان رقم معين ضمن هذا النطاق.

الناتج المتوقع:

// power(3) يجب أن ترجع 9 (3^2)
// power(2, exponent: 5) يجب أن ترجع 32 (2^5)
// applyToAll([1, 2, 3], (n) => n * 10) يجب أن ترجع [10, 20, 30]
// var check = createRangeChecker(1, 10);
// check(5) يجب أن ترجع true
// check(15) يجب أن ترجع false