ลองดูการใช้งานอย่างง่ายของสิ่งนี้ :
struct Parent {
count: u32,
}
struct Child<'a> {
parent: &'a Parent,
}
struct Combined<'a> {
parent: Parent,
child: Child<'a>,
}
impl<'a> Combined<'a> {
fn new() -> Self {
let parent = Parent { count: 42 };
let child = Child { parent: &parent };
Combined { parent, child }
}
}
fn main() {}
สิ่งนี้จะล้มเหลวด้วยข้อผิดพลาด:
error[E0515]: cannot return value referencing local variable `parent`
--> src/main.rs:19:9
|
17 | let child = Child { parent: &parent };
| ------- `parent` is borrowed here
18 |
19 | Combined { parent, child }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function
error[E0505]: cannot move out of `parent` because it is borrowed
--> src/main.rs:19:20
|
14 | impl<'a> Combined<'a> {
| -- lifetime `'a` defined here
...
17 | let child = Child { parent: &parent };
| ------- borrow of `parent` occurs here
18 |
19 | Combined { parent, child }
| -----------^^^^^^---------
| | |
| | move out of `parent` occurs here
| returning this value requires that `parent` is borrowed for `'a`
เพื่อให้เข้าใจถึงข้อผิดพลาดนี้ได้อย่างสมบูรณ์คุณต้องคิดเกี่ยวกับการแสดงค่าในหน่วยความจำและสิ่งที่เกิดขึ้นเมื่อคุณย้าย
ค่าเหล่านั้น ให้คำอธิบายประกอบCombined::new
กับที่อยู่หน่วยความจำสมมุติที่แสดงที่ตั้งของค่า:
let parent = Parent { count: 42 };
// `parent` lives at address 0x1000 and takes up 4 bytes
// The value of `parent` is 42
let child = Child { parent: &parent };
// `child` lives at address 0x1010 and takes up 4 bytes
// The value of `child` is 0x1000
Combined { parent, child }
// The return value lives at address 0x2000 and takes up 8 bytes
// `parent` is moved to 0x2000
// `child` is ... ?
อะไรจะเกิดขึ้นchild
? หากค่าเพิ่งถูกย้ายเช่นparent
เคยมันจะอ้างถึงหน่วยความจำที่ไม่รับประกันว่าจะมีค่าที่ถูกต้องในนั้น ส่วนอื่น ๆ ของรหัสได้รับอนุญาตให้เก็บค่าที่อยู่หน่วยความจำ 0x1000 การเข้าถึงหน่วยความจำนั้นโดยสมมติว่าเป็นจำนวนเต็มอาจทำให้เกิดข้อผิดพลาดและ / หรือข้อบกพร่องด้านความปลอดภัยและเป็นหนึ่งในประเภทหลักของข้อผิดพลาดที่ Rust ป้องกัน
นี่เป็นปัญหาที่อายุการใช้งานป้องกันได้อย่างแน่นอน อายุการใช้งานเป็นบิตของเมตาดาต้าที่ช่วยให้คุณและคอมไพเลอร์ที่จะรู้ว่านานเท่าไหร่ค่าจะถูกต้องที่ดีที่ตั้งของหน่วยความจำในปัจจุบัน นั่นเป็นข้อแตกต่างที่สำคัญเนื่องจากเป็นข้อผิดพลาดทั่วไปที่ผู้ใช้ใหม่ของ Rust สร้างขึ้น อายุการใช้งานของสนิมไม่ใช่ช่วงเวลาระหว่างที่วัตถุถูกสร้างขึ้นและเมื่อถูกทำลาย!
ในฐานะที่เป็นอุปมาอุปมัยให้คิดอย่างนี้: ในช่วงชีวิตของคน ๆ หนึ่งพวกเขาจะอาศัยอยู่ในสถานที่ต่าง ๆ กันโดยแต่ละแห่งมีที่อยู่ชัดเจน อายุการใช้งานสนิมเกี่ยวข้องกับที่อยู่ที่คุณอยู่ในปัจจุบันไม่เกี่ยวกับทุกครั้งที่คุณจะตายในอนาคต (แม้ว่าการตายจะเปลี่ยนที่อยู่ของคุณด้วย) ทุกครั้งที่คุณย้ายที่อยู่จะเกี่ยวข้องเนื่องจากที่อยู่ของคุณไม่ถูกต้องอีกต่อไป
สิ่งสำคัญคือต้องทราบว่าอายุการใช้งานจะไม่เปลี่ยนรหัสของคุณ รหัสของคุณควบคุมอายุการใช้งานอายุการใช้งานของคุณไม่ได้ควบคุมรหัส คำพูดที่แหลมคมคือ "อายุการใช้งานเป็นคำอธิบายไม่ได้กำหนด"
มาอธิบายCombined::new
ด้วยหมายเลขบรรทัดที่เราจะใช้เพื่อเน้นอายุการใช้งาน:
{ // 0
let parent = Parent { count: 42 }; // 1
let child = Child { parent: &parent }; // 2
// 3
Combined { parent, child } // 4
} // 5
อายุการใช้งานที่เป็นรูปธรรมของการparent
เป็น 1-4 รวม (ซึ่งผมจะเป็นตัวแทน[1,4]
) อายุการใช้งานที่เป็นรูปธรรมของchild
มีและอายุการใช้งานที่เป็นรูปธรรมของค่าตอบแทนเป็น[2,4]
[4,5]
เป็นไปได้ที่จะมีช่วงชีวิตที่เป็นรูปธรรมที่เริ่มต้นที่ศูนย์ - ซึ่งจะแสดงอายุการใช้งานของพารามิเตอร์ให้กับฟังก์ชันหรือบางสิ่งที่มีอยู่นอกบล็อก
โปรดทราบว่าอายุการใช้งานของchild
ตัวเองเป็น[2,4]
แต่มันหมายถึง[1,4]
ค่ากับอายุการใช้งานของ นี่เป็นเรื่องปกติตราบใดที่ค่าการอ้างอิงกลายเป็นโมฆะก่อนที่ค่าอ้างอิงจะทำ ปัญหาเกิดขึ้นเมื่อเราพยายามกลับchild
จากบล็อก สิ่งนี้จะ "ยืดอายุการใช้งาน" เกินกว่าความยาวตามธรรมชาติ
ความรู้ใหม่นี้ควรอธิบายสองตัวอย่างแรก Parent::child
หนึ่งในสามต้องมองไปที่การดำเนินงานของ โอกาสที่มันจะมีลักษณะเช่นนี้:
impl Parent {
fn child(&self) -> Child { /* ... */ }
}
นี้ใช้ตัดออกอายุการใช้งานที่จะหลีกเลี่ยงการเขียนอย่างชัดเจนพารามิเตอร์อายุการใช้งานทั่วไป มันเทียบเท่ากับ:
impl Parent {
fn child<'a>(&'a self) -> Child<'a> { /* ... */ }
}
ในทั้งสองกรณีวิธีการที่บอกว่าโครงสร้างจะถูกส่งกลับที่ได้รับการแปรกับอายุการใช้งานที่เป็นรูปธรรมของChild
self
กล่าวอีกวิธีหนึ่งChild
อินสแตนซ์ประกอบด้วยการอ้างอิงถึงสิ่งParent
ที่สร้างขึ้นและทำให้ไม่สามารถอยู่ได้นานกว่า
Parent
อินสแตนซ์นั้น
สิ่งนี้ยังช่วยให้เรารับรู้ว่ามีบางอย่างผิดปกติกับฟังก์ชั่นการสร้างของเรา:
fn make_combined<'a>() -> Combined<'a> { /* ... */ }
แม้ว่าคุณจะมีแนวโน้มที่จะเห็นสิ่งนี้เขียนในรูปแบบที่แตกต่างกัน:
impl<'a> Combined<'a> {
fn new() -> Combined<'a> { /* ... */ }
}
ในทั้งสองกรณีไม่มีพารามิเตอร์อายุการใช้งานที่ให้ผ่านการโต้แย้ง ซึ่งหมายความว่าอายุการใช้งานที่Combined
จะกำหนดค่าพารามิเตอร์ด้วยไม่ได้ถูก จำกัด โดยสิ่งใด - มันอาจเป็นสิ่งที่ผู้โทรต้องการให้เป็น นี่เป็นเรื่องไร้สาระเนื่องจากผู้โทรสามารถระบุ'static
อายุการใช้งานและไม่มีวิธีที่จะตอบสนองเงื่อนไขดังกล่าว
ฉันจะแก้ไขได้อย่างไร
ทางออกที่ง่ายที่สุดและแนะนำมากที่สุดคือการไม่พยายามที่จะนำสิ่งเหล่านี้เข้าด้วยกันในโครงสร้างเดียวกัน การทำเช่นนี้การทำโครงสร้างของคุณจะเลียนแบบอายุการใช้งานของรหัสของคุณ ประเภทสถานที่ที่ข้อมูลของตัวเองลงในโครงสร้างร่วมกันแล้วให้วิธีการที่ช่วยให้คุณได้รับการอ้างอิงหรือวัตถุที่มีการอ้างอิงตามความจำเป็น
มีกรณีพิเศษที่การติดตามอายุการใช้งานสูงเกินไป: เมื่อคุณมีบางสิ่งวางอยู่บนกอง นี้เกิดขึ้นเมื่อคุณใช้
Box<T>
ตัวอย่างเช่น ในกรณีนี้โครงสร้างที่ถูกย้ายประกอบด้วยตัวชี้ลงในกอง ค่าการชี้ที่จะยังคงมีเสถียรภาพ แต่ที่อยู่ของตัวชี้จะย้าย ในทางปฏิบัติสิ่งนี้ไม่สำคัญเนื่องจากคุณทำตามตัวชี้เสมอ
ลังเช่า (อีกต่อไปรักษาหรือสนับสนุน)หรือลัง owning_refเป็นวิธีที่เป็นตัวแทนของกรณีนี้ แต่พวกเขาต้องการที่อยู่ฐานไม่เคยย้าย กฎนี้ทำให้เกิดการกลายพันธุ์เวกเตอร์ซึ่งอาจทำให้เกิดการจัดสรรใหม่และการย้ายค่าที่จัดสรรฮีป
ตัวอย่างของปัญหาที่แก้ไขได้ด้วยการเช่า:
ในกรณีอื่น ๆ ที่คุณอาจต้องการที่จะย้ายไปประเภทของการอ้างอิงนับบางอย่างเช่นโดยการใช้หรือRc
Arc
ข้อมูลมากกว่านี้
หลังจากย้ายparent
เข้าสู่โครงสร้างแล้วทำไมคอมไพเลอร์จึงไม่สามารถรับการอ้างอิงใหม่parent
และกำหนดให้กับchild
โครงสร้างได้
ในขณะที่เป็นไปได้ในทางทฤษฎีในการทำเช่นนี้การทำเช่นนั้นจะแนะนำความซับซ้อนและค่าใช้จ่ายจำนวนมาก ทุกครั้งที่วัตถุถูกย้ายคอมไพเลอร์จะต้องใส่รหัสเพื่อ "แก้ไข" การอ้างอิง นี่หมายความว่าการคัดลอก struct ไม่ใช่การกระทำที่ราคาถูกมากอีกต่อไปซึ่งแค่เคลื่อนย้ายบิตไปรอบ ๆ มันอาจหมายถึงว่ารหัสเช่นนี้มีราคาแพงขึ้นอยู่กับว่าเครื่องมือเพิ่มประสิทธิภาพสมมุติจะดีแค่ไหน:
let a = Object::new();
let b = a;
let c = b;
แทนที่จะบังคับให้สิ่งนี้เกิดขึ้นสำหรับทุกการเคลื่อนไหวโปรแกรมเมอร์จะเลือกเมื่อสิ่งนี้จะเกิดขึ้นโดยการสร้างวิธีการที่จะใช้การอ้างอิงที่เหมาะสมเฉพาะเมื่อคุณเรียกพวกเขาเท่านั้น
ประเภทที่มีการอ้างอิงถึงตัวมันเอง
มีกรณีเฉพาะหนึ่งกรณีที่คุณสามารถสร้างประเภทที่มีการอ้างอิงถึงตัวเอง คุณต้องใช้สิ่งที่ต้องการOption
ทำในสองขั้นตอนแม้ว่า:
#[derive(Debug)]
struct WhatAboutThis<'a> {
name: String,
nickname: Option<&'a str>,
}
fn main() {
let mut tricky = WhatAboutThis {
name: "Annabelle".to_string(),
nickname: None,
};
tricky.nickname = Some(&tricky.name[..4]);
println!("{:?}", tricky);
}
ใช้งานได้ในบางกรณี แต่ค่าที่สร้างขึ้นนั้นถูก จำกัด อย่างมาก - ไม่สามารถเคลื่อนย้ายได้ ยวดนี่หมายความว่ามันไม่สามารถส่งคืนจากฟังก์ชั่นหรือส่งผ่านโดยค่าเพื่ออะไร ฟังก์ชั่นคอนสตรัคแสดงให้เห็นปัญหาเดียวกันกับอายุการใช้งานดังกล่าวข้างต้น:
fn creator<'a>() -> WhatAboutThis<'a> { /* ... */ }
เกี่ยวกับPin
อะไร
Pin
เสถียรใน Rust 1.33 มีสิ่งนี้ในเอกสารประกอบโมดูล :
ตัวอย่างสำคัญของสถานการณ์เช่นนี้คือการสร้างโครงสร้างอ้างอิงตนเองเนื่องจากการย้ายวัตถุที่มีตัวชี้ไปยังตัวเองจะทำให้พวกเขาใช้งานไม่ได้ซึ่งอาจทำให้เกิดพฤติกรรมที่ไม่ได้กำหนด
มันเป็นสิ่งสำคัญที่จะทราบว่า "ตัวอ้างอิง" ไม่จำเป็นต้องหมายถึงการใช้การอ้างอิง อันที่จริงตัวอย่างของโครงสร้างการอ้างอิงตนเองโดยเฉพาะกล่าวว่า (เหมืองเน้น):
เราไม่สามารถแจ้งผู้แปลเกี่ยวกับสิ่งนั้นด้วยการอ้างอิงปกติเนื่องจากรูปแบบนี้ไม่สามารถอธิบายได้ด้วยกฎการยืมปกติ แต่เราใช้ตัวชี้แบบดิบถึงแม้ว่าจะมีตัวชี้ที่ไม่เป็นโมฆะเพราะเรารู้ว่ามันเป็นตัวชี้
ความสามารถในการใช้ตัวชี้แบบ raw สำหรับลักษณะการทำงานนี้มีอยู่ตั้งแต่ Rust 1.0 อันที่จริงการเป็นเจ้าของการอ้างอิงและการเช่าใช้ตัวชี้แบบดิบภายใต้ประ
สิ่งเดียวที่Pin
เพิ่มลงในตารางเป็นวิธีการทั่วไปที่ระบุว่าค่าที่กำหนดนั้นรับประกันว่าจะไม่ย้าย
ดูสิ่งนี้ด้วย:
Parent
และChild
สามารถช่วย ...