مقال آخر عن عمر في الصدأ

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


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


لقد جعلني أكتب أيضًا ، على سبيل المثال ، من monad ، يمكنك العثور على بعض المواد التدريبية الرسمية ، لكنها ليست مفهومة جيدًا دائمًا ، ولا ينشأ الفهم إلا بعد قراءة شيء مثل "مقدمة أخرى" حول هذا الموضوع.


العمر


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


بادئ ذي بدء ، يتم تحديد العمر الافتراضي للقيمة من خلال الجزء التالي:


  • بداية الحياة: خلق القيمة. هذا أمر شائع بالنسبة لمعظم لغات البرمجة ، لذلك لا يحمل أي حمل غير عادي.
  • نهاية الحياة. هذا هو المكان الذي سيدعوه Rust تلقائيًا إلى المدمر وينسى القيمة. في كتلة النطاق ، سيحدث هذا في نهاية هذه الكتلة دون تحريك. إن التتبع العقلي لنهاية الحياة هو ، في رأيي ، مفتاح التفاعل الناجح مع المدقق.

سأضيف تفاصيل قد تكون في متناول اليد: إذا كان هناك العديد من القيم في النطاق ، فسوف يتم تدميرها بالترتيب العكسي للخليقة.


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


يمكن تشغيل الأمثلة هنا: https://play.rust-lang.org/


fn main() { { //    let a = "a".to_string(); // <-   "a" let b = 100; // <-   "b" // <-   b // <-   a } //    //     "a"  "b" } 

مع كتلة بسيطة ، كل شيء بسيط نسبياً ، تحدث المرحلة التالية عندما نستخدم أشياء تبدو بسيطة مثل الوظائف والإغلاقات:


تتحرك


أضف مفهومًا مثل تحريك القيمة. بعبارة بسيطة ، تعني كلمة "نقل" أن الكتلة الحالية لم تعد مهتمة بمصير القيمة وأنها تنسى عنها ، ويتم نقل مصيرها إلى كتلة أخرى ، على سبيل المثال ، إلى وظيفة أخرى ، أو إلى إغلاق ، أو ببساطة إلى قيمة أخرى.


 fn f<T: std::fmt::Display>(x: T) { //   ,         . println!("{}", x); // <-  ,   "a",    . } fn main() { let a = "a".to_string(); // "a"    let b = 2; f(a); //   "a"  f //        f(a) -   ,    "a"        .    a  b,    ,      Copy   . // "b" . } 

مع الاغلاقات.


من أجل إغلاق عملية نقل القيمة التي تم التقاطها إلى حظرها ، يتم استخدام نقل الكلمة الرئيسية ؛ إذا لم تكتب حركة ، فسيتم استعارة القيمة التي سأكتب عنها قريبًا.


 fn main() { let a = "a".to_string(); // "a"    let b = 2; let f_1 = move || {println!("{}", a)}; //   "a" //    "a"    . // let f_2 = move || {println!("{}", a)}; f_1(); } 

يمكنك الانتقال إلى الوظيفة ومن الوظيفة أو إلى قيمة أخرى.


يوضح هذا المثال كيفية تتبع الطرق التي تتحرك بها القيم من أجل العيش في سلام مع المدقق المقترض.


 fn f(x: String) -> String { x + " and x" //    x   +,     . //  +   String,    . } fn main() { let a = "a".to_string(); //  "a" let b = f(a); //  "a"  "f",  f     b. println!("{}", b); // "a"   . } 

الإقراض


نقدم هذا المفهوم الجديد: على عكس الحركة ، يعني هذا أن الكتلة الحالية تحتفظ بالتحكم في القيمة ، فهي تسمح ببساطة للكتلة الأخرى باستخدام قيمتها.


وألاحظ أن الاقتراض يحدث أيضًا عند نهايته ، وهو أمر غير مهم في هذه الأمثلة ، ولكنه سينبثق في الفقرة التالية.


ملاحظة: لن أكتب عن كيفية تحديد العمر مباشرة في الوظيفة ، لأن الصدأ الحديث يفعل هذا تلقائيًا بشكل أفضل مما كان عليه في الأيام القديمة ، والكشف عن كل هذا هو بضع صفحات أخرى.


 fn f(x: &String) { //   &,    . println!("{}", x); // <-  ,  "x"     } fn main() { let a = "a".to_string(); // "a"    f(&a); //   "a"  f //   f(&a); //    -  . println!("{}", a); //   // "a"  . } 

مع إغلاق بالمثل:


 fn main() { let mut a = "a".to_string(); // "a"    let f_1 = || a.push_str("and x"); //   "a" let f_2 = || a.push_str("and x"); //   f_1(); f_2(); println!("{}", a); // "a"  . } 

في الواقع ، في معظم هذه الإنشاءات البسيطة ، يحتاج المستخدم فقط إلى تحديد المكان الذي يريد إنهاء عمر القيمة فيه: في نهاية الكتلة الحالية وإقراضها لبعض الوظائف ، أو ، إذا علمنا أننا لم نعد بحاجة إلى القيمة ، فقم بنقلها إلى الوظيفة في النهاية من خلالها سيتم تدميرها بنفسها ، كلما قمنا بتحرير الذاكرة بشكل أسرع ، لكن القيمة لن تكون متوفرة في الكتلة الحالية.


قابلية التبادل


في الراستا ، على سبيل المثال ، في kotlin ، هناك تقسيم إلى قيم قابلة للتغيير وغير مستقرة. لكن المشكلة تكمن في أن قابلية التحويل لها تأثير على الإقراض:
يمكنك استعارة قيمة غير مستقرة عدة مرات ، ويمكن استعارة قيمة قابلة للتغيير مرة واحدة فقط. لا يمكنك تحويل قيمة تم اقتراضها بالفعل من قبل.


مثال لا يرتبط بالمثال السابق ، عندما ينقذنا هذا المفهوم من المشاكل عن طريق حظر الإقراض المتغير وغير المستقر المتزامن:


 fn main() { let mut a = "abc".to_string(); for x in a.chars() { //   a.push_str(" and "); //  .  . a.push(x); } } 

من الضروري بالفعل تخزين العديد من الحيل من أجل تلبية المطالب العادلة للراستا في معظم الأحيان. في المثال أعلاه ، أسهل طريقة هي استنساخ "a" -> سيحصل المستنسخ على قرض غير مستقر ، وليس مرتبطًا بـ "a" الأصلي.


 for x in a.clone().chars() { //  ,   . a.push_str(" and "); //  .      -   . 

لكن من الأفضل أن أعود إلى أمثلةنا للحفاظ على الاتساق. نحتاج إلى تغيير "a" ولا يمكننا القيام بذلك.


 fn main() { let mut a = "a".to_string(); // "a"    let mut f_1 = || a.push_str(" and x"); //   "a".   - ,  mut  mut. //      ,   f_1  . let mut f_2 = || a.push_str(" and y"); //     : second mutable borrow occurs here f_1(); f_2(); println!("{}", a); } 

طفرة خفية


من الناحية النظرية ، يمكن تمرير إغلاق لبعض الوظائف التي تعالج ، على سبيل المثال ، بشكل غير متزامن في سلسلة رسائل أخرى ، ومن ثم سنواجه مشاكل بالفعل ، ولكن في هذه الحالة ، يتم إعادة تأمين المدقق ، على الرغم من أن هذا لا يلغي حقيقة أننا بحاجة إلى الموافقة عليها بطريقة أو بأخرى .


خلاصة القول: نحتاج إلى اقتراضين متحورين ، لكن الصدأ لا يسمح إلا بشيء واحد ، لكن مخترعي الماكرة في الراستا يتوصلون إلى "طفرة خفية": RefCell.


RefCell - ما نلفه في RefCell - يعتبره النقطية أمرًا ضروريًا ، ومع ذلك ، وباستخدام دالة الاقتراض () يمكننا استخلاص ارتباط قابل للتغيير يمكن من خلاله تغيير القيمة ، ولكن هناك فارق بسيط مهم : لا يمكن الحصول على الرابط إلا عندما يتأكد RefCell في وقت التشغيل من عدم وجود آخرين قروض نشطة ، وإلا فإنه سوف يلقي الذعر ، أو إرجاع خطأ إذا تم استخدام try_borrow_mut (). أي هنا يعطي النمو كل المخاوف بشأن الإقراض لرعاية المستخدم ، ويجب عليه هو نفسه التأكد من أنه لا يستعير القيمة من عدة أماكن في وقت واحد.


 use std::cell::RefCell; fn main() { let a = RefCell::new("a".to_string()); // "a"    let f_1 = || a.borrow_mut().push_str(" and x"); //    "a" let f_2 = || a.borrow_mut().push_str(" and y"); //    f_1(); //      a.borrow_mut() ,           mut    . f_2(); //   . println!("{}", a.borrow()); //         . } 

الصليب الأحمر وصلة مكافحة


هذا البناء مألوف في العديد من اللغات ، ويستخدم في الصدأ ، عندما ، على سبيل المثال ، لا يمكننا استعارة قيمة لسبب ما ، وهناك حاجة إلى وجود العديد من القيم المرجعية لقيمة واحدة واحدة. Rc ، كما يوحي الاسم ، هو مجرد عداد مرجعي يمتلك قيمة ، ويمكنه استعارة روابط غير مستقرة ، وحساب عددهم ، وبمجرد إعادة تعيين الرقم ، يدمر القيمة ونفسه. اتضح أن Rc يسمح ، كما كان ، بتوسيع العمر الافتراضي للقيمة الموجودة به.


سأضيف أن الصدأ يمكن أن يقوم تلقائيًا بالتعرف على الهياكل التي تم تعريفه لها ، مما يعني أنه للعمل مع Rc ، كقاعدة عامة ، لا تحتاج إلى أي استخراج إضافي للقيمة الداخلية ونحن نعمل فقط مع Rc كما هو الحال مع القيمة بداخلها.


هنا تم التفكير في مثال بسيط قليلاً ، دعنا نحاول محاكاة حقيقة أن الإغلاق من المثال أعلاه لا يريد أن يقبل & T أو & String ، لكنه يريد سلسلة فقط:


 fn f(x: String) { //  String,    &String println!("{}", x); } fn main() { let a = "a".to_string(); let f_1 = move || f(a); //   move,    ... let f_2 = move || f(a); // ...     ,           f_1(); f_2(); println!("{}", a); } 

يمكن حل هذه المشكلة بسهولة إذا استطعنا تغيير الوظيفة إلى fn f(x: &String) (أو & str) ، ولكن دعونا نتخيل أنه لسبب ما لا يمكننا استخدام &


نحن نستخدم rc


 use std::rc::Rc; fn f(x: Rc<String>) { //       Rc println!("{}", x); //     ,  println          ,           ,       ,    . } fn main() { let a_rc = Rc::new("a".to_string()); //  Rc   let a_ref_1 = a.clone(); //   -,  . let a_ref_2 = a.clone(); //   let f_1 = move || f(a_ref_1); //      - let f_2 = move || f(a_ref_2); //  f_1(); f_2(); println!("{}", a_rc); //     Rc  . //    a_rc       . } 

سأضيف المثال الأخير ، لأن أحد أزواج الحاويات الأكثر شيوعًا التي يمكن العثور عليها هو Rc <RefCell>

 use std::rc::Rc; use std::cell::RefCell; fn f(x: Rc<RefCell<String>>) { x.borrow_mut().push_str(" and x"); //      ,       ,   . } fn main() { let a = Rc::new(RefCell::new("a".to_string())); //      let a_ref_1 = a.clone(); let a_ref_2 = a.clone(); let f_1 = move || f(a_ref_1); let f_2 = move || f(a_ref_2); f_1(); f_2(); println!("{}", a.borrow()); // Rc   ,   RefCell   } 

علاوة على ذلك ، سيكون من المنطقي نقل هذا البرنامج التعليمي إلى نسخة تماثلية آمنة من خيط Rc-Arc ومن ثم المتابعة حول Mutex ، لكنك لن تتحدث عن سلامة الخيط ومدقق المقترض في فقرة واحدة ، وليس من الواضح ما إذا كانت هناك حاجة إلى هذا النوع من المقالة على الإطلاق ، نظرًا لوجود خيط رسمي رسمي. لذلك أختتم.

Source: https://habr.com/ru/post/ar442962/


All Articles