اذهب إلى المحتوى

استخدام كائنات السمة Object Trait في لغة رست


Naser Dakhel

ذكرنا سابقًا في الفصل تخزين لائحة من القيم باستخدام الأشعة 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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...