نواصل اليوم سلسلة المنشورات حول موضوع تطوير الهاتف المحمول لنظام iOS. وإذا كانت المرة الأخيرة التي تحدثنا فيها عن ما تحتاجه وما لا تحتاج إلى طرحه في المقابلات ، فسوف نتطرق في هذه المقالة إلى موضوع البروتوكولات ، وهو أمر مهم في Swift. سيكون حول كيفية ترتيب البروتوكولات ، وكيف تختلف عن بعضها البعض ، وكيف تتحد مع واجهات Objective-C.

كما قلنا سابقًا ، تستمر لغة Apple الجديدة في التطور ، ويتم الإشارة إلى معظم معلماتها وميزاتها بوضوح في الوثائق. ولكن من يقرأ الوثائق عندما يحتاج الرمز إلى أن يكتب هنا والآن؟ لذا دعنا نستعرض الميزات الرئيسية لبروتوكولات Swift مباشرة في منشورنا.
بادئ ذي بدء ، تجدر الإشارة إلى أن بروتوكولات Apple هي مصطلح بديل لمفهوم "الواجهة" ، والذي يستخدم في لغات البرمجة الأخرى. في Swift ، يتم استخدام البروتوكولات للإشارة إلى أنماط بعض الهياكل (ما يسمى المخطط) التي يمكن العمل عليها على مستوى مجردة. بكلمات بسيطة ، يحدد البروتوكول عددًا من الأساليب والمتغيرات التي يجب أن يرثها نوع معين دون فشل.
في وقت لاحق من المقالة ، سيتم الكشف عن اللحظات تدريجيًا على النحو التالي: من البساطة والتي غالبًا ما يتم استخدامها إلى أكثر تعقيدًا. من حيث المبدأ ، في المقابلات ، يمكنك طرح أسئلة بهذا الترتيب ، لأنها تحدد مستوى كفاءة مقدم الطلب - من مستوى الصغار إلى مستوى كبار السن.
ما هي البروتوكولات المطلوبة في Swift؟
غالبًا ما يفعل مطورو الجوّال دون استخدام البروتوكولات على الإطلاق ، لكنهم يفقدون القدرة على العمل مع بعض الكيانات بشكل مجرد. إذا قمنا بتمييز الميزات الرئيسية للبروتوكولات في Swift ، فسوف نحصل على النقاط السبع التالية:
- توفر البروتوكولات الوراثة المتعددة
- لا يمكن للبروتوكولات تخزين الحالة
- قد يتم توريث البروتوكولات بواسطة بروتوكولات أخرى.
- يمكن تطبيق البروتوكولات على الهياكل (البنية) والفئات (الفئة) والتعدادات (التعداد) ، وتحديد وظيفة النوع
- تتيح لك البروتوكولات العامة تحديد التبعيات المعقدة بين الأنواع والبروتوكولات أثناء وراثتها
- لا تحدد البروتوكولات المراجع المتغيرة "القوية" أو "الضعيفة"
- في ملحقات البروتوكولات ، يمكن وصف عمليات تنفيذ محددة للأساليب والقيم المحسوبة
- تسمح بروتوكولات الفصل للفئات بالوراثة فقط
كما تعلم ، فإن جميع الأنواع البسيطة (سلسلة ، int) في Swift هي هياكل. في مكتبة Swift القياسية ، يبدو هذا ، على سبيل المثال ، كما يلي:
public struct Int: FixedWidthInteger, SignedInteger {
في الوقت نفسه ، يمكن أيضًا تجميع أنواع المجموعات (المجموعة) ، أي المصفوفة ، المجموعة ، القاموس ، في البروتوكول ، لأنها أيضًا هياكل. على سبيل المثال ، يتم تعريف القاموس على النحو التالي
public struct Dictionary<Key, Value> where Key: Hashable {
عادة في البرمجة الموجهة للكائنات ، يتم استخدام مفهوم الفئات ، والجميع يعرف آلية وراثة الأساليب والمتغيرات في الطبقة الأم من الفئة السلالة. في الوقت نفسه ، لم يمنعه أحد من احتواء طرق ومتغيرات إضافية.
في حالة البروتوكولات ، يمكنك إنشاء تسلسل هرمي أكثر إثارة للاهتمام للعلاقات. لوصف الصف التالي ، يمكنك استخدام العديد من البروتوكولات في نفس الوقت ، مما يسمح لك بإنشاء تصميمات معقدة إلى حد ما ترضي العديد من الشروط في نفس الوقت. من ناحية أخرى ، فإن القيود المفروضة على البروتوكولات المختلفة تجعل من الممكن تشكيل كائن ما مخصص فقط للتطبيق الضيق ويحتوي على عدد معين من الوظائف.
إن تنفيذ البروتوكول في Swift بسيط للغاية. يتضمن بناء الجملة اسمًا ، وعددًا من الأساليب ، والمعلمات (المتغيرات) التي ستحتوي عليها.
protocol Employee { func work() var hours: Int { get } }
بالإضافة إلى ذلك ، في Swift ، يمكن أن تحتوي البروتوكولات ليس فقط على أسماء الطرق ، ولكن أيضًا على تنفيذها. يضاف رمز الطريقة في البروتوكول من خلال ملحقات. في الوثائق ، يمكنك العثور على العديد من الإشارات إلى الامتدادات ، ولكن فيما يتعلق بالبروتوكولات في الامتدادات ، يمكنك وضع اسم الوظيفة ونصها.
extension Employee { func work() { print ("do my job") } }
يمكنك أن تفعل الشيء نفسه مع المتغيرات.
extension Employee { var hours: Int { return 8 } }
إذا استخدمنا الكائن المرتبط بالبروتوكول في مكان ما ، يمكننا تعيين متغير بقيمة ثابتة أو مرسلة فيه. في الواقع ، المتغير هو وظيفة صغيرة بدون معلمات إدخال ... أو مع إمكانية تعيين معلمة مباشرة.
يسمح لك تمديد البروتوكول في Swift بتنفيذ جسم المتغير ، ثم في الواقع سيكون قيمة محسوبة - معلمة محسوبة مع وظائف get و set. أي أن هذا المتغير لن يخزن أي قيم ، ولكنه سيلعب دور وظيفة أو وظائف ، أو سيلعب دور الوكيل لبعض المتغيرات الأخرى.
أو إذا أخذنا فئة أو هيكل ونفذنا البروتوكول ، فيمكننا استخدام المتغير المعتاد فيه:
class Programmer { var hours: Int = 24 } extension Programmer: Employee { }
تجدر الإشارة إلى أن المتغيرات في تعريف البروتوكول لا يمكن أن تكون ضعيفة. (ضعيف تنفيذ التباين).هناك أمثلة أكثر إثارة للاهتمام: يمكنك تنفيذ امتداد الصفيف وإضافة دالة هناك تتعلق بنوع بيانات الصفيف. على سبيل المثال ، إذا كان الصفيف يحتوي على قيم صحيحة أو لديه تنسيق عادل (مناسب للمقارنة) ، يمكن للدالة ، على سبيل المثال ، مقارنة جميع قيم خلايا الصفيف.
extension Array where Element: Equatable { var areAllElementsEqualToEachOther: Bool { if isEmpty { return false } var previousElement = self[0] for (index, element) in self.enumerated() where index > 0 { if element != previousElement { return false } previousElement = element } return true } } [1,1,1,1].areAllElementsEqualToEachOther
ملاحظة صغيرة. يمكن أن تكون المتغيرات والوظائف في البروتوكولات ثابتة.
باستخدام @
objc
الشيء الرئيسي الذي تحتاج إلى معرفته في هذا الأمر هو أن بروتوكولات
@
objc Swift مرئية في شفرة Objective-C. بالمعنى الدقيق للكلمة ، فإن "الكلمة السحرية"
@
objc موجودة. لكن كل شيء آخر لم يتغير
@objc protocol Typable { @objc optional func test() func type() } extension Typable { func test() { print("Extension test") } func type() { print("Extension type") } } class Typewriter: Typable { func test() { print("test") } func type() { print("type") } }
لا يمكن توريث البروتوكولات من هذا النوع إلا بواسطة الفئات. بالنسبة للقوائم والهياكل ، لا يمكن القيام بذلك.
هذه هي الطريقة الوحيدة.
@objc protocol Dummy { } class DummyClass: Dummy { }
من الجدير بالذكر أنه في هذه الحالة يصبح من الممكن تحديد الوظائف الاختيارية (obj اختياري func) ، والتي ، إذا رغبت في ذلك ، قد لا يتم تنفيذها ، كما هو الحال مع وظيفة الاختبار () في المثال السابق. ولكن يمكن أيضًا تنفيذ الوظائف الاختيارية المشروطة بتوسيع البروتوكول بتطبيق فارغ.
protocol Dummy { func ohPlease() } extension Dummy { func ohPlease() { } }
اكتب الميراث
من خلال إنشاء فئة أو بنية أو تعداد ، يمكننا أن نشير إلى وراثة بروتوكول معين - في هذه الحالة ، ستعمل المعلمات الموروثة لفئتنا ، وجميع الفئات الأخرى التي ترث هذه الفئة ، حتى إذا لم يكن لدينا الوصول إليها.
بالمناسبة ، في هذا السياق تظهر مشكلة واحدة مثيرة للاهتمام للغاية. لنفترض أن لدينا بروتوكولًا. هناك بعض الصف. والفئة تنفذ البروتوكول ، ولها وظيفة work (). ماذا يحدث إذا كان لدينا امتداد للبروتوكول ، والذي يحتوي أيضًا على طريقة work (). أيهما سيتم استدعاؤه عندما يتم استدعاء الطريقة؟
protocol Person { func work() } extension Person { func work() { print("Person") } } class Employee { } extension Employee: Person { func work() { print("Employee") } }
سيتم إطلاق طريقة الفصل - هذه هي ميزات طرق الإرسال في Swift. ويتم تقديم هذه الإجابة من قبل العديد من المتقدمين. ولكن فيما يتعلق بالسؤال عن كيفية التأكد من أن الشفرة لا تحتوي على طريقة فئة ، ولكن طريقة بروتوكول ، لا يعرف سوى عدد قليل الإجابة. ومع ذلك ، هناك حل لهذه المهمة أيضًا - فهو يتضمن إزالة الوظيفة من تعريف البروتوكول واستدعاء الطريقة على النحو التالي:
protocol Person { // func work() // } extension Person { func work() { print("Person") } } class Employee { } extension Employee: Person { func work() { print("Employee") } } let person: Person = Employee() person.work() //output: Person
البروتوكولات العامة
يحتوي Swift أيضًا على بروتوكولات عامة مع الأنواع المرتبطة التي تسمح لك بتحديد متغيرات النوع. يمكن تعيين مثل هذا البروتوكول شروط إضافية تفرض على الأنواع النقابية. تسمح لك العديد من هذه البروتوكولات ببناء هياكل معقدة ضرورية لتشكيل بنية التطبيق.
ومع ذلك ، لا يمكنك تنفيذ متغير كبروتوكول عام. يمكن أن تكون موروثة فقط. يتم استخدام هذه التركيبات لإنشاء تبعيات في الفئات. أي أنه يمكننا وصف بعض الفئات العامة المجردة لتحديد الأنواع المستخدمة فيها.
protocol Printer { associatedtype PrintableClass: Hashable func printSome(printable: PrintableClass) } extension Printer { func printSome(printable: PrintableClass) { print(printable.hashValue) } } class TheIntPrinter: Printer { typealias PrintableClass = Int } let intPrinter = TheIntPrinter() intPrinter.printSome(printable: 0) let intPrinterError: Printer = TheIntPrinter()
يجب أن نتذكر أن البروتوكولات العامة تتمتع بمستوى عالٍ من التجريد. لذلك ، في التطبيقات نفسها ، قد تكون زائدة عن الحاجة. ولكن في الوقت نفسه ، يتم استخدام البروتوكولات العامة عند برمجة المكتبات.
بروتوكولات الفصل
يحتوي Swift أيضًا على بروتوكولات مرتبطة بالفئة. يتم استخدام نوعين من بناء الجملة لوصفهما.
protocol Employee: AnyObject { }
أو
protocol Employee: class { }
وفقًا لمطوري اللغة ، فإن استخدام هذه التركيبات متكافئ ، ولكن الكلمة الأساسية للفئة تُستخدم فقط في هذا المكان ، على عكس AnyObject ، وهو بروتوكول.
في هذه الأثناء ، كما نرى خلال المقابلات ، لا يستطيع الناس غالبًا تفسير بروتوكول الفصل والسبب في الحاجة إليه. يكمن جوهرها في حقيقة أن لدينا الفرصة لاستخدام بعض الأشياء ، والتي ستكون بروتوكولًا ، وفي نفس الوقت ستعمل كنوع مرجعي. مثال:
protocol Handler: class {} class Presenter: Handler { weak var renderer: Renderer? } protocol Renderer {} class View: Renderer { }
ما هو الملح؟
يستخدم IOS إدارة الذاكرة باستخدام طريقة العد المرجعي التلقائي ، مما يعني وجود روابط قوية وضعيفة. وفي بعض الحالات ، يجب مراعاة المتغيرات - القوية (القوية) أو الضعيفة (الضعيفة) - المستخدمة في الصفوف.
تكمن المشكلة في أنه عند استخدام بعض البروتوكولات كنوع ، عند وصف متغير (وهو ارتباط قوي) ، قد تحدث دورة الاحتفاظ ، مما يؤدي إلى تسرب الذاكرة ، لأن الكائنات ستحتفظ في كل مكان بروابط قوية. أيضا ، يمكن أن تنشأ مشاكل إذا كنت لا تزال تقرر كتابة التعليمات البرمجية وفقًا لمبادئ SOLID.
protocol Handler {} class Presenter: Handler { var renderer: Renderer? } protocol Renderer {} class View: Renderer { var handler: Handler? }
لتجنب مثل هذه المواقف ، يستخدم Swift بروتوكولات فئة تسمح لك في البداية بتعيين المتغيرات "الضعيفة". يسمح لك بروتوكول الفصل بإبقاء الكائن مرجعًا ضعيفًا. ومن الأمثلة على ذلك التي غالبًا ما يستحق هذا التفكير فيها مندوب.
protocol TableDelegate: class {} class Table { weak var tableDelegate: TableDelegate? }
مثال آخر حيث يجب استخدام بروتوكولات الفئة هو إشارة صريحة إلى أن الكائن يتم تمريره عن طريق المرجع.
الوراثة والإرسال المتعدد
كما هو مذكور في بداية المقالة ، يمكن توريث البروتوكولات عدة مرات. هذا هو ،
protocol Pet { func waitingForItsOwner() } protocol Sleeper { func sleepOnAChair() } class Kitty: Pet, Sleeper { func eat() { print("yammy") } func waitingForItsOwner() { print("looking at the door") } func sleepOnAChair() { print("dreams") } }
هذا مفيد ، ولكن ما المزالق المخفية هنا؟ الشيء هو أن الصعوبات ، على الأقل للوهلة الأولى ، تنشأ بسبب إرسال الأساليب (إرسال الطريقة). بكلمات بسيطة ، قد لا يكون من الواضح أي طريقة سيتم استدعاؤها - الأصل أو من النوع الحالي.
أعلاه فقط ، قمنا بالفعل بتغطية موضوع كيفية عمل الكود ؛ يطلق عليه طريقة الفصل. هذا ، كما هو متوقع.
protocol Pet { func waitingForItsOwner() } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty: Pet = Kitty() kitty.waitingForItsOwner()
ولكن إذا حاولت إزالة توقيع الطريقة من تعريف البروتوكول ، فإن "السحر" يحدث. في الواقع ، هذا سؤال من المقابلة: "كيف يتم استدعاء وظيفة من بروتوكول؟"
protocol Pet { } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty: Pet = Kitty() kitty.waitingForItsOwner()
ولكن إذا كنت تستخدم المتغير ليس كبروتوكول ، ولكن كفئة ، فسيكون كل شيء على ما يرام.
protocol Pet { } extension Pet { func waitingForItsOwner() { print("Pet is looking at the door") } } class Kitty: Pet { func waitingForItsOwner() { print("Kitty is looking at the door") } } let kitty = Kitty() kitty.waitingForItsOwner()
الأمر كله يتعلق بأساليب الإرسال الثابتة عند توسيع البروتوكول. ويجب أن يؤخذ هذا بعين الاعتبار. وهنا تعدد الميراث؟ ولكن مع هذا: إذا كنت تأخذ بروتوكولين بوظائف منفذة ، فلن يعمل هذا الرمز. لكي يتم تنفيذ الوظيفة ، ستحتاج إلى الإرسال بشكل صريح إلى البروتوكول المطلوب. هذا هو صدى الميراث المتعدد من C ++.
protocol Pet { func waitingForItsOwner() } extension Pet { func yawn() { print ("Pet yawns") } } protocol Sleeper { func sleepOnAChair() } extension Sleeper { func yawn() { print ("Sleeper yawns") } } class Kitty: Pet, Sleeper { func eat() { print("yammy") } func waitingForItsOwner() { print("looking at the door") } func sleepOnAChair() { print("dreams") } } let kitty = Kitty() kitty.yawn()
ستكون قصة مماثلة إذا ورثت بروتوكولًا من آخر ، حيث توجد وظائف يتم تنفيذها في الملحقات. لن يسمح المترجم بالبناء.
protocol Pet { func waitingForItsOwner() } extension Pet { func yawn() { print ("Pet yawns") } } protocol Cat { func walk() } extension Cat { func yawn() { print ("Cat yawns") } } class Kitty:Cat { func eat() { print("yammy") } func waitingForItsOwner() { print("looking at the door") } func sleepOnAChair() { print("dreams") } } let kitty = Kitty()
يوضح المثالان الأخيران أنه لا يستحق استبدال البروتوكولات بالكامل بالفئات. يمكنك الحصول على الخلط في جدولة ثابتة.
الوراثة والبروتوكولات
يمكننا أن نقول أن هذا سؤال بعلامة النجمة ، والذي لا يحتاج إلى طرحه على الإطلاق. لكن المبرمجين يحبون الإنشاءات فائقة التجريد ، وبالطبع ، يجب أن يكون هناك فئتان عامتان غير ضروريتين في المشروع (حيث بدونه). لكن المبرمج لن يكون مبرمجًا إذا لم يكن يريد أن يختتم كل شيء في تجريد آخر. و Swift ، كونها لغة شابة ، ولكنها تتطور ديناميكيًا ، تعطي مثل هذه الفرصة ، ولكن بطريقة محدودة. (نعم ، هذا ليس عن الهواتف المحمولة).
أولاً ، الاختبار الكامل للميراث المحتمل هو فقط في Swift 4.2 ، أي أنه في الخريف فقط سيكون من الممكن استخدام هذا بشكل طبيعي في المشاريع. في Swift 4.1 ، تظهر رسالة تفيد بأن الفرصة لم يتم تنفيذها بعد.
protocol Property { } protocol PropertyConnection { } class SomeProperty { } extension SomeProperty: Property { } extension SomeProperty: PropertyConnection { } protocol ViewConfigurator { } protocol Connection { } class Configurator<T> where T: Property { var property: T init(property: T) { self.property = property } } extension Configurator: ViewConfigurator { } extension Configurator: Connection where T: PropertyConnection { } [Configurator(property: SomeProperty()) as ViewConfigurator] .forEach { configurator in if let connection = configurator as? Connection { print(connection) } }
بالنسبة إلى Swift 4.1 ، يتم عرض ما يلي:
warning: Swift runtime does not yet support dynamically querying conditional conformance ('__lldb_expr_1.Configurator<__lldb_expr_1.SomeProperty>': '__lldb_expr_1.Connection')
بينما يعمل Swift 4.2 على كل شيء كما هو متوقع:
__lldb_expr_5.Configurator<__lldb_expr_5.SomeProperty> connection
ومن الجدير بالذكر أيضًا أنه يمكنك وراثة البروتوكول بنوع واحد فقط من العلاقات. إذا كان هناك نوعان من الروابط ، فسيتم حظر التوريث على مستوى المترجم. يتم عرض شرح مفصل لما هو وغير ممكن
هنا .
protocol ObjectConfigurator { } protocol Property { } class FirstProperty: Property { } class SecondProperty: Property { } class Configurator<T> where T: Property { var properties: T init(properties: T) { self.properties = properties } } extension Configurator: ObjectConfigurator where T == FirstProperty { }
ولكن ، على الرغم من هذه الصعوبات ، فإن العمل مع الروابط في الأدوية العامة أمر مريح للغاية.
لتلخيص
تم تقديم البروتوكولات في Swift بشكلها الحالي لجعل التطوير أكثر هيكلية وتوفير نماذج وراثة أكثر تقدمًا من نفس الهدف-ج. لذلك ، نحن واثقون من أن استخدام البروتوكولات هو خطوة مبررة ، وتأكد من سؤال المرشحين للمطورين عما يعرفونه عن هذه العناصر من لغة Swift. في المنشورات التالية سنتطرق إلى طرق الإرسال.