ملاحظة مترجم: السجل بتاريخ 13 مايو 2014 ، لذلك قد لا تتوافق بعض التفاصيل ، بما في ذلك شفرة المصدر مع الحالة الحالية للأشياء. الجواب على السؤال حول سبب الحاجة إلى ترجمة مثل هذا المنشور القديم سيكون قيمة محتواه لتشكيل فهم لأحد المفاهيم الأساسية للغة الصدأ ، مثل الطلاقة.
بمرور الوقت ، أصبحت مقتنعًا بأنه سيكون من الأفضل التخلي عن التمييز بين المتغيرات المحلية القابلة للتغيير والثابتة في Rust. كثير من الناس على الأقل يشككون في هذه المسألة. أردت أن أوضح موقفي في العلن. سأعطيك دوافع مختلفة: فلسفية وتقنية وعملية ، بالإضافة إلى الدفاع الرئيسي عن النظام الحالي. (ملاحظة: رأيت هذا على أنه Rust RFC ، لكنني قررت أن النغمة أفضل لمشاركة مدونة وليس لدي الوقت لإعادة كتابتها الآن.)
شرح
لقد كتبت هذا المقال بشكل حاسم وأعتقد أن الخط الذي أدافع عنه سيكون صحيحا. ومع ذلك ، إذا لم ننتهي من دعم النظام الحالي ، فلن تكون هذه كارثة أو شيء من هذا القبيل. لها مزاياها ، وعموما أجدها ممتعة للغاية. أعتقد فقط أنه يمكننا تحسينه.
في كلمة واحدة
أود إزالة التمييز بين المتغيرات المحلية الثابتة والمتغيرة وإعادة تسمية مؤشرات &mut
إلى &my
، &only
or &uniq
(لا فرق بالنسبة لي). إذا لم يكن هناك أي كلمة رئيسية mut
.
دافع فلسفي
السبب الرئيسي لرغبتي في ذلك هو أنني أعتقد أن هذا سيجعل اللغة أكثر اتساقًا وسهولة في الفهم. بشكل أساسي ، سيعيد هذا توجيهنا من الحديث عن قابلية التغيير إلى الحديث عن استخدام الأسماء المستعارة (والتي سأطلق عليها "المشاركة" ، انظر أدناه).
يصبح التغيُّر نتيجةً للتفرد: "يمكنك دائمًا تغيير كل شيء لديك حق وصول فريد إليه. وعادةً ما تكون البيانات المشتركة ثابتة ، ولكن إذا احتجت إلى ذلك ، يمكنك تغييرها باستخدام نوع من أنواع Cell
".
وبعبارة أخرى ، بمرور الوقت ، أصبح من الواضح لي أن المشاكل المتعلقة بسباق البيانات وأمن الذاكرة تنشأ عندما يكون لديك كل من استخدام الأسماء المستعارة وقابلية التغيير. النهج الوظيفي لحل هذه المشكلة هو القضاء على التحولات. سيكون نهج روست هو إزالة استخدام الأسماء المستعارة. هذا يعطينا قصة يمكن روايتها وسوف تساعدنا على اكتشافها.
ملاحظة حول المصطلحات: أعتقد أننا يجب أن نشير إلى استخدام الأسماء المستعارة كفصل لا يعطي "اسم مستعار" فهماً لما هو على المحك ). في الماضي ، تجنبنا ذلك بسبب مراجعه المتعددة الخيوط. ومع ذلك ، إذا / عندما نفذنا خطط موازاة البيانات التي اقترحتها ، فإن هذا المفهوم ليس غير مناسب تمامًا. في الواقع ، نظرًا للعلاقة الوثيقة بين أمان الذاكرة وسباق البيانات ، أريد حقًا تعزيز هذا الدلالة.
الدافع التربوي
أعتقد أن القواعد الحالية أكثر صعوبة في الفهم مما ينبغي. ليس من الواضح ، على سبيل المثال ، أن &mut T
لا تعني أي ملكية مشتركة. بالإضافة إلى ذلك ، يشير التعيين &mut T
إلى أن &T
لا تعني أي طفرة ، وهي ليست دقيقة تمامًا ، نظرًا لأنواع مثل Cell
. ومن المستحيل الاتفاق على ما يمكن تسميتها ("الروابط القابلة للتغيير / غير القابلة للتغيير" هي الأكثر شيوعًا ، ولكن هذا ليس صحيحًا تمامًا).
في المقابل ، يبدو أن نوعًا مثل &my T
أو &only T
يبسط التفسير. هذا رابط فريد - بطبيعة الحال ، لا يمكنك إجبار اثنين منهم على الإشارة إلى نفس المكان. والتغير شيء متعامد: إنه يأتي من التفرد ، ولكنه ينطبق أيضًا على الخلايا. ونوع &T
هو عكس ذلك تمامًا ، وهو رابط مشترك . يوفر RFC PR # 58 عددًا من الحجج المتشابهة. لن أكررها هنا.
الدافع العملي
حاليًا ، هناك فجوة بين المؤشرات المستعارة ، والتي يمكن أن تكون إما مشتركة أو قابلة للتغيير + فريدة ومتغيرات محلية تكون دائمًا فريدة ، ولكنها يمكن أن تكون قابلة للتغيير أو ثابتة. والنتيجة النهائية لذلك هي أنه يجب على المستخدمين نشر إعلانات mut
على الأشياء التي لا يمكن تحريرها بشكل مباشر.
لا يمكن نمذجة المتغيرات المحلية باستخدام المراجع
تحدث هذه الظاهرة لأن الروابط ليست معبرة مثل المتغيرات المحلية. بشكل عام ، يمنع هذا التجريد. دعني أعطيك بعض الأمثلة لشرح ما أعنيه. تخيل أن لدي بنية بيئة يخزن المؤشر إلى عداد الأخطاء:
struct Env { errors: &mut usize }
يمكنني الآن إنشاء مثيلات لهذا الهيكل (واستخدامها):
let mut errors = 0; let env = Env { errors: &mut errors }; ... if some_condition { *env.errors += 1; }
حسنًا ، تخيل الآن أنني أريد فصل الرمز الذي يعدل env.errors
إلى وظيفة منفصلة. قد أعتقد أنه نظرًا لأن المتغير env
لم يتم الإعلان عنه على أنه قابل للتغيير ، يمكنني استخدام الرابط الثابت والثابت:
let mut errors = 0; let env = Env { errors: &mut errors }; helper(&env); fn helper(env: &Env) { ... if some_condition { *env.errors += 1;
لكن الأمر ليس كذلك. تكمن المشكلة في أن &Env
هو نوع ملكية مشتركة ( ملاحظة المترجم: كما تعلم ، يمكن أن يوجد أكثر من مرجع كائن غير قابل للتغيير في وقت واحد ) ، وبالتالي يظهر env.errors
في مساحة تسمح env.errors
كائن env
بشكل منفصل. لكي يعمل هذا الرمز ، يجب أن أعلن أن env
قابل للتغيير واستخدام رابط &mut
( ملاحظة المترجم: &mut
) لإخبار المترجم بأن env
فريد في الملكية ، حيث يمكن أن يوجد مرجع كائن قابل للتحويل واحد فقط في كل مرة ويتم استبعاد سباق البيانات ، لكن mut
لأنه لا يمكنك إنشاء مرجع قابل للتغيير إلى كائن غير قابل للتغيير ):
let mut errors = 0; let mut env = Env { errors: &mut errors }; helper(&mut env);
تنشأ هذه المشكلة لأننا نعلم أن المتغيرات المحلية فريدة من نوعها ، ولكن لا يمكننا وضع هذه المعرفة في مرجع مستعار دون جعلها قابلة للتغيير.
تحدث هذه المشكلة في عدد من الأماكن الأخرى. لقد كتبنا حتى الآن عن هذا الأمر بطرق مختلفة ، لكن ما زلت أشعر بالاشمئزاز من الشعور بأننا نتحدث عن استراحة ، وهذا ببساطة لا ينبغي أن يكون.
اكتب التحقق من الإغلاق
كان علينا أن نتغلب على هذا القيد في حالة الإغلاق. عمليات الإغلاق مفتوحة في الغالب في هياكل مثل Env
، ولكن ليس تمامًا. هذا لأنني لا أريد أن أطلب التصريح عن المتغيرات المحلية إذا تم استخدامها عن طريق &mut
في الإغلاق. بمعنى آخر ، خذ بعض التعليمات البرمجية ، على سبيل المثال:
fn foo(errors: &mut usize) { do_something(|| *errors += 1) }
سيؤدي التعبير الذي يصف الإغلاق فعليًا إلى إنشاء مثيل من بنية Env
:
struct ClosureEnv<'a, 'b> { errors: &uniq &mut usize }
تحقق من &uniq
الرابط. هذا ليس شيئًا يمكن للمستخدم النهائي إدخاله. وهو يعني مؤشر "فريد لكن ليس بالضرورة قابل للتغيير". هذا ضروري لاجتياز فحص النوع. إذا حاول المستخدم كتابة هذه البنية يدويًا ، فسيتعين عليه كتابة &mut &mut usize
، الأمر الذي سيتطلب بدوره أن يتم الإعلان عن معلمة mut errors: &mut usize
.
عمليات الإغلاق والتفريغ غير المعبأة
أتوقع أن هذا التقييد يمثل مشكلة للإغلاق غير المعبأ. اسمحوا لي أن أوضح التصميم الذي كنت أفكر فيه. كانت الفكرة في الأساس أن التعبير ||
ما يعادل نوعًا هيكليًا جديدًا ينفذ إحدى السمات Fn
:
trait Fn<A, R> { fn call(&self, ...); } trait FnMut<A, R> { fn call(&mut self, ...); } trait FnOnce<A, R> { fn call(self, ...); }
سيتم تحديد النوع الدقيق وفقًا للنوع المتوقع ، اعتبارًا من اليوم. في هذه الحالة ، يمكن لمستهلكي الإغلاق كتابة أحد أمرين:
fn foo(&self, closure: FnMut<usize, usize>) { ... } fn foo<T: FnMut<usize, usize>>(&self, closure: T) { ... }
ربما ... نريد إصلاح البنية ، ربما نضيف FnMut(usize) -> usize
مثل FnMut(usize) -> usize
، أو save | usize | -> استخدم ، إلخ. إنه ليس مهمًا جدًا ، من المهم أن نجتاز الإغلاق حسب القيمة . يرجى ملاحظة أنه وفقًا لقواعد DST (الأنواع ذات الحجم الديناميكي) الحالية ، يجوز تمرير نوع حسب القيمة كوسيطة FnMut<usize, usize>
، وبالتالي فإن الوسيطة FnMut<usize, usize>
هي DST صالحة وليست مشكلة.
جانبا : هذا المشروع لم يكتمل ، وسأصف جميع التفاصيل في رسالة منفصلة.
تكمن المشكلة في أن ارتباط &mut
مطلوب لاستدعاء الإغلاق. نظرًا لأن الإغلاق يتم تمريره بالقيمة ، فسيتعين على المستخدمين مرة أخرى كتابة mut
حيث يبدو خارج المكان:
fn foo(&self, mut closure: FnMut<usize, usize>) { let x = closure.call(3); }
هذه هي نفس المشكلة كما في المثال Env
أعلاه: ما يحدث بالفعل هنا هو أن FnMut
تريد فقط ارتباطًا فريدًا ، ولكن نظرًا لأنها ليست جزءًا من نظام النوع ، فإنها تطلب ارتباطًا قابلًا للتغيير .
الآن ربما يمكننا الالتفاف حول هذا بطرق مختلفة. أحد الخيارات التي يمكننا القيام بها هو ||
لن يمتد بناء الجملة إلى "نوع هيكلي معين" ، بل إلى "نوع هيكلي أو مؤشر إلى نوع هيكلي ، كما يمليه الاستدلال النوعي". في هذه الحالة ، يمكن للمتصل أن يكتب:
fn foo(&self, closure: &mut FnMut<usize, usize>) { let x = closure.call(3); }
لا أريد أن أقول إن هذه هي نهاية العالم. لكن هذه خطوة أخرى إلى الأمام في التشوهات المتزايدة التي يجب أن نمر بها للحفاظ على هذه الفجوة بين المتغيرات والمراجع المحلية.
أجزاء API أخرى
لم أقم بدراسة مستفيضة ، ولكن بالطبع هذا الاختلاف يتسلل إلى مكان آخر. على سبيل المثال ، للقراءة من Socket
، أحتاج إلى مؤشر فريد ، لذلك يجب أن أعلن أنه قابل للتغيير. لذلك ، في بعض الأحيان لا يعمل هذا:
let socket = Socket::new(); socket.read()
بطبيعة الحال ، وفقًا لاقتراحي ، سيعمل مثل هذا الرمز بشكل جيد. ستظل تتلقى رسالة خطأ إذا حاولت القراءة من &Socket
، ولكن بعد ذلك ستقرأ شيئًا مثل "من المستحيل إنشاء رابط فريد إلى رابط مشترك" ، والذي أعتبره شخصياً أكثر قابلية للفهم.
ولكن لا نحتاج إلى mut
للأمن؟
لا ، على الإطلاق. ستكون برامج الصدأ جيدة بنفس القدر إذا كنت قد أعلنت للتو عن الارتباطات على أنها mut
. المترجم قادر تمامًا على تتبع المتغيرات المحلية التي تتغير في أي وقت - على وجه التحديد لأنها محلية للوظيفة الحالية. ما يهم نظام النوع حقًا هو التفرد.
المعنى الذي أراه في القواعد الحالية لتطبيق mut
، ولن أنكر أن لها قيمة ، هو في المقام الأول أنها تساعد على إعلان النية. أي عندما أقرأ الشفرة ، فأنا أعرف المتغيرات التي يمكن إعادة تعيينها. من ناحية أخرى ، أقضي أيضًا الكثير من الوقت في قراءة كود C ++ ، وبصراحة ، لم ألاحظ أبدًا أن هذا يمثل عقبة رئيسية. (نفس الشيء ينطبق على الوقت الذي قضيته في قراءة التعليمات البرمجية في Java أو JavaScript أو Python أو Ruby.)
صحيح أيضًا أنني أجد أحيانًا أخطاء لأنني أعلنت أن المتغير متغير ونسيت تغييره. أعتقد أننا يمكن أن نحصل على فوائد مماثلة مع فحوصات أخرى أكثر عدوانية (على سبيل المثال ، لا يتغير أي من المتغيرات المستخدمة في حالة الحلقة في جسم الحلقة). أنا شخصياً لا أتذكر أن أواجه الموقف المعاكس: أي إذا قال المترجم أن شيئًا ما يجب أن يكون متغيرًا ، فهذا يعني دائمًا أنني نسيت كلمة mut
الرئيسية في مكان ما. (فكر: متى كانت آخر مرة استجبت فيها لخطأ مترجم حول تغيير غير صالح عن طريق القيام بشيء آخر غير إعادة هيكلة الرمز لجعل التغيير صالحًا؟)
البدائل
أرى ثلاثة بدائل للنظام الحالي:
- الذي قدمته حيث يمكنك التخلص من "قابلية التغيير" وتتبع التفرد فقط.
- واحد حيث لديك ثلاثة أنواع مرجعية:
&
، &uniq
و &mut
. (كما كتبت ، هذا في الواقع هو نظام النوع الذي لدينا اليوم ، على الأقل من وجهة نظر مدقق الاقتراض.) خيار أكثر صرامة ، حيث تعتبر المتغيرات غير المتحولة دائمًا منفصلة. هذا يعني أنه يجب عليك الكتابة:
let mut errors = 0; let mut p = &mut errors;
تحتاج إلى الإعلان عن p
أنه mut
، لأنه بخلاف ذلك سيتم اعتبار المتغير منفصلاً ، على الرغم من أنه متغير محلي ، وبالتالي لا يُسمح بتغيير *p
. الغريب في هذا المخطط هو أن المتغير المحلي لا يسمح بملكية منفصلة ، ونحن نعلم على وجه اليقين ، لأنه عندما تحاول إنشاء اسم مستعار له ، فإنه يتحرك ، وسيبدأ المدمر عليه ، إلخ. أي أنه لا يزال لدينا مفهوم "مملوكة" ، والذي يختلف عن "لا يسمح بملكية منفصلة".
من ناحية أخرى ، إذا وصفنا هذا النظام ، قائلين أن قابلية التغيير موروثة عبر مؤشرات &mut
، دون حتى التأتأة حول الملكية المشتركة ، فقد يكون هذا منطقيًا.
من بين هؤلاء الثلاثة ، أفضل بالتأكيد رقم 1. إنه أبسط ، والآن أنا مهتم للغاية بكيفية تبسيط الصدأ من خلال الحفاظ على شخصيته. خلاف ذلك ، أعطي الأفضلية لما لدينا الآن.
الخلاصة
في الأساس ، أجد أن القواعد الحالية بشأن قابلية التغيير لها بعض القيمة ، ولكنها باهظة الثمن. إنها نوع من التجريد المتدفق: أي أنها تروي قصة بسيطة ، والتي تبين في الواقع أنها غير مكتملة. هذا يؤدي إلى الارتباك عندما ينتقل الناس من فهم أولي ، حيث تعكس &mut
كيفية عمل قابلية التغيير ، إلى فهم كامل: في بعض الأحيان mut
الحاجة ضرورية فقط لضمان التفرد ، وأحيانًا يتم تحقيق قابلية التغيير دون الكلمة الأساسية mut
.
علاوة على ذلك ، يجب أن نتصرف بحذر من أجل الحفاظ على الخيال ، الذي يشير mut
إلى قابلية التغيير ، وليس التفرد. أضفنا حالات خاصة للمقترض للتحقق من الإغلاق. يجب أن نجعل القواعد المتعلقة &mut
mutable أكثر تعقيدًا بشكل عام. يجب علينا إما إضافة mut
إلى عمليات الإغلاق حتى نتمكن من الاتصال بها ، أو جعل بناء الإغلاق مفتوحًا بطريقة أقل وضوحًا. وهكذا دواليك.
في النهاية ، يتحول كل شيء إلى لغة أكثر تعقيدًا ككل. بدلاً من مجرد التفكير في الملكية المشتركة والتفرد ، يجب على المستخدم التفكير في الملكية المشتركة وقابلية التغيير ، وكلاهما خاطئ بشكل ما.
لا أعتقد أن الأمر يستحق ذلك.