استخدم Dictionary(grouping:by:) لجمع قيم Swift الاصطناعية حسب المفتاح، ثم استخدم mapValues وreduce لحساب إجمالي كل مجموعة. يوضح هذا البرنامج التعليمي لنظام Mac أيضًا التجميع المباشر والتحقق من المدخلات الفارغة والإجماليات الصفرية وتجاوز سعة الأعداد الصحيحة.
المطلوب هو تحويل مصفوفة موجودة في الذاكرة من تسميات أشكال محايدة وكميات أعداد صحيحة إلى قاموس [String: Int] واحد. جميع المدخلات قيم حرفية اصطناعية. لا يقرأ الحساب أي سجلات خارجية، ولا يجري طلبات شبكية، ولا يحفظ أي نتائج.
تحديد معنى إجمالي واحد لكل مفتاح
قبل اختيار أساليب المجموعات، حدّد عقد النتيجة. هنا، يساهم كل عنصر إدخال بكميته مرة واحدة بالضبط في إجمالي التسمية الخاصة به. تستخدم التسميات مساواة سلاسل Swift المعتادة؛ ولا ينفذ البرنامج أي إزالة للمسافات أو تحويل لحالة الأحرف. يجب أن تتوافق مفاتيح القاموس مع Hashable، وتدعم Swift استخدام السلاسل كمفاتيح. راجع توثيق Dictionary من Apple.
تتضمن المدخلات التوضيحية قيمتين للدائرة، وقيمتين للمربع، وقيمة واحدة للمثلث. ومن السهل التحقق يدويًا من الإجماليات المقصودة:
| التسمية | كميات الإدخال | الإجمالي المتوقع |
|---|---|---|
circle |
4، 6 |
10 |
square |
3، 5 |
8 |
triangle |
0 |
0 |
يستخدم هذا المثال القواعد التالية:
- ينتج عن الإدخال الفارغ قاموس فارغ.
- تبقى التسمية التي تظهر بكمية صفر موجودة بإجمالي صفر.
- يُسمح بالكميات السالبة، وتُضاف باستخدام الجمع المعتاد.
- لا تظهر في النتيجة أي تسمية غير موجودة في الإدخال.
- يجب أن يكون كل مجموع وسيط ضمن نطاق
Int؛ وإلا فترمي الدالة خطأ ولا تعيد أي إجماليات.
التمييز بين التسمية الغائبة والإجمالي الصفري مهم. فالتسمية المفقودة تعني عدم وجود مدخل مطابق. أما التسمية الموجودة بإجمالي صفر فقد تمثل صفرًا صريحًا أو كميات ألغى بعضها بعضًا. حافظ على هذا التمييز ما لم يكن قارئ النتيجة بحاجة إلى اصطلاح مختلف تحديدًا.
تشغيل المثال الكامل على جهاز Mac
استخدم سلسلة أدوات Swift مثبتة. يوثق دليل التثبيت لنظام macOS من Swift.org إعداد سلسلة الأدوات والأوامر اللازمة لإنشاء حزمة تنفيذية وتشغيلها. بعد تجهيز سلسلة الأدوات، افتح Terminal وأنشئ مشروعًا محليًا صغيرًا:
mkdir SyntheticTotals
cd SyntheticTotals
swift package init --name SyntheticTotals --type executable
افتح ملف المصدر التنفيذي المُنشأ ضمن Sources واستبدل محتوياته بالبرنامج الكامل أدناه. قد يكون الملف المُنشأ داخل مجلد فرعي يحمل اسم الهدف. أبقِ بيان الحزمة كما أُنشئ؛ فهذا البرنامج لا يحتاج إلى تبعيات أو عمليات استيراد إضافية، لأن أنواعه وأساليب حسابه تنتمي إلى مكتبة Swift القياسية.
struct SyntheticValue: Sendable {
let key: String
let quantity: Int
}
enum TotalError: Error {
case overflow(key: String)
}
func checkedAdd(_ total: Int, _ value: SyntheticValue) throws -> Int {
let addition = total.addingReportingOverflow(value.quantity)
guard !addition.overflow else {
throw TotalError.overflow(key: value.key)
}
return addition.partialValue
}
func groupThenTotal(_ values: [SyntheticValue]) throws -> [String: Int] {
let groups = Dictionary(grouping: values, by: { $0.key })
return try groups.mapValues { group in
try group.reduce(0) { total, value in
try checkedAdd(total, value)
}
}
}
func totalDirectly(_ values: [SyntheticValue]) throws -> [String: Int] {
return try values.reduce(into: [String: Int]()) { totals, value in
let previous = totals[value.key, default: 0]
totals[value.key] = try checkedAdd(previous, value)
}
}
let sample: [SyntheticValue] = [
SyntheticValue(key: "circle", quantity: 4),
SyntheticValue(key: "square", quantity: 3),
SyntheticValue(key: "circle", quantity: 6),
SyntheticValue(key: "triangle", quantity: 0),
SyntheticValue(key: "square", quantity: 5)
]
let expected = ["circle": 10, "square": 8, "triangle": 0]
do {
let groupedTotals = try groupThenTotal(sample)
let directTotals = try totalDirectly(sample)
precondition(groupedTotals == expected)
precondition(directTotals == expected)
let emptyGrouped = try groupThenTotal([])
let emptyDirect = try totalDirectly([])
precondition(emptyGrouped.isEmpty && emptyDirect.isEmpty)
let cancellation = [
SyntheticValue(key: "circle", quantity: 2),
SyntheticValue(key: "circle", quantity: -2)
]
let zeroGrouped = try groupThenTotal(cancellation)
let zeroDirect = try totalDirectly(cancellation)
precondition(zeroGrouped == ["circle": 0])
precondition(zeroDirect == ["circle": 0])
for key in groupedTotals.keys.sorted() {
if let total = groupedTotals[key] {
print("\(key): \(total)")
}
}
let tooLarge = [
SyntheticValue(key: "circle", quantity: Int.max),
SyntheticValue(key: "circle", quantity: 1)
]
do {
_ = try totalDirectly(tooLarge)
preconditionFailure("Expected overflow to be reported")
} catch TotalError.overflow(let key) {
print("Overflow check: rejected \(key)")
}
} catch {
fatalError("Unexpected calculation failure: \(error)")
}
من مجلد الحزمة، شغّل الملف التنفيذي باستخدام الأمر الموثق في دليل macOS من Swift.org:
swift run SyntheticTotals
ناتج البرنامج المتوقع، باستثناء رسائل البناء، هو:
circle: 10
square: 8
triangle: 0
Overflow check: rejected circle
يتضمن البرنامج عمليات تحقق من الإجماليات المحسوبة، والإدخال الفارغ، والإلغاء إلى صفر، وتجاوز السعة المتعمد. هذه نتائج متوقعة مستمدة من المثال وواجهات البرمجة الموثقة. جرت مراجعة الشيفرة، لكنها لم تُنفّذ أثناء الإعداد لأن البيئة المتاحة لم تتضمن مترجم Swift.
كيف يعمل التجميع متبوعًا بالاختزال؟
تفصل الدالة الأولى المهمة عمدًا إلى مراحل واضحة. ينشئ Dictionary(grouping: values, by: { $0.key }) قاموسًا تكون قيمه مصفوفات من العناصر المتطابقة. توثق Apple أن كل مجموعة ناتجة غير فارغة وتحافظ على ترتيب عناصرها في التسلسل المصدر. راجع init(grouping:by:).
بالنسبة إلى المثال، النوع الوسيط هو [String: [SyntheticValue]]. تحتوي مجموعة الدائرة على الكميتين 4 و6؛ وتحتوي مجموعة المربع على 3 و5؛ وتحتوي مجموعة المثلث على 0. يفيد هذا التمثيل الوسيط عندما يحتاج جزء آخر من حساب محلي أيضًا إلى فحص عناصر كل مجموعة.
بعد ذلك، يحافظ mapValues على مفاتيح القاموس ويحوّل كل مصفوفة مجموعة إلى عدد صحيح واحد. هذا الحفاظ على المفاتيح هو السلوك الموثق في mapValues(_:). ويوضح أيضًا سبب بقاء إدخال المثلث رغم أن إجماليه صفر.
داخل هذا التحويل، يبدأ reduce(0) من الصفر ويمرر كل عنصر تالٍ إلى دالة الدمج. وتعيد الدالة الإجمالي الجاري التالي. توثق Apple أن الاختزال يجري بالتسلسل، وتحدد أن اختزال تسلسل فارغ يعيد القيمة الابتدائية. راجع reduce(::).
هناك حالتان مختلفتان للفراغ هنا. فاختزال مصفوفة كميات فارغة بدءًا من الصفر يعطي صفرًا. لكن تجميع إدخال فارغ ينشئ مجموعات معدومة، لذا تعيد الدالة كاملة قاموسًا فارغًا بدلًا من اختراع تسمية بإجمالي صفر.
متى ينبغي تجميع الإجماليات مباشرةً؟
استخدم totalDirectly عندما تكون الإجماليات النهائية هي النتيجة المطلوبة فقط. يبدأ المُجمّع بقاموس [String: Int] فارغ. ولكل عنصر إدخال، يبحث عن الإجمالي السابق لتلك التسمية، ويضيف الكمية، ثم يخزن الإجمالي المحدّث.
يوفر البحث مع قيمة افتراضية صفرًا عند غياب المفتاح. مجرد قراءة قيمة افتراضية لا تنشئ إدخالًا؛ فالإسناد في السطر التالي يخزن الإجمالي المحسوب. يشرح توثيق الرمز الفرعي للقيمة الافتراضية من Apple كلًا من البحث الاحتياطي والتحديث الموضعي للقيم ذات دلالات القيمة.
يمنح reduce(into:) دالته إمكانية تعديل المُجمّع. توصي Apple بهذا الشكل بدلًا من reduce العادي عند إنشاء نتيجة ذات سلوك النسخ عند الكتابة، مثل القاموس. راجع reduce(into:_:).
| ما الذي تحتاج إليه الخطوة التالية | الاختيار | السبب |
|---|---|---|
| أعضاء المجموعات وإجمالياتها | التجميع ثم الاختزال | تظل المصفوفات الوسيطة متاحة للفحص أو إعادة الاستخدام. |
| عدد صحيح واحد فقط لكل تسمية | التجميع المباشر | يتجنب إنشاء مصفوفات المجموعات الوسيطة. |
| مخرجات نصية قابلة للتكرار | أي منهما، ثم فرز المفاتيح | ترتيب المخرجات قرار مستقل عن التجميع. |
ينبع الفرق في التخزين الوسيط من الشيفرة: يحتفظ التجميع بكل عناصر الإدخال في مصفوفات المجموعات، بينما يحتفظ التجميع المباشر بإجمالي جارٍ واحد لكل مفتاح مميز. هذه مقارنة في التنفيذ وليست ادعاءً بشأن أداء مقاس. لا يقدم المثال أي معيار توقيت.
كيف تتعامل مع تجاوز السعة وثبات ترتيب المخرجات؟
يستدعي checkedAdd الدالة addingReportingOverflow، التي تعيد نتيجة جزئية وعلامة تجاوز سعة. تحدد Apple أن القيمة الجزئية تحتوي على المجموع الكامل فقط عندما تكون العلامة غير مفعّلة. عند حدوث تجاوز، تُقتطع القيمة الجزئية. لذلك يرمي البرنامج خطأ قبل تخزينها. راجع addingReportingOverflow(_:).
يتحقق هذا العقد من كل عملية جمع بترتيب الإدخال داخل كل مفتاح. على سبيل المثال، تُرفض الكميات Int.max و1 و-1 لتسمية واحدة عند عملية الجمع الثانية، رغم أن مجموعها الرياضي النهائي يقع ضمن نطاق Int. يتطلب دعم مثل هذه المدخلات استراتيجية حسابية مختلفة؛ فقبول القيمة الوسيطة المقتطعة سيغيّر العقد.
إذا تجاوزت عدة مجموعات السعة، فلا يضمن تنفيذ التجميع أي تسمية سيظهر خطؤها أولًا: ترتيب المرور على القاموس غير متوقع. أما التنفيذ المباشر فيعالج العناصر بترتيب المصدر. يرفض كلاهما الحساب الذي يتجاوز السعة، لكن قد يختلف المفتاح الذي يُبلّغ عن خطئه أولًا.
يجعل فرز مفاتيح النتيجة الإجماليات المعروضة قابلة للتوقع. قواميس Swift غير مرتبة، لذا لا ينبغي استخدام طباعة القاموس مباشرةً لفرض ترتيب العرض. تصف Apple هذا الحد المتعلق بالترتيب في توثيق Dictionary. ولا يفرز المثال المفاتيح إلا بعد التجميع، فيترك الحساب بلا تغيير.
هل تغيّر Foundation أو التزامن في Swift هذا الحساب؟
لا حاجة إلى Foundation لهذا العقد الخاص بالقيم. توثق Apple أن NSDictionary بديل عند الحاجة إلى دلالات المرجع. وفي هذه الحالة، يمثّل قاموس Swift ذو النوع المحدد الربط المطلوب بين التسميات وإجماليات الأعداد الصحيحة. راجع توثيق NSDictionary في Foundation.
الحساب متزامن وينفذ المُجمّع القابل للتعديل داخل الدالة. يصرّح المثال بأن SyntheticValue يتوافق صراحةً مع Sendable؛ وهذا يعبّر عن عقد قيمة متعلق بالتزامن، ولا يعني طلب تشغيل الاختزال بالتوازي. يشرح دليل التزامن في Swift القيم القابلة للإرسال ومتطلبات البنى التي تكون خصائصها المخزنة قابلة للإرسال.
لهذا الحساب الصغير على سطح المكتب، استدعِ أيًا من الدالتين مباشرةً وافحص القاموس المعاد. اختر التجميع عندما تظل عناصر المجموعات مفيدة، أو التجميع المباشر عندما تكفي الإجماليات. حافظ على قواعد الإدخال الفارغ والإجمالي الصفري، وارفض تجاوز السعة الحسابية صراحةً، ولا تفرز إلا عند عرض النتيجة.
المصادر
- تثبيت Swift — macOS — Swift.org.
- Dictionary — توثيق مطوري Apple.
- init(grouping:by:) — توثيق مطوري Apple.
- mapValues(_:) — توثيق مطوري Apple.
- reduce(::) — توثيق مطوري Apple.
- reduce(into:_:) — توثيق مطوري Apple.
- subscript(_:default:) — توثيق مطوري Apple.
- addingReportingOverflow(_:) — توثيق مطوري Apple.
- NSDictionary — توثيق مطوري Apple.
- Concurrency — لغة برمجة Swift.