ใช้ 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
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
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 ไม่มีขนาดที่รู้ ซึ่งเราจะสาธิต
enum List {
Cons(i32, List),
Nil,
}
fn main() {}
i32สังเกต — เรากำลัง implement cons list ที่เก็บเฉพาะค่า
i32เพื่อ จุดประสงค์ของตัวอย่างนี้ เรา implement มันโดยใช้ generic ก็ได้ ดังที่เราพูดถึงในบทที่ 10 เพื่อนิยามประเภท cons list ที่เก็บค่า ของ type ใดก็ได้
การใช้ type List เพื่อเก็บ list 1, 2, 3 จะดูเหมือนโค้ดใน Listing
15-3
enum List {
Cons(i32, List),
Nil,
}
// --snip--
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
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
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
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 ซึ่งจะคอมไพล์ได้
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))))));
}
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 ดูเป็นยังไงตอนนี้
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 สองตัวนี้ในรายละเอียดมากขึ้น