ذكرنا سابقًا في الفصل تخزين لائحة من القيم باستخدام الأشعة Vectors وما بعده أن أحد قيود الشعاع vector هي تخزينه لعناصر من نوع واحد فقط، وقد أنشأنا حلًا بديلًا فيما بعد في الشيفرة 8 من الفصل الأخطاء والتعامل معها في لغة رست Rust، إذ عرّفنا التعداد SpreadsheetCell
وداخله متغايرات variants تحتوي على أعداد صحيحة integers وعشرية floats ونص text، وهذا يعني أنه يمكننا تخزين أنواع مختلفة من البيانات في كل خلية مع المحافظة على شعاع يمثل صفًا من الخلايا. يعد هذا حلًا جيدًا عندما تمثّل العناصر القابلة للتبديل مجموعةً ثابتةً من الأنواع التي نعرّفها عند تصريف الشيفرة البرمجية الخاصة بنا.
نريد أحيانًا أن يتمكن مستخدم مكتبتنا من توسيع مجموعة الأنواع الصالحة في حالة معينة، ولإظهار كيف يمكننا تحقيق ذلك سننشئ مثالًا لأداة واجهة المستخدم الرسومية graphical user interface -أو اختصارًا GUI- التي تتكرر من خلال قائمة من العناصر مستدعيةً تابع draw
على كل عنصر لرسمه على الشاشة، وهي تقنية شائعة لأدوات واجهة المستخدم الرسومية. سننشئ وحدة مكتبة مصرّفة library crate تدعى gui
تحتوي على هيكل مكتبة لأدوات واجهة المستخدم الرسومية GUI، وقد تتضمن هذه الوحدة المصرّفة بعض الأنواع ليستخدمها الأشخاص، مثل Button
، أو TextField
، كما سيرغب مستخدمو gui
بإنشاء أنواعهم الخاصة التي يمكن رسمها، فعلى سبيل المثال قد يضيف أحد المبرمجين Image
وقد يضيف آخر SelectBox
.
لن نبرمج كامل مكتبة GUI في هذا المثال لكننا سنبين كيف ستعمل الأجزاء معًا. لا يمكننا في وقت كتابة المكتبة معرفة وتعريف جميع الأنواع التي قد يرغب المبرمجون الآخرون بإنشائها، لكننا نعلم أن gui
تحتاج إلى تتبُّع العديد من القيم ومن أنواع مختلفة وتحتاج إلى استدعاء تابع draw
على كل من هذه القيم المكتوبة بصورةٍ مختلفة. لا يتطلب الأمر معرفة ماذا سيحدث فعلًا عندما نستدعي تابع draw
، ومن الكافي معرفة أن القيمة ستحتوي على تابع متاح ضمنها يمكننا استدعاؤه.
لتحقيق هذا الأمر في لغة برمجة تحتوي على خاصية التوريث قد نعرّف صنفًا يدعى Component
له تابع يسمى draw
. ترث الأصناف الأخرى، مثل Button
، و Image
، و SelectBox
من Component
، وبالتالي ترث تابع draw
. يمكن لكل من الأصناف السابقة إعادة تعريف تابع draw
لتعريف سلوكهم المخصص، لكن إطار العمل framework قد يتعامل مع جميع الأنواع كما لو كانت نُسخًا instance من Component
ويستدعي التابع draw
عليها، ولكن بما أن رست لا تحتوي على توريث، فنحن بحاجة إلى طريقة أخرى لهيكلة مكتبة gui
للسماح للمستخدمين بتوسيعها بأنواع جديدة.
تعريف سمة لسلوك مشترك
سنعرّف سمةً باسم Draw
يكون لها تابعٌ واحد يسمى draw
، لتنفيذ السلوك الذي نريد من الوحدة المصرّفة gui
أن تملكه. بعد ذلك، يمكننا تعريف شعاع يأخذ كائن سمة trait object، الذي يشير إلى نسخة من نوع ينفّذ السمة المحددة لدينا، إضافةً إلى جدول يُستخدم للبحث عن توابع السمات في هذا النوع في وقت التنفيذ. ننشئ كائن سمة عن طريق استخدام نوع من المؤشرات مثل المرجع &
، أو المؤشر الذكي Box<T>
، متبوعًا بالكلمة المفتاحية dyn
، ثم تحديد السمة ذات الصلة. سنتحدث عن السبب الذي يجعل من المحتمل لكائنات السمة أن تستخدم مؤشرًا لاحقًا. يمكننا استخدام كائنات السمات بدلًا من النوع المعمم أو الحقيقي. يُضمّن نظام النوع في رست وقت التصريف أينما نستخدم كائن سمة، وإن أي قيمة مُستخدمة في هذا السياق ستنفّذ سمة كائن السمة، وبالتالي لا نحتاج إلى معرفة جميع الأنواع الممكنة وقت التصريف.
لقد ذكرنا أننا نمتنع في رست عن تسمية الهياكل structs والتعدادات enums بالكائنات لتمييزها عن كائنات اللغات البرمجية الأخرى؛ إذ تُفصل البيانات الموجودة في البنية أو التعداد في حقول الهيكل والسلوك في كتل impl
؛ بينما تُسمّى البيانات والسلوك معًا في اللغات الأخرى غالبًا مثل كائن. مع ذلك، تشبه كائنات السمة إلى حد كبير الكائنات في اللغات الأخرى بمعنى أنها تجمع بين البيانات والسلوك، لكن تختلف كائنات السمة عن الكائنات التقليدية في أنه لا يمكننا إضافة بيانات إلى كائن سمة. لا تعدّ كائنات السمة مفيدةً عمومًا مثل الكائنات في اللغات الأخرى، إذ أن الغرض المحدد منها هو السماح بالتجريد عبر سلوكها المشترك.
توضّح الشيفرة 3 كيفية تعريف سمة تسمىDraw
مع تابع واحد يسمى draw
.
اسم الملف: src/lib.rs
pub trait Draw { fn draw(&self); }
[الشيفرة 3: تعريف السمة Draw
]
يجب أن تبدو الشيفرة السابقة مألوفةً من حديثنا عن كيفية تعريف السمات سابقًا في الفصل مقدمة إلى مفهوم الأنواع المعممة Generic Types. إلا أن هناك بعض الأشياء الجديدة: إذ تعرٍّف الشيفرة 4 هيكلًا يدعى Screen
يحمل شعاعًا باسم components
. هذا الشعاع من النوع Box<dyn Draw>
، وهو كائن سمة، ويُعدّ بديلًا لأي نوع داخل Box
ينفّذ السمة Draw
.
اسم الملف: src/lib.rs
pub struct Screen { pub components: Vec<Box<dyn Draw>>, }
[الشيفرة 4: تعريف هيكل Screen
مع حقل components
الذي يحمل شعاعًا من كائنات سمة تطبّق السمة Draw
]
نعرّف على هيكل Screen
تابعًا يدعى run
يستدعي التابع draw
على كل من components
الخاصة به كما هو موضح في الشيفرة 5.
اسم الملف: src/lib.rs
impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } }
[الشيفرة 5: تابع run
على Screen
الذي يستدعي تابع draw
على كل مكون]
يعمل هذا بصورةٍ مختلفة عن تعريف هيكل يستخدم معامل نوع معمّم generic type مع حدود السمة trait bounds، إذ لا يمكن استبدال معامل النوع المعمم إلا بنوع واحد صريح في كل مرة، بينما تسمح كائنات السمة بأنواع حقيقية متعددة لتحل مكان كائن السمة وقت التنفيذ. على سبيل المثال، كان من الممكن أن نعرّف هيكل Screen
باستخدام نوع معمم وحدود سمة كما في الشيفرة 6.
اسم الملف: src/lib.rs
pub struct Screen<T: Draw> { pub components: Vec<T>, } impl<T> Screen<T> where T: Draw, { pub fn run(&self) { for component in self.components.iter() { component.draw(); } } }
[الشيفرة 6: تنفيذ بديل لهيكل Screen
وتابعه run
باستخدام أنواع معممة وحدود السمة]
يقيّدنا هذا بنسخة Screen
التي تحتوي على قائمة من المكونات جميعها من النوع Button
، أو من النوع TextField
؛ فإذا كان لديك مجموعات متجانسة homogeneous collections فقط، يُفضّل استعمال أنواع معممة وحدود السمات لأن التعريفات ستكون أحادية الشكل monomorphized في وقت التصريف لاستخدام الأنواع الفعلية.
من جهة أخرى، يمكن لنسخة Screen
واحدة أن تحمل النوع Vec<T>
باستخدام التابع الذي يستخدم كائنات السمة، الذي يحتوي بدوره على <Box<Button
، إضافةً إلى <Box<TextField
. لنلقي نظرةً على كيفية عمل ذلك، ثم سنتحدث عن الآثار المترتبة على وقت التنفيذ.
تنفيذ السمة
سنضيف الآن بعض الأنواع التي تنفّذ السمة Draw
، وسنأخذ النوع Button
مثالًا على هذه الأنواع. يُعد تنفيذ مكتبة GUI كما ذكرنا سابقًا خارج موضوعنا هنا، لذا لن يكون لتابع draw
أي تنفيذ فعلي داخله. لنتخيل الشكل الذي قد يبدو عليه التنفيذ، فقد يحتوي هيكل Button
على حقول لكل من width
و height
و label
كما هو موضح في الشيفرة 7.
اسم الملف: src/lib.rs
pub struct Button { pub width: u32, pub height: u32, pub label: String, } impl Draw for Button { fn draw(&self) { // الشيفرة البرمجية المسؤولة عن رسم الزر } }
[الشيفرة 7: هيكل Button
الذي بطبّق السمة Draw
]
ستختلف حقول width
و height
و label
الموجودة في Button
عن الحقول الموجودة في المكونات الأخرى، فعلى سبيل المثال قد يحتوي النوع TextField
نفس الحقول، إضافةً إلى حقل placeholder
. ستنفّذ كل الأنواع التي نريد رسمها على الشاشة سمة Draw
، لكن ستستعمل شيفرةً برمجيةً مختلفة في التابع draw
لتعريف كيفية رسم ذلك النوع تحديدًا، كما في Button
هنا (بدون شيفرة برمجية لمكتبة GUI فعلية كما ذكرنا سابقًا). قد يحتوي النوع Button
على سبيل المثال كتلة impl
إضافية تحتوي على توابع مرتبطة بما يحدث عندما يضغط مستخدم الزر، ولا تنطبق هذه الأنواع من التوابع على أنواع مثل TextField
.
إذا قرر شخص ما استعمال مكتبتنا لتطبيق هيكل SelectBox
الذي يحتوي الحقولwidth
و height
و options
، فسينفّذ سمة Draw
على النوع SelectBox
أيضًا كما هو موضح بالشيفرة 8.
اسم الملف: src/main.rs
use gui::Draw; struct SelectBox { width: u32, height: u32, options: Vec<String>, } impl Draw for SelectBox { fn draw(&self) { // الشيفرة البرمجية المسؤولة عن رسم صندوق الاختيار } }
[الشيفرة 8: وحدة مصرفة أخرى تستعمل gui
وتنفذ سمة Draw
على هيكل SelectBox
]
يمكن لمستخدم مكتبتنا الآن كتابة الدالة main
ليُنشئ نسخةً من Screen
، ثم إضافة كل من SelectBox
و Button
لنسخة Screen
بوضع كل واحدة منها في <Box<T
لتصبح سمة كائن، ويمكنه بعد ذلك استدعاء التابع run
على نسخةScreen
التي ستستدعي draw
على كل من المكونات، وتوضح الشيفرة 9 التطبيق المذكور.
اسم الملف: src/main.rs
use gui::{Button, Screen}; fn main() { let screen = Screen { components: vec![ Box::new(SelectBox { width: 75, height: 10, options: vec![ String::from("Yes"), String::from("Maybe"), String::from("No"), ], }), Box::new(Button { width: 50, height: 10, label: String::from("OK"), }), ], }; screen.run(); }
[الشيفرة 9: استخدام كائنات السمة لتخزين قيم لأنواع مختلفة تنفذ السمة ذاتها]
لم نفترض عند كتابتنا للمكتبة بأن شخصًا ما قد يضيف النوع SelectBox
، إلا أن تطبيق Screen
لدينا قادرٌ على العمل مع النوع الجديد ورسمه، وذلك لأن SelectBox
ينفّذ سمة Draw
، ما يعني أنه ينفّذ تابع draw
.
هذا المفهوم - المتمثل بالاهتمام فقط بالرسائل التي تستجيب لها القيمة بدلًا من النوع الحقيقي للقيمة - مشابهٌ لمفهوم كتابة البطة duck typing في اللغات البرمجية المكتوبة ديناميكيًا؛ بمعنى أنه إذا كان شيء ما يسير مثل البطة ويصدر صوتًا مثل البطة، فيجب أن يكون بطة لا محالة. لا يحتاج run
إلى معرفة النوع الحقيقي لكل مكون عند تنفيذ run
على Screen
في الشيفرة 5، فهو لا يتحقق ما إذا كان المكوِّن نسخةً من النوع Button
أو SelectBox
بل يستدعي فقط التابع draw
على المكوّن. عرّفنا Screen
لتحتاج إلى قيم يمكننا استدعاء تابع draw
عليها من خلال تحديد <Box<dyn Draw
على أنه نوع القيم في الشعاع components
.
ميزة استخدام كائنات السمة ونظام نوع رست لكتابة شيفرة برمجية مشابهة للشيفرة البرمجية التي تستعمل كتابة البطة هي أننا لا نضطر أبدًا إلى التحقّق ما إذا كانت القيمة تنفذ تابعًا معينًا وقت التنفيذ، أو القلق بشأن حدوث أخطاء إذا كانت القيمة لا تنفّذ التابع، لكننا نستدعيه بغضّ النظر عن ذلك. لن تصرّف رست الشيفرة البرمجية الخاصة بنا إذا كانت القيم لا تنفّذ السمات التي تحتاجها كائنات السمة.
تُظهر الشيفرة 10 على سبيل المثال ما يحدث إذا حاولنا إنشاء Screen
مع String
مثل مكوِّن:
اسم الملف: src/main.rs
use gui::Screen; fn main() { let screen = Screen { components: vec![Box::new(String::from("Hi"))], }; screen.run(); }
[الشيفرة 10: محاولة استخدام نوع لا ينفّذ سمة كائن السمة]
سنحصل على الخطأ التالي، وذلك لأن String
لا ينفّذ السمة Draw
:
$ cargo run Compiling gui v0.1.0 (file:///projects/gui) error[E0277]: the trait bound `String: Draw` is not satisfied --> src/main.rs:5:26 | 5 | components: vec![Box::new(String::from("Hi"))], | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String` | = help: the trait `Draw` is implemented for `Button` = note: required for the cast from `String` to the object type `dyn Draw` For more information about this error, try `rustc --explain E0277`. error: could not compile `gui` due to previous error
يتيح لنا هذا الخطأ معرفة ما إذا كنّا نمرّر شيئًا ما إلى Screen
لم نقصد تمريره، ولذا يجب أن نمرّر نوعًا مختلفًا أو يجب أن نطبّق Draw
على String
حتى تتمكن Screen
من استدعاء draw
عليه.
الإرسال الديناميكي لكائنات السمة
باستذكار حديثنا عن عملية توحيد الشكل monomorphization المنفذة بواسطة المصرف عندما نستخدم حدود السمة على الأنواع المعممّة وذلك في قسم (أداء الشيفرة باستعمال الأنواع المعممة) في الفصل كيفية استخدام أنواع البيانات المعممة Generic Data Types: يولّد المصرّف تطبيقات غير معممّة للدوال والتوابع لكل نوع حقيقي نستخدمه بدلًا من معامل نوع مُحدّد. تنجز الشيفرة البرمجية الناتجة من عملية توحيد الشكل إيفادًا ساكنًا static dispatch، والذي يحدث عندما يعرِف المصرّف التابع الذي تستدعيه وقت التصريف. يتعارض هذا مع الإيفاد الديناميكي الذي يحدث عندما يتعذر على المصرف أن يخبرك بالتابع الذي تستدعيه وقت التصريف. يرسل المصرّف في حالات الإيفاد الديناميكي شيفرة برمجية تُحدّد في وقت التنفيذ التابع الذي يجب استدعاؤه.
يجب أن تستخدم رست الإيفاد الديناميكي عندما نستخدم كائنات السمة، إذ لا يعرف المصرّف جميع الأنواع الممكن استعمالها مع الشيفرة البرمجية التي تستخدم كائنات السمة، لذا فهو لا يعرف التابع الذي يُنفّذ على النوع المطلوب استدعاؤه. وتستخدم رست بدلًا من ذلك المؤشرات داخل كائن السمة في وقت التنفيذ لمعرفة التابع الذي يجب استدعاؤه، ويتسبب هذا البحث بزيادة في وقت التنفيذ مقارنةً بالإيفاد الثابت. يمنع الإيفاد الديناميكي أيضًا المصرف من اختيار تضمين شيفرة التابع البرمجية التي تمنع بدورها بعض التحسينات، ومع ذلك فقد حصلنا على مزيد من المرونة في الشيفرة البرمجية التي كتبناها في الشيفرة 5 وتمكنا من دعمها في الشيفرة 9، لذا فهي مقايضة يجب أخذها بالحسبان.
ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming Features of Rust من كتاب The Rust Programming Language.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.