أبقِ الخصائص المستخدمة في المساواة والتجزئة مستقرة ما دام الكائن مفتاحًا في القاموس. لإجراء حساب محلي بلغة Swift على جهاز Mac، اختر هوية الكائن أو معرّفًا غير قابل للتغيير أو لقطة للقيمة، وفقًا لمعنى البحث الذي تريده.
يستخدم هذا الشرح مستطيلات اصطناعية وحسابات صغيرة للمساحة تُحفظ كلها في الذاكرة. والمهمة العملية هي السماح بتغيير تسمية المستطيل أو أبعاده من دون فقدان مدخله المرتبط في القاموس، ومن دون الخلط بين نجاح البحث وحداثة الحساب.
لماذا قد يؤدي تغيير كائن إلى تعطل البحث في القاموس؟
إن Dictionary في Swift جدول تجزئة، ومفاتيحه تلتزم بالبروتوكول Hashable. ويعيد الوصول إلى عنصر باستخدام مفتاح قيمة اختيارية، لأن المفتاح المطلوب قد لا يكون موجودًا. تساعد التجزئة في العثور على المدخل، بينما تحدد المساواة المفاتيح التي تُعد متكافئة. راجع توثيق Dictionary من Apple.
يربط عقد Hashable بين هاتين العمليتين: يجب أن تمرر النسخ المتساوية المكونات الجوهرية نفسها إلى Hasher وبالترتيب نفسه. عند تنفيذ مفتاح مخصص، استخدم الخصائص التي تحدد المساواة في hash(into:). توضح Apple هذا الشرط في Hashable.
ويضيف المفتاح من نوع مرجعي اعتبارًا آخر. فإسناد نسخة من صنف يمرر مرجعًا إلى الكائن الموجود بدلًا من إنشاء كائن مستقل. كما أن تعريف المرجع باستخدام let لا يجمّد خصائصه القابلة للتغيير. هذه قواعد للغة موثقة في البنى والأصناف.
لنفترض أن صنفًا للمستطيلات يقارن تسميته القابلة للتحرير ويجزّئها. تُدرج الكائن عندما تكون تسميته \"Tile A\"، ثم تغيّرها إلى \"Tile B\". عندئذٍ يعرض المرجع المخزّن حالة مختلفة تحدد المفتاح. لم يعد موضعه الأصلي في القاموس متوافقًا على نحو مستقر مع عمليات البحث اللاحقة.
تترتب على عقدي جدول التجزئة والمساواة قاعدة التصميم: أبقِ الحالة التي تحدد مساواة المفتاح مستقرة طوال فترة وجوده في القاموس. وتصرح Apple بالشرط المقابل لكائنات Foundation: يجب ألا تتغير تجزئة الكائن ما دام ضمن مجموعة تستخدم تلك التجزئة لتحديد موضعه. راجع NSObjectProtocol.hash.
لا تبنِ اختبارًا على افتراض أن كل تغيير غير صالح سيعيد فورًا nil. بعد الإخلال بعقد المفتاح، لا يكفي نجاح البحث الظاهري دليلًا على سلامة التصميم. اختبر قواعد المساواة المقصودة والسلوك المستقر بدلًا من ذلك.
أي مفتاح يناسب المهمة؟
اختر معنى مدخل القاموس قبل كتابة المطابقة. في أداة لحساب مساحة المستطيلات، تؤدي هذه الأسئلة الثلاثة إلى تصاميم مختلفة:
| ما الذي ينبغي أن يمثله المدخل؟ | المفتاح المناسب | ماذا يحدث عند تغيّر المستطيل؟ |
|---|---|---|
| نسخة المستطيل هذه بعينها | مفتاح صنف قائم على الهوية | يظل المدخل مرتبطًا بتلك النسخة |
| مستطيل منطقي يُعرّف بمعزل عن كائنه الحالي | قيمة معرّف غير قابل للتغيير | يظل المدخل مرتبطًا بالمعرّف |
| حساب لأبعاد معيّنة | لقطة أبعاد غير قابلة للتغيير | تؤدي الأبعاد المتغيرة إلى مفتاح بحث مختلف |
هذا الجدول أداة مساعدة للتصميم مستنبطة من قواعد الهوية والقيم والتجزئة في Swift. كما يفصل بين واجبين: إبقاء الارتباط قابلًا للوصول، وإبقاء النتيجة المحسوبة محدّثة. يحل مفتاح الهوية المستقر الواجب الأول؛ أما إعادة الحساب أو تضمين جميع المدخلات في لقطة فيعالج الواجب الثاني.
استخدم هوية الكائن عندما يرتبط المدخل بنسخة واحدة
لربط داخل الذاكرة ينبغي أن يتبع كائنًا واحدًا عبر التعديلات، عرّف المساواة باستخدام === واجزّئ ObjectIdentifier الخاص به. يختبر عامل الهوية في Swift ما إذا كانت المراجع تشير إلى النسخة نفسها. وتوثق Apple أن ObjectIdentifier معرّف قابل للتجزئة، وتكون مقارناته صالحة خلال عمر النسخة. راجع البنى والأصناف وObjectIdentifier.
البرنامج التالي كامل ومتزامن. يحتاج إلى جهاز Mac تتوفر عليه أدوات سطر أوامر Swift؛ ولا يستخدم إلا مكتبة Swift القياسية المستوردة تلقائيًا. احفظه باسم IdentityKeys.swift وشغّل swift IdentityKeys.swift في Terminal. جرى فحص الأمثلة، لكن تعذّر تنفيذها في بيئة البحث لعدم توفر Swift.
final class Rectangle: Hashable {
var label: String
var width: Int
var height: Int
init(label: String, width: Int, height: Int) {
self.label = label
self.width = width
self.height = height
}
static func == (lhs: Rectangle, rhs: Rectangle) -> Bool {
lhs === rhs
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
func demonstrateIdentityKeys() {
let rectangle = Rectangle(label: "Tile A", width: 4, height: 3)
let alias = rectangle
let separate = Rectangle(label: "Tile A", width: 4, height: 3)
var areas: [Rectangle: Int] = [
rectangle: rectangle.width * rectangle.height
]
rectangle.label = "Tile B"
assert(areas[alias] == 12)
assert(areas[separate] == nil)
rectangle.width = 5
assert(areas[rectangle] == 12) // Still the previous calculation.
areas[rectangle] = rectangle.width * rectangle.height
assert(areas[rectangle] == 15)
assert(areas.count == 1)
print("Renamed lookup: 12")
print("Recalculated area: \(areas[rectangle]!)")
}
demonstrateIdentityKeys()
المخرجات المتوقعة هي Renamed lookup: 12 ثم Recalculated area: 15. وتعبّر التأكيدات عن العقد المفيد: يعثر الاسم المستعار على المدخل الموجود، ولا يعثر مستطيل أُنشئ منفصلًا عليه، ولا يؤدي تعديل الكائن الأصلي إلى إنشاء مفتاح آخر.
يترك تغيير العرض المساحة المخزّنة عند 12 عمدًا إلى أن يعيّن البرنامج 15. تظل الهوية مستقرة، لكن القاموس لا يراقب تبعيات الحساب ولا يعيد حساب القيم. قرر ما إذا كان تطبيقك سيحدّث القيمة فورًا بعد التعديل أم سيزيلها حتى الحساب التالي.
المساواة القائمة على الهوية قرار يخص النوع بأكمله. فكل مقارنة == على هذا الصنف تعني الآن «النسخة نفسها». إذا كان النموذج يعرّف المساواة أصلًا استنادًا إلى الأبعاد أو إلى قيمة أخرى، فاستخدم تصميمًا منفصلًا للمفتاح بدلًا من تغيير هذا المعنى من أجل قاموس واحد.
يمكنك أيضًا جعل مفتاح القاموس ObjectIdentifier مباشرة، لكن المعرّف وحده لا يوفر المستطيل اللازم للعمليات اللاحقة. أبقِ الكائن المعني حيًا طوال مدة استخدام معرّفه للمقارنة، وأزل المدخلات التي لم تعد مطلوبة. يجعل حد العمر الموثق هذه الآلية مناسبة لهوية محصورة ضمن نطاق وفي الذاكرة. راجع ObjectIdentifier.
استخدم لقطة قيمة عندما يمثّل المدخل حسابًا
غالبًا ما تكون الأبعاد نفسها هي المفتاح المناسب لنتائج المساحة القابلة لإعادة الاستخدام. يمكن لمستطيلين أبعادهما 4 في 3 مشاركة النتيجة نفسها، حتى إن كانا كائنين منفصلين بتسميتين مختلفتين. وينبغي أن يؤدي تغيير الأبعاد إلى اختيار مدخل مختلف.
تتبع البنى في Swift دلالات القيم، ويمكن للمترجم إنشاء مطابقة Hashable تلقائيًا عندما تكون خصائصها المخزنة قابلة للتجزئة. لذلك توفر لقطة تحتوي على أعداد صحيحة غير قابلة للتغيير مفتاحًا مباشرًا لهذا الحساب. راجع البنى والأصناف وHashable.
شغّل هذا البديل الكامل مستقلًا باستخدام swift SnapshotKeys.swift. ومدخلاته الاصطناعية أعداد صحيحة موجبة وصغيرة، لذا لا يحتاج المثال إلى واجهة عامة لإدخال الأعداد.
final class EditableRectangle {
var label: String
var width: Int
var height: Int
init(label: String, width: Int, height: Int) {
self.label = label
self.width = width
self.height = height
}
}
struct AreaInputs: Hashable, Sendable {
let width: Int
let height: Int
}
func demonstrateSnapshotKeys() {
let rectangle = EditableRectangle(
label: "Tile A", width: 4, height: 3
)
var areas: [AreaInputs: Int] = [:]
let original = AreaInputs(
width: rectangle.width, height: rectangle.height
)
areas[original] = original.width * original.height
rectangle.label = "Tile B"
rectangle.width = 5
let current = AreaInputs(
width: rectangle.width, height: rectangle.height
)
assert(areas[original] == 12)
assert(areas[current] == nil)
areas[current] = current.width * current.height
assert(areas[current] == 15)
assert(areas.count == 2)
print("Original area: \(areas[original]!)")
print("Current area: \(areas[current]!)")
}
demonstrateSnapshotKeys()
المخرجات المتوقعة هي Original area: 12 ثم Current area: 15. وتظل النتيجتان قابلتين للوصول لأن البرنامج أدرج لقطتين مختلفتين. وما زالت النتيجة القديمة تصف الأبعاد القديمة على نحو صحيح.
أدرج في اللقطة كل مدخل يؤثر في الحساب. فالتسمية لا تؤثر في مساحة المستطيل، لذا استُبعدت هنا. وإذا اعتمد حساب لاحق على إعداد آخر، فأضف هذا الإعداد إلى المفتاح. وإلا فقد تتشارك حسابات مختلفة مدخلًا واحدًا عن طريق الخطأ. وهذا شرط يتعلق بتصميم الحساب، مستقل عن استيفاء المفتاح للبروتوكول Hashable.
البنية التي لا تفعل سوى تغليف مرجع إلى صنف قابل للتغيير لا تنشئ تلقائيًا لقطة مجمّدة لذلك الكائن. انسخ القيم ذات الصلة إلى اللقطة كما يفعل هذا المثال. وقرر أيضًا متى تتخلص من اللقطات القديمة إذا كانت التعديلات المتكررة ستراكم نتائج لم تعد بحاجة إليها.
متى ينبغي استخدام معرّف غير قابل للتغيير بدلًا من ذلك؟
استخدم معرّفًا عندما ينبغي لنسخ متعددة من الكائن أن تشير إلى المدخل المنطقي نفسه. في هذه الأداة الاصطناعية، قد يكفي معرّف صحيح غير قابل للتغيير: خزّن النتائج في [Int: Int]، واستخدم let id: Int الخاص بالمستطيل للوصول إليها، وأبقِ التسميات والأبعاد القابلة للتحرير خارج المفتاح.
وفقًا لهذا التصميم، يشير كائنان يحملان المعرّف 7 عمدًا إلى مدخل القاموس نفسه. ويجب أن تضمن قواعد الإنشاء ألا تحصل المستطيلات المنطقية المختلفة على المعرّف نفسه عن طريق الخطأ. يؤدي تحديث مفتاح موجود إلى استبدال القيمة المرتبطة به، كما هو موضح في Dictionary.
يتطلب جدول المساحات القائم على المعرّفات سياسة تحديث بعد تغيّر الأبعاد أيضًا. إذا أردت نتائج لكل مجموعة متميزة من المدخلات، فاستخدم اللقطات. وإذا أردت أحدث نتيجة فقط لكل مستطيل منطقي، فالمعرّف مع إعادة الحساب الصريحة أنسب.
هل تحل قواميس Foundation أو الممثلون مشكلة المفاتيح القابلة للتغيير؟
لـ Foundation عقد إدراج مختلف. يتطلب NSMutableDictionary.setObject(_:forKey:) مفتاحًا يلتزم بـ NSCopying وينسخه عند الإدراج. ويمكن لنسخة المفتاح إنشاء لقطة إذا كان تنفيذ النسخ يوفّر حالة مفتاح مستقرة. ويجب أن تطابق عمليات البحث اللاحقة تلك النسخة المخزّنة. راجع setObject(_:forKey:) من Apple.
لا تستنتج من هذا السلوك أن قاموس Swift الأصلي ينشئ نسخة مستقلة من نسخة الصنف المستخدمة مفتاحًا. فإسناد الأصناف في Swift يتبع دلالات المراجع. وبالنسبة إلى مفاتيح Foundation المخصصة، تحقّق من سلوك النسخ والمساواة والتجزئة معًا؛ فما زال شرط التجزئة من Foundation ساريًا.
التزامن مسألة منفصلة. تنسق الممثلون الوصول إلى حالتهم القابلة للتغيير والمعزولة، ويصف Sendable القيم التي يمكنها الانتقال بين نطاقات التزامن وفقًا لشروطه. ولا يغيّر أي منهما معنى مساواة المفتاح أو يصلح مفتاحًا تتغير حالته المحدِّدة بعد إدراجه. راجع فصل التزامن في Swift وتوثيق Sendable من Apple.
يصرّح مثال اللقطة صراحةً بـ Sendable لأن أعداده الصحيحة المخزّنة تستوفي متطلبات البروتوكول الخاصة بأنواع القيم. وإذا نقلت حسابات المساحة لاحقًا إلى ممثل، فأرسل لقطة كاملة ودع الممثل يملك قاموس النتائج. ويمكن أن يظل المستطيل القابل للتغيير ضمن نطاق العزل الحالي. لا تحتاج هذه الأمثلة المتزامنة إلى ممثل.
تحقّق من العقد قبل الاعتماد على البحث
استخدم تسلسلًا صغيرًا وحتميًا للتحقق من التصميم المختار:
- أدرج مدخلًا وتحقق من البحث الأولي عنه.
- غيّر خاصية مستبعدة من المساواة والتجزئة، وتحقق من بقاء المدخل نفسه قابلًا للوصول.
- قارن اسمًا مستعارًا بكائن أو لقطة أُنشئا بشكل منفصل، وتحقق من توافق النتائج مع المعنى المقصود للمساواة.
- غيّر أحد مدخلات الحساب، وتحقق من تحديث النتيجة أو إزالتها أو الوصول إليها عبر لقطة جديدة.
- تحقّق من عدد عناصر القاموس للتأكد من أن الاستبدال والإدراج يجريان كما هو مقصود.
إذا كان تنفيذ قائم يغيّر حالة تحدد المفتاح، فأصلح نموذج المفتاح قبل الاعتماد على عمليات البحث اللاحقة. في هذا الحساب المحلي الصغير، إعادة بناء النتائج من قائمة منفصلة بالمدخلات الاصطناعية أوضح من اعتبار عمليات القاموس اللاحقة وسيلة للإصلاح. احتفظ بقدر كافٍ من المدخلات الأصلية لإعادة الحساب بدلًا من إسقاط مدخل بصمت لا تستطيع العثور عليه.
اختر الهوية لارتباط بكائن حي واحد، ومعرّفًا غير قابل للتغيير لعنصر منطقي واحد، ولقطة قيمة لحساب واحد. ثم حدّد سياسة تحديث النتيجة بوضوح. تضمن هذه القرارات مجتمعةً بقاء عمليات البحث ذات معنى مع تغيّر المستطيل القابل للتحرير.
المصادر
- Hashable — Apple
- Dictionary — Apple
- Structures and Classes — Swift.org
- ObjectIdentifier — Apple
- hash — Apple
- setObject(_:forKey:) — Apple
- Concurrency — Swift.org
- Sendable — Apple