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

ตัวอย่างโปรแกรมที่ใช้ Struct

เพื่อเข้าใจว่าเมื่อไหร่เราอาจอยากใช้ struct ลองเขียนโปรแกรมที่คำนวณพื้นที่ ของสี่เหลี่ยมผืนผ้า เราจะเริ่มด้วยการใช้ตัวแปรเดี่ยว ๆ แล้ว refactor โปรแกรมจนกระทั่งเราใช้ struct แทน

มาสร้างโปรเจกต์ binary ใหม่กับ Cargo ชื่อ rectangles ที่จะรับความกว้าง และความสูงของสี่เหลี่ยมผืนผ้าที่ระบุเป็น pixel แล้วคำนวณพื้นที่ของสี่ เหลี่ยมผืนผ้า Listing 5-8 แสดงโปรแกรมสั้น ๆ ที่ทำสิ่งนั้นในไฟล์ src/main.rs ของโปรเจกต์เรา

Filename: 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
}
Listing 5-8: คำนวณพื้นที่ของสี่เหลี่ยมผืนผ้าที่ระบุด้วยตัวแปรความกว้างและความสูงแยกกัน

ทีนี้รันโปรแกรมนี้ด้วย 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

Filename: src/main.rs
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
}
Listing 5-9: ระบุความกว้างและความสูงของสี่เหลี่ยมผืนผ้าด้วย tuple

ในด้านหนึ่ง โปรแกรมนี้ดีขึ้น 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

Filename: src/main.rs
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
}
Listing 5-10: ประกาศ struct 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 ที่เราใช้ในบทก่อน อย่างไรก็ตาม นี่จะไม่ทำงาน

Filename: src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1}");
}
Listing 5-11: พยายามพิมพ์ instance 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

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {rect1:?}");
}
Listing 5-12: เพิ่ม attribute เพื่อ derive trait 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 ของเรา