เหตุใดฉันไม่สามารถเก็บค่าและการอ้างอิงถึงค่านั้นในโครงสร้างเดียวกันได้


223

ฉันมีค่าและฉันต้องการเก็บค่านั้นและการอ้างอิงถึงสิ่งที่อยู่ภายในค่านั้นในประเภทของฉันเอง:

struct Thing {
    count: u32,
}

struct Combined<'a>(Thing, &'a u32);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing { count: 42 };

    Combined(thing, &thing.count)
}

บางครั้งฉันมีค่าและฉันต้องการเก็บค่านั้นและการอ้างอิงถึงค่านั้นในโครงสร้างเดียวกัน:

struct Combined<'a>(Thing, &'a Thing);

fn make_combined<'a>() -> Combined<'a> {
    let thing = Thing::new();

    Combined(thing, &thing)
}

บางครั้งฉันไม่ได้อ้างอิงค่าและได้รับข้อผิดพลาดเดียวกัน:

struct Combined<'a>(Parent, Child<'a>);

fn make_combined<'a>() -> Combined<'a> {
    let parent = Parent::new();
    let child = parent.child();

    Combined(parent, child)
}

ในแต่ละกรณีฉันได้รับข้อผิดพลาดว่าหนึ่งในค่า "ไม่ได้อยู่นานพอ" ข้อผิดพลาดนี้หมายความว่าอย่างไร


1
สำหรับตัวอย่างหลังคำจำกัดความของParentและChildสามารถช่วย ...
Matthieu M.

1
@MatthieuM ฉันถกเถียงกันเรื่องนั้น แต่ตัดสินด้วยคำถามสองข้อที่เชื่อมโยงกัน คำถามเหล่านั้นไม่ได้ดูคำจำกัดความของโครงสร้างหรือวิธีการที่เป็นปัญหาดังนั้นฉันคิดว่ามันเป็นการดีที่สุดที่จะเลียนแบบว่าผู้คนสามารถจับคู่คำถามนี้กับสถานการณ์ของตนเองได้ง่ายขึ้น โปรดทราบว่าฉันจะแสดงลายเซ็นวิธีการในคำตอบ
Shepmaster

คำตอบ:


245

ลองดูการใช้งานอย่างง่ายของสิ่งนี้ :

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เป็นวิธีที่เป็นตัวแทนของกรณีนี้ แต่พวกเขาต้องการที่อยู่ฐานไม่เคยย้าย กฎนี้ทำให้เกิดการกลายพันธุ์เวกเตอร์ซึ่งอาจทำให้เกิดการจัดสรรใหม่และการย้ายค่าที่จัดสรรฮีป

ตัวอย่างของปัญหาที่แก้ไขได้ด้วยการเช่า:

ในกรณีอื่น ๆ ที่คุณอาจต้องการที่จะย้ายไปประเภทของการอ้างอิงนับบางอย่างเช่นโดยการใช้หรือRcArc

ข้อมูลมากกว่านี้

หลังจากย้าย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เพิ่มลงในตารางเป็นวิธีการทั่วไปที่ระบุว่าค่าที่กำหนดนั้นรับประกันว่าจะไม่ย้าย

ดูสิ่งนี้ด้วย:


1
เป็นเช่นนี้ ( is.gd/wl2IAt ) ถือว่าเป็นสำนวนหรือไม่ คือการเปิดเผยข้อมูลผ่านวิธีการแทนข้อมูลดิบ
Peter Hall

2
@PeterHall แน่ใจว่ามันก็หมายความว่าCombinedเป็นเจ้าของซึ่งเป็นเจ้าของChild Parentนั่นอาจจะใช่หรือไม่ใช่ก็ได้ขึ้นอยู่กับประเภทที่แท้จริงของคุณ การอ้างอิงกลับไปยังข้อมูลภายในของคุณเป็นเรื่องปกติ
Shepmaster

การแก้ไขปัญหาฮีปคืออะไร
derekdreery

@derekdreery บางทีคุณสามารถขยายความคิดเห็นของคุณ? ทำไมวรรคทั้งพูดคุยเกี่ยวกับowning_refลังไม่เพียงพอ?
Shepmaster

1
@FynnBecker ยังคงเป็นไปไม่ได้ที่จะเก็บข้อมูลอ้างอิงและค่าในการอ้างอิงนั้น Pinส่วนใหญ่จะเป็นวิธีที่จะรู้ว่าความปลอดภัยของโครงสร้างที่มีตัวเองอ้างอิงชี้ ความสามารถในการใช้ตัวชี้แบบดิบสำหรับจุดประสงค์เดียวกันมีอยู่ตั้งแต่ Rust 1.0
Shepmaster

4

ปัญหาที่แตกต่างกันเล็กน้อยซึ่งทำให้ข้อความคอมไพเลอร์คล้ายกันมากคือการพึ่งพาอายุการใช้งานของวัตถุแทนที่จะเก็บการอ้างอิงที่ชัดเจน ตัวอย่างของนั่นคือไลบรารีssh2 เมื่อพัฒนาบางสิ่งที่ใหญ่กว่าโครงการทดสอบมันเป็นเรื่องดึงดูดที่จะลองใส่SessionและChannelได้รับจากเซสชั่นนั้นเข้าด้วยกันเป็นโครงสร้างโดยซ่อนรายละเอียดการใช้งานจากผู้ใช้ อย่างไรก็ตามโปรดทราบว่าChannelคำจำกัดความ'sessมีอายุการใช้งานในคำอธิบายประกอบประเภทของมันในขณะที่Sessionไม่ได้

สิ่งนี้ทำให้เกิดข้อผิดพลาดคอมไพเลอร์ที่คล้ายกันซึ่งเกี่ยวข้องกับอายุการใช้งาน

วิธีหนึ่งในการแก้ไขด้วยวิธีที่ง่ายมากคือการประกาศบุคคลSessionภายนอกจากผู้โทรและจากนั้นใส่คำอธิบายประกอบในโครงสร้างด้วยอายุการใช้งานคล้ายกับคำตอบในโพสต์ฟอรัมผู้ใช้ของ Rust ที่พูดถึงปัญหาเดียวกันในขณะเดียวกัน . สิ่งนี้จะดูไม่หรูหราและอาจไม่สามารถใช้ได้เสมอไป - เพราะตอนนี้คุณมีสองหน่วยงานที่จะจัดการกับแทนที่จะเป็นสิ่งที่คุณต้องการ!

ปรากฎว่าลังเช่าหรือowning_ref ลังจากคำตอบอื่น ๆ คือคำตอบสำหรับปัญหานี้เช่นกัน ลองพิจารณา owning_ref OwningHandleซึ่งมีวัตถุพิเศษสำหรับวัตถุประสงค์ที่แท้จริงนี้: เพื่อหลีกเลี่ยงการเคลื่อนย้ายวัตถุพื้นฐานเราจัดสรรมันบนฮีปโดยใช้ a Boxซึ่งให้วิธีการแก้ปัญหาที่เป็นไปได้ต่อไปนี้:

use ssh2::{Channel, Error, Session};
use std::net::TcpStream;

use owning_ref::OwningHandle;

struct DeviceSSHConnection {
    tcp: TcpStream,
    channel: OwningHandle<Box<Session>, Box<Channel<'static>>>,
}

impl DeviceSSHConnection {
    fn new(targ: &str, c_user: &str, c_pass: &str) -> Self {
        use std::net::TcpStream;
        let mut session = Session::new().unwrap();
        let mut tcp = TcpStream::connect(targ).unwrap();

        session.handshake(&tcp).unwrap();
        session.set_timeout(5000);
        session.userauth_password(c_user, c_pass).unwrap();

        let mut sess = Box::new(session);
        let mut oref = OwningHandle::new_with_fn(
            sess,
            unsafe { |x| Box::new((*x).channel_session().unwrap()) },
        );

        oref.shell().unwrap();
        let ret = DeviceSSHConnection {
            tcp: tcp,
            channel: oref,
        };
        ret
    }
}

ผลลัพธ์ของรหัสนี้คือเราไม่สามารถใช้งานได้Sessionอีกต่อไป แต่จะถูกเก็บไว้พร้อมกับสิ่งChannelที่เราจะใช้ เพราะOwningHandledereferences วัตถุBoxซึ่ง dereferences ไปChannelเมื่อเก็บไว้ใน struct เราชื่อมันเป็นเช่นนี้ หมายเหตุ:นี่เป็นเพียงความเข้าใจของฉัน ผมมีความสงสัยนี้อาจไม่ถูกต้องเพราะมันดูเหมือนจะค่อนข้างใกล้เคียงกับการอภิปรายของOwningHandleไม่ปลอดภัย

รายละเอียดที่น่าสนใจอย่างหนึ่งที่นี่ก็คือSessionเหตุผลที่ว่ามีความสัมพันธ์แบบเดียวกันกับTcpStreamที่ChannelมีSessionแต่ความเป็นเจ้าของไม่ได้ถูกนำมาใช้และไม่มีคำอธิบายประกอบประเภทรอบ ๆ การทำเช่นนั้น แต่ขึ้นอยู่กับผู้ใช้ที่จะดูแลสิ่งนี้ตามที่เอกสารของวิธีการจับมือกันพูดว่า:

เซสชั่นนี้ไม่ได้เป็นเจ้าของซ็อกเก็ตที่มีให้ขอแนะนำเพื่อให้แน่ใจว่าซ็อกเก็ตยังคงอยู่ตลอดชีวิตของเซสชั่นนี้เพื่อให้แน่ใจว่าการสื่อสารดำเนินการอย่างถูกต้อง

ขอแนะนำเป็นอย่างยิ่งว่าไม่ควรใช้สตรีมที่ให้มาพร้อมกันที่อื่นในช่วงเวลาของเซสชันนี้เนื่องจากอาจรบกวนการทำงานของโปรโตคอล

ดังนั้นด้วยการTcpStreamใช้งานจะขึ้นอยู่กับโปรแกรมเมอร์อย่างสมบูรณ์เพื่อให้แน่ใจว่าถูกต้องของรหัส ด้วยOwningHandleความสนใจไปที่ "เวทมนตร์อันตราย" ที่เกิดขึ้นจะถูกดึงขึ้นมาโดยใช้unsafe {}บล็อก

การอภิปรายเพิ่มเติมในระดับสูงและมากขึ้นของปัญหานี้อยู่ในกระทู้ฟอรั่มของผู้ใช้ Rustนี้- ซึ่งรวมถึงตัวอย่างที่แตกต่างกันและวิธีการแก้ปัญหาโดยใช้ลังเช่าซึ่งไม่มีบล็อกที่ไม่ปลอดภัย

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.