كانت مشكلتنا باستخدام الصف tuple في المقالة السابقة (الشيفرة 4-5) هو أنه علينا إعادة النوع String
إلى القيمة المُستدعية ليتسنى لنا استخدام النوع String
حتى بعد استدعاء الدالة calculate_length
، وذلك لأن النوع String
نُقل إلى calculate_length
. بدلًا مما سبق يمكننا استخدام مرجع reference إلى القيمة String
؛ والمرجع هو أشبه بالمؤشر pointer، إذ يُمثل عنوانًا يمكنك اتباعه للوصول إلى البيانات المخزنة ضمن العنوان المذكور، وتعود ملكية هذه البيانات إلى متغيرات أخرى مختلفة. من المضمون للمراجع -على عكس المؤشرات- أن تُشير إلى قيمة صالحة لنوع معين طوال دورة حياة المرجع.
إليك كيفية تعريف الدالة calculate_length
واستخدامها مع احتوائها على مرجع لكائن بمثابة معاملٍ لها بدلًا من أخذ ملكية القيمة:
اسم الملف: src/main.rs
fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
لاحظ أولًا أننا أزلنا الشيفرة البرمجية الخاصة بالصف في تصريح المتغير وقيمة الدالة المُعادة، كما أننا مررنا أيضًا s1&
إلى الدالة calculate_length
وأخذنا في تعريفها String&
بدلًا من String
وتُمثّل هذه الإشارة &
المرجع، وتسمح لك بالإشارة إلى قيمة دون أخذ ملكيتها، ويوضح الشكل التالي هذا المفهوم.
[شكل 5: مخطط يوضح String s&
الذي يُشير إلى String s1
]
ملاحظة: عكس عملية المرجع باستخدام &
هي التحصيل dereferencing ويمكن تحقيقها باستخدام عامل التحصيل *
وسنرى بعض استخدامات التحصيل بالإضافة لتفاصيل العملية لاحقًا.
دعنا نأخذ نظرةً أعمق على استدعاء الدالة هنا:
let s1 = String::from("hello"); let len = calculate_length(&s1);
تسمح لك الكتابة s1&
بإنشاء مرجع يشير إلى القيمة s1
إلا أنه لا ينقل الملكية، وبالتالي -وبما أنه لا يملكها- لن تُسقط القيمة (باستخدام drop
) التي يشير إليها عندما يتوقف المرجع عن استخدامها.
وبالمثل، تستخدم شارة الدالة الرمز &
للإشارة إلى أن نوع المعامل s
هو مرجع. دعنا نضيف بعض التعليقات التوضيحية:
fn calculate_length(s: &String) -> usize { // يمثل s مرجعًا إلى String s.len() } // يخرج s من النطاق هنا إلا أنه لا يُسقط لأنه لا يمتلك القيمة التي يشير إليها
النطاق الذي يحتوي على المتغير s
هو مماثل لأي نطاق معامل دالة، إلا أن القيمة المُشار إليها باستخدام المرجع لا تُسقَط عندما نتوقف عن استخدام s
وذلك لأن s
لا يملك القيمة. لا نحتاج لإعادة القيم عندما تحتوي الدوال على مراجع مثل معاملات بدلًا من القيم الفعلية حتى نستطيع منح الملكية مجددًا، وذلك لأننا لم ننقل الملكية في المقام الأول.
نطلق على عملية إنشاء المراجع عملية الاستعارة borrowing، فكما الحال في الحياة الواقعية، إذا امتلك شخصٌ ما غرضًا، فيمكنك استعارته منه، وعند الانتهاء من الغرض عليك أن تُعيده إلى ذلك الشخص لأنك لا تملك الغرض.
ما الذي يحصل إذًا عندما نحاول التعديل على شيء مُستعار؟ جرّب تنفيذ الشيفرة 4-6 (تحذير: لن تعمل)
اسم الملف: src/main.rs
fn main() { let s = String::from("hello"); change(&s); } fn change(some_string: &String) { some_string.push_str(", world"); }
[الشيفرة 4-6: محاولة التعديل على قيمة مُستعارة]
وسيظهر الخطأ التالي:
$ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference --> src/main.rs:8:5 | 7 | fn change(some_string: &String) { | ------- help: consider changing this to be a mutable reference: `&mut String` 8 | some_string.push_str(", world"); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable For more information about this error, try `rustc --explain E0596`. error: could not compile `ownership` due to previous error
المراجع غير قابلة للتعديل immutable افتراضيًا كما هو الحال مع المتغيرات، لذا لا يُمكنك التعديل على شيء باستخدام المرجع.
المراجع القابلة للتعديل
يُمكننا تصحيح الشيفرة 4-6 السابقة لكي تسمح لنا بتعديل قيمة مُستعارة باستخدام تعديلات بسيطة، وذلك باستخدامنا مرجعًا قابلًا للتعديل mutable reference:
اسم الملف: src/main.rs
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
علينا أولًا أن نعدل المتغير s
ليصبح mut
، ثم نُنشئ مرجعًا قابلًا للتعديل باستخدام mut s&
عند نقطة استدعاء الدالة chang
، ومن ثم تحديث شارة الدالة حتى تقبل مرجعًا قابلًا للتعديل بكتابة some_string: &mut String
، إذ يدلنا هذا بكل وضوح على أن الدالة chang
ستُعدّل من القيمة التي استعارتها.
للمراجع القابلة للتعديل قيدٌ واحدٌ كبير، وهو أنه لا يمكنك الحصول على أكثر من مرجع إلى قيمة إذا كان لتلك القيمة مرجعًا قابلًا للتعديل. نُحاول في الشيفرة البرمجية التالية إنشاء مرجعين قابلَين للتعديل يشيران إلى s
:
اسم الملف: src/main.rs
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2);
إليك الخطأ:
$ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0499]: cannot borrow `s` as mutable more than once at a time --> src/main.rs:5:14 | 4 | let r1 = &mut s; | ------ first mutable borrow occurs here 5 | let r2 = &mut s; | ^^^^^^ second mutable borrow occurs here 6 | 7 | println!("{}, {}", r1, r2); | -- first borrow later used here For more information about this error, try `rustc --explain E0499`. error: could not compile `ownership` due to previous error
يدل هذا الخطأ على أن الشيفرة البرمجية غير صالحة لأنه لا يُمكننا استعارة القيمة s
على أنها قيمة قابلة للتعديل أكثر من مرة واحدة، إذ نستعير القيمة القابلة للتعديل للمرة الأولى في r1
ويجب أن نحافظ على الاستعارة حتى تُستخدم القيمة في !println
، إلا أننا حاولنا أيضًا إنشاء مرجع قابل للتعديل آخر في r2
يستعير نفس البيانات الموجودة في r1
وذلك بين إنشاء المرجع القابل للتعديل الأول وبين استخدامه.
يسمح القيد الذي يمنع وجود عدة مراجع قابلة للتعديل تشير لنفس البيانات في نفس الوقت بتعديل البيانات ولكن بطريقة مُقيّدة جدًا، وهو شيء يعاني منه معظم متعلمي لغة رست الجدُد وذلك لأن معظم اللغات تسمح لك بتعديل ما تشاء. الميزة من هذا القيد هو أن رست تمنع سباق البيانات data races عند وقت التصريف، وسباق البيانات هو مشابه لحالة السباق race condition ويحدث عند حدوث أحد الحالات الثلاث:
- محاولة مؤشرين أو أكثر الوصول إلى نفس البيانات في نفس الوقت.
- استخدام واحد من المؤشرات على الأقل للكتابة إلى البيانات.
- عدم وجود آلية مُستخدمة لمزامنة الوصول إلى البيانات.
تتسبب سباقات البيانات بسلوك غير معرّف undefined behaviour وقد يكون تشخيص المشكلة وتصحيحها صعبًا عندما تحاول تتبع الخطأ عند وقت التشغيل، وتمنع رست هذه المشكلة برفض تصريف الشيفرة البرمجية التي تحتوي على سباقات البيانات.
يُمكننا استخدام الأقواس المعقوصة curly brackets لإنشاء نطاق جديد، مما يسمح بوجود مراجع قابلة للتعديل، إلا أن الشرط هو عدم تواجد المراجع القابلة للتعديل ضمن النطاق في ذات الوقت:
let mut s = String::from("hello"); { let r1 = &mut s; } // يخرج r1 من النطاق هنا وبالتالي يمكننا إنشاء مرجع جديد دون أي مشاكل let r2 = &mut s;
تُجبرنا رست على قاعدة مشابهة لجمع المراجع القابلة وغير القابلة للتعديل. تولّد الشيفرة البرمجية التالية خطأً:
let mut s = String::from("hello"); let r1 = &s; // لا توجد مشكلة let r2 = &s; // لا توجد مشكلة let r3 = &mut s; // هناك مشكلة كبيرة println!("{}, {}, and {}", r1, r2, r3);
إليك الخطأ:
$ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> src/main.rs:6:14 | 4 | let r1 = &s; // no problem | -- immutable borrow occurs here 5 | let r2 = &s; // no problem 6 | let r3 = &mut s; // BIG PROBLEM | ^^^^^^ mutable borrow occurs here 7 | 8 | println!("{}, {}, and {}", r1, r2, r3); | -- immutable borrow later used here For more information about this error, try `rustc --explain E0502`. error: could not compile `ownership` due to previous error
كما أنه لا يمكننا إنشاء مرجع قابل للتعديل بينما لدينا مرجع غير قابل للتعديل يشير إلى القيمة ذاتها.
لا يتوقع مستخدموا المراجع غير القابلة للتعديل بأن تتغير القيمة من تلقاء نفسها، لكن تسمح المراجع المتعددة غير القابلة للتعديل بذلك لأنه لا يوجد أي شيء يقرأ البيانات ولديه القدرة على التأثير على أي من المراجع التي تقرأ البيانات.
لاحظ بأن نطاق المراجع يبدأ من مكان إنشائها ويستمر إلى آخر مكان استُخدم فيه المرجع، فعلى سبيل المثال، يمكن تصريف الشيفرة البرمجية التالية بنجاح لأن استخدام المراجع غير القابلة للتعديل الأخير في !println
يحدث قبل إنشاء المرجع القابل للتعديل:
let mut s = String::from("hello"); let r1 = &s; // لا يوجد مشكلة let r2 = &s; // لا يوجد مشكلة println!("{} and {}", r1, r2); // لن يُستخدم المتغيرين r1 و r2 بعد هذه النقطة let r3 = &mut s; // لا يوجد مشكلة println!("{}", r3);
ينتهي نطاق كل من المرجعَين غير القابلَين للتعديل r1
و r2
بعد !println
وهي آخر نقطة لاستخدامهما قبل إنشاء المرجع القابل للتعديل r3
. لا تتداخل النطاقات ولذلك تكون الشيفرة البرمجية صالحة، وتدعى قدرة المصرف على معرفة المراجع التي لا تُستخدم بعد الآن في نهاية النطاق باسم دورات الحياة غير المُعجمية Non-Lexical Lifetimes -أو اختصارًا NLL -ويمكنك القراءة عنها من rust-lang.org.
على الرغم من كون أخطاء الاستعارة أخطاءً مثيرةً للإحباط في بعض الأحيان، إلا أنه يجب عليك أن تتذكر أن مصرّف رست يُشير إلى خطأ قبل أوانه (عند وقت التصريف بدلًا من وقت التشغيل) ويوضح لك بالتفصيل مكان المشكلة حتى لا تضطر إلى تعقب المشكلة إذا كانت بياناتك تحتوي قيمًا مختلفة لتوقعاتك.
المراجع المعلقة
من السهل في اللغات التي تدعم المؤشرات أن تُنشئ مؤشرات معلقة dangling pointer بصورةٍ خاطئة؛ وهي مؤشرات تشير إلى موضع في الذاكرة مُعطًى لشخص آخر وذلك بتحرير المساحة مع المحافظة على المؤشر الذي يشير إلى الذاكرة، وهذا غير ممكن الحصول في رست، فعلى النقيض تمامًا يضمن المصرف ألا تكون جميع المراجع مُعلّقة، لأنه سيتأكد من عدم مغادرة البيانات التي يشير إليها المرجع النطاق قبل أن يُغادر المرجع النطاق أولًا.
دعنا نجرّب إنشاء مرجع معلّق لملاحظة كيف تمنع رست وجودها بخطأ عند وقت التصريف:
اسم الملف: src/main.rs
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s }
إليك الخطأ:
$ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0106]: missing lifetime specifier --> src/main.rs:5:16 | 5 | fn dangle() -> &String { | ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from help: consider using the `'static` lifetime | 5 | fn dangle() -> &'static String { | ~~~~~~~~ For more information about this error, try `rustc --explain E0106`. error: could not compile `ownership` due to previous error
يُشير هذا الخطأ إلى الميزة التي لم نكتشفها بعد، ألا وهي دورات الحياة lifetimes، وسنناقشها بتوسُّع لاحقًا، ولكن إذا تغاضينا عن الجزء الخاص بدورات الحياة، فإن رسالة الخطأ تحتوي سبب المشكلة:
this function's return type contains a borrowed value, but there is no value for it to be borrowed from
دعنا نأخذ نظرةً أقرب لما يحدث في كل مرحلة من مراحل الدالة dangle
:
اسم الملف: src/main.rs
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { // تُعيد الدالة dangle مرجعًا إلى النوع String let s = String::from("hello"); // يشكل s السلسلة String الجديدة &s // نُعيد المرجع إلى النوع String واسمه s } // يخرج s من النطاق هنا ويُسقَط وتُحرّر ذاكرته. خطر!
ستُحرّر s
عند الانتهاء من تنفيذ الشيفرة البرمجية داخل الدالة dangle
، لأن s
مُنشأة بداخل dangle
، إلا أننا حاولنا إعادة مرجع إليها وهذا يعني أن المرجع سيشير إلى قيمة String
غير صالحة، وهذا ما يجب تجنُّب حدوثه، بالتالي لن تسمح لنا رست بفعل ذلك.
يكمن الحل هنا بإعادة String
مباشرةً:
fn main() { let string = no_dangle(); } fn no_dangle() -> String { let s = String::from("hello"); s }
يعمل هذا الحل دون أي مشاكل، إذ تُنقل الملكية ولا يُحرّر أي حيز من الذاكرة.
قوانين المراجع
دعنا نلخص أهم النقاط التي ناقشناها بخصوص المراجع:
- يُمكنك في نقطة من الزمن استخدام مرجع واحد قابل للتعديل أو عدة مراجع غير قابلة للتعديل.
- يجب أن تكون المراجع صالحة على الدوام.
سننظر في الفقرات التالية إلى نوع مختلف من المراجع هو الشرائح slices.
نوع الشريحة
تسمح لك الشرائح بالإشارة reference إلى سلسلة متتابعة من العناصر ضمن تجميعة collection بدلًا من الإشارة إلى كامل التجميعة، وتشبه الشريحة المرجع ولذا فهو لا يحتوي على ملكية.
إليك مشكلةً برمجيةً صغيرة: اكتب دالةً تأخذ سلسلةً نصيةً من كلمات مفصول ما بينها بمسافات وتُعيد الكلمة الأولى التي تجدها ضمن تلك السلسلة النصية، إذا لم تعثر الدالة على مسافة ضمن السلسلة النصية، فهذا يعني أن السلسلة النصية مؤلفةٌ من كلمة واحدة فقط وعليها أن تُعيد كامل السلسلة النصية.
لنرى كيف يمكننا كتابة بصمة signature الدالة دون استخدام الشرائح، وذلك حتى نفهم المشكلة التي تحلها الشرائح:
fn first_word(s: &String) -> ?
للدالة first_word
معامل &String
، وهذا ما لا بأس فيه لأننا لا نحتاج الملكية عليه، لكن ما الذي يجب أن تُعيده الدالة؟ لا توجد لدينا أي طريقة لتحديد جزء من السلسلة النصية، إلا أننا نستطيع إعادة دليل index نهاية الكلمة بالاستفادة من المسافة، لنجرّب هذه الطريقة كما هو موضح في الشيفرة 4-7.
اسم الملف: src/main.rs
fn first_word(s: &String) -> usize { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return i; } } s.len() }
[الشيفرة 4-7: الدالة first_word
تُعيد قيمة دليل بحجم بايت إلى معامل String
]
علينا أن نحوّل String
إلى مصفوفة من البايتات لأننا بحاجة للمرور بعناصر String
عنصرًا تلو الآخر للبحث عن مسافات، وذلك باستخدام التابع as_bytes
:
let bytes = s.as_bytes();
من ثم نُنشئ مُكرّرًا iterator على مصفوفة البايتات باستخدام تابع iter
:
for (i, &item) in bytes.iter().enumerate() {
سنُناقش المُكرّرات بالتفصيل لاحقًا، أما الآن فكل ما عليك معرفته هو أن iter
تابع يُعيد كل عنصر في تجميعة، وأن enumerate
يُغلّف wraps نتيجة iter
ويُعيد كل عنصر على أنه جزءٌ من الصف tuple بدلًا من ذلك، إذ يمثّل العنصر الأول من الصف المُعاد من التابع enumerate
دليلًا، بينما يمثل العنصر الثاني مرجعًا إلى العنصر في التجميعة، وهذه الطريقة أسهل من حساب أدلة العناصر بأنفسنا.
يُمكننا استخدام الأنماط patterns لتفكيك الصف وذلك بالنظر إلى أن التابع enumerate
يُعيد صفًا، وسنناقش الأنماط بالتفصيل لاحقًا. نُحدد في حلقة for
النمط الذي يحتوي على i
، والذي يمثل الدليل الموجود في الصف و &item
للبايت الوحيد الموجود في الصف، ونستخدم &
في النمط لأننا نحصل على مرجع للعنصر من iter().enumerate().
.
نبحث عن البايت الذي يمثل المسافة داخل حلقة for
باستخدام البايت المجرّد، وإن وجدنا مسافةً نُعيد مكانها، وإلا فنعيد طول السلسلة النصية باستخدامs.len()
:
if item == b' ' { return i; } } s.len()
لدينا طريقةٌ الآن لمعرفة دليل نهاية الكلمة الأولى في السلسلة النصية، إلا أن هناك مشكلةُ في هذه الطريقة؛ إذ أننا نُعيد النوع usize
بصورةٍ منفردة إلا أنه ذو معنى فقط في سياق &String
، بكلمات أخرى، لا يوجد أي ضمان أن القيمة ستكون صالحة في المستقبل نظرًا لأنها قيمة منفصلة عن String
. ألقِ نظرةً على الشيفرة 4-8 التي تستخدم الدالة first_word
من الشيفرة 4-7.
اسم الملف: src/main.rs
fn main() { let mut s = String::from("hello world"); let word = first_word(&s); // ستُخزّن القيمة 5 في word s.clear(); // تُفرغ هذه التعليمة السلسلة النصية وتجعلها مساوية إلى القيمة “” // للمتغير word القيمة 5 هنا إلا أنه لا يوجد أي سلسلة نصية تجعل من هذه القيمة ذات معنى، وبالتالي القيمة عديمة الفائدة! }
[الشيفرة 4-8: تخزين القيمة من استدعاء الدالة first_word
ومن ثمّ تغيير محتويات String]
يُصرَّف البرنامج السابق دون أي أخطاء وسيعمل دون مشاكل إذا استخدمنا word
بعد استدعاء s.clear()
، وذلك لأن word
ليست مرتبطة بحالة s
إطلاقًا، إذ ما زالت تحتوي word
على القيمة 5 حتى بعد استدعاء s.clear()
، ويمكننا استخدام القيمة 5 مع المتغير s
حتى نحاول استخراج الكلمة الأولى إلا أن ذلك سيتسبب بخطأ لأن محتويات s
تغيرت منذ أن خزّننا 5 في word
.
تُعد مراقبة الدليل الموجود في word
خشيةً من فقدان صلاحيته بالنسبة للمتغير s
عمليةً رتيبةً ومعرضةً للخطأ، وستصبح أسوأ إذا كتبنا دالة second_word
تكون بصمتها على النحو التالي:
fn second_word(s: &String) -> (usize, usize) {
الآن وبما أننا نتتبع دليل البداية والنهاية، فهذا يعني أنه لدينا قيم أكثر لحسابها من البيانات في حالة معينة، إلا أن هذه القيم غير مرتبطة بحالة ما إطلاقًا، أصبح لدينا الآن إذًا ثلاثة متغيرات غير مرتبطة مع بعضها بعضًا ويجب أن نربطها.
لدى رست لحسن الحظ الحل لهذه المشكلة، وهي سلسلة الشرائح النصية string slices.
شرائح السلاسل النصية
شرائح السلاسل النصية هي مرجعٌ لجزء من النوع String
وتُكتب بالشكل التالي:
fn main() { let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11]; }
نستخدم المرجع hello
الذي يمثّل مرجعًا لجزء من النوع String
بدلًا من استخدام مرجع لكامل النوع، والجزء مُحدّد باستخدام [5..0]
بت. نُنشئ هنا شرائحًا باستخدام مجال باستخدام الأقواس وذلك بتحديد دليل البداية ودليل النهاية بالشكل التالي: [starting_index..ending_index]
؛ إذ يُمثل starting_index
موضع بداية الشريحة؛ بينما يمثل ending_index
الموضع الذي يلي موضع نهاية الشريحة. يُخزّن هيكل بيانات الشريحة داخليًا كلًا من موضع البداية وطول الشريحة، الذي تحصل عليه من طرح ending_index
من starting_index
، لذا في حالة let world = &s[6..11]
نحصل على شريحة باسم world
تحتوي على مؤشر يشير إلى البايت الموجود في الدليل 6 للسلسلة s
بقيمة طول مساوية إلى 5.
يوضح الشكل 6 العملية السابقة.
[شكل 6: شريحة سلسلة نصية تُشير إلى جزء من String
]
يُمكنك إهمال دليل البدء قبل النقطتين ..
في المجال إذا أردت البدء من الدليل صفر، أي أن الشريحتين التاليتن متماثلتان:
let s = String::from("hello"); let slice = &s[0..2]; let slice = &s[..2];
كما يمكنك إهمال دليل النهاية إذا أردت أن تُضمّن السلسلة النصية إلى النهاية، أي أن الشريحتين التاليتين متماثلتان:
let s = String::from("hello"); let len = s.len(); let slice = &s[3..len]; let slice = &s[3..];
أخيرًا، يمكنك إهمال كل من دليل البداية ودليل النهاية ضمن المجال إذا أردت الحصول على كامل السلسلة النصية، والشريحتان التاليتان متماثلتان:
let s = String::from("hello"); let len = s.len(); let slice = &s[0..len]; let slice = &s[..];
ملاحظة: يجب أن تمثّل أدلّة شرائح السلاسل النصية محرفًا صالحًا من ترميز UTF-8، وإلا سيتوقف البرنامج ويعرض خطأً إذا حاولت إنشاء شريحة سلسلة نصية منتصف محرف متعدد البايتات multibyte character، ونفرض هنا في هذا القسم أن المحارف بترميز آسكي ASCII فقط بهدف البساطة، وسنناقش مفصّلًا التعامل مع محارف بترميز UTF-8 لاحقًا.
بعد تعرّفنا لما سبق، دعنا نكتب دالة تُعيد شريحة نسميها first_word
، والنوع الذي يمثّل شريحة السلسلة النصية هو str&
:
اسم الملف: src/main.rs
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() {}
نحصل في البرنامج السابق على دليل نهاية الكلمة بصورةٍ مشابهة لما فعلناه في الشيفرة 4-7 وذلك بالبحث عن أوّل ظهور لمسافة، وعندما نجد هذه المسافة نُعيد شريحة سلسلة نصية باستخدام بداية السلسلة النصية مثل دليل بداية ودليل المسافة مثل دليل نهاية.
نحصل على قيمة واحدة متعلقة بالبيانات التي لدينا بعد استدعاء الدالة first_word
، وتتألف القيمة من مرجع إلى نقطة البداية لشريحة السلسلة النصية وعدد العناصر في تلك الشريحة.
يمكننا أن نجعل الدالة second_word
تُعيد شريحة أيضًا:
fn second_word(s: &String) -> &str {
أصبح لدينا الآن واجهة برمجية API واضحة صعب العبث فيها، إذ سيتأكد المصرّف من أن مراجع النوع String
هي مراجع صالحة. أتتذكر الخطأ الذي واجهناه في البرنامج الموجود في الشيفرة 4-8 عندما حصلنا على دليل نهاية الكلمة الأولى ومن ثمّ مسحنا السلسلة النصية مما جعل الدليل غير صالح؟ كانت الشيفرة تلك غير صحيحة منطقيًا ولكننا لم نحصل على أي أخطاء مباشرة واضحة، وستظهر المشكلة لاحقًا إذا حاولت استخدام دليل السلسلة النصية الفارغة، إلا أن شرائح السلاسل النصية تجعل من هذا الخطأ مستحيلًا وستعلمنا بحدوث خطأ في شيفرتنا البرمجية في وقت مبكّر، وعلى سبيل المثال، نحصل على خطأ وقت التصريف إذا استخدمنا إصدار شريحة السلسلة النصية من الدالة first_word
.
اسم الملف: src/main.rs
fn first_word(s: &String) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let mut s = String::from("hello world"); let word = first_word(&s); s.clear(); // خطأ! println!("the first word is: {}", word); }
إليك خطأ التصريف:
$ cargo run Compiling ownership v0.1.0 (file:///projects/ownership) error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable --> src/main.rs:18:5 | 16 | let word = first_word(&s); | -- immutable borrow occurs here 17 | 18 | s.clear(); // error! | ^^^^^^^^^ mutable borrow occurs here 19 | 20 | println!("the first word is: {}", word); | ---- immutable borrow later used here For more information about this error, try `rustc --explain E0502`. error: could not compile `ownership` due to previous error
تذكّر من قواعد الاستعارة borrowing rules أنه لا يمكننا أخذ مرجع قابل للتعديل من شيء إذا كان لدينا مرجع غير قابل للتعديل لهذا الشيء مسبقًا، ولأن clear
بحاجة لحذف محتويات String
، فهي بحاجة للحصول على مرجع قابل للتعديل، يستخدم println!
بعد الاستدعاء للدالة clear
المرجع في word
، لذلك لا بدّ للمرجع غير القابل للتعديل أن يكون صالحًا بحلول تلك النقطة ولذلك تمنع رست وجود مرجع قابل للتعديل في clear
ومرجع غير قابل للتعديل في word
في الوقت ذاته مما يتسبب بفشل عملية التصريف. لم تكتفي رست بجعل الواجهة البرمجية أسهل للتعامل بل أقصَت صنفًا كاملًا من الأخطاء ممكنة الحدوث عند وقت التصريف.
السلاسل النصية المجردة هي شرائح
تذكر أننا تحدثنا عن السلاسل النصية المجردة بكونها تُخزّن بداخل الملف التنفيذي الثنائي، ويمكننا الآن فهم السلاسل النصية المجردة بوضوح بما أننا نعرف الشرائح:
let s = "Hello, world!";
نوع s
هنا هو &str
وهي شريحة تُشير إلى جزء محدد من الملف الثنائي، وهذا السبب في كون السلاسل النصية غير قابلة للتعديل، وبالتالي يكون المرجع &str
مرجعًا غير قابل للتعديل.
شرائح السلاسل النصية مثل معاملات
تدلنا معرفة أنه يُمكننا أخذ شرائح من السلاسل النصية المجردة والقيم من النوع String
على أنه نستطيع إجراء تحسين واحد إضافي على first_word
وهو بصمة الدالة:
fn first_word(s: &String) -> &str {
قد يكتب مبرمج لغة رست خبير بصمة الدالة السابقة الموضحة في الشيفرة 4-9 بدلًا من ذلك والسبب في هذا هو أن البصمة السابقة تسمح لنا باستخدام الدالة ذاتها على قيمتَي &String
و &str
.
fn first_word(s: &str) -> &str {
[الشيفرة 4-9: تحسين دالة first_word
باستخدام شريحة سلسلة نصية لنوع المُعامل s
]
يُمكننا تمرير شريحة السلسلة النصية مباشرةً في هذه الحالة، فعلى سبيل المثال يمكننا تمرير شريحة من النوع String
أو مرجع إليه إذا كان لدينا النوع String
في معاملات الدالة، وهذا الأمر ممكن بفضل ميزة التحصيل القسري deref corecions التي سنتكلم عنها بالتفصيل لاحقًا. يجعل تعريف الدالة لتأخذ شريحة سلسلة نصية بدلًا من مرجع إلى النوع String
من واجهتنا البرمجية شاملة الاستخدام أكثر ومفيدةً دون خسارة أي من وظائفها.
اسم الملف: src/main.rs
fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..] } fn main() { let my_string = String::from("hello world"); // تعمل الدالة first_word على شرائح النوع String سواءً كانت شريحة جزئية أو شريحة تشكل كامل السلسلة let word = first_word(&my_string[0..6]); let word = first_word(&my_string[..]); // تعمل الدالة first_word على مراجع النوع String والمساوية إلى كامل شرائح String let word = first_word(&my_string); let my_string_literal = "hello world"; // تعمل الدالة first_word على شرائح سلاسل نصية مجردة سواءً كانت مجردة أو جزئية let word = first_word(&my_string_literal[0..6]); let word = first_word(&my_string_literal[..]); // بما أن السلاسل النصية المجردة هي شرائح سلاسل نصية بالأصل، فالتعليمة التالية تعمل أيضًا دون طريقة كتابة الشريحة let word = first_word(my_string_literal); }
الشرائح الأخرى
شرائح السلاسل النصية هي شرائح خاصة بالسلاسل النصية كما قد تتوقع، إلا أن هناك أنواع شرائح عامة أكثر، ألقِ نظرةً على المصفوفة التالية:
let a = [1, 2, 3, 4, 5];
قد نحتاج لاستخدام مرجع يشير إلى جزء من مصفوفة بطريقة مماثلة للسلسلة النصية، ويمكننا تحقيق ذلك الأمر وفق ما يلي:
let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]);
للشريحة النوع &[i32]
وتعمل بطريقة مماثلة لشريحة السلسلة النصية وذلك بتخزين مرجع للعنصر الأول وطول الشريحة، وستستخدم هذا النوع من الشرائح لكافة أنواع التجميعات الأخرى، وسنناقش هذه التجميعات بالتفصيل عندما نتحدث عن الأشعة لاحقًا.
ترجمة -وبتصرف- لقسم من فصل Understanding Ownership من كتاب The Rust Programming Language
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.