الصدأ. مدقق الاقتراض من خلال التكرارات

مرحبا يا هابر!

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

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

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


صور مأخوذة من هنا ومن هنا

مقدمة


في لغات jvm ، من المعتاد إخفاء العمل باستخدام الروابط ، حيث نعمل دائمًا تقريبًا مع أنواع البيانات المرجعية ، لذلك قررنا إخفاء علامة العطف (&).

في الراستا ، هناك روابط صريحة ، على سبيل المثال إلى عدد صحيح - `& i32` ، يمكن إلغاء الإشارة إلى الرابط عبر` *` ، ويمكن أيضًا أن يكون هناك رابط للرابط ومن ثم يجب إلغاء الإشارة إليه مرتين **.

المكرر


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

    val vec = Vector(1,2,3,4)
    val result = vec.filter(e => e % 2 == 0)

دعونا نلقي نظرة على الأنواع:

  private[scala] def filterImpl(p: A => Boolean, isFlipped: Boolean): Repr = {
    val b = newBuilder
    for (x <- this)
      if (p(x) != isFlipped) b += x

    b.result
  }

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

الآن دعونا نحاول أن نفعل نفس الشيء في الصدأ. سأعطي على الفور مثالاً عمليًا ، ثم سأدرس الاختلافات.

    let v: Vec<i32> = vec![1, 2, 3, 4];
    let result: Vec<&i32> = v.iter().filter(|e| **e % 2 == 0).collect();

واو ، واو ماذا؟ الإشارة المزدوجة؟ فقط لتصفية المتجه؟ صعب :( لكن هناك أسباب لذلك.

دعونا نكتشف كيف يختلف هذا الرمز عن الصخرة:

  1. الحصول على المكرر بشكل صريح على المتجه (`iter ()`)
  2. في دالة المسند ، لسبب ما ، فإننا نلغي الإشارة إلى المؤشر مرتين
  3. استدعاء `جمع ()`
  4. نتج عنه أيضًا ناقل أنواع مرجعية Vec <& i32> ، وليس عمليات إدخال عادية

مدقق الاقتراض


لماذا ندعو صراحة `iter ()` على المجموعة؟ من الواضح لأي صخرة أنه إذا اتصلت بـ ".filter (...)` ، فأنت بحاجة إلى التكرار فوق المجموعة. لماذا في الصدأ يكتب صراحة ما يمكن عمله ضمنا؟ لأن هناك ثلاث تكرارات مختلفة!



لمعرفة لماذا ثلاثة؟ الحاجة للمس على الاقتراض (الاقتراض، الاقتراض) المدقق 'أ. الشيء نفسه الذي يعمل فيه الصدأ بدون GC وبدون تخصيص ذاكرة صريحة / تخصيص.

لماذا هو مطلوب؟

  1. لتجنب المواقف عندما تشير عدة مؤشرات إلى نفس منطقة الذاكرة ، مما يسمح لك بتغييرها. هذه حالة سباق.
  2. حتى لا تطلق نفس الذاكرة عدة مرات.

كيف يتحقق ذلك؟

بسبب مفهوم الملكية.

بشكل عام ، مفهوم الملكية بسيط - يمكن للمرء فقط امتلاك شيء (حتى الحدس).

قد يتغير المالك ، لكنه دائمًا ما يكون وحيدًا. عندما نكتب `let x: i32 = 25` ، هذا يعني أنه تم تخصيص الذاكرة لعدد 32 بت int وامتلاك x` معينة لها. فكرة الملكية موجودة فقط في ذهن المترجم ، في مدقق الاقتراض. عندما يترك المالك ، في هذه الحالة ، `x` النطاق (يخرج عن النطاق) ، سيتم مسح ذاكرته التي يمتلكها.

فيما يلي رمز لا يفوتك المدقق:


struct X; // 

fn test_borrow_checker () -> X {
    let first = X; //  
    let second = first; //  
    let third = first; //   ,   first   
//    value used here after move

    return third;
}

`البنية X` شيء يشبه` حالة الفئة X () `- بنية بلا حدود.

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

لماذا احتجت إلى إنشاء هيكل خاص بك ، ولماذا لا تستخدم عددًا صحيحًا منتظمًا؟
— (`struct X`), , , integer. , , :


fn test_borrow_checker () -> i32 {
    let first = 32;
    let second = first; 
    let third = first; 

    return third;
}

, borrow checker, , . Copy, . `i32` second , ( ), - third . X Copy, .

. , , «» . Clone, , . copy clone.

العودة إلى التكرارات. مفهوم "القبض" من بينها IntoIter . "يبتلع" المجموعة ، ويمتلك عناصرها. في الكود ، ستنعكس هذه الفكرة على النحو التالي:


let coll_1 = vec![1,2,3];
let coll_2: Vec<i32> = coll_1.into_iter().collect();
//coll_1 doesn't exists anymore

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

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

المؤشرات


يمكن للمالك ، كما اكتشفنا ، أن يكون واحدًا فقط. ولكن يمكنك الحصول على أي عدد من الروابط.


#[derive(Debug)]
struct Y; // 

fn test_borrow_checker() -> Y {
    let first = Y; //  
    let second: &Y = &first; //   ,     
    let third = &first; //    

// 
    println!("{:?}", second);
    println!("{:?}", third);

    return first;
}


هذا الرمز صالح بالفعل ، لأن المالك لا يزال واحدًا. يتم التحقق من كل منطق الملكية فقط في مرحلة التجميع ، دون التأثير على تخصيص / نقل الذاكرة. علاوة على ذلك ، يمكنك أن ترى أن نوع الثانية قد تغير إلى `& Y`! أي أن دلالات الملكية والروابط تنعكس في الأنواع ، مما يسمح لك بالتحقق أثناء التجميع ، على سبيل المثال ، عدم وجود حالة سباق.

كيف يمكنني الحماية من حالة السباق في وقت الترجمة؟

من خلال تحديد عدد الروابط القابلة للتغيير!

يمكن أن يكون الارتباط القابل للتغيير في لحظة واحدة في الوقت واحدًا فقط (بدون ثبات). أي إما واحد / عدة غير قابلة للتغيير ، أو واحد قابل للتغيير. يبدو الرمز كما يلي:


// 
struct X {
    x: i32,
} 

fn test_borrow_checker() -> X {
    let mut first = X { x: 20 }; //  
    let second: &mut X = &mut first; //   
    let third: &mut X = &mut first; //    .        `second`        - .
//    second.x = 33;  //    ,             ,    
    third.x = 33;

    return first;
}

دعنا نراجع التغييرات في المثال السابق النسبي. أولاً ، أضفنا حقلاً واحدًا إلى الهيكل بحيث يكون هناك شيء للتغيير ، لأننا بحاجة إلى قابلية التغيير. ثانيًا ، ظهر `mut` في إعلان المتغير` let mut أولاً = ...` ، هذه علامة للمترجم حول قابلية التغيير ، مثل `val` و` var` في الصخرة. ثالثًا ، لقد غيرت جميع الروابط نوعها من `& X` إلى` & mut X` (تبدو ، بالطبع ، وحشية. وهذا دون أي وقت للحياة ...) ، والآن يمكننا تغيير القيمة المخزنة بواسطة الرابط.

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

في هذه الحالة ، يُسمح بوجود رابطين متغيرين في وقت واحد لأننا نستخدم رابطًا واحدًا فقط ، أي أنه يمكن التخلص من الثاني ولن يتغير شيء. كما يمكن استخدام " الثاني" حتىإنشاء "ثالث" ثم كل شيء سيكون على ما يرام. ولكن ، إذا ألغيت تعليق "second.x = 33 ؛" ، فقد اتضح أن هناك رابطين قابلين للتغيير في وقت واحد ولا يمكنك الخروج من هنا على أي حال - ترجمة خطأ الوقت.

المكرر


لذا ، لدينا ثلاثة أنواع من الإرسال:

  1. الامتصاص ، الاقتراض ، الحركة
  2. حلقة الوصل
  3. رابط قابل للتغيير

يحتاج كل نوع مكرر خاص به.

  1. IntoIter يمتص كائنات من المجموعة الأصلية
  2. يعمل Iter على ارتباطات الكائنات
  3. يعمل IterMut على مراجع الكائنات القابلة للتغيير

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

لنفترض أن هناك مدرسة ، يوجد بها فصل ، والطلاب في الفصل.


#[derive(PartialEq, Eq)]
enum Sex {
    Male,
    Female
}

struct Scholar {
    name: String,
    age: i32,
    sex: Sex
}

let scholars: Vec<Scholar> = ...;

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


fn bad_idea() {
    let scholars: Vec<Scholar> = Vec::new();
    let girls_c = scholars
        .into_iter()
        .filter(|s| (*s).sex == Sex::Female)
        .count();

    let boys_c = scholars 
        .into_iter()
        .filter(|s| (*s).sex == Sex::Male)
        .count();
}

سيكون هناك خطأ "القيمة المستخدمة هنا بعد الانتقال" على الخط لحساب الأولاد. من الواضح أيضًا أن المكرر القابل للتغيير لا فائدة لنا. هذا هو السبب في أنها فقط `iter ()` وتعمل مع رابط مزدوج:


fn good_idea() {
    let scholars: Vec<Scholar> = Vec::new();
    let girls_c = scholars.iter().filter(|s| (**s).sex == Sex::Female).count();
    let boys_c = scholars.iter().filter(|s| (**s).sex == Sex::Male).count();
}

هنا ، لزيادة عدد المجندين المحتملين في البلد ، يلزم بالفعل مكرر قابل للتغيير:


fn very_good_idea() {
    let mut scholars: Vec<Scholar> = Vec::new();
    scholars.iter_mut().for_each(|s| (*s).sex = Sex::Male);
}

بتطوير الفكرة ، يمكننا أن نجعل الجنود من "الرجال" وأن نظهر المكرر "الممتص":


impl Scholar {
    fn to_soldier(self) -> Soldier {
        Soldier { forgotten_name: self.name, number: some_random_number_generator() }
    }
}

struct Soldier {
    forgotten_name: String,
    number: i32
}

fn good_bright_future() {
    let mut scholars: Vec<Scholar> = Vec::new();
    scholars.iter_mut().for_each(|s| (*s).sex = Sex::Male);
    let soldiers: Vec<Soldier> = scholars.into_iter().map(|s| s.to_soldier()).collect();
    //   scholars,    
}

على هذه الملاحظة الرائعة ، ربما هذا كل شيء.

يبقى السؤال الأخير - من أين أتت إزالة الإشارة المزدوجة للروابط في "الفلتر". الحقيقة هي أن المسند هو وظيفة تأخذ إشارة إلى حجة (حتى لا يتم التقاطها):


    fn filter<P>(self, predicate: P) -> Filter<Self, P> where
        Self: Sized, P: FnMut(&Self::Item) -> bool,

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

استمرار


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

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

الروابط


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

All Articles