EduFlow
أساسيات Swift لتطبيقات iOS
⁦0%⁩

الوحدة 1 · لغة Swift

الدوال والـ closures

18 دقراءة 4 داملأ الفراغات

الدرس في لمحة

اكتب دوال بتسميات وسائط واضحة وقيم افتراضية، ثم استبدل حلقات التكرار المكتوبة يدويًا بـ closures تمرّرها إلى filter وmap وreduce وsorted.

  1. 1دوال تُقرأ كالجمل
  2. 2الـ closures: دوال بلا أسماء
  3. 3filter وmap وreduce وsorted
  4. 4الـ closures تلتقط ما حولها

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

حلقات for من الدرس السابق تعمل، لكنها تجعلك تقرأ خمسة أسطر لتعرف معلومة واحدة، مثل «مجموع ما أُنفق على الطعام». الدوال تعطي قطعة من المنطق اسمًا. والـ closures تتيح لك أن تسلّم قطعة صغيرة من المنطق إلى طرف آخر، مثل «أبقِ ما يزيد على 50»، ودوال المجموعات في Swift مبنية حولها. معًا تحوّلان معظم حلقات التكرار في Pocket Budget إلى سطر واحد مقروء.

للأمثلة، إليك نوع مصروف بسيطًا. ستتعلّم ما يعنيه struct فعلًا في القسم التالي؛ أما الآن فاقرأه على أنه «قيمة لها عنوان ومبلغ وتصنيف».

Expense.swift
import Foundation

struct Expense {
    let title: String
    let amount: Decimal
    let category: String
}

let expenses = [
    Expense(title: "Groceries", amount: 64.2, category: "Food"),
    Expense(title: "Bus pass", amount: 45, category: "Transport"),
    Expense(title: "Coffee", amount: 4.5, category: "Food"),
    Expense(title: "Concert", amount: 80, category: "Fun"),
]

دوال تُقرأ كالجمل

Swift
func total(of expenses: [Expense], in category: String? = nil) -> Decimal {
    var sum: Decimal = 0
    for expense in expenses where category == nil || expense.category == category {
        sum += expense.amount
    }
    return sum
}

total(of: expenses)                  // 193.7
total(of: expenses, in: "Food")      // 68.7

لكل معامل اسمان. تسمية الوسيط (argument label) مثل of وin هي ما يكتبه من يستدعي الدالة؛ واسم المعامل (parameter name) مثل expenses وcategory هو ما يستخدمه جسم الدالة. هذا الفصل هو سبب قراءة مواضع الاستدعاء في Swift كأنها جمل إنجليزية: “total of expenses in Food”. والجزء = nil يعطي category قيمة افتراضية، فيستطيع المستدعي حذفه. اكتب _ كتسمية حين لا تضيف شيئًا، كما في print(_:).

يعلن -> Decimal نوع القيمة المُعادة. والدالة التي لا تعيد شيئًا تحذف السهم.

الـ closures: دوال بلا أسماء

الـ closure (دالة مجهولة الاسم) كتلة كود يمكنك تخزينها في ثابت أو تمريرها كوسيط. إليك closure يقرّر ما إذا كان المصروف كبيرًا:

Swift
let isLarge: (Expense) -> Bool = { expense in
    expense.amount > 50
}
isLarge(expenses[0])     // true

يُقرأ النوع (Expense) -> Bool «يأخذ Expense ويعيد Bool». المعاملات تأتي قبل in، والجسم بعدها. والجسم المكوّن من تعبير واحد يعيد قيمته دون كتابة return.

الآن مرّر منطقًا كهذا إلى filter. هذه الأسطر الثلاثة هي الاستدعاء نفسه، مختصرًا شيئًا فشيئًا:

Swift
let large1 = expenses.filter({ (expense: Expense) -> Bool in
    return expense.amount > 50
})
let large2 = expenses.filter { expense in expense.amount > 50 }
let large3 = expenses.filter { $0.amount > 50 }

حين يكون الـ closure آخر وسيط، يمكنك كتابته بعد الأقواس (صيغة الـ trailing closure) وحذف الأقواس الفارغة كليًا. وSwift تعرف الأنواع مسبقًا من توقيع filter، فيمكنك حذفها أيضًا. $0 هو المعامل الأول، و$1 الثاني.

filter وmap وreduce وsorted

هذه الأربع تغطّي معظم ما تفعله مع المجموعات:

Swift
let food = expenses.filter { $0.category == "Food" }        // keep matching expenses
let titles = food.map { $0.title }                           // ["Groceries", "Coffee"]
let foodTotal = food.reduce(0) { $0 + $1.amount }            // 68.7
let biggestFirst = expenses.sorted { $0.amount > $1.amount } // Concert, Groceries, ...

يبدأ reduce من قيمة ابتدائية (0) ويطوي كل عنصر في نتيجة متراكمة: $0 هو المجموع حتى الآن و$1 هو المصروف التالي. ويسأل sorted(by:) الـ closure الخاص بك: «هل يأتي $0 قبل $1؟».

اربطها في سلسلة، فيُقرأ خطّ المعالجة بالترتيب الذي يُنفَّذ به:

Swift
let foodSpend = expenses
    .filter { $0.category == "Food" }
    .map(\.amount)
    .reduce(0, +)

\.amount هو key path (مسار مفتاح)، اختصار لـ { $0.amount }، و+ هو نفسه دالة تأخذ قيمتين وتعيد مجموعهما، فيمكنك تمريره مباشرة إلى reduce.

أربعة مصاريف تدخل إلى filter الذي يُبقي مصروفي الطعام؛ ثم يحوّلهما map إلى مبلغين؛ ثم يجمعهما reduce في مجموع واحد هو 68.7. 4 مصاريف [Expense] filter عنصرا طعام map(\.amount) [64.2, 4.5] reduce 68.7 كل خطوة تعيد قيمة جديدة؛ والمصفوفة الأصلية لا تتغيّر أبدًا
filter وmap وreduce كخطّ معالجة

الـ closures تلتقط ما حولها

يستطيع الـ closure أن يستخدم متغيّرات من النطاق الذي أُنشئ فيه، ويُبقيها حيّة:

Swift
func makeLimitCheck(limit: Decimal) -> (Decimal) -> Bool {
    return { amount in amount > limit }
}

let overFoodLimit = makeLimitCheck(limit: 60)
overFoodLimit(64.2)     // true

انتهت makeLimitCheck، ومع ذلك ما زال الـ closure يعرف أن limit يساوي 60. وحين تخزّن دالة معامل closure لتستدعيه لاحقًا، بدل أن تستدعيه قبل أن تعود، تُلزمك Swift بتعليم هذا المعامل بـ @escaping. هذه الكلمة ملصق تحذيري للقرّاء: هذا الـ closure يعيش أطول من الاستدعاء، فكل ما يلتقطه يبقى حيًّا أيضًا.

هذا الالتقاط (capturing) هو ما يسمح لأزرار SwiftUI بتشغيل كود يشير إلى بيانات الـ view بعد بنائه بوقت طويل. وله حدّ حادّ واحد، هو دورة المراجع (reference cycle) حين يلتقط class وclosure كلٌّ منهما الآخر، وستقابله مع الـ classes في الدرس التالي.

صار بإمكانك الآن أن تسمّي المنطق، وتمرّره، وتستبدل حلقات التكرار بخطوط معالجة. بهذا تكتمل أساسيات اللغة. القسم التالي عن تصميم البيانات: أن تقرّر هل تكون بيانات Pocket Budget من نوع struct أو class أو enum، ولماذا يغيّر هذا الاختيار سلوك تطبيقك.

أهم النقاط

  • تسميات الوسائط تجعل مواضع الاستدعاء تُقرأ كجمل؛ استخدم _ فقط حين لا تضيف التسمية شيئًا.
  • الـ closure دالة بلا اسم، وصيغة الـ trailing closure تتيح لك كتابته بعد أقواس الاستدعاء.
  • filter يُبقي العناصر، وmap يحوّلها، وreduce يجمعها في قيمة واحدة، وsorted(by:) يعيد مصفوفة جديدة مرتّبة.
  • الـ closure يلتقط المتغيّرات التي يستخدمها، فيستطيع أن يحمل سياقًا مثل حدّ معيّن إلى أي مكان يُستدعى فيه لاحقًا.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

لديك func total(of expenses: [Expense], in category: String? = nil) -> Decimal. أيّ استدعاء يُترجَم؟

السؤال 2 من 4

ماذا ينتج [3, 8, 12].map { $0 * 2 }.filter { $0 > 10 }؟

السؤال 3 من 4

كتبت expenses.sorted { $0.amount > $1.amount } في سطر مستقل ولم يتغيّر شيء. لماذا؟

السؤال 4 من 4

let check = makeLimitCheck(limit: 60) تعيد الـ closure‏ { amount in amount > limit }. ماذا يعيد check(64) بعد انتهاء makeLimitCheck بوقت طويل؟

تدريب

املأ الفراغات

استبدل حلقات التكرار بـ closures

تحتاج شاشة الملخّص في Pocket Budget إلى أربع قيم. املأ كل فراغ بحيث يحمل large المصاريف التي تزيد على 50، ويحمل titles عناوينها، ويكون spent مجموع كل المبالغ، ويعرض biggestFirst كل المصاريف من أكبر مبلغ إلى أصغره.

// expenses: [Expense], each with title, amount and category let large = expenses. { $0.amount > 50 } let titles = large. { $0.title } let spent = expenses.(0) { $0 + $1.amount } let biggestFirst = expenses.sorted { $0.amount $1.amount }