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

ใช้ Box<T> ชี้ไปยังข้อมูลบน Heap

smart pointer ที่ตรงไปตรงมาที่สุดคือ box ซึ่ง type ของมันถูกเขียน Box<T> Box อนุญาตให้คุณเก็บข้อมูลบน heap ไม่ใช่ stack สิ่งที่เหลือ บน stack คือ pointer ไปยังข้อมูล heap อ้างอิงบทที่ 4 เพื่อทบทวนความ แตกต่างระหว่าง stack และ heap

Box ไม่มี overhead performance นอกจากการเก็บข้อมูลของพวกมันบน heap แทนบน stack แต่พวกมันก็ไม่มีความสามารถเพิ่มมาก คุณจะใช้พวกมันบ่อยที่ สุดในสถานการณ์เหล่านี้:

  • เมื่อคุณมี type ที่ขนาดไม่รู้ที่ compile time และคุณต้องการใช้ค่า ของ type นั้นใน context ที่ต้องการขนาดแน่นอน
  • เมื่อคุณมีข้อมูลจำนวนมาก และคุณต้องการโอน ownership แต่ต้องการ มั่นใจว่าข้อมูลจะไม่ถูก copy เมื่อคุณทำ
  • เมื่อคุณต้องการ own ค่า และคุณสนใจเพียงว่ามันเป็น type ที่ implement trait เฉพาะ ไม่ใช่เป็น type เฉพาะ

เราจะสาธิตสถานการณ์แรกใน “เปิดใช้ Recursive Type ด้วย Box” ในกรณีที่สอง การโอน ownership ของข้อมูลจำนวนมากใช้เวลานานเพราะข้อมูล ถูก copy ไปมาบน stack เพื่อปรับปรุง performance ในสถานการณ์นี้ เรา เก็บข้อมูลจำนวนมากบน heap ใน box ได้ จากนั้น เฉพาะข้อมูล pointer จำนวนน้อยถูก copy ไปมาบน stack ในขณะที่ข้อมูลที่มันอ้างถึงอยู่ที่ เดียวบน heap กรณีที่สามเรียกว่า trait object และ “ใช้ Trait Object เพื่อ Abstract เหนือพฤติกรรมร่วม” ในบทที่ 18 อุทิศให้กับหัวข้อนั้น ดังนั้น สิ่งที่คุณเรียนที่นี่คุณจะ ใช้อีกในส่วนนั้น!

เก็บข้อมูลบน Heap

ก่อนที่เราจะพูดถึง use case เก็บ heap สำหรับ Box<T> เราจะครอบคลุม syntax และวิธี interact กับค่าที่เก็บภายใน Box<T>

Listing 15-1 แสดงวิธีใช้ box เพื่อเก็บค่า i32 บน heap

Filename: src/main.rs
fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}
Listing 15-1: เก็บค่า i32 บน heap โดยใช้ box

เรานิยามตัวแปร b ให้มีค่าของ Box ที่ชี้ไปยังค่า 5 ซึ่ง allocate บน heap โปรแกรมนี้จะ print b = 5 — ในกรณีนี้ เราเข้าถึงข้อมูลใน box คล้ายกับวิธีที่เราจะถ้าข้อมูลนี้อยู่บน stack เช่นเดียวกับค่า own ใด ๆ เมื่อ box ออกจาก scope เช่นที่ b ทำที่ท้ายสุดของ main มัน จะถูก deallocate การ deallocate เกิดทั้งสำหรับ box (เก็บบน stack) และข้อมูลที่มันชี้ (เก็บบน heap)

การใส่ค่าเดียวบน heap ไม่มีประโยชน์มาก ดังนั้นคุณจะไม่ใช้ box โดย ตัวเองในวิธีนี้บ่อยมาก การมีค่าเช่น i32 เดียวบน stack ที่พวกมัน ถูกเก็บเป็นค่าเริ่มต้น เหมาะสมมากกว่าในสถานการณ์ส่วนใหญ่ มาดูกรณีที่ box อนุญาตให้เรานิยาม type ที่เราจะไม่ได้รับอนุญาตให้นิยามถ้าเราไม่ มี box

เปิดใช้ Recursive Type ด้วย Box

ค่าของ recursive type สามารถมีค่าอื่นของ type เดียวกันเป็นส่วนของ ตัวเองได้ Recursive type ก่อให้เกิดประเด็นเพราะ Rust ต้องรู้ที่ compile time ว่า type ใช้พื้นที่เท่าไร อย่างไรก็ตาม การซ้อนของค่า ของ recursive type ในทางทฤษฎีอาจดำเนินไปไม่สิ้นสุด ดังนั้น Rust รู้ ไม่ได้ว่าค่าต้องการพื้นที่เท่าไร เพราะ box มีขนาดที่รู้ เราเปิดใช้ recursive type โดย insert box ในนิยาม recursive type ได้

เป็นตัวอย่างของ recursive type มาสำรวจ cons list นี่เป็นประเภทข้อมูล ที่พบบ่อยในภาษาโปรแกรม functional ประเภท cons list ที่เราจะนิยาม ตรงไปตรงมา ยกเว้นการ recursion — ดังนั้น แนวคิดในตัวอย่างที่เราจะ ทำงานด้วยจะมีประโยชน์เมื่อใดก็ตามที่คุณเข้าไปในสถานการณ์ซับซ้อน มากขึ้นที่เกี่ยวข้องกับ recursive type

เข้าใจ Cons List

cons list คือโครงสร้างข้อมูลที่มาจากภาษาโปรแกรม Lisp และ dialect ของมัน ประกอบด้วยคู่ซ้อน และเป็นเวอร์ชัน Lisp ของ linked list ชื่อ ของมันมาจากฟังก์ชัน cons (ย่อมาจาก construct function) ใน Lisp ที่สร้างคู่ใหม่จากสองอาร์กิวเมนต์ของมัน โดยเรียก cons บนคู่ที่ ประกอบด้วยค่าและอีกคู่ เราสร้าง cons list ที่ประกอบด้วยคู่ recursive ได้

ตัวอย่างเช่น นี่คือ pseudocode ของ cons list ที่บรรจุ list 1, 2, 3 ที่แต่ละคู่อยู่ในวงเล็บ:

(1, (2, (3, Nil)))

แต่ละ item ใน cons list บรรจุสอง element — ค่าของ item ปัจจุบันและ ของ item ถัดไป item สุดท้ายใน list บรรจุเพียงค่าที่เรียก Nil โดย ไม่มี item ถัดไป cons list ถูกผลิตโดยเรียกฟังก์ชัน cons แบบ recursive ชื่อ canonical เพื่อ denote กรณีฐานของ recursion คือ Nil สังเกตว่านี่ไม่เหมือนกับแนวคิด “null” หรือ “nil” ที่พูดถึงใน บทที่ 6 ซึ่งเป็นค่าที่ไม่ valid หรือไม่มี

cons list ไม่ใช่โครงสร้างข้อมูลที่ใช้ทั่วไปใน Rust ส่วนใหญ่ของเวลา เมื่อคุณมี list ของ item ใน Rust Vec<T> เป็นทางเลือกที่ดีกว่าใช้ ประเภทข้อมูล recursive อื่นที่ซับซ้อนกว่า มี ประโยชน์ในสถานการณ์ ต่าง ๆ แต่โดยเริ่มด้วย cons list ในบทนี้ เราสำรวจวิธีที่ box ให้เรา นิยามประเภทข้อมูล recursive โดยไม่ต้องเสียสมาธิมากได้

Listing 15-2 บรรจุนิยาม enum สำหรับ cons list สังเกตว่าโค้ดนี้ยังจะ ไม่คอมไพล์ เพราะ type List ไม่มีขนาดที่รู้ ซึ่งเราจะสาธิต

Filename: src/main.rs
enum List {
    Cons(i32, List),
    Nil,
}

fn main() {}
Listing 15-2: การพยายามครั้งแรกที่นิยาม enum เพื่อแทนโครงสร้างข้อมูล cons list ของค่า i32

สังเกต — เรากำลัง implement cons list ที่เก็บเฉพาะค่า i32 เพื่อ จุดประสงค์ของตัวอย่างนี้ เรา implement มันโดยใช้ generic ก็ได้ ดังที่เราพูดถึงในบทที่ 10 เพื่อนิยามประเภท cons list ที่เก็บค่า ของ type ใดก็ได้

การใช้ type List เพื่อเก็บ list 1, 2, 3 จะดูเหมือนโค้ดใน Listing 15-3

Filename: src/main.rs
enum List {
    Cons(i32, List),
    Nil,
}

// --snip--

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}
Listing 15-3: ใช้ enum List เพื่อเก็บ list 1, 2, 3

ค่า Cons แรกเก็บ 1 และค่า List อื่น ค่า List นี้คือค่า Cons อื่นที่เก็บ 2 และค่า List อื่น ค่า List นี้เป็นค่า Cons อีกตัวที่เก็บ 3 และค่า List ซึ่งสุดท้ายคือ Nil variant ที่ไม่ recursive ที่ส่งสัญญาณการสิ้นสุดของ list

ถ้าเราพยายามคอมไพล์โค้ดใน Listing 15-3 เราได้ error ที่แสดงใน Listing 15-4

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

error[E0391]: cycle detected when computing when `List` needs drop
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
  |
  = note: ...which immediately requires computing when `List` needs drop again
  = note: cycle used when computing whether `List` needs drop
  = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
Listing 15-4: error ที่เราได้เมื่อพยายามนิยาม enum recursive

error แสดงว่า type นี้ “มีขนาด infinite” เหตุผลคือเรานิยาม List ด้วย variant ที่เป็น recursive — มันเก็บค่าอื่นของตัวเองโดยตรง ผลคือ Rust หาไม่ได้ว่าต้องการพื้นที่เท่าไรเพื่อเก็บค่า List มาแยกย่อยว่า ทำไมเราได้ error นี้ ก่อนอื่น เราจะดูว่า Rust ตัดสินอย่างไรว่าต้องการ พื้นที่เท่าไรเพื่อเก็บค่าของ type ที่ไม่ recursive

คำนวณขนาดของ Type ที่ไม่ Recursive

จำได้ enum Message ที่เรานิยามใน Listing 6-2 เมื่อเราพูดถึงนิยาม enum ในบทที่ 6:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}

เพื่อตัดสินว่า allocate พื้นที่เท่าไรสำหรับค่า Message Rust ผ่าน แต่ละ variant เพื่อดูว่า variant ไหนต้องการพื้นที่มากที่สุด Rust เห็นว่า Message::Quit ไม่ต้องการพื้นที่ใด Message::Move ต้องการ พื้นที่พอที่เก็บค่า i32 สองตัว และเช่นนั้นต่อไป เพราะจะถูกใช้เพียง variant เดียว พื้นที่มากที่สุดที่ค่า Message จะต้องการคือพื้นที่ ที่จะใช้เก็บ variant ที่ใหญ่ที่สุดของมัน

เปรียบเทียบนี้กับสิ่งที่เกิดเมื่อ Rust พยายามตัดสินว่า recursive type เช่น enum List ใน Listing 15-2 ต้องการพื้นที่เท่าไร compiler เริ่ม โดยดูที่ variant Cons ซึ่งเก็บค่าของ type i32 และค่าของ type List ดังนั้น Cons ต้องการพื้นที่จำนวนเท่ากับขนาดของ i32 บวก ขนาดของ List เพื่อหาว่า type List ต้องการ memory เท่าไร compiler ดู variant เริ่มด้วย variant Cons variant Cons เก็บค่าของ type i32 และค่าของ type List และกระบวนการนี้ดำเนินไปไม่สิ้นสุด ดังที่ แสดงใน Figure 15-1

An infinite Cons list: a rectangle labeled 'Cons' split into two smaller rectangles. The first smaller rectangle holds the label 'i32', and the second smaller rectangle holds the label 'Cons' and a smaller version of the outer 'Cons' rectangle. The 'Cons' rectangles continue to hold smaller and smaller versions of themselves until the smallest comfortably sized rectangle holds an infinity symbol, indicating that this repetition goes on forever.

Figure 15-1: List infinite ที่ประกอบด้วย variant Cons infinite

ได้ Recursive Type ที่มีขนาดรู้

เพราะ Rust หาไม่ได้ว่า allocate พื้นที่เท่าไรสำหรับ type ที่นิยาม แบบ recursive compiler ให้ error ด้วยข้อเสนอแนะที่ช่วยนี้:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

ในข้อเสนอแนะนี้ indirection หมายความว่าแทนการเก็บค่าโดยตรง เราควร เปลี่ยนโครงสร้างข้อมูลให้เก็บค่าโดยอ้อมโดยเก็บ pointer ไปยังค่าแทน

เพราะ Box<T> คือ pointer Rust รู้เสมอว่า Box<T> ต้องการพื้นที่ เท่าไร — ขนาดของ pointer ไม่เปลี่ยนตามจำนวนข้อมูลที่มันชี้ นี่หมาย ความว่าเราใส่ Box<T> ภายใน variant Cons แทนค่า List อื่นโดยตรง ได้ Box<T> จะชี้ไปยังค่า List ถัดไปที่จะอยู่บน heap ไม่ใช่ภายใน variant Cons ในเชิงแนวคิด เรายังมี list สร้างด้วย list ที่เก็บ list อื่น แต่ implementation นี้ตอนนี้เหมือนการวาง item ถัดจากกัน ไม่ใช่ภายในกัน

เราเปลี่ยนนิยามของ enum List ใน Listing 15-2 และการใช้ List ใน Listing 15-3 ให้เป็นโค้ดใน Listing 15-5 ซึ่งจะคอมไพล์ได้

Filename: src/main.rs
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Listing 15-5: นิยามของ List ที่ใช้ Box<T> เพื่อมีขนาดที่รู้

variant Cons ต้องการขนาดของ i32 บวกพื้นที่ที่จะเก็บข้อมูล pointer ของ box variant Nil ไม่เก็บค่า ดังนั้นมันต้องการพื้นที่น้อยกว่า บน stack กว่า variant Cons เรารู้ตอนนี้ว่าค่า List ใด ๆ จะใช้ ขนาดของ i32 บวกขนาดของข้อมูล pointer ของ box โดยใช้ box เราตัด chain infinite recursive ดังนั้น compiler หาขนาดที่ต้องการเก็บค่า List ได้ Figure 15-2 แสดงว่า variant Cons ดูเป็นยังไงตอนนี้

A rectangle labeled 'Cons' split into two smaller rectangles. The first smaller rectangle holds the label 'i32', and the second smaller rectangle holds the label 'Box' with one inner rectangle that contains the label 'usize', representing the finite size of the box's pointer.

Figure 15-2: List ที่ไม่มีขนาด infinite เพราะ Cons เก็บ Box

Box ให้เฉพาะ indirection และ heap allocation — พวกมันไม่มีความสามารถ พิเศษอื่นใด เช่นที่เราจะเห็นกับ smart pointer type อื่น พวกมันยังไม่ มี overhead performance ที่ความสามารถพิเศษเหล่านี้ก่อ ดังนั้นพวกมัน มีประโยชน์ในกรณีเช่น cons list ที่ indirection เป็นฟีเจอร์เดียวที่ เราต้องการ เราจะดู use case เพิ่มสำหรับ box ในบทที่ 18

type Box<T> เป็น smart pointer เพราะมัน implement trait Deref ซึ่งอนุญาตให้ค่า Box<T> ถูกปฏิบัติเหมือน reference เมื่อค่า Box<T> ออกจาก scope ข้อมูล heap ที่ box กำลังชี้ก็ถูกทำความสะอาด ด้วยเพราะ implementation trait Drop trait สองตัวนี้จะสำคัญมากขึ้น ต่อ functionality ที่ smart pointer type อื่นที่เราจะพูดถึงในส่วนที่ เหลือของบทนี้ให้มา มาสำรวจ trait สองตัวนี้ในรายละเอียดมากขึ้น