الوحدة 1 · لغة Swift
الدوال والـ closures
18 دقراءة 4 داملأ الفراغات
الدرس في لمحة
اكتب دوال بتسميات وسائط واضحة وقيم افتراضية، ثم استبدل حلقات التكرار المكتوبة يدويًا بـ closures تمرّرها إلى filter وmap وreduce وsorted.
- 1دوال تُقرأ كالجمل
- 2الـ closures: دوال بلا أسماء
- 3filter وmap وreduce وsorted
- 4الـ closures تلتقط ما حولها
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
حلقات for من الدرس السابق تعمل، لكنها تجعلك تقرأ خمسة أسطر لتعرف معلومة واحدة، مثل «مجموع ما أُنفق على الطعام». الدوال تعطي قطعة من المنطق اسمًا. والـ closures تتيح لك أن تسلّم قطعة صغيرة من المنطق إلى طرف آخر، مثل «أبقِ ما يزيد على 50»، ودوال المجموعات في Swift مبنية حولها. معًا تحوّلان معظم حلقات التكرار في Pocket Budget إلى سطر واحد مقروء.
للأمثلة، إليك نوع مصروف بسيطًا. ستتعلّم ما يعنيه struct فعلًا في القسم التالي؛ أما الآن فاقرأه على أنه «قيمة لها عنوان ومبلغ وتصنيف».
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"),
]دوال تُقرأ كالجمل
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 يقرّر ما إذا كان المصروف كبيرًا:
let isLarge: (Expense) -> Bool = { expense in
expense.amount > 50
}
isLarge(expenses[0]) // trueيُقرأ النوع (Expense) -> Bool «يأخذ Expense ويعيد Bool». المعاملات تأتي قبل in، والجسم بعدها. والجسم المكوّن من تعبير واحد يعيد قيمته دون كتابة return.
الآن مرّر منطقًا كهذا إلى filter. هذه الأسطر الثلاثة هي الاستدعاء نفسه، مختصرًا شيئًا فشيئًا:
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
هذه الأربع تغطّي معظم ما تفعله مع المجموعات:
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؟».
اربطها في سلسلة، فيُقرأ خطّ المعالجة بالترتيب الذي يُنفَّذ به:
let foodSpend = expenses
.filter { $0.category == "Food" }
.map(\.amount)
.reduce(0, +)\.amount هو key path (مسار مفتاح)، اختصار لـ { $0.amount }، و+ هو نفسه دالة تأخذ قيمتين وتعيد مجموعهما، فيمكنك تمريره مباشرة إلى reduce.
الـ closures تلتقط ما حولها
يستطيع الـ closure أن يستخدم متغيّرات من النطاق الذي أُنشئ فيه، ويُبقيها حيّة:
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 يلتقط المتغيّرات التي يستخدمها، فيستطيع أن يحمل سياقًا مثل حدّ معيّن إلى أي مكان يُستدعى فيه لاحقًا.