المراجع والاستعارة (References and Borrowing)
المشكلة في كود الصفوف (tuple) في القائمة 4-5 هي أننا نضطر إلى إعادة String إلى الدالة المستدعية حتى نتمكن من الاستمرار في استخدام String بعد استدعاء calculate_length ، لأن String تم نقله (moved) إلى calculate_length. بدلاً من ذلك، يمكننا تقديم مرجع (reference) لقيمة String. المرجع يشبه المؤشر (pointer) في أنه عنوان يمكننا اتباعه للوصول إلى البيانات المخزنة في ذلك العنوان؛ تلك البيانات مملوكة لمتغير آخر. على عكس pointer، يضمن المرجع أن يشير إلى قيمة صالحة من نوع معين طوال فترة حياة ذلك المرجع.
إليك كيف يمكنك تعريف واستخدام دالة calculate_length التي تحتوي على مرجع لكائن كمعلمة (parameter) بدلاً من أخذ ملكية القيمة:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
أولاً، لاحظ أن كل كود tuple في تصريح المتغير وقيمة إرجاع الدالة قد اختفى. ثانياً، لاحظ أننا نمرر &s1 إلى calculate_length وفي تعريفها، نأخذ &String بدلاً من String. تمثل علامات الاند (ampersands) هذه المراجع، وهي تسمح لك بالإشارة إلى قيمة ما دون أخذ ملكيتها. يوضح الشكل 4-6 هذا المفهوم.
الشكل 4-6: مخطط لـ &String s يشير إلى String s1
ملاحظة: عكس عملية الإسناد المرجعي (referencing) باستخدام
&هو إلغاء المرجعية (dereferencing)، والذي يتم تحقيقه باستخدام عامل إلغاء المرجعية (dereference operator)*. سنرى بعض استخدامات dereference operator في الفصل 8 ونناقش تفاصيل dereferencing في الفصل 15.
دعونا نلقي نظرة فاحصة على استدعاء الدالة هنا:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
يسمح لنا بناء جملة &s1 بإنشاء مرجع يشير إلى قيمة s1 ولكنه لا يملكها. ولأن المرجع لا يملكها، فإن القيمة التي يشير إليها لن يتم حذفها (dropped) عندما يتوقف استخدام المرجع.
وبالمثل، يستخدم توقيع الدالة & للإشارة إلى أن نوع parameter s هو مرجع. دعونا نضيف بعض التوضيحات الشارحة:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
// it refers to, the String is not dropped.
النطاق (scope) الذي يكون فيه المتغير s صالحاً هو نفس نطاق أي parameter للدالة، ولكن القيمة التي يشير إليها المرجع لا يتم حذفها عندما يتوقف استخدام s ، لأن s لا يملك الملكية. عندما تحتوي الدوال على مراجع كمعلمات بدلاً من القيم الفعلية، فلن نحتاج إلى إعادة القيم من أجل إعادة الملكية، لأننا لم نمتلك الملكية أبداً.
نسمي عملية إنشاء مرجع استعارة (borrowing). كما هو الحال في الحياة الواقعية، إذا كان شخص ما يملك شيئاً ما، يمكنك استعارته منه. عندما تنتهي، عليك إعادته. أنت لا تملكه.
لذا، ماذا يحدث إذا حاولنا تعديل شيء نستعيره؟ جرب الكود في القائمة 4-6. تنبيه: إنه لا يعمل!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
إليك الخطأ:
$ 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
|
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
تماماً كما أن المتغيرات غير قابلة للتغيير (immutable) بشكل افتراضي، كذلك المراجع. لا يُسمح لنا بتعديل شيء لدينا مرجع له.
المراجع القابلة للتغيير (Mutable References)
يمكننا إصلاح الكود من القائمة 4-6 للسماح لنا بتعديل قيمة مستعارة ببعض التعديلات الصغيرة التي تستخدم، بدلاً من ذلك، مرجعاً قابلاً للتغيير (mutable reference):
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
أولاً، نقوم بتغيير s ليكون mut. بعد ذلك، ننشئ mutable reference باستخدام &mut s حيث نستدعي دالة change ونقوم بتحديث توقيع الدالة لقبول mutable reference باستخدام some_string: &mut String. هذا يجعل من الواضح جداً أن دالة change ستقوم بتغيير (mutate) القيمة التي تستعيرها.
المراجع القابلة للتغيير لها قيد واحد كبير: إذا كان لديك mutable reference لقيمة ما، فلا يمكن أن يكون لديك مراجع أخرى لتلك القيمة. هذا الكود الذي يحاول إنشاء اثنين من mutable references لـ s سيفشل:
fn main() {
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` (bin "ownership") due to 1 previous error
يقول هذا الخطأ أن هذا الكود غير صالح لأننا لا نستطيع استعارة s كـ mutable أكثر من مرة في كل مرة. الاستعارة القابلة للتغيير الأولى موجودة في r1 ويجب أن تستمر حتى يتم استخدامها في println! ، ولكن بين إنشاء ذلك المرجع القابل للتغيير واستخدامه، حاولنا إنشاء mutable reference آخر في r2 يستعير نفس البيانات مثل r1.
القيد الذي يمنع وجود مراجع متعددة قابلة للتغيير لنفس البيانات في نفس الوقت يسمح بالتغيير ولكن بطريقة محكومة للغاية. إنه شيء يعاني منه الـ Rustaceans الجدد لأن معظم اللغات تسمح لك بالتغيير وقتما تشاء. الفائدة من وجود هذا القيد هي أن Rust يمكنها منع سباقات البيانات (data races) في وقت التصريف (compile time). سباق البيانات يشبه حالة السباق (race condition) ويحدث عند وقوع هذه السلوكيات الثلاثة:
- وصول اثنين أو أكثر من pointers إلى نفس البيانات في نفس الوقت.
- استخدام واحد على الأقل من pointers للكتابة في البيانات.
- عدم وجود آلية مستخدمة لمزامنة الوصول إلى البيانات.
تسبب سباقات البيانات سلوكاً غير محدد (undefined behavior) ويمكن أن يكون من الصعب تشخيصها وإصلاحها عندما تحاول تعقبها في وقت التشغيل (runtime)؛ تمنع Rust هذه المشكلة برفض تصريف الكود الذي يحتوي على data races!
كما هو الحال دائماً، يمكننا استخدام الأقواس المتعرجة لإنشاء scope جديد، مما يسمح بوجود مراجع متعددة قابلة للتغيير، ولكن ليس مراجع متزامنة (simultaneous):
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
}
تفرض Rust قاعدة مماثلة للجمع بين المراجع القابلة للتغيير وغير القابلة للتغيير. يؤدي هذا الكود إلى حدوث خطأ:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{r1}, {r2}, and {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!("{r1}, {r2}, and {r3}");
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
يا للهول! لا يمكننا أيضاً الحصول على mutable reference بينما لدينا مرجع غير قابل للتغيير لنفس القيمة.
لا يتوقع مستخدمو المرجع غير القابل للتغيير أن تتغير القيمة فجأة من تحتهم! ومع ذلك، يُسمح بوجود مراجع متعددة غير قابلة للتغيير لأن لا أحد يقرأ البيانات فقط لديه القدرة على التأثير على قراءة أي شخص آخر للبيانات.
لاحظ أن scope المرجع يبدأ من حيث يتم تقديمه ويستمر حتى آخر مرة يتم فيها استخدام ذلك المرجع. على سبيل المثال، سيتم تصريف هذا الكود لأن آخر استخدام للمراجع غير القابلة للتغيير هو في println! ، قبل تقديم mutable reference:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// Variables r1 and r2 will not be used after this point.
let r3 = &mut s; // no problem
println!("{r3}");
}
تنتهي نطاقات المراجع غير القابلة للتغيير r1 و r2 بعد println! حيث تم استخدامهما لآخر مرة، وهو ما يحدث قبل إنشاء mutable reference r3. هذه النطاقات لا تتداخل، لذا فإن هذا الكود مسموح به: يمكن لـ compiler معرفة أن المرجع لم يعد مستخدماً عند نقطة قبل نهاية scope.
على الرغم من أن أخطاء borrowing قد تكون محبطة في بعض الأحيان، تذكر أن compiler في Rust هو من يشير إلى خطأ محتمل في وقت مبكر (في compile time بدلاً من runtime) ويوضح لك بالضبط مكان المشكلة. عندها، لن تضطر إلى تعقب سبب عدم كون بياناتك كما كنت تعتقد.
المراجع المعلقة (Dangling References)
في اللغات التي تحتوي على pointers، من السهل إنشاء مؤشر معلق (dangling pointer) عن طريق الخطأ — وهو مؤشر يشير إلى موقع في الذاكرة ربما تم إعطاؤه لشخص آخر — عن طريق تحرير بعض الذاكرة مع الاحتفاظ بمؤشر لتلك الذاكرة. في Rust، على النقيض من ذلك، يضمن compiler أن المراجع لن تكون أبداً مراجع معلقة (dangling references): إذا كان لديك مرجع لبعض البيانات، فسيضمن compiler أن البيانات لن تخرج عن النطاق قبل أن يخرج المرجع للبيانات عن النطاق.
دعونا نحاول إنشاء dangling reference لنرى كيف تمنعها Rust بخطأ في وقت التصريف:
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, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
تشير رسالة الخطأ هذه إلى ميزة لم نغطها بعد: فترات الحياة (lifetimes). سنناقش lifetimes بالتفصيل في الفصل 10. ولكن، إذا تجاهلت الأجزاء المتعلقة بـ lifetimes، فإن الرسالة تحتوي بالفعل على المفتاح لسبب كون هذا الكود مشكلة:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
دعونا نلقي نظرة فاحصة على ما يحدث بالضبط في كل مرحلة من مراحل كود dangle الخاص بنا:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
// Danger!
لأن s تم إنشاؤه داخل dangle ، فعند انتهاء كود dangle ، سيتم إلغاء تخصيص (deallocated) s. لكننا حاولنا إعادة مرجع له. هذا يعني أن هذا المرجع سيشير إلى String غير صالح. هذا ليس جيداً! لن تسمح لنا Rust بالقيام بذلك.
الحل هنا هو إعادة String مباشرة:
fn main() {
let string = no_dangle();
}
fn no_dangle() -> String {
let s = String::from("hello");
s
}
هذا يعمل دون أي مشاكل. يتم نقل الملكية للخارج، ولا يتم إلغاء تخصيص أي شيء.
قواعد المراجع (The Rules of References)
دعونا نلخص ما ناقشناه حول المراجع:
- في أي وقت معين، يمكنك الحصول على إما مرجع واحد قابل للتغيير أو أي عدد من المراجع غير القابلة للتغيير.
- يجب أن تكون المراجع صالحة دائماً.
بعد ذلك، سننظر في نوع مختلف من المراجع: الشرائح (slices).