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

معاملة المؤشرات الذكية Smart Pointers كمراجع نمطية باستخدام Deref في لغة رست


Naser Dakhel

يسمح لك تطبيق سمة Deref بتخصيص سلوك عامل التحصيل dereference operator * (انتبه عدم الخلط مع عامل عمليات الضرب أو عامل glob) يمكننا عند تطبيق deref بطريقة معينة تسمح بمعاملة المؤشرات الذكية smart pointers مثل مراجع نمطية، كتابة الشيفرة البرمجية بحيث تعمل على المراجع وتُستخدم بالمؤشرات الذكية أيضًا.

لننظر أولًا إلى كيفية عمل عامل التحصيل مع المراجع النمطية regular references، ومن ثم سنحاول تعريف نوع مخصص يتصرف مثل Box<T>‎، وسنرى سبب عدم عمل عامل التحصيل مثل مرجع على النوع المخصص المعرف حديثاً. سنكتشف كيف يسمح تطبيق سمة Deref للمؤشرات الذكية بأن تعمل بطريقة مماثلة للمراجع، ثم سننظر إلى ميزة التحصيل القسري deref coercion في رست وكيف تسمح لنا بالعمل مع المراجع أو المؤشرات الذكية.

ملاحظة: هناك فرق كبير بين النوع MyBox<T>‎ الذي سننشئه و Box<T>‎ الحقيقي: إذ لن يخزن إصدارنا منه البيانات على الكومة heap، وسنركز في مثالنا هذا على السمة deref، فمكان تخزين البيانات ليس مهمًا بقدر أهمية السلوك المشابه لسلوك المؤشر.

تتبع المؤشر للوصول إلى القيمة

المرجع النمطي هو نوع من المؤشرات، ويمكنك التفكير بالمؤشر على أنه سهم يشير إلى قيمة مخزنة في مكان آخر. أنشأنا في الشيفرة 6 مرجعًا إلى قيمة من النوع i32 ومن ثم استخدمنا عامل التحصيل لتتبع هذا المرجع وصولًا إلى القيمة.

اسم الملف: src/main.rs

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

[الشيفرة 6: استخدام عامل التحصيل لتتبع المرجع وصولًا إلى قيمة من النوع i32]

يخزّن المتغير x قيمةً من النوع i32 هي 5. ضبطنا قيمة y بحيث تساوي مرجعًا إلى المتغير x، ويمكننا التأكد أن x تساوي 5، ولكن إذا أردنا التأكد من قيمة y علينا استخدام ‎*y لتتبع المرجع وصولًا للقيمة التي يدل عليها لتحصيلها (هذا هو السبب في حصول عملية التحصيل على اسمها)، وذلك لكي يستطيع المصرّف أن يقارنها مع القيمة الفعلية. نستطيع الحصول على قيمة العدد الصحيح y بعد تحصيل y وهي القيمة التي تشير على ما يمكن مقارنته مع 5.

نحصل على الخطأ التالي عند التصريف إذا حاولنا كتابة assert_eq!(5,y);‎:

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
  |
  = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
  = help: the following other types implement trait `PartialEq<Rhs>`:
            f32
            f64
            i128
            i16
            i32
            i64
            i8
            isize
          and 6 others
  = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` due to previous error

مقارنة مرجع لرقم مع رقم غير مسموح لأنهما من نوعين مختلفين ويجب علينا استخدام عامل التحصيل لتتبع المرجع وصولًا إلى القيمة التي يشير إليها.

استخدام Box‎ مثل مرجع

يمكننا إعادة كتابة الشيفرة البرمجية في الشيفرة 6 باسخدام Box<T>‎ بدلاً من مرجع، وذلك عن طريق استخدام عامل التحصيل الذي استخدمناه على Box<T>‎ كما هو موضح في الشيفرة 7 بطريقة مماثلة لعمل عامل التحصيل المُستخدم في الشيفرة 6:

اسم الملف:src/main.rs

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

[الشيفرة 7: استخدام عامل التحصيل على Box<i32>‎]

الفرق الأساسي بين الشيفرة 7 والشيفرة 6 هو أننا حددنا y هنا لتكون نسخةً instance عن Box<T>‎ وتشير إلى قيمة منسوخة من x بدلًا من أن تكون مرجعًا يشير إلى قيمة x، ويمكننا في التوكيد assertion الأخير استخدام عامل التحصيل لتتبُّع مؤشر Box<T>‎ بالطريقة ذاتها التي اتبعناها عندما كان المرجع هو y. سنبحث تاليًا عن الشيء الذي يميّز Box<T>‎ ليسمح لنا باستخدام عامل التحصيل بتعريف نوع خاص بنا.

تعريف المؤشر الذكي الخاص بنا

دعنا نبني مؤشرًا ذكيًا خاصًا بنا بصورةٍ مماثلة للنوع Box<T>‎ الذي تزودنا به المكتبة القياسية لملاحظة كيف أن المؤشرات الذكية تتصرف على نحوٍ مختلف عن المراجع افتراضيًا، وسننظر بعدها إلى كيفية إضافة قدرة استخدام عامل التحصيل.

النوع Box<T>‎ معرفٌ مثل هيكل صف tuple struct بعنصر واحد، لذا نعرّف في الشيفرة 8 نوع MyBox<T>‎ بالطريقة ذاتها، كما نعرف أيضاً دالة new لتطابق الدالة new المعرفة في Box<T>‎.

اسم الملف: src/main.rs

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {}

[الشيفرة 8: تعريف النوع MyBox<T>‎]

نعرف بنية بالاسم MyBox ونعرف معاملًا مُعمّمًا ‎‏generic ‏ ‎T‏ لأننا نريد لنوعنا أن يحتفظ بكل أنواع القيم. نوع MyBox هو صف tuple بعنصر واحد من النوع T. تأخذ دالة MyBox::new معاملًا واحدًا من النوع T وتُعيد نسخةً من MyBox تحتفظ بالقيمة المُمرّرة.

لنجرب إضافة دالة main الموجودة في الشيفرة 7 إلى الشيفرة 8 ونعدلّها بحيث تستخدم النوع MyBox<T>‎ الذي عرفناه بدلًا من Box<T>‎. لن تُصّرف الشيفرة 9 لأن رست لا تعرف كيفية تحصيل قيمة MyBox.

اسم الملف: src/main.rs

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

[الشيفرة 9: محاولة استخدام Mybox<T>‎ بطريقة استخدام المراجع وBox<T>‎]

إليك الخطأ التصريفي الناتج عن الشيفرة السابقة:

$ cargo run
   Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^

For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` due to previous error

لا يمكن تحصيل النوع MyBox<T>‎ الخاص بنا لأننا لم نطبق هذه الميزة على نوعنا، ولتطبيق التحصيل باستخدام العامل *، نطبّق السمة Deref.

معاملة النوع مثل مرجع بتطبيق السمة Deref

لتطبيق السمة نحن بحاجة تأمين تطبيقات لتوابع السمة المطلوبة كما ذكرنا سابقًا، إذ تحتاج السمة Deref الموجودة في المكتبة القياسية لتطبيق تابع واحد اسمه deref يستعير self ويعيد مرجعًا للبيانات الداخلية. تحتوي الشيفرة 10 على تطبيق Deref لإضافة تعريف MyBox:

اسم الملف: src/main.rs

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

[الشيفرة 10: تطبيق Deref على MyBox<T>‎]

نعرّف في السطر type Target = T;‎ نوعًا مرتبط associated type لسمة Deref لتستخدمه، وتختلف الأنواع المرتبطة قليلًا في تعريف المعاملات المعمّمة، ولكن لا داعي للقلق بخصوصها حاليَا إذ سنتطرق لهذا الموضوع لاحقًا.

نكتب في متن التابع deref المرجع ‎&self.0 بحيث يُعيد deref مرجعًا للقيمة التي نريد الوصول إليها باستخدام العامل * (تذكر سابقًا أن ‎.0 تصل إلى القيمة الأولى في هيكل الصف). تستطيع الدالة main في الشيفرة 9 التي تستدعي * على القيمة MyBox<T>‎ أن تُصرّف الآن مع نجاح التأكيدات.

يستطيع المصرّف أن يحصّل المراجع & فقط بدون سمة Deref، إذ يعطي تابع deref المصرف القدرة على أن يأخذ القيم من أي نوع يطبق Deref وأن يستدعي التابع deref للحصول على مرجع & يعرف كيفية تحصيله.

عندما أدخلنا ‎*y في الشيفرة 9، نفذت رست الشيفرة التالية خلف الكواليس:

*(y.deref())

تستبدل رست العامل * باستدعاء للتابع deref ومن ثم إلى تحصيل عادي حتى لا نحتاج إلى التفكير فيما إذا كنا نريد استدعاء تابع deref، وتسمح لنا ميزة رست هذه بكتابة شيفرة برمجية تعمل بالطريقة نفسها سواءً أكان لدينا مرجع عادي أو نوع يطبق Deref.

يعود السبب في إعادة تابع deref المرجع إلى قيمة وأن التحصيل العادي خارج القوسين في ‎*(y.deref())‎ لا يزال ضروريًا إلى نظام الملكية؛ فإذا أعاد التابع deref القيمة مباشرة بدلاً من مرجع للقيمة فإن القيمة ستنتقل خارج self، ولا نريد أن نأخذ ملكية القيمة الداخلية في MyBox<T>‎ في هذه الحالة وفي معظم الحالات التي نستخدم فيها معامل التحصيل.

لاحظ أن المعامل * يُستبدل باستدعاءٍ للتابع deref ثم استدعاء للمعامل * مرةً واحدةً وذلك في كل مرة نستخدم * في شيفرتنا البرمجية. ينتهي بنا المطاف بقيمة من النوع i32 تُطابق "5" في assert_eq!‎ في الشيفرة 9 وذلك لأن عملية استبدال المعامل * لا تُنفّذ إلى ما لا نهاية.

التحصيل القسري الضمني مع الدالات والتوابع

يحوّل التحصيل القسري المرجع من نوع يطبق السمة Deref إلى مرجع من نوع آخر، فمثلاً يحوّل التحصيل القسري ‎&String إلى ‎&str لأن String يطبق السمة Deref بطريقة تُعيد ‎&str. التحصيل القسري هو عملية ملائمة في رست تُجرى على وسطاء arguments الدوال والتوابع وتعمل فقط على الأنواع التي تطبق السمة Deref، وتحصل هذه العملية تلقائيًا عندما نمرر مرجعًا لقيمة ذات نوع معين مثل وسيط لدالة أو تابع لا يطابق نوع المعامل في تعريف الدالة أو التابع. تحوِّل سلسلةً من الاستدعاءات إلى التابع deref النوع المُقدم إلى نوع يحتاجه المعامل.

أُضيف التحصيل القسري إلى رست بحيث لا يضطر المبرمجون الذين يكتبون استدعاءات لدالات وتوابع إلى إضافة العديد من المراجع الصريحة والتحصيلات باستخدام & و *، كما تسمح لنا خاصية التحصيل القسري أيضاً بكتابة شيفرة برمجية تعمل لكل من المراجع أو المؤشرات الذكية في الوقت ذاته.

لمشاهدة عمل التحصيل القسري عمليًا، نستخدم النوع MyBox<T>‎ الذي عرفناه في الشيفرة 8 بالإضافة إلى تطبيق Deref الذي أضفناه في الشيفرة 10. توضح الشيفرة 11 تعريف دالة تحتوي على معامل شريحة سلسلة نصية string slice:

اسم الملف: src/main.rs

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {}

[الشيفرة 11: الدالة hello التي تحتوي على معامل name من النوع ‎&str]

بإمكاننا استدعاء الدالة hello باستخدام شريحة سلسلة نصية بمثابة وسيط مثل hello(“Rust”);‎. يجعل التحصيل القسري استدعاء hello مع مرجع للقيمة MyBox<String>‎ ممكنًا كما هو موضح في الشيفرة 12:

اسم الملف: src/main.rs

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

[الشيفرة 12: استدعاء hello باستخدام مرجع إلى القيمة MyBox<String>‎ ويمكن تنفيذ ذلك بفضل التحصيل القسري]

نستدعي هنا الدالة hello مع الوسيط ‎&m الذي يمثل مرجعًا إلى القيمة MyBox<String>‎، وتستطيع رست تحويل ‎&MyBox<String>‎ إلى ‎&String باستدعاء deref وذلك لأننا طبقنا السمة Deref على MyBox<T>‎ كما هو موضح في الشيفرة 10. تقّدم المكتبة القياسية تطبيقًا للسمة Deref على النوع String الذي يعيد لنا شريحة سلسلة نصية ويمكنك العثور على هذه التفاصيل في توثيق الواجهة البرمجية الخاصة بالسمة Deref. تستدعي رست التابع deref مجددًا لتحويل ‎&String إلى ‎&str الذي يطابق تعريف دالة hello.

إذا لم تطبق رست التحصيل القسري فسيتوجب علينا كتابة الشيفرة 13 بدلًا من الشيفرة 12 لاستدعاء hello مع قيمة من النوع ‎&MyBox<String>‎.

اسم الملف: src/main.rs

use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0
    }
}

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn hello(name: &str) {
    println!("Hello, {name}!");
}

fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

[الشيفرة 13: الشيفرة التي يجب علينا كتابتها إذا لم تحتوي رست على ميزة التحصيل القسري]

يحصّل (m*) النوع MyBox<String>‎ إلى String ومن ثم إلى & وتأخذ [..] شريحة سلسلة نصية String تساوي قيمة السلسلة النصية كاملةً وذلك لمطابقة بصمة الدالة hello. ستكون هذه الشيفرة البرمجية صعبة القراءة والفهم بدون التحصيل القسري وذلك مع كل الرموز اللازمة، إذ يسمح التحصيل القسري للغة رست بمعالجة هذه التحويلات تلقائياً نيابةً عنّا.

عندما تُعرَّف سمة Deref للأنواع المستخدمة، ستحلّل رست هذه الأنواع وتستخدم Deref::deref بعدد المرات اللازم لتحصل على مرجع يطابق نوع المعامل، ويُعرَّف عدد مرات إضافة Deref::deref اللازمة عند وقت التصريف لذا لا يوجد أي عبء إضافي عند وقت التشغيل runtime مع التحصيل القسري.

كيفية تعامل التحصيل القسري مع قابلية التغيير

يمكننا استخدام السمة DerefMut لتجاوز عمل العامل * على المراجع المتغيّرة mutable references بصورةٍ مشابهة لاستخدامنا لسمة Deref لتجاوز عمل العامل * على المراجع الثابتة immutable.

تنفّذ رست عملية التحصيل القسري عندما تجد تطبيقات لأنواع وسمات في ثلاث حالات معيّنة:

  • من ‎&T إلى ‎&U عندما T: Deref<Target=U>‎.
  • من ‎&mut T إلى ‎&mut U عندما T: DerefMut<Target=U>‎.
  • من ‎&mut T إلى ‎&U عندما T: Deref<Target=U>‎.

الحالتان الأولى والثانية متماثلتان مع فرق أن الثانية هي تطبيق لحالة متغيّرة، بينما تنصّ الحالة الأولى أنه إذا كان لديك ‎&T وتطبّق T سمة Deref لنوع ما من U، يمكن الحصول على ‎&U بوضوح، والحالة الثانية تشير إلى أن عملية التحصيل القسري ذاتها تحدث للمراجع المتغيّرة.

تعدّ الحالة الثالثة أكثر تعقيدًا؛ إذ تجبر رست تحويل مرجع متغيّر إلى مرجع ثابت، إلا أن العكس غير ممكن، فالمراجع الثابتة لن تُحوّل قسريًا إلى مراجع متغيرة، وذلك بسبب قواعد الاستعارة، وإذا كان لديك مرجعًا متغيّرًا فإن هذا المرجع سيكون المرجع الوحيد لتلك البيانات (وإلا فلن يمكنك تصريف البرنامج). لن يكسرتحويل مرجع متغيّرإلى مرجع ثابت قواعد الاستعارة الافتراضية. تتطلب عملية تحويل المرجع الثابت إلى مرجع متغيّر أن يكون المرجع الثابت الابتدائي هو المرجع الوحيد الثابت للبيانات الخاصة به، إلا أن قوانين الاستعارة لا تضمن لك ذلك، وبالتالي لا يمكن لرست الافتراض بأن تحويل مرجع ثابت إلى مرجع متغيّر هي شيء ممكن.

ترجمة -وبتصرف- لقسم من الفصل Smart Pointers من كتاب 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.


×
×
  • أضف...