في متابعة للموضوع ، سوف ندرس أنواع البروتوكول والرمز المعمم.
سيتم النظر في القضايا التالية على طول الطريق:
- تنفيذ تعدد الأشكال دون الميراث وأنواع المراجع
- كيف يتم تخزين كائنات نوع البروتوكول واستخدامها
- كيف يعمل أسلوب الإرسال معهم
أنواع البروتوكول
تنفيذ تعدد الأشكال دون الميراث وأنواع المراجع:
protocol Drawable { func draw() } struct Point: Drawable { var x, y: Int func draw() { ... } } struct Line: Drawable { var x1, x2, y1, y2: Int func draw() { ... } } var drawbles = [Drawable]() for d in drawbles { d.draw() }
- تشير إلى بروتوكول Drawable ، الذي يحتوي على طريقة رسم.
- نحن نطبق هذا البروتوكول على Point and Line - الآن يمكنك التعامل معهم كما هو الحال مع Drawable (استدعاء طريقة السحب)
لا يزال لدينا رمز متعدد الأشكال. يحتوي عنصر d الخاص بمصفوفة drawables على واجهة واحدة ، والتي يشار إليها في البروتوكول Drawable ، ولكن لها تطبيقات مختلفة لطرقها ، والتي يشار إليها في Line and Point.
المبدأ الرئيسي (المخصص) لتعدد الأشكال: "واجهة مشتركة - العديد من التطبيقات"
إيفاد ديناميكي دون الجدول الظاهري
تذكر أن تعريف التنفيذ الصحيح للأسلوب عند العمل مع الفئات (أنواع المراجع) يتحقق من خلال التقديم الديناميكي والجدول الافتراضي. يحتوي كل نوع من الفصول على جدول افتراضي ، ويقوم بتخزين تطبيقات أساليبه. يحدد الإرسال الديناميكي تنفيذ الطريقة لنوع ما من خلال النظر في الجدول الافتراضي الخاص به. كل هذا ضروري بسبب إمكانية الميراث وإلغاء الأساليب.
في حالة الهياكل ، فإن الميراث ، وكذلك إعادة تعريف الأساليب ، أمر مستحيل. ثم ، للوهلة الأولى ، ليست هناك حاجة لجدول افتراضي ، ولكن كيف إذن سيعمل الإرسال الديناميكي؟ كيف يمكن لبرنامج ما فهم الطريقة التي سيتم استدعاؤها على d.draw ()؟
تجدر الإشارة إلى أن عدد تطبيقات هذه الطريقة يساوي عدد الأنواع التي تتوافق مع بروتوكول Drawable.
جدول شاهد البروتوكول
هو الجواب على هذا السؤال. كل نوع يقوم بتنفيذ البروتوكول له هذا الجدول. مثل الجدول الافتراضي للفئات ، فإنه يخزن تطبيقات الطرق التي يتطلبها البروتوكول.
فيما يلي ، سيطلق على جدول شهود البروتوكول "جدول طريقة البروتوكول"
حسنًا ، نحن نعرف الآن مكان البحث عن تطبيقات الطريقة. يبقى سؤالان فقط:
- كيفية العثور على جدول طريقة البروتوكول المناسب لكائن قام بتطبيق هذا البروتوكول؟ كيف في حالتنا للعثور على هذا الجدول لعنصر من مجموعة drawables؟
- يجب أن تكون عناصر المصفوفة بنفس الحجم (هذا هو جوهر المصفوفة). ثم كيف يمكن للصفيف القابل للرسوم تلبية هذا المطلب إذا كان يمكنه تخزين كل من الخط والنقطة فيهما ، ولهما أحجام مختلفة؟
MemoryLayout.size(ofValue: Line(...))
حاوية موجودة
لمعالجة هاتين المسألتين ، يستخدم Swift نظام تخزين خاصًا لمثيلات أنواع البروتوكولات التي تسمى حاوية وجودية. يبدو مثل هذا:

يستغرق 5 كلمات آلة (في نظام x64 بت 5 * 8 = 40 بت). وهي مقسمة إلى ثلاثة أجزاء:
قيمة المخزن المؤقت - مساحة للمثيل نفسه
vwt - المؤشر إلى قيمة الجدول الشهود
pwt - مؤشر إلى جدول شاهد البروتوكول
النظر في الأجزاء الثلاثة بمزيد من التفصيل:
محتوى المخزن المؤقت
فقط ثلاث كلمات آلة لتخزين مثيل. إذا كان يمكن احتواء المثيل في المخزن المؤقت للمحتوى ، فسيتم تخزينه فيه. إذا كان المثيل يحتوي على أكثر من 3 كلمات آلية ، فلن يتم احتواؤه في المخزن المؤقت ويتم إجبار البرنامج على تخصيص ذاكرة على الكومة ووضع المثيل هناك ووضع مؤشر على هذه الذاكرة في المخزن المؤقت للمحتويات. النظر في مثال:
let point: Drawable = Point(...)
يحتل Point () كلمتين آليتين وينسجم تمامًا في المخزن المؤقت للقيمة - سيقوم البرنامج بوضعه هناك:

let line: Drawable = Line(...)
يشغل Line () 4 كلمات من الجهاز ولا يمكن احتوائه في مخزن مؤقت للقيمة - سيقوم البرنامج بتخصيص الذاكرة له من أجل كومة الذاكرة المؤقتة ، وإضافة مؤشر إلى هذه الذاكرة في مخزن القيمة:

يشير ptr إلى مثيل Line () الموصول على الكومة:

جدول دورة الحياة
بالإضافة إلى جدول أسلوب البروتوكول ، يحتوي كل جدول على البروتوكول على هذا الجدول. أنه يحتوي على تنفيذ أربعة أساليب: تخصيص ، نسخ ، تدمير ، إلغاء تخصيص. تتحكم هذه الطرق في دورة حياة الكائن بالكامل. النظر في مثال:
- عند إنشاء كائن (Point (...) كـ Drawable) ، فإن طريقة التخصيص من T.Zh. هذا الكائن. تحدد طريقة التخصيص المكان الذي ينبغي أن توضع فيه محتويات الكائن (في المخزن المؤقت للقيمة أو على الكومة) ، وإذا كان ينبغي وضعها على الكومة ، فسوف تخصص الكمية المطلوبة من الذاكرة
- ستضع طريقة النسخ محتويات الكائن في المكان المناسب.
- بعد الانتهاء من العمل مع الكائن ، سيتم استدعاء طريقة التدمير ، مما يقلل من جميع تعدادات الارتباط ، إن وجدت
- بعد التدمير ، سيتم استدعاء طريقة إلغاء تخصيص ، والتي ستحرر الذاكرة المخصصة على كومة الذاكرة المؤقتة ، إن وجدت
جدول طريقة البروتوكول
كما هو موضح أعلاه ، فإنه يحتوي على تطبيقات الطرق التي يتطلبها البروتوكول للنوع الذي يرتبط به هذا الجدول.
حاوية موجودة - إجابات
وبالتالي ، أجبنا على سؤالين مطروحين:
- يتم تخزين جدول بروتوكول الأسلوب في حاوية Existential من هذا الكائن ويمكن الحصول عليها بسهولة
- إذا كان نوع عنصر الصفيف عبارة عن بروتوكول ، فإن أي عنصر من هذه الصفيف يأخذ قيمة ثابتة من 5 كلمات آلية - وهذا هو بالضبط ما هو مطلوب لحاوية Existential. إذا كانت محتويات العنصر لا يمكن وضعها في مخزن القيمة ، فسيتم وضعها على الكومة. إذا كان ذلك ممكنًا ، فسيتم وضع كل المحتوى في مخزن القيمة. في أي حال ، نحصل على أن حجم الكائن بنوع البروتوكول هو 5 كلمات آلية (40 بت) ، ويترتب على ذلك أن جميع عناصر الصفيف سيكون لها نفس الحجم.
let line: Drawable = Line(...) MemoryLayout.size(ofValue: line)
حاوية موجودة - مثال
خذ بعين الاعتبار سلوك حاوية وجودية في هذا الرمز:
func drawACopy(local: Drawable) { local.draw() } let val: Drawable = Line(...) drawACopy(val)
يمكن تمثيل حاوية وجودية مثل هذا:
struct ExistContDrawable { var valueBuffer: (Int, Int, Int) var vwt: ValueWitnessTable var pwt: ProtocolWitnessTable }
الكود الزائف
وراء الكواليس ، تأخذ وظيفة drawACopy في ExistContDrawable:
func drawACopy(val: ExistContDrawable) { ... }
يتم إنشاء معامل الوظيفة يدويًا: إنشاء حاوية ، وملء حقولها من الوسيطة المستلمة:
func drawACopy(val: ExistContDrawable) { var local = ExistContDrawable() let vwt = val.vwt let pwt = val.pwt local.type = type local.pwt = pwt ... }
نقرر أين سيتم تخزين المحتوى (في المخزن المؤقت أو كومة الذاكرة المؤقتة). ندعو vwt.allocate و vwt.copy لملء المحتويات المحلية بـ val:
func drawACopy(val: ExistContDrawable) { ... vwt.allocateBufferAndCopy(&local, val) }
نحن نسمي طريقة السحب ونمررها مؤشرًا على الذات (ستقرر طريقة projectBuffer مكان وجود الذات - في المخزن المؤقت أو على الكومة - وإرجاع المؤشر الصحيح):
func drawACopy(val: ExistContDrawable) { ... pwt.draw(vwt.projectBuffer(&local)) }
ننتهي من العمل مع المحلية. نحن تنظيف جميع الروابط الورك من المحلية. تُرجع الدالة قيمة - نقوم بمسح جميع الذاكرة المخصصة لـ drawACopy (إطار مكدس):
func drawACopy(val: ExistContDrawable) { ... vwt.destructAndDeallocateBuffer(&local) }
حاوية موجودة - الغرض
يتطلب استخدام حاوية وجودية الكثير من العمل - المثال أعلاه أكد هذا - ولكن لماذا هو ضروري ، فما الغرض؟ الهدف هو تنفيذ تعدد الأشكال باستخدام البروتوكولات والأنواع التي تنفذها. في OOP ، نستخدم الطبقات المجردة ونرثها عن طريق تجاوز الطرق. في EPP ، نستخدم البروتوكولات وننفذ متطلباتها. مرة أخرى ، حتى مع البروتوكولات ، يعد تنفيذ تعدد الأشكال مهمة كبيرة تستهلك الطاقة. لذلك ، لتجنب العمل "غير الضروري" ، يجب أن تفهم متى تكون الحاجة إلى تعدد الأشكال ، وعندما لا تكون كذلك.
تعدد الأشكال في تنفيذ EPP يفوز في حقيقة أنه باستخدام الهياكل ، لا نحتاج إلى حساب مرجع ثابت ، لا يوجد وراثة فئة. نعم ، كل شيء مشابه جدًا ، تستخدم الفئات جدولًا افتراضيًا لتحديد تنفيذ طريقة ، تستخدم البروتوكولات طريقة بروتوكول. يتم وضع الفصول الدراسية على الكومة ، ويمكن أيضًا وضع الهياكل في بعض الأحيان. ولكن المشكلة تكمن في أنه يمكن توجيه أكبر عدد ممكن من المؤشرات إلى الفئة الموضوعة على الكومة ، ويكون حساب المرجع ضروريًا ، ومؤشر واحد فقط إلى الهياكل الموضوعة على الكومة ويتم تخزينه في حاوية وجودية.
في الحقيقة ، من المهم ملاحظة أن البنية المخزنة في حاوية وجودية ستحتفظ بدلالات أنواع القيم ، بغض النظر عما إذا كان يتم وضعها على المكدس أم الكومة. جدول دورة الحياة هو المسؤول عن الحفاظ على دلالات لأنه يصف الطرق التي تحدد دلالات.
حاوية موجودة - خصائص مخزنة
درسنا كيف يتم تمرير متغير نوع البروتوكول واستخدامه بواسطة دالة. دعنا نفكر في كيفية تخزين هذه المتغيرات:
struct Pair { init(_ f: Drawable, _ s: Drawable) { first = f second = s } var first: Drawable var second: Drawable } var pair = Pair(Line(), Point())
كيف يتم تخزين هذين الهيكلين Drawable داخل بنية الزوج؟ ما هي محتويات الزوج؟ وهو يتكون من حاويتين وجوديتين - واحدة للأولى والثانية للأخرى. لا يمكن احتواء الخط في المخزن المؤقت ويتم وضعه على الكومة. نقطة تناسب في المخزن المؤقت. كما يسمح لبنية الزوج بتخزين الكائنات ذات الأحجام المختلفة:
pair.second = Line()
الآن ، يتم وضع محتويات الثانية أيضًا على كومة الذاكرة المؤقتة ، نظرًا لأنها لا تلائم المخزن المؤقت. النظر في ما قد يؤدي هذا إلى:
let aLine = Line(...) let pair = Pair(aLine, aLine) let copy = pair
بعد تنفيذ هذا الرمز ، سيتلقى البرنامج حالة الذاكرة التالية:

لدينا 4 تخصيصات للذاكرة على الكومة ، وهي ليست جيدة. دعنا نحاول إصلاح:
- إنشاء خط الطبقة التناظرية
class LineStorage: Drawable { var x1, y1, x2, y2: Double func draw() {} }
- نستخدمها في زوج
let lineStorage = LineStorage(...) let pair = Pair(lineStorage, lineStorage) let copy = pair
نحصل على موضع واحد على الكومة و 4 مؤشرات عليها:

لكننا نتعامل مع السلوك المرجعي. سيؤثر تغيير copy.first على pair.first (الشيء نفسه بالنسبة لـ .second) ، وهو ما لا نريده دائمًا.
التخزين غير المباشر والنسخ عند التغيير (نسخ بالكتابة)
قبل ذلك ، تم ذكر أن String عبارة عن بنية نسخ عند الكتابة (تخزن محتوياتها على الكومة وتنسخها عندما تتغير). ضع في اعتبارك كيف يمكنك تنفيذ هيكلك ، والذي يتم نسخه عند التغيير:
struct BetterLine: Drawable { private var storage: LineStorage init() { storage = LineStorage((0, 0), (10, 10)) } func draw() -> Double { ... } mutating func move() { if !isKnownUniquelyReferenced(&storage) { storage = LineStorage(self.storage) }
- تقوم BetterLine بتخزين جميع الخصائص في التخزين ، والتخزين فئة ويتم تخزينه على الكومة.
- يمكن تغيير التخزين فقط باستخدام طريقة النقل. في ذلك ، نتحقق من أن مؤشر واحد فقط يشير إلى التخزين. إذا كان هناك المزيد من المؤشرات ، فإن BetterLine تقوم بمشاركة التخزين مع شخص ما ، ولكي تتصرف BetterLine بشكل كامل كهيكل ، يجب أن تكون وحدة التخزين فردية - نصنع نسخة ونعمل معها في المستقبل.
دعونا نرى كيف يعمل في الذاكرة:
let aLine = BetterLine() let pair = Pair(aLine, aLine) let copy = pair copy.second.x1 = 3.0
نتيجة لتنفيذ هذا الرمز ، نحصل على:

بمعنى آخر ، لدينا حالتان من الزوج تشتركان في نفس وحدة التخزين: LineStorage. عند تغيير التخزين في أحد مستخدميه (الأول / الثاني) ، سيتم إنشاء نسخة منفصلة من التخزين لهذا المستخدم بحيث لا يؤثر تغييره على الآخرين. هذا يحل مشكلة انتهاك دلالات أنواع القيمة من المثال السابق.
أنواع البروتوكول - ملخص
- القيم الصغيرة . إذا عملنا مع كائنات تستهلك القليل من الذاكرة ويمكن وضعها في المخزن المؤقت لحاوية وجودية ، فقم بما يلي:
- لن يكون هناك موضع على الكومة
- لا مرجع العد
- تعدد الأشكال (الإرسال الديناميكي) باستخدام جدول البروتوكول
- قيمة كبيرة. إذا عملنا مع كائنات لا تتلاءم مع المخزن المؤقت ، فقم بما يلي:
- التنسيب كومة
- مرجع الحساب إذا كانت الكائنات تحتوي على روابط.
لقد تم توضيح آليات استخدام إعادة الكتابة من أجل التغيير والتخزين غير المباشر ويمكن أن تحسن الوضع بشكل كبير مع حساب المرجع في حالة وجود عدد كبير منها.
لقد وجدنا أن أنواع البروتوكول ، مثل الفئات ، قادرة على تحقيق تعدد الأشكال. يحدث هذا عن طريق تخزين في حاوية وجود واستخدام جداول البروتوكول - جداول دورة الحياة وجداول طريقة البروتوكول.