على الأصابع: الأنواع المرتبطة في الصدأ وما هو اختلافها عن وسيطات الكتابة

لماذا يوجد لدى Rust أنواع مرتبطة ، وما هو الفرق بينها وبين وسيطات تُعرف أيضًا باسم generics ، لأنها متشابهة جدًا؟ ألا يكفي هذا الأخير فقط ، كما هو الحال في جميع اللغات العادية؟ بالنسبة لأولئك الذين بدأوا للتو في تعلم Rust ، وخاصة بالنسبة للأشخاص الذين يأتون من لغات أخرى ("هذا هو الأدوية الجنيسة!" - سوف يقول javist ، الحكيم لسنوات) ، هذا السؤال يطرح بانتظام. هيا بنا


TL ؛ DR السابق يتحكم في الكود المسمى ، والأخير المتصل.


الوراثة مقابل الأنواع المرتبطة


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


trait Foo<T> { fn bar(self, x: T); } 

هنا T هو بالضبط وسيطة النوع. يبدو أن هذا يجب أن يكون كافيًا للجميع (مثل 640 كيلو بايت من الذاكرة). لكن في Rust ، هناك أيضًا أنواع مرتبطة ، شيء مثل هذا:


 trait Foo { type Bar; //    fn bar(self, x: Self::Bar); } 

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


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


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


 trait AsRef<T> { fn as_ref(&self) -> &T; } 

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


 let foo = Foo::new(); let bar: &Bar = foo.as_ref(); 

هنا ، سيستخدم المترجم ، الذي يستخدم معرفة bar: &Bar ، تطبيق AsRef<Bar> لاستدعاء طريقة as_ref() ، لأنه نوع as_ref() المطلوب من قبل المتصل. غني عن القول أن نوع Foo يجب أن يقوم بتطبيق سمة AsRef AsRef<Bar> ، وإلى جانب ذلك ، يمكنه تطبيق العديد من AsRef<T> ، والتي يختار المتصل من بينها الخيار المرغوب.


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


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


 trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } 

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


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


 trait GenericIterator<T> { fn next(&mut self) -> Option<T>; } 

لكن الآن ، أولاً ، يجب الإشارة إلى النوع T مرارًا وتكرارًا في كل مكان تم ذكر مكرر فيه ، وثانيًا ، أصبح من الممكن الآن تنفيذ هذه السمة عدة مرات مع أنواع مختلفة ، والتي تبدو غريبة بالنسبة للمكرر. هنا مثال:


 struct MyIterator; impl GenericIterator<i32> for MyIterator { fn next(&mut self) -> Option<i32> { unimplemented!() } } impl GenericIterator<String> for MyIterator { fn next(&mut self) -> Option<String> { unimplemented!() } } fn test() { let mut iter = MyIterator; let lolwhat: Option<_> = iter.next(); // Error! Which impl of GenericIterator to use? } 

رؤية الصيد؟ لا يمكننا أن نأخذها ونطلق عليها iter.next() بدون يجلس القرفصاء - نحتاج إلى iter.next() المترجم ، صراحة أو ضمنيًا ، بالنوع الذي سيتم إرجاعه. ويبدو الأمر محرجًا: لماذا يجب أن نعرف (ونخبر المترجم!) من جانب الدعوة ، النوع الذي سيعود التكرار ، بينما يجب أن يعرف هذا التكرار بشكل أفضل نوعه الذي سيعود؟! وكل ذلك لأننا كنا قادرين على تطبيق GenericIterator GenericIterator مرتين باستخدام معلمة مختلفة لنفس MyIterator ، والتي MyIterator من وجهة نظر دلالات التكرار سخيفة أيضًا: لماذا يمكن أن يقوم نفس التكرار بإرجاع قيم من أنواع مختلفة؟


إذا عدنا إلى البديل مع النوع المرتبط ، فيمكن تجنب كل هذه المشكلات:


 struct MyIter; impl Iterator for MyIter { type Item = String; fn next(&mut self) -> Option<Self::Item> { unimplemented!() } } fn test() { let mut iter = MyIter; let value = iter.next(); } 

هنا ، أولاً ، سينتج عن المحول البرمجي value: Option<String> بشكل صحيح value: Option<String> النوع بدون كلمات غير ضرورية ، وثانياً ، لن يعمل على تنفيذ MyIter Iterator لـ MyIter مرة ثانية بنوع إرجاع مختلف ، وبالتالي تدمير كل شيء.


لإصلاح. يمكن أن تقوم المجموعة بتنفيذ مثل هذه الميزة لتتمكن من تحويل نفسها إلى مكرر:


 trait IntoIterator { type Item; type IntoIter: Iterator<Item=Self::Item>; fn into_iter(self) -> Self::IntoIter; } 

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


أكثر على الأصابع


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


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


 trait Add<RHS> { type Output; fn add(self, rhs: RHS) -> Self::Output; } 

هنا لدينا وسيطة RHS "الإدخال" - هذا هو النوع الذي سنطبق عليه عملية الإضافة بنوعنا. وهناك حجة "إخراج" Add::Output - هذا هو النوع الذي سينتج عن الإضافة. في الحالة العامة ، يمكن أن تختلف عن نوع المصطلحات ، والتي بدورها يمكن أن تكون أيضًا من أنواع مختلفة (تضاف لذيذ إلى الأزرق وتصبح ناعمة - لكن ماذا أفعل هذا طوال الوقت). يتم تحديد الأول باستخدام وسيطة الكتابة ، ويتم تحديد الثاني باستخدام النوع المقترن.


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


دعنا نحاول تنفيذ هذه الصفة:


 use std::ops::Add; struct Foo(&'static str); #[derive(PartialEq, Debug)] struct Bar(&'static str, i32); impl Add<i32> for Foo { type Output = Bar; fn add(self, rhs: i32) -> Bar { Bar(self.0, rhs) } } fn test() { let x = Foo("test"); let y = x + 42; //      <Foo as Add>::add(42)  x assert_eq!(y, Bar("test", 42)); } 

في هذا المثال ، يتم تحديد نوع المتغير y بواسطة خوارزمية الإضافة ، وليس رمز الاستدعاء. سيكون غريباً للغاية إذا كان من الممكن كتابة شيء مثل let y: Baz = x + 42 ، أي فرض عملية الإضافة لإرجاع نتيجة من نوع غريب. من مثل هذه الأشياء ، يؤمن لنا النوع المرتبط Add::Output .


المجموع


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


هل فشلت العملة؟ قتلي مع التعليقات.

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


All Articles