دعونا نلقي نظرة فاحصة على موضوع البرمجة الموجهة للبروتوكول. للراحة ، تم تقسيم المواد إلى ثلاثة أجزاء.
هذه المادة هي ترجمة تعليق للعرض التقديمي WWDC 2016 . على عكس الاعتقاد الشائع بأن الأشياء "تحت الغطاء" يجب أن تبقى هناك ، يكون من المفيد للغاية في بعض الأحيان معرفة ما يحدث هناك. سيساعد هذا في استخدام العنصر بشكل صحيح ولغرضه المقصود.
سيعالج هذا الجزء المشكلات الرئيسية في البرمجة الموجهة للكائنات وكيف يحلها بروتوكول POP. سيتم النظر في كل شيء في واقع لغة سويفت ، وسوف تعتبر التفاصيل "مقصورة المحرك" من البروتوكولات.
مشاكل OOP ولماذا نحتاج إلى POP
من المعروف أنه يوجد في OOP عدد من نقاط الضعف التي يمكن أن "تثقل كاهل" تنفيذ البرنامج. النظر في الأكثر صراحة وشائعة:
- تخصيص: كومة أو كومة؟
- مرجع العد: أكثر أو أقل؟
- إرسال الأسلوب: ثابت أو ديناميكي؟
1.1 تخصيص - المكدس
المكدس هو بنية بيانات بسيطة إلى حد ما وبدائية. يمكننا وضع الجزء العلوي من الحزمة (الضغط) ، ويمكننا أن نأخذها من أعلى الحزمة (pop). البساطة هي أن هذا هو كل ما يمكننا القيام به.
للبساطة ، دعنا نفترض أن كل رصة لها متغير (مؤشر رصة). يتم استخدامه لتتبع الجزء العلوي من المكدس وتخزين عدد صحيح (عدد صحيح). ويترتب على ذلك أن سرعة العمليات مع المكدس تساوي سرعة إعادة كتابة عدد صحيح في هذا المتغير.
دفع - وضعت في الجزء العلوي من المكدس ، وزيادة مؤشر المكدس.
pop - تقليل مؤشر المكدس.
أنواع القيمة
دعونا ننظر في مبادئ عملية المكدس في سويفت باستخدام الهياكل (الهيكل).
في Swift ، تكون أنواع القيم هي بنيات (بنية) وتعدادات (تعداد) ، وأنواع المراجع هي فئات (فئة) ووظائف / عمليات إغلاق (func). يتم تخزين أنواع القيم على المكدس ، ويتم تخزين أنواع المرجع على كومة الذاكرة المؤقتة.
struct Point { var x, y: Double func draw() {...} } let point1 = Point(...)

- نضع الهيكل الأول على المكدس
- نسخ محتويات الهيكل الأول
- تغيير ذاكرة الهيكل الثاني (يبقى الأول على حاله)
- نهاية الاستخدام. ذاكرة حرة
1.2 تخصيص - كومة
الكومة هي بنية بيانات تشبه الشجرة. لن يتأثر موضوع تطبيق الكومة هنا ، لكننا سنحاول مقارنته بالمكدس.
لماذا ، إن أمكن ، هل يستحق استخدام Stack بدلاً من Heap؟ إليك السبب:
- عد مرجع
- إدارة الذاكرة الحرة والبحث عن التخصيص
- إعادة كتابة الذاكرة ل deallocation
كل هذا هو مجرد جزء صغير مما يجعل Heap يعمل ووزنه واضح مقارنةً بـ Stack.
على سبيل المثال ، عندما نحتاج إلى ذاكرة خالية على Stack ، فإننا ببساطة نأخذ قيمة مؤشر المكدس ونزيدها (لأن كل شيء فوق مؤشر المكدس في Stack هو ذاكرة حرة) - O (1) هي عملية ثابتة في الوقت المناسب.
عندما نحتاج إلى ذاكرة مجانية على Heap ، نبدأ في البحث عنها باستخدام خوارزمية البحث المناسبة في بنية شجرة البيانات - في أفضل الأحوال ، لدينا عملية O (logn) ، وهي ليست ثابتة في الوقت وتعتمد على تطبيقات محددة.
في الواقع ، Heap أكثر تعقيدًا: يتم توفير عملها من خلال مجموعة من الآليات الأخرى التي تعيش في أحشاء أنظمة التشغيل.
تجدر الإشارة أيضًا إلى أن استخدام Heap في وضع multithreading يؤدي إلى تفاقم الموقف بشكل كبير ، لأنه من الضروري التأكد من مزامنة المورد المشترك (الذاكرة) لمؤشرات الترابط المختلفة. يتم تحقيق ذلك عن طريق استخدام الأقفال (الإشارات الدورانية ، والأشكال المغزليّة ، إلخ).
أنواع المرجع
دعونا نلقي نظرة على كيفية عمل Heap في Swift باستخدام الفصول الدراسية.
class Point { var x, y: Double func draw() {...} } let point1 = Point(...)

1. ضع فئة الجسم على كومة. ضع المؤشر على هذا الجسم على المكدس.
- انسخ المؤشر الذي يشير إلى نص الفصل
- نحن نغير مجموعة من الصف
- نهاية الاستخدام. ذاكرة حرة
1.3 التخصيص - مثال صغير و "حقيقي"
في بعض الحالات ، لا يؤدي اختيار Stack إلى تبسيط معالجة الذاكرة فحسب ، بل يؤدي أيضًا إلى تحسين جودة الرمز. النظر في مثال:
enum Color { case red, green, blue } enum Orientation { case left, right } enum Tail { case none, tail, bubble } var cache: [String: UIImage] = [] func makeBalloon(_ color: Color, _ orientation: Orientation, _ tail: Tail) -> UIImage { let key = "\(color):\(orientation):\(tail)" if let image = cache[key] { return image } ... }
إذا كان لقاموس ذاكرة التخزين المؤقت قيمة مع مفتاح المفتاح ، فستقوم الدالة ببساطة بإرجاع UIImage المخزنة مؤقتًا.
مشاكل هذا الكود هي:
ليس من الممارسات الجيدة استخدام String كمفتاح في الذاكرة المخبأة مؤقتًا ، لأن السلسلة في النهاية "يمكن أن تتحول إلى شيء".
السلسلة عبارة عن بنية نسخ عند الكتابة ، لتنفيذ ديناميكيتها ، وتقوم بتخزين كل الأحرف الموجودة على كومة الذاكرة المؤقتة. وبالتالي ، String هي بنية ، ويتم تخزينها في Stack ، ولكنها تخزن جميع محتوياتها على Heap.
يعد هذا ضروريًا لتوفير القدرة على تغيير الخط (إزالة جزء من السطر ، إضافة سطر جديد إلى هذا السطر). إذا تم تخزين جميع أحرف السلسلة على المكدس ، فسيكون هذا التلاعب مستحيلًا. على سبيل المثال ، في C ، تكون السلاسل ثابتة ، مما يعني أنه لا يمكن زيادة حجم السلسلة في وقت التشغيل حيث يتم تخزين جميع المحتويات على المكدس. للحصول على نسخ أكثر تفصيلاً للخطوط في سويفت ، يرجى النقر هنا .
الحل:
استخدم البنية بوضوح تام هنا بدلاً من السلسلة:
struct Attributes: Hashable { var color: Color var orientation: Orientation var tail: Tail }
تغيير القاموس إلى:
var cache: [Attributes: UIImage] = []
تخلص من السلسلة
let key = Attributes(color: color, orientation: orientation, tail: tail)
في بنية السمات ، يتم تخزين جميع الخصائص على المكدس ، حيث يتم تخزين التعداد على المكدس. هذا يعني أنه لا يوجد استخدام ضمني لـ Heap هنا ، والآن يتم تعريف مفاتيح قاموس ذاكرة التخزين المؤقت بدقة شديدة ، مما زاد من أمان وضوح هذا الرمز. لقد تخلصنا أيضًا من الاستخدام الضمني للكومة.
الحكم: المكدس أسهل وأسرع بكثير من كومة الذاكرة المؤقتة - اختيار معظم الحالات واضح.
2. مرجع العد
من اجل ماذا؟
يجب أن يعرف Swift متى يمكن تحرير قطعة من الذاكرة على كومة الذاكرة المؤقتة ، والتي تشغلها ، على سبيل المثال ، نسخة من فئة أو وظيفة. يتم تنفيذ ذلك من خلال آلية حساب الارتباط - كل مثيل (فئة أو وظيفة) المستضافة على كومة يحتوي على متغير يقوم بتخزين عدد الارتباطات إليه. عندما لا توجد روابط لمثيل ، يقرر Swift تحرير جزء من الذاكرة مخصص له.
تجدر الإشارة إلى أنه من أجل التنفيذ "عالي الجودة" لهذه الآلية ، هناك حاجة إلى موارد أكثر بكثير من زيادة وخفض مؤشر Stack-pointer. هذا يرجع إلى حقيقة أن قيمة عدد الارتباطات يمكن أن تزيد من مؤشرات ترابط مختلفة (لأنه يمكنك الرجوع إلى فئة أو وظيفة من مؤشرات ترابط مختلفة). أيضًا ، لا تنس الحاجة إلى ضمان تزامن المورد المشترك (عدد متغير من الروابط) لمؤشرات الترابط المختلفة (spinlocks ، الإشارات ، وما إلى ذلك).
المكدس: العثور على ذاكرة مجانية وتحرير الذاكرة المستخدمة - تشغيل مؤشر المكدس
كومة الذاكرة المؤقتة: البحث عن ذاكرة حرة وتحرير الذاكرة المستخدمة - خوارزمية البحث عن الأشجار والعد المرجعي.
في بنية السمات ، يتم تخزين جميع الخصائص على المكدس ، حيث يتم تخزين التعداد على المكدس. هذا يعني أنه لا يوجد استخدام ضمني لـ Heap هنا ، والآن يتم تعريف مفاتيح قاموس ذاكرة التخزين المؤقت بدقة شديدة ، مما زاد من أمان وضوح هذا الرمز. لقد تخلصنا أيضًا من الاستخدام الضمني للكومة.
الكود الزائف
ضع في اعتبارك قطعة صغيرة من الكود الكاذب لشرح كيفية عمل عد الارتباط:
class Point { var refCount: Int var x, y: Double func draw() {...} init(...) { ... self.refCount = 1 } } let point1 = Point(x: 0, y: 0) let point2 = point1 retain(point2)
البنية
عند العمل مع الهياكل ، لا يلزم ببساطة آلية مثل حساب المرجع:
- هيكل غير مخزنة على كومة
- هيكل - نسخ على مهمة ، وبالتالي ، لا توجد إشارات
نسخ الروابط
مرة أخرى ، يتم نسخ البنية وأي أنواع قيم أخرى في Swift عند التعيين. إذا كان الهيكل يخزن الروابط في حد ذاته ، فسيتم نسخها أيضًا:
struct Label { let text: String let font: UIFont ... init() { ... text.refCount = 1 font.refCount = 1 } } let label = Label(text: "Hi", font: font) let label2 = label retain(label2.text._storage)
مشاركة العلامة و label2 في الحالات الشائعة المستضافة على كومة الذاكرة المؤقتة:
وبالتالي ، إذا كان الهيكل يخزن الروابط في حد ذاته ، فعند نسخ هذا الهيكل ، يتضاعف عدد الروابط ، مما يؤثر سلبًا على "سهولة" البرنامج ، إن لم يكن ضروريًا.
ومرة أخرى المثال "الحقيقي":
struct Attachment { let fileUrl: URL
مشاكل هذا الهيكل هي أنه:
- 3 تخصيص كومة
- لأن سلسلة يمكن أن يكون أي سلسلة ، تتأثر الأمن والوضوح رمز.
في الوقت نفسه ، فإن uuid و mimeType هما أشياء محددة بدقة:
uuid عبارة عن سلسلة تنسيق xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
mimeType عبارة عن سلسلة تنسيق نوع / ملحق.
قرار
let uuid: UUID
في حالة mimeType ، يعمل التعداد بشكل جيد:
enum MimeType { init?(rawValue: String) { switch rawValue { case "image/jpeg": self = .jpeg case "image/png": self = .png case "image/gif": self = .gif default: return nil } } case jpeg, png, gif }
أو أفضل وأسهل:
enum MimeType: String { case jpeg = "image/jpeg" case png = "image/png" case gif = "image/gif" }
ولا تنسَ التغيير:
let mimeType: MimeType
3.1 طريقة الإرسال
- هذه خوارزمية تبحث عن رمز الطريقة الذي تم استدعاؤه
قبل الحديث عن تنفيذ هذه الآلية ، يجدر تحديد "الرسالة" و "الطريقة" الموجودة في هذا السياق:
- الرسالة هي الاسم الذي نرسله إلى الكائن. لا يزال من الممكن إرسال الحجج مع الاسم.
circle.draw(in: origin)
الرسالة هي رسم - اسم الأسلوب. الكائن المتلقي هو دائرة. الأصل هو أيضا حجة مرت.
- الطريقة هي الكود الذي سيتم إرجاعه استجابة للرسالة.
ثم Method Dispatch هي خوارزمية تحدد الطريقة التي يجب أن تعطى لرسالة معينة.
بشكل أكثر تحديدا حول طريقة الإرسال في سويفت
نظرًا لأننا يمكن أن نرث من الفصل الأصل وتجاوز أساليبه ، يجب أن يعرف Swift تمامًا أي تطبيق لهذه الطريقة يحتاج إلى استدعاء في موقف معين.
class Parent { func me() { print("parent") } } class Child: Parent { override func me() { print("child") } }
أنشئ حالتين واتصل بالطريقة:
let parent = Parent() let child = Child() parent.me()
مثال واضح وبسيط إلى حد ما. وماذا لو:
let array: [Parent] = [Child(), Child(), Parent(), Child()] array.forEach { $0.me()
هذا ليس واضحًا ويتطلب موارد وآلية معينة لتحديد التنفيذ الصحيح لطريقة me. الموارد هي المعالج وذاكرة الوصول العشوائي. الآلية هي أسلوب الإرسال.
بمعنى آخر ، Method Dispatch هي الطريقة التي يحدد بها البرنامج طريقة تنفيذ الاستدعاء.
عندما يتم استدعاء طريقة في الكود ، يجب أن يكون تنفيذها معروفًا. إذا كانت معروفة ل
في وقت التجميع ، ثم هذا هو إرسال ثابت. إذا تم تحديد التطبيق مباشرة قبل الاتصال (في وقت التشغيل ، في وقت تنفيذ التعليمات البرمجية) ، فهذا هو الديناميكي الإرسال.
3.2 طريقة الإرسال - إرسال ثابت
الأكثر مثالية ، منذ:
- يعرف المترجم أي كتلة من التعليمات البرمجية (تنفيذ الأسلوب) سيتم استدعاؤها. بفضل هذا ، يمكنه تحسين هذا الرمز قدر الإمكان واللجوء إلى مثل هذه الآلية المضمنة.
- أيضا ، في وقت تنفيذ التعليمات البرمجية ، سيقوم البرنامج ببساطة بتنفيذ هذه الكتلة من التعليمات البرمجية المعروفة للمترجم. لن يتم إنفاق أي موارد ووقت على تحديد التنفيذ الصحيح للطريقة ، مما سيسرع في تنفيذ البرنامج.
3.3 طريقة الإرسال - الإرسال الديناميكي
ليست الأكثر مثالية ، منذ:
- سيتم تحديد التنفيذ الصحيح للطريقة في وقت تنفيذ البرنامج ، الأمر الذي يتطلب موارد ووقتًا
- لا تحسينات مترجم غير وارد
3.4 طريقة إرسال - مضمنة
تم ذكر آلية مثل تضمين ، ولكن ما هو؟ النظر في مثال:
struct Point { var x, y: Double func draw() {
- ستتم معالجة طريقة point.draw () ووظيفة drawAPoint من خلال إرسال ثابت ، حيث لا توجد صعوبة في تحديد التنفيذ الصحيح للمترجم (حيث لا يوجد أي وراثة وإعادة التعريف مستحيل)
- نظرًا لأن المترجم يعرف ما سيتم القيام به ، فيمكنه تحسين ذلك. يقوم أولاً بتحسين drawAPoint ، ببساطة استبدال استدعاء الوظيفة برمزها:
let point = Point(x: 0, y: 0) point.draw()
- ثم يحسن point.draw ، حيث أن تنفيذ هذه الطريقة معروف أيضًا:
let point = Point(x: 0, y: 0)
لقد أنشأنا نقطة ونفذنا رمز طريقة السحب - قام المترجم ببساطة باستبدال الكود الضروري لهذه الوظائف بدلاً من استدعاءها. في Dynamic Dispatch ، سيكون هذا أكثر تعقيدًا بعض الشيء.
3.5 طريقة الإرسال - تعدد الأشكال القائم على الميراث
لماذا أحتاج إلى Dynamic Dispatch؟ بدونها ، من المستحيل تحديد الأساليب التي تجاوزتها الطبقات الفرعية. تعدد الأشكال لن يكون ممكنا. النظر في مثال:
class Drawable { func draw() {} } class Point: Drawable { var x, y: Double override func draw() { ... } } class Line: Drawable { var x1, y1, x2, y2: Double override func draw() { ... } } var drawables: [Drawable] for d in drawables { d.draw() }
- يمكن أن تحتوي مجموعة drawables على نقطة وخط
- حدسي ، إرسال ثابت غير ممكن هنا. د في حلقة يمكن أن يكون الخط ، أو ربما نقطة. لا يمكن للمترجم تحديد ذلك ، ولكل نوع تطبيقه الخاص للسحب
ثم كيف يعمل الإرسال الديناميكي؟ كل كائن لديه حقل الكتابة. So Point (...). النوع سيكون مساويًا لـ Point و Line (...). سيكون النوع مساويًا لـ Line. يوجد أيضًا في مكان ما في الذاكرة (الثابتة) للبرنامج جدول (جدول افتراضي) ، حيث يوجد لكل نوع قائمة بها تطبيقات طريقة.
في Objective-C ، يُعرف حقل الكتابة بحقل عيسى. إنه موجود على كل كائن Object-C (NSObject).
يتم تخزين طريقة الفصل في الجدول الظاهري وليس لديه أي فكرة عن النفس. من أجل استخدام الذات داخل هذه الطريقة ، يجب أن يتم تمريرها هناك (ذاتي).
وبالتالي ، سيقوم المترجم بتغيير هذا الرمز إلى:
class Point: Drawable { ... override func draw(_ self: Point) { ... } } class Line: Drawable { ... override func draw(_ self: Line) { ... } } var drawables: [Drawable] for d in drawables { vtable[d.type].draw(d) }
في وقت تنفيذ التعليمات البرمجية ، تحتاج إلى النظر إلى الجدول الظاهري ، والعثور على الفئة d هناك ، وتأخذ طريقة السحب من القائمة الناتجة وتمريرها ككائن من النوع d بذاته. هذا عمل لائق لاستدعاء طريقة بسيطة ، ولكن من الضروري التأكد من أن تعدد الأشكال يعمل. وتستخدم آليات مماثلة في أي لغة OOP.
طريقة إرسال - ملخص
- تتم معالجة أساليب الفئة بشكل افتراضي من خلال الإرسال الديناميكي. ولكن ليست كل طرق الفصل بحاجة إلى التعامل معها من خلال إرسال ديناميكي. إذا لم يتم تجاوز الطريقة ، فيمكنك توجيهها باستخدام الكلمة الأساسية النهائية ، ومن ثم يعرف المترجم أنه لا يمكن تجاوز هذه الطريقة وأنها ستقوم بمعالجتها من خلال إرسال ثابت.
- لا يمكن تجاوز الأساليب غير الصفية (نظرًا لأن البنية والتعداد لا يدعمان الوراثة) وتتم معالجتها من خلال إرسال ثابت
مشاكل OOP - ملخص
من الضروري الانتباه إلى تفاهات مثل:
- عند إنشاء مثيل: أين سيكون موقعه؟
- عند العمل مع هذه الحالة: كيف سيتم ربط حساب العمل؟
- عند استدعاء طريقة: كيف سيتم معالجتها؟
إذا دفعنا ثمن الديناميكية دون إدراكها ودون الحاجة إليها ، فسيؤثر ذلك سلبًا على البرنامج الجاري تنفيذه.
تعدد الأشكال هو شيء مهم جدا ومفيد. في الوقت الحالي ، كل ما هو معروف هو أن تعدد الأشكال في Swift يرتبط مباشرة بالفئات وأنواع المراجع. نحن بدورنا نقول أن الفصول بطيئة وثقيلة ، والبنية بسيطة وسهلة. هل يتحقق تعدد الأشكال من خلال الهياكل الممكنة؟ يمكن للبرمجة الموجهة للبروتوكول أن تقدم إجابة على هذا السؤال.