जब तक कोई ऑब्जेक्ट डिक्शनरी कुंजी हो, समानता और हैशिंग में उपयोग होने वाले गुणों को स्थिर रखें। अपने Mac पर स्थानीय Swift गणना के लिए, लुकअप का आशय क्या है, उसके अनुसार ऑब्जेक्ट आइडेंटिटी, अपरिवर्तनीय ID या वैल्यू स्नैपशॉट चुनें।
इस वॉकथ्रू में सिंथेटिक आयतों और पूरी तरह मेमोरी में रखी गई छोटी क्षेत्रफल गणनाओं का उपयोग किया गया है। व्यावहारिक काम यह है कि आयत का लेबल या उसके आयाम बदलने पर भी उससे जुड़ी डिक्शनरी प्रविष्टि बनी रहे—और सफल लुकअप को अद्यतन गणना समझने की भूल न हो।
ऑब्जेक्ट बदलने से डिक्शनरी लुकअप क्यों टूट सकता है?
Swift का Dictionary एक हैश टेबल है, जिसकी कुंजियाँ Hashable के अनुरूप होती हैं। कुंजी-आधारित सबस्क्रिप्ट वैकल्पिक मान लौटाता है, क्योंकि माँगी गई कुंजी मौजूद न भी हो सकती है। हैशिंग प्रविष्टि खोजने में मदद करती है; समानता तय करती है कि कौन-सी कुंजियाँ समतुल्य मानी जाएँगी। Apple का Dictionary दस्तावेज़ देखें।
Hashable का अनुबंध इन कार्रवाइयों को जोड़ता है: समान इंस्टेंस को Hasher में एक ही क्रम में समान आवश्यक घटक देने चाहिए। कस्टम कुंजी लागू करते समय, hash(into:) में समानता परिभाषित करने वाले गुणों का उपयोग करें। Apple इस आवश्यकता का वर्णन Hashable में करता है।
रेफ़रेंस टाइप वाली कुंजी में एक और बात ध्यान में रखनी होती है। किसी क्लास इंस्टेंस को असाइन करने पर स्वतंत्र ऑब्जेक्ट नहीं बनता, बल्कि मौजूदा ऑब्जेक्ट का रेफ़रेंस मिलता है। रेफ़रेंस को let के रूप में घोषित करने से उसके परिवर्तनशील गुण स्थिर नहीं हो जाते। ये भाषा के नियम Structures and Classes में दर्ज हैं।
मान लें कि आयत की क्लास अपने संपादन योग्य लेबल की तुलना और हैशिंग करती है। आप ऑब्जेक्ट तब डालते हैं जब उसका लेबल \"Tile A\" होता है, फिर उसे बदलकर \"Tile B\" कर देते हैं। अब संग्रहित रेफ़रेंस कुंजी को परिभाषित करने वाली अलग अवस्था दिखाता है। डिक्शनरी में मूल स्थान और बाद का लुकअप अब स्थिर सहमति में नहीं रहते।
हैश टेबल और समानता के अनुबंधों से डिज़ाइन का नियम निकलता है: कुंजी की समानता परिभाषित करने वाली अवस्था को डिक्शनरी में उसकी सदस्यता के दौरान स्थिर रखें। Foundation ऑब्जेक्ट्स के लिए Apple यही आवश्यकता स्पष्ट रूप से बताता है: किसी ऑब्जेक्ट का हैश उस संग्रह में रहते हुए नहीं बदलना चाहिए जो उसे स्थान देने के लिए उस हैश का उपयोग करता है। NSObjectProtocol.hash देखें।
इस धारणा पर आधारित परीक्षण न बनाएँ कि हर अमान्य बदलाव के बाद तुरंत nil ही लौटेगा। कुंजी का अनुबंध टूट जाने पर, दिखने में सफल लुकअप इस बात का पर्याप्त प्रमाण नहीं है कि डिज़ाइन काम करता है। इसके बजाय, अपेक्षित समानता नियमों और स्थिर व्यवहार का परीक्षण करें।
काम के अनुरूप कुंजी कौन-सी है?
कॉनफ़ॉर्मेंस लिखने से पहले तय करें कि डिक्शनरी की प्रविष्टि का अर्थ क्या है। आयत-क्षेत्रफल टूल के लिए ये तीन प्रश्न अलग-अलग डिज़ाइन सुझाते हैं:
| प्रविष्टि किसका प्रतिनिधित्व करे? | उपयुक्त कुंजी | आयत बदलने पर क्या होगा? |
|---|---|---|
| यही सटीक आयत ऑब्जेक्ट | आइडेंटिटी-आधारित क्लास कुंजी | प्रविष्टि उसी इंस्टेंस से जुड़ी रहती है |
| वर्तमान ऑब्जेक्ट से स्वतंत्र रूप से पहचाना गया तार्किक आयत | अपरिवर्तनीय ID वैल्यू | प्रविष्टि ID से जुड़ी रहती है |
| खास आयामों की गणना | अपरिवर्तनीय आयाम स्नैपशॉट | बदले हुए आयामों से अलग लुकअप कुंजी बनती है |
यह तालिका Swift के आइडेंटिटी, वैल्यू और हैशिंग नियमों से निकला डिज़ाइन-सहायक है। यह दो दायित्वों को भी अलग करती है: किसी संबद्धता को खोजने योग्य बनाए रखना और गणना के नतीजे को अद्यतन रखना। स्थिर आइडेंटिटी कुंजी पहले दायित्व को पूरा करती है; दोबारा गणना या इनपुट का पूरा स्नैपशॉट दूसरे को संबोधित करता है।
जब प्रविष्टि एक इंस्टेंस की हो, तो ऑब्जेक्ट आइडेंटिटी अपनाएँ
मेमोरी में ऐसी संबद्धता के लिए जिसे संपादन के दौरान एक ही ऑब्जेक्ट के साथ बने रहना चाहिए, === से समानता परिभाषित करें और उसके ObjectIdentifier का हैश बनाएँ। Swift का आइडेंटिटी ऑपरेटर जाँचता है कि रेफ़रेंस एक ही इंस्टेंस की ओर संकेत करते हैं या नहीं। Apple ObjectIdentifier को ऐसे हैश योग्य आइडेंटिफ़ायर के रूप में बताता है, जिसकी तुलनाएँ इंस्टेंस के जीवनकाल में मान्य होती हैं। Structures and Classes और ObjectIdentifier देखें।
नीचे एक पूरा, सिंक्रोनस प्रोग्राम है। इसके लिए Mac पर Swift कमांड-लाइन टूल उपलब्ध होने चाहिए; यह केवल अपने आप इंपोर्ट होने वाली Swift स्टैंडर्ड लाइब्रेरी का उपयोग करता है। इसे IdentityKeys.swift के नाम से सेव करें और Terminal में swift IdentityKeys.swift चलाएँ। उदाहरणों की जाँच की गई, लेकिन शोध परिवेश में इन्हें चलाया नहीं जा सका क्योंकि वहाँ 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 अपने आप बना सकता है। अपरिवर्तनीय पूर्णांकों वाला स्नैपशॉट इस गणना के लिए सरल कुंजी देता है। Structures and Classes और 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 के अनुरूप होने से अलग है।
सिर्फ़ परिवर्तनशील क्लास रेफ़रेंस को समेटने वाला स्ट्रक्चर उस ऑब्जेक्ट का स्थिर स्नैपशॉट अपने आप नहीं बनाता। इस उदाहरण की तरह, संबंधित मानों को स्नैपशॉट में कॉपी करें। बार-बार संपादन से पुराने स्नैपशॉट जमा हो सकते हैं, इसलिए यह भी तय करें कि अब जिनकी ज़रूरत नहीं है उन्हें कब हटाना है।
इसके बजाय अपरिवर्तनीय ID कब अपनाएँ?
जब कई ऑब्जेक्ट इंस्टेंस को एक ही तार्किक प्रविष्टि तक पहुँचना हो, तब ID अपनाएँ। इस सिंथेटिक टूल के लिए एक अपरिवर्तनीय पूर्णांक ID पर्याप्त हो सकती है: नतीजे [Int: Int] में रखें, पहुँच के लिए आयत की let id: Int का उपयोग करें और संपादन योग्य लेबल तथा आयामों को कुंजी से बाहर रखें।
इस डिज़ाइन में ID 7 वाले दो ऑब्जेक्ट जानबूझकर उसी डिक्शनरी प्रविष्टि तक पहुँचते हैं। आपके निर्माण नियमों को यह सुनिश्चित करना चाहिए कि अलग-अलग तार्किक आयतों को ग़लती से एक ही ID न मिले। Dictionary के दस्तावेज़ के अनुसार, मौजूदा कुंजी को अद्यतन करने पर उससे जुड़ा मान बदल जाता है।
ID-आधारित क्षेत्रफल तालिका में भी आयाम बदलने के बाद नतीजे अद्यतन करने की नीति चाहिए। अगर आप हर अलग इनपुट-समूह का नतीजा चाहते हैं, तो स्नैपशॉट अपनाएँ। अगर हर तार्किक आयत के लिए केवल नवीनतम नतीजा चाहिए, तो ID के साथ स्पष्ट पुनर्गणना बेहतर है।
क्या Foundation डिक्शनरी या ऐक्टर परिवर्तनशील कुंजियों की समस्या हल करते हैं?
Foundation में इन्सर्शन का अनुबंध अलग है। NSMutableDictionary.setObject(_:forKey:) को NSCopying कुंजी चाहिए और यह इन्सर्शन के दौरान उसकी कॉपी बनाता है। अगर कॉपी बनाने का इम्प्लीमेंटेशन स्थिर कुंजी अवस्था देता है, तो कुंजी की कॉपी स्नैपशॉट बना सकती है। बाद के लुकअप को उस संग्रहीत कॉपी से मेल खाना होगा। Apple का setObject(_:forKey:) देखें।
इस व्यवहार से यह निष्कर्ष न निकालें कि Swift की नेटिव डिक्शनरी कुंजी के रूप में इस्तेमाल किए गए क्लास इंस्टेंस की स्वतंत्र कॉपी बनाती है। Swift क्लास असाइनमेंट रेफ़रेंस सेमांटिक्स का पालन करता है। कस्टम Foundation कुंजियों के लिए कॉपी, समानता और हैश के व्यवहार की एक साथ जाँच करें; Foundation की हैश संबंधी आवश्यकता फिर भी लागू होती है।
कनकरेंसी एक अलग चिंता है। ऐक्टर अपनी आइसोलेटेड परिवर्तनशील अवस्था तक पहुँच का समन्वय करते हैं, जबकि Sendable उन वैल्यू का वर्णन करता है जो उसकी आवश्यकताओं के तहत कनकरेंसी डोमेन के पार जा सकती हैं। इनमें से कोई भी कुंजी की समानता का अर्थ नहीं बदलता या इन्सर्शन के बाद अपनी परिभाषित अवस्था बदलने वाली कुंजी को ठीक नहीं करता। Swift का Concurrency अध्याय और Apple का Sendable दस्तावेज़ देखें।
स्नैपशॉट उदाहरण स्पष्ट रूप से Sendable घोषित करता है, क्योंकि उसके संग्रहित पूर्णांक प्रोटोकॉल की वैल्यू-टाइप आवश्यकताएँ पूरी करते हैं। अगर बाद में आप क्षेत्रफल की गणनाएँ किसी ऐक्टर में ले जाएँ, तो पूरा स्नैपशॉट भेजें और नतीजों वाली डिक्शनरी का स्वामित्व ऐक्टर को दें। परिवर्तनशील आयत अपने मौजूदा आइसोलेशन डोमेन में रह सकता है। इन सिंक्रोनस उदाहरणों को ऐक्टर की ज़रूरत नहीं है।
लुकअप पर भरोसा करने से पहले अनुबंध जाँचें
चुने गए डिज़ाइन को परखने के लिए एक छोटा, नियतात्मक क्रम अपनाएँ:
- एक प्रविष्टि डालें और उसका शुरुआती लुकअप सही होने की पुष्टि करें।
- समानता और हैशिंग से बाहर के किसी गुण को बदलें; पुष्टि करें कि वही प्रविष्टि अब भी खोजी जा सकती है।
- किसी उपनाम की तुलना अलग से बनाए गए ऑब्जेक्ट या स्नैपशॉट से करें; पुष्टि करें कि नतीजे समानता के अपेक्षित अर्थ से मेल खाते हैं।
- गणना का इनपुट बदलें; पुष्टि करें कि नतीजा ताज़ा किया गया है, हटा दिया गया है या नए स्नैपशॉट के ज़रिए उपलब्ध है।
- डिक्शनरी की गिनती जाँचें, ताकि बदलने और डालने का व्यवहार अपेक्षा के अनुसार हो।
अगर मौजूदा इम्प्लीमेंटेशन कुंजी को परिभाषित करने वाली अवस्था बदलता है, तो आगे के लुकअप पर भरोसा करने से पहले कुंजी मॉडल ठीक करें। इस छोटी स्थानीय गणना के लिए, सिंथेटिक इनपुट की अलग सूची से नतीजे दोबारा बनाना बाद के डिक्शनरी ऑपरेशनों को मरम्मत के उपाय की तरह मानने से अधिक स्पष्ट है। मूल इनपुट इतना सुरक्षित रखें कि दोबारा गणना हो सके; ऐसी प्रविष्टि चुपचाप न छोड़ें जिसे आप ढूँढ़ नहीं सकते।
एक जीवित ऑब्जेक्ट से जुड़ी संबद्धता के लिए आइडेंटिटी, एक तार्किक आइटम के लिए अपरिवर्तनीय ID और एक गणना के लिए वैल्यू स्नैपशॉट चुनें। फिर नतीजे अद्यतन करने की नीति स्पष्ट करें। ये निर्णय मिलकर संपादन योग्य आयत बदलने पर लुकअप को अर्थपूर्ण बनाए रखते हैं।
स्रोत
- Hashable — Apple
- Dictionary — Apple
- Structures and Classes — Swift.org
- ObjectIdentifier — Apple
- hash — Apple
- setObject(_:forKey:) — Apple
- Concurrency — Swift.org
- Sendable — Apple