الخطوات الأولى للصدأ

الصورة


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


  • سأبدأ مع السمات الرئيسية ، في رأيي ، ميزات Rust
  • سوف أصف تفاصيل بناء الجملة المثيرة للاهتمام
  • سأشرح لماذا من غير المرجح أن يستحوذ الصدأ على العالم

سأوضح على الفور أنني أكتب في جافا لمدة عشر سنوات ، لذلك سأجادل من برج الجرس الخاص بي.


ميزة القاتل


يحاول Rust اتخاذ موقف متوسط ​​بين اللغات ذات المستوى المنخفض مثل C / C ++ و Java / C # / Python / Ruby عالي المستوى ... كلما كانت اللغة أقرب إلى الجهاز ، زاد التحكم ، وأسهل التنبؤ بالكيفية التي سيتم بها تنفيذ التعليمات البرمجية. ولكن الوصول الكامل إلى الذاكرة هو أسهل بكثير لاطلاق النار ساقك. على عكس C / C ++ ، ظهر Python / Java وكل الباقي. لا يحتاجون إلى التفكير في مسح الذاكرة. أسوأ شيء هو NPE ، التسريبات ليست شائعة. ولكن لكي ينجح هذا ، فأنت بحاجة ، على الأقل ، إلى جامع للقمامة ، والذي بدوره يبدأ في عيش حياته بالتوازي مع رمز المستخدم ، مما يقلل من إمكانية التنبؤ به. لا يزال الجهاز الظاهري يوفر استقلالية المنصة ، ولكن ما مدى الحاجة هو نقطة نقاش ، ولن أثيرها الآن.


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


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


يمكن إظهار هذا المفهوم في الجزء التالي من التعليمات البرمجية. يتم استدعاء Test () من الطريقة الرئيسية () ، التي تنشئ بنية بيانات متكررة MyStruct التي تنفذ واجهة destructor. يسمح لك Drop بإعداد المنطق للتنفيذ قبل إتلاف الكائن. شيء مشابه لبرنامج finalizer في Java ، على عكس Java فقط ، فإن استدعاء أسلوب drop () مؤكد تمامًا.


fn main() { test(); println!("End of main") } fn test() { let a = MyStruct { v: 1, s: Box::new( Some(MyStruct { v: 2, s: Box::new(None), }) ), }; println!("End of test") } struct MyStruct { v: i32, s: Box<Option<MyStruct>>, } impl Drop for MyStruct { fn drop(&mut self) { println!("Cleaning {}", self.v) } } 

الاستنتاج سيكون على النحو التالي:


 End of test Cleaning 1 Cleaning 2 End of main 

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


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


ما آخر للاهتمام


بعد ذلك ، أدرج ميزات اللغة بترتيب تنازلي من حيث الأهمية ، في رأيي.


عفوًا


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


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


 fn main() { MyPrinter { value: 10 }.print(); } trait Printer { fn print(&self); } impl Printer { fn print(&self) { println!("hello!") } } struct MyPrinter { value: i32 } impl Printer for MyPrinter { fn print(&self) { println!("{}", self.value) } } 

من الميزات التي لاحظتها ، تجدر الإشارة إلى ما يلي:


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

بعض المزيد من الأمن


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


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


 let mut x = Some(2); let y = x.take(); assert_eq!(x, None); assert_eq!(y, Some(2)); 

التحويلات


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


 pub enum Option<T> { None, Some(T), } 

هناك بناء خاص لمعالجة هذه القيم:


 fn main() { let a = Some(1); match a { None => println!("empty"), Some(v) => println!("{}", v) } } 

كذلك


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


  • لن يخيب مشجعو البرمجة الوظيفية ؛ فهناك خرافات لهم. يحتوي التكرار على طرق لمعالجة المجموعة ، على سبيل المثال ، التصفية و for_each . شيء مثل تدفقات جافا.
  • يمكن أيضًا استخدام بنية المطابقة لأشياء أكثر تعقيدًا من التعداد العادي ، على سبيل المثال ، لمعالجة الأنماط.
  • يوجد عدد كبير من الفصول المدمجة ، على سبيل المثال ، المجموعات: Vec ، LinkedList ، HashMap ، إلخ.
  • يمكنك إنشاء وحدات ماكرو
  • من الممكن إضافة طرق للفئات الموجودة
  • نوع الاستدلال التلقائي المدعومة
  • جنبا إلى جنب مع اللغة يأتي إطار اختبار قياسي
  • يتم استخدام أداة الشحن المدمجة في بناء وإدارة التبعيات

يطير في مرهم


هذا القسم ضروري لاستكمال الصورة.


مشكلة القاتل


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


  • Box - قيمة ثابتة على الكومة ، تناظرية للأغلفة للبدائية في Java
  • قيمة الخلية المتغيرة
  • RefCell - قيمة متغيرة يمكن الوصول إليها بالرجوع إليها
  • Rc - مرجع العداد ، لإشارات متعددة لكائن واحد

وهذه قائمة غير كاملة. بالنسبة لعينة Rust الأولى ، قررت بتهور كتابة قائمة مرتبطة منفردة مع الطرق الأساسية. في النهاية ، أدى الارتباط بالعقدة إلى الخيار التالي <Rc <RefCell <ListNode> >> :


  • الخيار - لمعالجة ارتباط فارغ
  • الصليب الأحمر - لروابط متعددة ، كما تتم الإشارة إلى العقدة الأخيرة بواسطة العقدة السابقة والورقة نفسها
  • RefCell - للرابط القابل للتغيير
  • ListNode - العنصر التالي نفسه

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


 struct ListNode { val: i32, next: Node, } pub struct LinkedList { root: Node, last: Node, } type Node = Option<Rc<RefCell<ListNode>>>; impl LinkedList { pub fn add(mut self, val: i32) -> LinkedList { let n = Rc::new(RefCell::new(ListNode { val: val, next: None })); if (self.root.is_none()){ self.root = Some(n.clone()); } self.last.map(|v| { v.borrow_mut().next = Some(n.clone()) }); self.last = Some(n); self } ... 

في Kotlin ، يبدو الأمر أكثر بساطة:


 public fun add(value: Int) { val newNode = ListNode(null, value); root = root ?: newNode; last?.next = newNode last = newNode; } 

كما اكتشفت لاحقًا ، فإن هذه الهياكل ليست نموذجية بالنسبة إلى Rust ، ورمزي غير مثالي تمامًا. الناس حتى يكتبون مقالات كاملة:



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


صعوبة التعلم


العملية الطويلة لتعلم الصدأ تتبع إلى حد كبير من القسم السابق. قبل كتابة أي شيء على الإطلاق ، عليك قضاء الوقت في إتقان المفهوم الرئيسي لملكية الذاكرة ، كما يتخلل كل سطر. على سبيل المثال ، أخذتني القائمة الأكثر بساطة في أمسيات ، بينما تمت كتابة نفس الشيء في Kotlin خلال 10 دقائق ، على الرغم من أن هذه ليست لغتي العاملة. بالإضافة إلى ذلك ، فإن العديد من الطرق المألوفة لكتابة الخوارزميات أو بنيات البيانات في Rust ستبدو مختلفة أو لن تعمل على الإطلاق. أي عند التبديل إليه ، ستكون هناك حاجة إلى إعادة هيكلة أعمق للتفكير ، ولن يكون مجرد إتقان بناء الجملة كافيًا. هذا أبعد ما يكون عن JavaScript ، والذي يبتلع كل شيء ويتحمله. أعتقد أن روست لن تكون أبدًا اللغة التي يدرس بها الأطفال في مدرسة برمجة. حتى C / C ++ لديه المزيد من الفرص في هذا المعنى.


في النهاية


لقد وجدت أن فكرة إدارة الذاكرة في مرحلة الترجمة ممتعة للغاية. في C / C ++ ، ليس لدي أي خبرة ، لذلك لن أقارن مع المؤشر الذكي. بناء الجملة لطيف بشكل عام ولا يوجد شيء لا لزوم له. لقد انتقدت Rust بسبب تعقيد تطبيق هياكل بيانات الرسم البياني ، لكنني أظن أن هذه سمة من سمات جميع لغات البرمجة التي لا تنتمي إلى GC. ربما المقارنة مع Kotlin لم تكن صادقة تماما.


تودو


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


قراءة


إذا كنت مهتمًا بالصدأ ، فإليك بعض الروابط:



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

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


All Articles