ตัวอย่างโปรแกรมที่ใช้ Struct
เพื่อเข้าใจว่าเมื่อไหร่เราอาจอยากใช้ struct ลองเขียนโปรแกรมที่คำนวณพื้นที่ ของสี่เหลี่ยมผืนผ้า เราจะเริ่มด้วยการใช้ตัวแปรเดี่ยว ๆ แล้ว refactor โปรแกรมจนกระทั่งเราใช้ struct แทน
มาสร้างโปรเจกต์ binary ใหม่กับ Cargo ชื่อ rectangles ที่จะรับความกว้าง และความสูงของสี่เหลี่ยมผืนผ้าที่ระบุเป็น pixel แล้วคำนวณพื้นที่ของสี่ เหลี่ยมผืนผ้า Listing 5-8 แสดงโปรแกรมสั้น ๆ ที่ทำสิ่งนั้นในไฟล์ src/main.rs ของโปรเจกต์เรา
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
ทีนี้รันโปรแกรมนี้ด้วย cargo run:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
โค้ดนี้สำเร็จในการหาพื้นที่ของสี่เหลี่ยมผืนผ้า โดยเรียกฟังก์ชัน area
ด้วยแต่ละมิติ แต่เราทำมากกว่านี้ได้เพื่อทำให้โค้ดนี้ชัดเจนและอ่านง่าย
ปัญหากับโค้ดนี้เห็นได้ชัดใน signature ของ area:
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
ฟังก์ชัน area ควรจะคำนวณพื้นที่ของสี่เหลี่ยมหนึ่งตัว แต่ฟังก์ชันที่เรา
เขียนมีสอง parameter และไม่ชัดเจนที่ไหนในโปรแกรมของเราว่า parameter ผูก
กัน มันจะอ่านง่ายและจัดการง่ายขึ้นถ้าจัดกลุ่ม width และ height เข้าด้วยกัน
เราพูดถึงวิธีหนึ่งที่เราอาจทำได้ในส่วน “Tuple Type”
ของบทที่ 3 — โดยใช้ tuple
Refactor ด้วย Tuple
Listing 5-9 แสดงอีก version ของโปรแกรมเราที่ใช้ tuple
fn main() {
let rect1 = (30, 50);
println!(
"The area of the rectangle is {} square pixels.",
area(rect1)
);
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
ในด้านหนึ่ง โปรแกรมนี้ดีขึ้น Tuple ให้เราเพิ่มโครงสร้างนิดหน่อย และตอนนี้ เราส่งแค่ argument เดียว แต่ในอีกด้านหนึ่ง version นี้ชัดเจนน้อยลง — tuple ไม่ตั้งชื่อ element ดังนั้นเราต้อง index เข้าส่วนของ tuple ทำให้การคำนวณ ของเราไม่ชัดเจน
การสับสนระหว่าง width และ height ไม่สำคัญสำหรับการคำนวณพื้นที่ แต่ถ้าเรา
อยากวาดสี่เหลี่ยมผืนผ้าบนหน้าจอ มันจะสำคัญ! เราจะต้องจำว่า width คือ
index 0 ของ tuple และ height คือ index 1 ของ tuple นี่ยิ่งยากกว่า
สำหรับคนอื่นที่จะรู้และจำ ถ้าเขาจะใช้โค้ดของเรา เพราะเราไม่ได้สื่อความหมาย
ของข้อมูลในโค้ด ตอนนี้มันง่ายขึ้นที่จะแนะนำ error
Refactor ด้วย Struct
เราใช้ struct เพิ่มความหมายโดยติด label กับข้อมูล เราเปลี่ยน tuple ที่เรา ใช้ให้เป็น struct ที่มีชื่อสำหรับทั้งหมด รวมถึงชื่อสำหรับส่วน ดังที่แสดง ใน Listing 5-10
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
area(&rect1)
);
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
Rectangleที่นี่ เราประกาศ struct และตั้งชื่อมัน Rectangle ภายใน curly bracket
เราประกาศ field เป็น width และ height ซึ่งทั้งคู่มี type u32 จาก
นั้นใน main เราสร้าง instance เฉพาะของ Rectangle ที่มี width 30
และ height 50
ฟังก์ชัน area ของเราตอนนี้ประกาศด้วย parameter หนึ่งตัว ที่เราตั้งชื่อ
rectangle ซึ่ง type เป็น immutable borrow ของ instance struct
Rectangle ตามที่กล่าวในบทที่ 4 เราอยาก borrow struct แทนการรับ ownership
ของมัน วิธีนี้ main ยังเก็บ ownership และใช้ rect1 ต่อได้ ซึ่งเป็น
เหตุผลที่เราใช้ & ใน signature ของฟังก์ชันและตรงที่เราเรียกฟังก์ชัน
ฟังก์ชัน area เข้าถึง field width และ height ของ instance
Rectangle (หมายเหตุว่าการเข้าถึง field ของ instance struct ที่ borrow
ไม่ move ค่า field ซึ่งเป็นเหตุผลที่คุณมักเห็น borrow ของ struct)
Signature ของฟังก์ชันเราสำหรับ area ตอนนี้บอกสิ่งที่เราหมายถึงเป๊ะ ๆ —
คำนวณพื้นที่ของ Rectangle โดยใช้ field width และ height ของมัน นี่
สื่อว่า width และ height ผูกกัน และให้ชื่อที่บรรยายค่า แทนการใช้ค่า index
ของ tuple 0 และ 1 นี่เป็นชัยชนะของความชัดเจน
เพิ่ม Functionality ด้วย Derived Trait
มันจะมีประโยชน์ที่จะพิมพ์ instance ของ Rectangle ขณะเรา debug โปรแกรม
และเห็นค่าของ field ทั้งหมด Listing 5-11 ลองใช้ println! macro
ที่เราใช้ในบทก่อน อย่างไรก็ตาม นี่จะไม่ทำงาน
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
Rectangleเมื่อเรา compile โค้ดนี้ เราได้ error พร้อมข้อความแกนกลางนี้:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
println! macro ทำ formatting ได้หลายแบบ และโดย default curly bracket
บอก println! ให้ใช้ formatting ที่รู้จักในชื่อ Display — output ที่
ตั้งใจให้ end user บริโภคโดยตรง primitive type ที่เราเห็นมาแล้ว implement
Display โดย default เพราะมีแค่วิธีเดียวที่คุณจะอยากแสดง 1 หรือ
primitive type อื่นใดให้ user แต่กับ struct วิธีที่ println! ควร format
output ชัดเจนน้อยลง เพราะมีความเป็นไปได้ในการแสดงมากขึ้น — คุณอยากได้
comma หรือไม่? คุณอยากพิมพ์ curly bracket ไหม? ควรแสดง field ทั้งหมดไหม?
เพราะความกำกวมนี้ Rust ไม่พยายามเดาสิ่งที่เราต้องการ และ struct ไม่มี
implementation ของ Display ให้ใช้กับ println! และ placeholder {}
ถ้าเราอ่าน error ต่อ เราจะพบหมายเหตุที่มีประโยชน์นี้:
| |`Rectangle` cannot be formatted with the default formatter
| required by this formatting parameter
ลองดู! การเรียก println! macro ตอนนี้จะหน้าตาเป็น
println!("rect1 is {rect1:?}"); การใส่ specifier :? ใน curly bracket
บอก println! ว่าเราอยากใช้ output format ชื่อ Debug trait Debug ให้
เราพิมพ์ struct ในแบบที่มีประโยชน์สำหรับนักพัฒนา เพื่อให้เราเห็นค่าของมัน
ขณะเรา debug โค้ด
Compile โค้ดด้วยการเปลี่ยนนี้ ให้ตายสิ! เราก็ยังได้ error:
error[E0277]: `Rectangle` doesn't implement `Debug`
แต่อีกครั้ง compiler ให้หมายเหตุที่มีประโยชน์:
| required by this formatting parameter
|
Rust มี functionality สำหรับพิมพ์ข้อมูล debug แต่เราต้อง opt in แบบ
explicit เพื่อให้ functionality นั้นใช้กับ struct ของเราได้ ในการทำสิ่งนั้น
เราเพิ่ม outer attribute #[derive(Debug)] ก่อนการประกาศ struct ดังที่
แสดงใน Listing 5-12
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1:?}");
}
Debug และพิมพ์ instance Rectangle ด้วย debug formattingทีนี้เมื่อเรารันโปรแกรม เราจะไม่ได้ error และเราจะเห็น output ต่อไปนี้:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
ดี! ไม่ใช่ output ที่สวยที่สุด แต่มันแสดงค่าของ field ทั้งหมดสำหรับ
instance นี้ ซึ่งจะช่วยตอน debug แน่นอน เมื่อเรามี struct ใหญ่ขึ้น มี
ประโยชน์ที่มี output ที่อ่านง่ายขึ้นนิดหน่อย ในกรณีเหล่านั้น เราใช้
{:#?} แทน {:?} ใน string println! ได้ ในตัวอย่างนี้ การใช้สไตล์
{:#?} จะ output ต่อไปนี้:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
อีกวิธีในการพิมพ์ค่าโดยใช้ Debug format คือใช้ dbg! macro
ซึ่งรับ ownership ของ expression (ต่างจาก println! ที่รับ reference)
พิมพ์ไฟล์และเลขบรรทัดที่การเรียก dbg! macro เกิดในโค้ดของคุณ พร้อมค่า
ผลของ expression นั้น และ return ownership ของค่า
หมายเหตุ: การเรียก
dbg!macro พิมพ์ไปยัง standard error console stream (stderr) ต่างจากprintln!ซึ่งพิมพ์ไปยัง standard output console stream (stdout) เราจะพูดถึงstderrและstdoutมากขึ้นใน ส่วน “Redirect Error ไปยัง Standard Error” ในบทที่ 12
นี่คือตัวอย่างที่เราสนใจค่าที่ assign ให้ field width รวมถึงค่าของทั้ง
struct ใน rect1:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};
dbg!(&rect1);
}
เราใส่ dbg! รอบ expression 30 * scale ได้ และเพราะ dbg! return
ownership ของค่า expression field width จะได้ค่าเดียวกับที่ไม่มีการ
เรียก dbg! ตรงนั้น เราไม่อยากให้ dbg! รับ ownership ของ rect1 เรา
จึงใช้ reference ของ rect1 ในการเรียกถัดไป นี่คือสิ่งที่ output ของ
ตัวอย่างนี้ดูเป็นแบบนี้:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
เราเห็น output ส่วนแรกมาจาก src/main.rs บรรทัด 10 ที่เรา debug
expression 30 * scale และค่าผลคือ 60 (Debug formatting ที่ implement
สำหรับ integer คือพิมพ์แค่ค่า) การเรียก dbg! ในบรรทัด 14 ของ
src/main.rs output ค่าของ &rect1 ซึ่งเป็น struct Rectangle output
นี้ใช้ pretty Debug formatting ของ type Rectangle dbg! macro มี
ประโยชน์มากเมื่อคุณพยายามคิดว่าโค้ดของคุณกำลังทำอะไร!
นอกจาก trait Debug Rust ให้ trait หลายตัวที่เราใช้กับ attribute
derive ได้ ที่เพิ่มพฤติกรรมที่มีประโยชน์ให้ type custom ของเรา trait
เหล่านั้นและพฤติกรรมของพวกมัน list ใน ภาคผนวก C
เราจะครอบคลุมวิธี implement trait เหล่านี้ด้วยพฤติกรรม custom รวมถึงวิธี
สร้าง trait ของคุณเองในบทที่ 10 ยังมี attribute หลายตัวอื่นนอกจาก derive
สำหรับข้อมูลเพิ่มเติม ดู ส่วน “Attribute” ของ Rust Reference
ฟังก์ชัน area ของเราเฉพาะมาก — มันคำนวณแค่พื้นที่ของสี่เหลี่ยมผืนผ้า
จะมีประโยชน์ที่จะผูกพฤติกรรมนี้ใกล้กับ struct Rectangle ของเรามากขึ้น
เพราะมันจะไม่ทำงานกับ type อื่นใด มาดูว่าเรา refactor โค้ดนี้ต่อได้ยังไง
โดยเปลี่ยนฟังก์ชัน area ให้เป็นเมธอด area ที่ประกาศบน type
Rectangle ของเรา