Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

โคลนเพื่อเอาใจ borrow checker

คำอธิบาย

ระบบ borrow checker ช่วยป้องกันไม่ให้นักพัฒนาเขียนโค้ดที่ไม่ปลอดภัยใน Rust โดยมีกฎเหล็กที่เข้มงวดว่าจะต้องตรงตามเงื่อนไขข้อใดข้อหนึ่ง: นั่นคือมี mutable reference ได้เพียงตัวเดียวเท่านั้น หรือมี immutable reference ได้หลายตัวพร้อมกัน หากโค้ดที่เขียนขึ้นละเมิดเงื่อนไขเหล่านี้ แอนติแพตเทิร์นนี้มักจะเกิดขึ้นเมื่อนักพัฒนาเลือกแก้ compile error ให้ผ่านไปทีด้วยการสั่ง clone ตัวแปรนั้นแทน

ตัวอย่าง

#![allow(unused)]
fn main() {
// define any variable
let mut x = 5;

// Borrow `x` -- but clone it first
let y = &mut (x.clone());

// without the x.clone() two lines prior, this line would fail on compile as
// x has been borrowed
// thanks to x.clone(), x was never borrowed, and this line will run.
println!("{x}");

// perform some action on the borrow to prevent rust from optimizing this
//out of existence
*y += 1;
}

ที่มาและเหตุผล

การเลือกใช้แพตเทิร์นนี้เพื่อแก้ปัญหาที่ชวนสับสนกับ borrow checker เป็นสิ่งที่ยั่วใจอย่างยิ่ง โดยเฉพาะอย่างยิ่งสำหรับผู้ที่เพิ่งเริ่มต้นเขียน Rust อย่างไรก็ดี วิธีนี้ส่งผลกระทบตามมาอย่างมีนัยสำคัญ เพราะการเรียก .clone() จะสร้างสำเนาข้อมูลชุดใหม่ขึ้นมาในหน่วยความจำ และการเปลี่ยนแปลงข้อมูลระหว่างตัวแปรทั้งสองตัวจะไม่ซิงค์ถึงกันเลย เสมือนว่าเป็นตัวแปรสองตัวที่แยกขาดจากกันโดยสิ้นเชิง

ทว่าก็มีกรณีพิเศษอยู่บ้าง เช่น Rc<T> ที่ถูกออกแบบมาให้รับมือกับการ .clone() อย่างชาญฉลาด โดยภายในตัวมันจะเก็บสำเนาข้อมูลไว้เพียงชุดเดียว การเรียก .clone() บน Rc จะได้อินสแตนซ์ของ Rc ตัวใหม่ที่ชี้ไปยังข้อมูลชุดเดิมของ Rc ต้นทาง พร้อมกับเพิ่มค่านับการอ้างอิง (reference count) ขึ้นหนึ่ง ซึ่งหลักการเดียวกันนี้ก็ถูกนำไปใช้ใน Arc ที่เป็นคู่แฝดสำหรับการทำงานข้ามเธรดได้อย่างปลอดภัย (thread-safe) เช่นกัน

โดยทั่วไปแล้ว การ clone ข้อมูลควรทำอย่างมีจุดประสงค์ชัดเจนและเข้าใจผลลัพธ์ที่ตามมาอย่างถ่องแท้ หากมีการใช้ clone เพียงเพื่อให้ error ของ borrow checker หายไป นั่นคือข้อบ่งชี้ที่ชัดเจนว่าโค้ดของคุณอาจกำลังตกอยู่ในแอนติแพตเทิร์นนี้

แม้ว่าการใช้ .clone() พร่ำเพรื่อจะเป็นสัญญาณของแพตเทิร์นที่ไม่ดี แต่ในบางสถานการณ์ การเขียนโค้ดที่ประสิทธิภาพยังไม่เหมาะสมที่สุดก็เป็นสิ่งที่ยอมรับได้ เช่นในกรณีต่อไปนี้:

  • นักพัฒนายังเป็นมือใหม่และกำลังทำความเข้าใจระบบความเป็นเจ้าของ (ownership)
  • โค้ดไม่ได้มีข้อจำกัดเรื่องความเร็วหรือปริมาณการใช้หน่วยความจำที่เข้มงวด (เช่น โค้ดต้นแบบ หรือโปรเจกต์งาน Hackathon)
  • การจัดระเบียบโค้ดเพื่อให้ผ่านกฎของ borrow checker มีความซับซ้อนสูงมาก และคุณต้องการเน้นความเรียบง่ายในการอ่านโค้ดมากกว่าการเค้นประสิทธิภาพสูงสุด

หากสงสัยว่ามีการเรียก clone โดยไม่จำเป็น ควรศึกษาและทำความเข้าใจเนื้อหาใน บทว่าด้วย Ownership ในหนังสือ The Rust Book ให้กระจ่างก่อนตัดสินใจว่าการ clone นั้นจำเป็นจริงหรือไม่

นอกจากนี้ อย่าลืมรันคำสั่ง cargo clippy ในโปรเจกต์ของคุณอยู่เสมอ เพราะ clippy จะช่วยตรวจจับกรณีต่างๆ ที่ไม่จำเป็นต้องเรียกใช้ .clone() ให้โดยอัตโนมัติ

ดูเพิ่มเติม