The Rust Programming Language
เขียนโดย Steve Klabnik, Carol Nichols และ Chris Krycho พร้อมการช่วยเหลือจาก Rust Community
แปลภาษาไทยโดย round.online
หนังสือเล่มนี้อ้างอิง Rust 1.90.0 (release วันที่ 2025-09-18) เป็นต้นไป โดยใช้
edition = "2024" ในไฟล์ Cargo.toml ของทุกโปรเจกต์ เพื่อให้ใช้สำนวนของ Rust
2024 Edition ดูวิธีติดตั้งหรือ update Rust ได้ใน หัวข้อ “การติดตั้ง” ของบทที่ 1
และดูข้อมูลเรื่อง edition ได้ใน ภาคผนวก E
ฉบับ HTML ต้นฉบับภาษาอังกฤษอ่านออนไลน์ได้ที่
https://doc.rust-lang.org/stable/book/
และอ่าน offline ได้หลังติดตั้ง Rust ด้วย rustup โดยรันคำสั่ง rustup doc --book
มี ฉบับแปลภาษาอื่น ๆ จาก community ให้เลือกอ่าน
ต้นฉบับภาษาอังกฤษมีจำหน่ายในรูปแบบ paperback และ ebook จาก No Starch Press
🚨 อยากได้ประสบการณ์การเรียนแบบ interactive มากกว่านี้? ลอง Rust Book อีกฉบับ ที่มี quiz, highlight, visualization และอื่น ๆ: https://rust-book.cs.brown.edu
💡 หมายเหตุจากผู้แปล: นี่คือฉบับแปลภาษาไทยที่มีการดัดแปลง ของหนังสือ The Rust Programming Language ต้นฉบับ — ถ้าพบข้อผิดพลาดในการแปล กรุณาเปิด issue ที่ https://github.com/roundonline/rust-book-th
คำนำ
ภาษา Rust เดินทางมาไกลในเวลาไม่กี่ปี จากจุดเริ่มต้นและการบ่มเพาะโดย community เล็ก ๆ ของกลุ่มคนที่หลงรักภาษานี้ มาสู่การเป็นหนึ่งในภาษาโปรแกรมที่คนรักและเป็น ที่ต้องการมากที่สุดในโลก ถ้ามองย้อนกลับไป ก็เป็นเรื่องที่หลีกเลี่ยงไม่ได้ว่า พลังและคำมั่นสัญญาของ Rust จะดึงดูดความสนใจและหยั่งรากในวงการ systems programming แต่สิ่งที่ไม่ได้คาดคิดไว้คือการเติบโตของความสนใจและนวัตกรรมใน ระดับโลก ที่แทรกซึมผ่าน open source community และกระตุ้นให้เกิดการใช้งานในวง กว้างข้ามอุตสาหกรรม
ณ จุดนี้ ไม่ยากเลยที่จะชี้ให้เห็นฟีเจอร์ดี ๆ ของ Rust ที่ใช้อธิบายความสนใจและ การใช้งานที่ระเบิดออกมาแบบนี้ ใครจะไม่อยากได้ความปลอดภัยด้านหน่วยความจำ และ performance ที่รวดเร็ว และ compiler ที่เป็นมิตร และ tooling ที่ยอดเยี่ยม รวมถึงฟีเจอร์ดี ๆ อีกมากมาย? Rust ที่คุณเห็นในวันนี้ คือการผสมผสานของการวิจัย ด้าน systems programming หลายปี เข้ากับภูมิปัญญาเชิงปฏิบัติของ community ที่ มีชีวิตชีวาและเปี่ยมไปด้วยใจรัก ภาษานี้ถูกออกแบบมาอย่างมีจุดมุ่งหมายและสร้าง ขึ้นด้วยความใส่ใจ เพื่อให้นักพัฒนามีเครื่องมือที่ช่วยให้เขียนโค้ดที่ปลอดภัย รวดเร็ว และเชื่อถือได้ง่ายขึ้น
แต่สิ่งที่ทำให้ Rust พิเศษอย่างแท้จริง คือรากฐานที่มุ่งเสริมพลังให้คุณ — ผู้ใช้ — บรรลุเป้าหมายของตัวเอง นี่คือภาษาที่อยากเห็นคุณประสบความสำเร็จ และหลักการ ของการเสริมพลังนี้ก็แทรกซึมอยู่ในแกนกลางของ community ที่สร้าง ดูแล และผลักดัน ภาษานี้ นับตั้งแต่ฉบับก่อนของหนังสือเล่มนี้ Rust ได้พัฒนาต่อจนกลายเป็นภาษา ระดับโลกที่ได้รับความเชื่อมั่นอย่างแท้จริง ปัจจุบัน Rust Project ได้รับการ สนับสนุนอย่างมั่นคงจาก Rust Foundation ซึ่งยังลงทุนในโครงการสำคัญต่าง ๆ เพื่อ ให้แน่ใจว่า Rust จะปลอดภัย เสถียร และยั่งยืน
ฉบับของ The Rust Programming Language นี้เป็นการ update ครั้งใหญ่ที่ ครอบคลุม สะท้อนวิวัฒนาการของภาษาตลอดหลายปี และมีข้อมูลใหม่ ๆ ที่มีคุณค่า แต่ มันไม่ใช่แค่คู่มือเรื่อง syntax และ library เท่านั้น — มันคือคำเชิญให้เข้าร่วม community ที่ให้คุณค่ากับคุณภาพ performance และการออกแบบที่รอบคอบ ไม่ว่าคุณ จะเป็นนักพัฒนามากประสบการณ์ที่กำลังจะลอง Rust เป็นครั้งแรก หรือ Rustacean ที่ช่ำชองและอยากฝึกฝีมือให้คมขึ้น หนังสือฉบับนี้มีบางสิ่งให้คุณเสมอ
การเดินทางของ Rust เป็นการเดินทางของการร่วมมือ การเรียนรู้ และการพัฒนาอย่างต่อ เนื่อง การเติบโตของภาษาและ ecosystem ของมัน คือภาพสะท้อนโดยตรงของ community ที่มีชีวิตชีวาและหลากหลายเบื้องหลัง การมีส่วนร่วมของนักพัฒนาหลายพันคน ตั้งแต่ ผู้ออกแบบภาษาในระดับ core ไปจนถึง contributor ที่เข้ามาช่วยเป็นครั้งคราว ทั้งหมดนี้คือสิ่งที่ทำให้ Rust เป็นเครื่องมือที่ทั้งโดดเด่นและทรงพลัง การ หยิบหนังสือเล่มนี้ขึ้นมาอ่าน คุณไม่ได้แค่กำลังเรียนภาษาโปรแกรมใหม่ — คุณกำลัง เข้าร่วมการเคลื่อนไหวที่จะทำให้ software ดีขึ้น ปลอดภัยขึ้น และน่าทำงานด้วย มากขึ้น
ยินดีต้อนรับสู่ Rust community!
- Bec Rumbul, Executive Director ของ Rust Foundation
บทนำ
หมายเหตุ: หนังสือฉบับนี้เหมือนกับ The Rust Programming Language ที่จำหน่ายในรูปแบบหนังสือเล่มและ ebook โดย No Starch Press
ยินดีต้อนรับสู่ The Rust Programming Language หนังสือแนะนำเกี่ยวกับ Rust ภาษา Rust ช่วยให้คุณเขียน software ที่เร็วและน่าเชื่อถือขึ้น โดยปกติแล้วการ ออกแบบภาษาโปรแกรมมักต้องเลือกระหว่าง ergonomics ระดับสูง กับการควบคุมระดับต่ำ — Rust ท้าทายความขัดแย้งนี้ ด้วยการสร้างสมดุลระหว่างความสามารถทางเทคนิคที่ทรง พลัง กับประสบการณ์ที่ดีของนักพัฒนา Rust จึงเปิดทางให้คุณควบคุมรายละเอียดระดับ ต่ำ (เช่น การใช้หน่วยความจำ) ได้ โดยไม่ต้องเจอความยุ่งยากที่มักมาคู่กับการ ควบคุมแบบนั้น
Rust เหมาะกับใคร
Rust เหมาะกับคนหลายกลุ่มด้วยเหตุผลที่หลากหลาย ลองดูกลุ่มสำคัญ ๆ กัน
ทีมนักพัฒนา
Rust พิสูจน์ตัวเองแล้วว่าเป็นเครื่องมือที่ productive สำหรับการทำงานร่วมกัน ในทีมขนาดใหญ่ที่สมาชิกมีความรู้เรื่อง systems programming ในระดับที่แตกต่างกัน โค้ดระดับต่ำมักมี bug แบบ subtle ซึ่งในภาษาอื่น ๆ ส่วนใหญ่จะจับได้ก็ต่อเมื่อ ผ่านการเทสอย่างละเอียดและการ review code อย่างระมัดระวังโดยนักพัฒนาที่มี ประสบการณ์เท่านั้น สำหรับ Rust compiler จะทำหน้าที่เป็นด่านตรวจ โดยปฏิเสธที่ จะ compile โค้ดที่มี bug ที่ตามจับยากเหล่านี้ รวมถึง bug เรื่อง concurrency ด้วย เมื่อทำงานเคียงข้างกับ compiler ทีมก็สามารถใช้เวลาไปกับ logic ของ โปรแกรมแทนที่จะมาตามไล่ล่า bug
Rust ยังนำเครื่องมือพัฒนาสมัยใหม่มาสู่โลกของ systems programming ด้วย
- Cargo ซึ่งเป็น dependency manager และ build tool ที่มาให้ในตัว ช่วยให้การ เพิ่ม compile และจัดการ dependency เป็นเรื่องง่ายและสอดคล้องกันใน ecosystem ของ Rust
rustfmtเครื่องมือจัด format ทำให้สไตล์การเขียนโค้ดสอดคล้องกันระหว่างนัก พัฒนา- Rust Language Server ขับเคลื่อนการ integrate กับ integrated development environment (IDE) สำหรับการ complete code และแสดง error message inline
ด้วยการใช้เครื่องมือเหล่านี้และอื่น ๆ ใน ecosystem ของ Rust นักพัฒนาก็สามารถ ทำงาน systems-level ได้อย่าง productive
นักเรียน นักศึกษา
Rust เหมาะกับนักเรียน นักศึกษา และคนที่สนใจเรียนรู้แนวคิดเกี่ยวกับ systems หลายคนใช้ Rust เรียนรู้หัวข้อต่าง ๆ เช่นการพัฒนา operating system community เป็นมิตรและยินดีตอบคำถามของนักศึกษา ผ่านความพยายามต่าง ๆ รวมถึงหนังสือเล่มนี้ ทีม Rust ต้องการทำให้แนวคิดเรื่อง systems เข้าถึงได้กับคนจำนวนมากขึ้น โดยเฉพาะคนที่เพิ่งเริ่มเขียนโปรแกรม
บริษัท
บริษัทหลายร้อยแห่ง ทั้งใหญ่และเล็ก ใช้ Rust ใน production สำหรับงานหลากหลาย ตั้งแต่เครื่องมือ command line, web service, DevOps tooling, อุปกรณ์ embedded, การวิเคราะห์และ transcode เสียงและวิดีโอ, cryptocurrency, bioinformatics, search engine, แอปพลิเคชัน Internet of Things, machine learning ไปจนถึงส่วน สำคัญ ๆ ของ web browser Firefox
นักพัฒนา Open Source
Rust เหมาะกับคนที่อยากร่วมสร้างภาษา Rust, community, เครื่องมือพัฒนา และ library เรายินดีให้คุณมา contribute กับภาษา Rust
คนที่ให้คุณค่ากับความเร็วและความเสถียร
Rust เหมาะกับคนที่ต้องการความเร็วและความเสถียรในภาษา คำว่า “ความเร็ว” หมาย ถึงทั้งความเร็วที่โค้ด Rust รันได้ และความเร็วที่ Rust ช่วยให้คุณเขียน โปรแกรมได้ การตรวจสอบของ compiler รับประกันความเสถียรเมื่อมีการเพิ่มฟีเจอร์ และ refactor ตรงข้ามกับ legacy code ที่เปราะบางในภาษาที่ไม่มีการตรวจสอบเหล่า นี้ ซึ่งนักพัฒนามักไม่กล้าแก้ ด้วยการมุ่งสู่ zero-cost abstraction — ฟีเจอร์ ระดับสูงที่ compile ออกมาเป็นโค้ดระดับต่ำที่เร็วเทียบเท่ากับโค้ดที่เขียนเอง — Rust พยายามทำให้โค้ดที่ปลอดภัยเป็นโค้ดที่เร็วด้วยเช่นกัน
ภาษา Rust หวังจะรองรับผู้ใช้กลุ่มอื่น ๆ อีกมาก กลุ่มที่กล่าวมานี้เป็นเพียง stakeholder กลุ่มใหญ่ ๆ เท่านั้น โดยรวมแล้ว ความใฝ่ฝันที่ยิ่งใหญ่ที่สุดของ Rust คือการขจัดการต้องเลือกระหว่างสิ่งต่าง ๆ ที่โปรแกรมเมอร์ยอมรับมาเป็น ทศวรรษ ด้วยการมอบทั้งความปลอดภัย และ productivity, ทั้งความเร็ว และ ergonomics ลอง Rust ดู แล้วดูว่าตัวเลือกของมันเหมาะกับคุณหรือไม่
หนังสือเล่มนี้เหมาะกับใคร
หนังสือเล่มนี้ตั้งสมมติฐานว่าคุณเคยเขียนโค้ดด้วยภาษาโปรแกรมอื่นมาก่อน แต่ไม่ได้ ตั้งสมมติฐานว่าคุณเขียนภาษาไหน เราพยายามทำให้เนื้อหาเข้าถึงได้สำหรับคนจาก หลากหลายพื้นเพการเขียนโปรแกรม เราไม่ได้ใช้เวลามากกับการพูดว่าการเขียนโปรแกรม คือ อะไร หรือควรคิดเกี่ยวกับมันยังไง ถ้าคุณยังใหม่ต่อการเขียนโปรแกรมโดยสิ้น เชิง คงจะดีกว่าถ้าอ่านหนังสือที่แนะนำการเขียนโปรแกรมโดยเฉพาะ
วิธีอ่านหนังสือเล่มนี้
โดยทั่วไป หนังสือเล่มนี้ตั้งสมมติฐานว่าคุณจะอ่านตามลำดับจากต้นไปท้าย บทหลัง ๆ สร้างขึ้นบนแนวคิดของบทก่อน ๆ และบทแรก ๆ อาจไม่ลงรายละเอียดเรื่องใดเรื่องหนึ่ง แต่จะกลับมาทบทวนหัวข้อนั้นในบทหลัง
ในหนังสือเล่มนี้คุณจะพบบทอยู่สองแบบ: บทแนวคิด (concept chapter) และบทโปรเจกต์ (project chapter) ในบทแนวคิด คุณจะเรียนแง่มุมต่าง ๆ ของ Rust ในบทโปรเจกต์ เรา จะมาเขียนโปรแกรมเล็ก ๆ ร่วมกัน โดยนำสิ่งที่เรียนมาไปประยุกต์ใช้ บทที่ 2, 12 และ 21 เป็นบทโปรเจกต์ ส่วนที่เหลือเป็นบทแนวคิด
บทที่ 1 อธิบายวิธีติดตั้ง Rust วิธีเขียนโปรแกรม “Hello, world!” และวิธี ใช้ Cargo ซึ่งเป็น package manager และ build tool ของ Rust บทที่ 2 เป็น การแนะนำการเขียนโปรแกรมใน Rust แบบลงมือทำ โดยให้คุณสร้างเกมทายตัวเลข ในบทนี้ เราจะกล่าวถึงแนวคิดต่าง ๆ ในระดับ high-level และบทหลัง ๆ จะให้รายละเอียดเพิ่ม เติม ถ้าคุณอยากลงมือทำเลย บทที่ 2 คือที่สำหรับคุณ ถ้าคุณเป็นคนเรียนแบบละเอียด ที่ชอบเรียนรู้ทุกรายละเอียดก่อนไปต่อ คุณอาจจะข้ามบทที่ 2 ไป บทที่ 3 ซึ่ง ครอบคลุมฟีเจอร์ของ Rust ที่คล้ายกับภาษาโปรแกรมอื่น ๆ แล้วค่อยกลับมาที่บทที่ 2 ตอนที่อยากทำโปรเจกต์เพื่อประยุกต์รายละเอียดที่ได้เรียนมา
ใน บทที่ 4 คุณจะได้เรียนเรื่องระบบ ownership ของ Rust บทที่ 5 พูดถึง
struct และเมธอด บทที่ 6 ครอบคลุม enum, match expression และโครงสร้าง
control flow แบบ if let และ let...else คุณจะใช้ struct และ enum สร้าง
type ของตัวเอง
ใน บทที่ 7 คุณจะได้เรียนเรื่องระบบ module ของ Rust และ privacy rule สำหรับจัดระเบียบโค้ดและ application programming interface (API) สาธารณะของ มัน บทที่ 8 พูดถึงโครงสร้างข้อมูล collection ที่ใช้บ่อย ๆ ที่ standard library มีให้: vector, string และ hash map บทที่ 9 สำรวจปรัชญาและเทคนิค การจัดการ error ของ Rust
บทที่ 10 เจาะลึก generic, trait และ lifetime ซึ่งให้พลังคุณในการกำหนด
โค้ดที่ใช้กับ type หลายแบบ บทที่ 11 เรื่องการเทสทั้งหมด ซึ่งแม้จะมีการ
รับประกันความปลอดภัยของ Rust ก็ยังจำเป็นเพื่อให้แน่ใจว่า logic ของโปรแกรม
ถูกต้อง ใน บทที่ 12 เราจะสร้าง implementation ของฟีเจอร์ย่อย ๆ บางส่วน
ของเครื่องมือ command line ชื่อ grep ซึ่งใช้ค้นหาข้อความในไฟล์ ในบทนี้เรา
จะใช้แนวคิดหลายอย่างที่พูดถึงในบทก่อน ๆ
บทที่ 13 สำรวจ closure และ iterator: ฟีเจอร์ของ Rust ที่มาจากภาษา โปรแกรมแบบ functional ใน บทที่ 14 เราจะดู Cargo ในเชิงลึกขึ้น และพูดถึง best practice ในการแชร์ library ของคุณกับคนอื่น บทที่ 15 พูดถึง smart pointer ที่ standard library มีให้ และ trait ที่ทำให้ฟังก์ชันเหล่านั้นทำงาน ได้
ใน บทที่ 16 เราจะเดินผ่าน model ต่าง ๆ ของ concurrent programming และ พูดถึงวิธีที่ Rust ช่วยให้คุณเขียนโปรแกรมแบบหลาย thread อย่างไร้ความกลัว ใน บทที่ 17 เราจะต่อยอดจากนั้น โดยสำรวจ syntax async/await ของ Rust พร้อม ทั้ง task, future, stream และ model ของ concurrency แบบ lightweight ที่ มันเอื้อให้
บทที่ 18 ดูว่าสำนวนของ Rust เทียบกับหลักการของ object-oriented programming ที่คุณอาจคุ้นเคยอย่างไร บทที่ 19 คือ reference เรื่อง pattern และ pattern matching ซึ่งเป็นวิธีที่ทรงพลังในการแสดงไอเดียตลอด โปรแกรม Rust บทที่ 20 ประกอบด้วยหัวข้อขั้นสูงที่น่าสนใจหลายหัวข้อ รวม ถึง unsafe Rust, macro และรายละเอียดเพิ่มเติมเรื่อง lifetime, trait, type, function และ closure
ใน บทที่ 21 เราจะทำโปรเจกต์ให้เสร็จ โดย implement web server แบบ multithreaded ระดับต่ำ!
สุดท้าย ภาคผนวกบางส่วนมีข้อมูลที่เป็นประโยชน์เกี่ยวกับภาษาในรูปแบบที่คล้าย reference มากกว่า ภาคผนวก A ครอบคลุม keyword ของ Rust, ภาคผนวก B ครอบคลุม operator และสัญลักษณ์ของ Rust, ภาคผนวก C ครอบคลุม derivable trait ที่ standard library มีให้, ภาคผนวก D ครอบคลุมเครื่องมือพัฒนาที่ มีประโยชน์ และ ภาคผนวก E อธิบาย edition ของ Rust ใน ภาคผนวก F คุณ จะพบรายชื่อฉบับแปลของหนังสือ และใน ภาคผนวก G เราจะพูดถึงวิธีพัฒนา Rust และ nightly Rust คืออะไร
ไม่มีวิธีอ่านหนังสือเล่มนี้ที่ผิด ถ้าอยากกระโดดไปข้างหน้าก็เชิญ! คุณอาจต้อง กระโดดกลับมาบทแรก ๆ ถ้าเกิดความสับสน แต่ทำอะไรก็ตามที่ใช้ได้กับคุณ
ส่วนสำคัญของการเรียน Rust คือการเรียนรู้ที่จะอ่าน error message ที่ compiler แสดง — message พวกนี้จะนำทางคุณไปสู่โค้ดที่ทำงานได้ ดังนั้นเราจะให้ตัวอย่าง หลายตัวที่ compile ไม่ผ่าน พร้อม error message ที่ compiler จะแสดงในแต่ละ สถานการณ์ พึงรู้ว่าถ้าคุณป้อนและรันตัวอย่างแบบสุ่ม มันอาจ compile ไม่ผ่าน! อย่าลืมอ่านข้อความรอบ ๆ เพื่อดูว่าตัวอย่างที่กำลังจะรันนั้นตั้งใจให้ error หรือไม่ ในกรณีส่วนใหญ่ เราจะนำคุณไปสู่ version ที่ถูกต้องของโค้ดที่ compile ไม่ผ่าน Ferris จะช่วยให้คุณแยกแยะโค้ดที่ไม่ได้ตั้งใจให้ทำงาน:
| Ferris | ความหมาย |
|---|---|
| โค้ดนี้ compile ไม่ผ่าน! | |
| โค้ดนี้ panic! | |
| โค้ดนี้ไม่ได้ให้พฤติกรรมตามที่ต้องการ |
ในกรณีส่วนใหญ่ เราจะนำคุณไปสู่ version ที่ถูกต้องของโค้ดที่ compile ไม่ผ่าน
Source Code
ไฟล์ source ที่ใช้ generate หนังสือเล่มนี้ดูได้ที่ GitHub
เริ่มต้นใช้งาน
มาเริ่มต้นการเดินทางสู่ Rust กันเถอะ! มีอะไรให้เรียนรู้เยอะ แต่ทุกการเดินทาง ก็ต้องเริ่มจากที่ใดที่หนึ่ง ในบทนี้เราจะพูดถึง:
- การติดตั้ง Rust บน Linux, macOS และ Windows
- การเขียนโปรแกรมที่พิมพ์
Hello, world! - การใช้
cargoซึ่งเป็น package manager และ build system ของ Rust
การติดตั้ง
การติดตั้ง
ขั้นตอนแรกคือการติดตั้ง Rust เราจะ download Rust ผ่าน rustup ซึ่งเป็น
เครื่องมือ command line สำหรับจัดการ version ของ Rust และเครื่องมือที่
เกี่ยวข้อง คุณจะต้องเชื่อมต่ออินเทอร์เน็ตเพื่อ download
หมายเหตุ: ถ้าคุณไม่อยากใช้
rustupด้วยเหตุผลใดก็ตาม โปรดดู หน้า Other Rust Installation Methods สำหรับตัวเลือกเพิ่มเติม
ขั้นตอนต่อไปนี้จะติดตั้ง Rust compiler version stable ล่าสุด การรับประกัน ความเสถียรของ Rust ทำให้แน่ใจได้ว่าตัวอย่างทั้งหมดในหนังสือที่ compile ได้ จะยัง compile ได้กับ Rust version ใหม่ ๆ ด้วย output อาจต่างกันเล็กน้อย ระหว่าง version เพราะ Rust มักปรับปรุง error message และ warning อยู่เสมอ พูดอีกอย่าง Rust version stable ใหม่ ๆ ที่คุณติดตั้งด้วยขั้นตอนเหล่านี้ ควร ทำงานได้ตามที่คาดกับเนื้อหาของหนังสือเล่มนี้
สัญลักษณ์ใน Command Line
ในบทนี้และตลอดทั้งเล่ม เราจะแสดงคำสั่งบางคำสั่งที่ใช้ใน terminal บรรทัดที่
คุณควรพิมพ์ลงใน terminal จะขึ้นต้นด้วย $ คุณไม่ต้องพิมพ์ตัวอักษร $ มัน
เป็น prompt ของ command line ที่แสดงเพื่อบอกจุดเริ่มต้นของแต่ละคำสั่ง
บรรทัดที่ไม่ได้ขึ้นต้นด้วย $ มักแสดง output ของคำสั่งก่อนหน้า นอกจากนี้
ตัวอย่างที่เฉพาะเจาะจงสำหรับ PowerShell จะใช้ > แทน $
การติดตั้ง rustup บน Linux หรือ macOS
ถ้าคุณใช้ Linux หรือ macOS เปิด terminal แล้วป้อนคำสั่งต่อไปนี้:
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
คำสั่งนี้ download script และเริ่มติดตั้งเครื่องมือ rustup ซึ่งจะติดตั้ง
Rust version stable ล่าสุด คุณอาจถูกถาม password ของระบบ ถ้าติดตั้งสำเร็จ
จะมีบรรทัดต่อไปนี้ปรากฏ:
Rust is installed now. Great!
คุณจะต้องมี linker ด้วย ซึ่งเป็นโปรแกรมที่ Rust ใช้รวม output ที่ compile ออกมาให้เป็นไฟล์เดียว เป็นไปได้สูงว่าคุณมีอยู่แล้ว ถ้าเจอ linker error คุณ ควรติดตั้ง C compiler ซึ่งโดยทั่วไปจะมี linker มาด้วย C compiler ยังมี ประโยชน์เพราะ Rust package ที่ใช้บ่อยบางตัวพึ่งพาโค้ด C และต้องใช้ C compiler
บน macOS คุณสามารถได้ C compiler โดยรัน:
$ xcode-select --install
ผู้ใช้ Linux โดยทั่วไปควรติดตั้ง GCC หรือ Clang ตามเอกสารของ distribution
ของตน เช่น ถ้าใช้ Ubuntu คุณสามารถติดตั้ง package build-essential
การติดตั้ง rustup บน Windows
บน Windows ให้ไปที่ https://www.rust-lang.org/tools/install และทำตามคำแนะนำในการติดตั้ง Rust ในระหว่างการติดตั้ง คุณจะถูกถามให้ติดตั้ง Visual Studio ซึ่งจะให้ linker และ native library ที่จำเป็นสำหรับการ compile โปรแกรม ถ้าต้องการความช่วยเหลือเพิ่มเติมในขั้นตอนนี้ ดูที่ https://rust-lang.github.io/rustup/installation/windows-msvc.html
เนื้อหาที่เหลือของหนังสือใช้คำสั่งที่ทำงานได้ทั้งใน cmd.exe และ PowerShell ถ้ามีความแตกต่างเฉพาะ เราจะอธิบายว่าควรใช้แบบไหน
แก้ไขปัญหา
หากต้องการเช็คว่าคุณติดตั้ง Rust ถูกต้องหรือไม่ เปิด shell แล้วป้อนบรรทัดนี้:
$ rustc --version
คุณควรเห็นเลข version, commit hash และวันที่ commit ของ version stable ล่าสุดที่ release ออกมา ในรูปแบบ:
rustc x.y.z (abcabcabc yyyy-mm-dd)
ถ้าคุณเห็นข้อมูลนี้ แสดงว่าคุณติดตั้ง Rust สำเร็จแล้ว! ถ้าไม่เห็นข้อมูลนี้
ให้เช็คว่า Rust อยู่ใน system variable %PATH% ของคุณ ดังนี้
ใน Windows CMD ใช้:
> echo %PATH%
ใน PowerShell ใช้:
> echo $env:Path
ใน Linux และ macOS ใช้:
$ echo $PATH
ถ้าทุกอย่างถูกต้องแล้ว Rust ยังไม่ทำงาน มีที่หลายแห่งที่คุณขอความช่วยเหลือ ได้ ดูวิธีติดต่อกับ Rustacean คนอื่น ๆ (ชื่อเล่นน่ารัก ๆ ที่เราเรียกตัวเอง) ได้ที่ หน้า community
Update และ Uninstall
เมื่อติดตั้ง Rust ผ่าน rustup แล้ว การ update เป็น version ใหม่ที่ release
ออกมาเป็นเรื่องง่าย จาก shell ของคุณ ให้รัน update script ต่อไปนี้:
$ rustup update
ถ้าต้องการ uninstall Rust และ rustup ให้รัน uninstall script ต่อไปนี้
จาก shell:
$ rustup self uninstall
อ่าน Documentation ที่ติดตั้งไว้ในเครื่อง
การติดตั้ง Rust จะแถม documentation ฉบับ local มาด้วย เพื่อให้คุณอ่าน
offline ได้ รัน rustup doc เพื่อเปิด documentation ใน browser
ทุกครั้งที่ type หรือ function ถูกให้มาโดย standard library แล้วคุณไม่แน่ใจ ว่ามันทำอะไรหรือใช้ยังไง ให้ใช้ documentation ของ application programming interface (API) เพื่อหาคำตอบ!
ใช้ Text Editor และ IDE
หนังสือเล่มนี้ไม่ได้ตั้งสมมติฐานเกี่ยวกับเครื่องมือที่คุณใช้เขียนโค้ด Rust แทบทุก text editor ใช้ทำงานได้ทั้งนั้น! อย่างไรก็ตาม text editor และ integrated development environment (IDE) หลายตัวมี support สำหรับ Rust ใน ตัว คุณหารายชื่อ editor และ IDE ที่ค่อนข้างใหม่ได้ที่ หน้า tools บนเว็บไซต์ Rust
ทำงาน Offline กับหนังสือเล่มนี้
ในตัวอย่างหลายตัวอย่าง เราจะใช้ Rust package นอก standard library การจะ
ทำตามตัวอย่างเหล่านั้นได้ คุณต้องมีการเชื่อมต่ออินเทอร์เน็ตหรือ download
dependency เหล่านั้นไว้ล่วงหน้า ถ้าจะ download dependency ไว้ล่วงหน้า ให้
รันคำสั่งต่อไปนี้ (เราจะอธิบายว่า cargo คืออะไรและคำสั่งแต่ละตัวทำอะไร
ในรายละเอียดทีหลัง)
$ cargo new get-dependencies
$ cd get-dependencies
$ cargo add [email protected] [email protected]
การทำแบบนี้จะ cache การ download package เหล่านี้ไว้ จึงไม่ต้อง download
ใหม่ทีหลัง เมื่อรันคำสั่งนี้แล้ว ไม่จำเป็นต้องเก็บโฟลเดอร์ get-dependencies
ไว้ ถ้าคุณรันคำสั่งนี้แล้ว สามารถใช้ flag --offline กับคำสั่ง cargo
ทั้งหมดในส่วนที่เหลือของหนังสือ เพื่อใช้ version ที่ cache ไว้แทนการพยายาม
ใช้ network
Hello, World!
Hello, World!
ตอนนี้คุณติดตั้ง Rust แล้ว ก็ถึงเวลาเขียนโปรแกรม Rust แรกของคุณ มันเป็น
ธรรมเนียมเวลาเรียนภาษาใหม่ที่จะเขียนโปรแกรมเล็ก ๆ ที่พิมพ์ข้อความ
Hello, world! ออกหน้าจอ เราก็จะทำแบบเดียวกันที่นี่!
หมายเหตุ: หนังสือเล่มนี้ตั้งสมมติฐานว่าคุณคุ้นเคยกับ command line ระดับ พื้นฐาน Rust ไม่ได้เรียกร้องเฉพาะเกี่ยวกับ editor หรือ tooling หรือว่าโค้ด ของคุณอยู่ที่ไหน ดังนั้นถ้าคุณชอบใช้ IDE แทน command line ก็ใช้ IDE ที่คุณ ชอบได้เลย IDE หลายตัวตอนนี้มี support สำหรับ Rust ในระดับหนึ่ง เช็ค documentation ของ IDE สำหรับรายละเอียด ทีม Rust ได้เน้นการสนับสนุน IDE ที่ยอดเยี่ยมผ่าน
rust-analyzerดูรายละเอียดเพิ่มเติมใน ภาคผนวก D
Setup directory ของโปรเจกต์
คุณจะเริ่มด้วยการสร้าง directory เก็บโค้ด Rust ของคุณ สำหรับ Rust ไม่สำคัญ ว่าโค้ดอยู่ที่ไหน แต่สำหรับแบบฝึกหัดและโปรเจกต์ในหนังสือเล่มนี้ เราแนะนำให้ สร้าง directory projects ใน home directory และเก็บโปรเจกต์ทั้งหมดไว้ที่ นั่น
เปิด terminal แล้วป้อนคำสั่งต่อไปนี้ เพื่อสร้าง directory projects และ directory สำหรับโปรเจกต์ “Hello, world!” ภายใน directory projects
สำหรับ Linux, macOS และ PowerShell บน Windows ให้ป้อน:
$ mkdir ~/projects
$ cd ~/projects
$ mkdir hello_world
$ cd hello_world
สำหรับ Windows CMD ให้ป้อน:
> mkdir "%USERPROFILE%\projects"
> cd /d "%USERPROFILE%\projects"
> mkdir hello_world
> cd hello_world
พื้นฐานโปรแกรม Rust
ขั้นต่อไป สร้างไฟล์ source ใหม่ตั้งชื่อว่า main.rs ไฟล์ Rust ลงท้ายด้วย นามสกุล .rs เสมอ ถ้าคุณใช้คำมากกว่าหนึ่งคำในชื่อไฟล์ convention คือใช้ underscore คั่น เช่นใช้ hello_world.rs แทน helloworld.rs
ตอนนี้เปิดไฟล์ main.rs ที่เพิ่งสร้าง แล้วป้อนโค้ดใน Listing 1-1
fn main() {
println!("Hello, world!");
}
Hello, world!บันทึกไฟล์แล้วกลับไปที่ terminal ใน directory ~/projects/hello_world บน Linux หรือ macOS ป้อนคำสั่งต่อไปนี้เพื่อ compile และรันไฟล์:
$ rustc main.rs
$ ./main
Hello, world!
บน Windows ใช้คำสั่ง .\main แทน ./main:
> rustc main.rs
> .\main
Hello, world!
ไม่ว่า OS อะไร string Hello, world! ควรพิมพ์ออกมาที่ terminal ถ้าคุณไม่
เห็น output นี้ ย้อนกลับไปดู ส่วน “แก้ไขปัญหา”
ของหัวข้อการติดตั้ง เพื่อหาวิธีขอความช่วยเหลือ
ถ้า Hello, world! พิมพ์ออกมาได้ ขอแสดงความยินดี! คุณเขียนโปรแกรม Rust
อย่างเป็นทางการแล้ว นั่นทำให้คุณกลายเป็น Rust programmer — ยินดีต้อนรับ!
กายวิภาคของโปรแกรม Rust
มาทบทวนโปรแกรม “Hello, world!” นี้แบบละเอียดกัน นี่คือชิ้นแรกของจิ๊กซอว์:
fn main() {
}
บรรทัดเหล่านี้กำหนดฟังก์ชันชื่อ main ฟังก์ชัน main พิเศษ: มันเป็นโค้ด
แรกที่รันในโปรแกรม Rust ที่ executable ได้ทุกตัวเสมอ ตรงนี้ บรรทัดแรก
ประกาศฟังก์ชันชื่อ main ที่ไม่มี parameter และไม่ return อะไร ถ้ามี
parameter มันจะอยู่ภายในวงเล็บ (())
ตัว body ของฟังก์ชันห่อด้วย {} Rust กำหนดให้ใช้ curly bracket ครอบ body
ของทุกฟังก์ชัน เป็นสไตล์ที่ดีที่จะวาง curly bracket เปิดในบรรทัดเดียวกับ
การประกาศฟังก์ชัน โดยเพิ่ม space หนึ่งช่องคั่น
หมายเหตุ: ถ้าคุณอยากใช้สไตล์มาตรฐานในโปรเจกต์ Rust คุณใช้เครื่องมือจัด format อัตโนมัติชื่อ
rustfmtเพื่อจัด format โค้ดในสไตล์เฉพาะได้ (อ่านเพิ่มเรื่องrustfmtใน ภาคผนวก D) ทีม Rust ได้รวมเครื่องมือนี้มาในชุด Rust มาตรฐานเช่นเดียวกับrustcดังนั้น มันน่าจะติดตั้งไว้ในเครื่องของคุณแล้ว!
Body ของฟังก์ชัน main มีโค้ดต่อไปนี้:
#![allow(unused)]
fn main() {
println!("Hello, world!");
}
บรรทัดนี้ทำงานทั้งหมดในโปรแกรมเล็ก ๆ นี้ คือพิมพ์ข้อความออกหน้าจอ มีราย ละเอียดสำคัญสามอย่างที่ควรสังเกต
ประการแรก println! เรียก macro ของ Rust ถ้ามันเรียก function แทน มันจะ
ถูกป้อนเป็น println (ไม่มี !) Rust macro เป็นวิธีเขียนโค้ดที่ generate
โค้ดเพื่อขยาย syntax ของ Rust เราจะพูดถึงพวกมันในรายละเอียดเพิ่มเติมใน
บทที่ 20 ตอนนี้คุณแค่ต้องรู้ว่าการใช้ !
หมายความว่าคุณกำลังเรียก macro แทน function ปกติ และ macro ไม่ได้ทำตาม
กฎเดียวกันกับ function เสมอไป
ประการที่สอง คุณเห็น string "Hello, world!" เราส่ง string นี้เป็น
argument ให้ println! และ string นั้นถูกพิมพ์ออกหน้าจอ
ประการที่สาม เราจบบรรทัดด้วย semicolon (;) ซึ่งบ่งบอกว่า expression นี้
จบแล้ว และ expression ถัดไปพร้อมเริ่มได้ บรรทัดส่วนใหญ่ของโค้ด Rust จบ
ด้วย semicolon
Compile และ Execute
คุณเพิ่งรันโปรแกรมที่สร้างใหม่ ลองมาดูแต่ละขั้นตอนในกระบวนการกัน
ก่อนรันโปรแกรม Rust คุณต้อง compile มันด้วย Rust compiler โดยป้อนคำสั่ง
rustc และส่งชื่อไฟล์ source ให้ ดังนี้:
$ rustc main.rs
ถ้าคุณมีพื้นฐาน C หรือ C++ คุณจะสังเกตว่ามันคล้าย gcc หรือ clang หลัง
compile สำเร็จ Rust จะ output binary ที่ executable ได้
บน Linux, macOS และ PowerShell บน Windows คุณสามารถเห็น executable โดย
ป้อนคำสั่ง ls ใน shell:
$ ls
main main.rs
บน Linux และ macOS คุณจะเห็นสองไฟล์ บน PowerShell บน Windows คุณจะเห็น สามไฟล์เดียวกับที่จะเห็นด้วย CMD บน CMD ของ Windows คุณจะป้อน:
> dir /B %= the /B option says to only show the file names =%
main.exe
main.pdb
main.rs
จะเห็นไฟล์ source code นามสกุล .rs, ไฟล์ executable (main.exe บน Windows แต่ main บนทุก platform อื่น) และเมื่อใช้ Windows จะมีไฟล์ที่ มี debugging information นามสกุล .pdb จากตรงนี้ คุณรันไฟล์ main หรือ main.exe ดังนี้:
$ ./main # หรือ .\main บน Windows
ถ้า main.rs ของคุณคือโปรแกรม “Hello, world!” บรรทัดนี้จะพิมพ์ Hello, world! ออกที่ terminal
ถ้าคุณคุ้นเคยกับภาษา dynamic อย่าง Ruby, Python หรือ JavaScript คุณอาจ ไม่คุ้นกับการ compile และรันโปรแกรมเป็นขั้นตอนที่แยกกัน Rust เป็นภาษาแบบ ahead-of-time compiled ซึ่งหมายความว่าคุณ compile โปรแกรมแล้วส่ง executable ให้คนอื่น และเขารันได้แม้ไม่ได้ติดตั้ง Rust ถ้าคุณส่งไฟล์ .rb, .py หรือ .js ให้ใคร เขาต้องติดตั้ง Ruby, Python หรือ JavaScript implementation ตามลำดับ แต่ในภาษาเหล่านั้น คุณต้องการเพียง คำสั่งเดียวเพื่อ compile และรันโปรแกรม ทุกอย่างคือ trade-off ในการออกแบบ ภาษา
แค่ compile ด้วย rustc ก็โอเคสำหรับโปรแกรมง่าย ๆ แต่เมื่อโปรเจกต์ของคุณ
ใหญ่ขึ้น คุณจะอยากจัดการ option ทั้งหมดและทำให้ง่ายต่อการแชร์โค้ด ขั้นต่อไป
เราจะแนะนำเครื่องมือ Cargo ให้คุณรู้จัก ซึ่งจะช่วยให้คุณเขียนโปรแกรม Rust
ในโลกจริงได้
Hello, Cargo!
Hello, Cargo!
Cargo คือ build system และ package manager ของ Rust Rustacean ส่วนใหญ่ใช้ เครื่องมือนี้จัดการโปรเจกต์ Rust ของตน เพราะ Cargo จัดการงานหลายอย่างให้ คุณ เช่น build โค้ด, download library ที่โค้ดของคุณพึ่งพา และ build library เหล่านั้น (เราเรียก library ที่โค้ดของคุณต้องการว่า dependency)
โปรแกรม Rust ที่ง่ายที่สุดอย่างที่เราเขียนมา ไม่มี dependency เลย ถ้าเรา build โปรเจกต์ “Hello, world!” ด้วย Cargo มันก็จะใช้แค่ส่วนของ Cargo ที่ จัดการการ build โค้ด เมื่อคุณเขียนโปรแกรม Rust ที่ซับซ้อนขึ้น คุณจะเพิ่ม dependency และถ้าคุณเริ่มโปรเจกต์ด้วย Cargo การเพิ่ม dependency จะทำได้ ง่ายกว่ามาก
เพราะโปรเจกต์ Rust ส่วนใหญ่ใช้ Cargo เนื้อหาที่เหลือของหนังสือเล่มนี้จึง สมมติว่าคุณใช้ Cargo เช่นกัน Cargo มาพร้อม Rust ถ้าคุณใช้ installer ทางการ ที่พูดถึงในส่วน “การติดตั้ง” ถ้าคุณติดตั้ง Rust ด้วยวิธีอื่น ให้เช็คว่า Cargo ติดตั้งอยู่หรือไม่ โดยป้อนคำสั่งต่อไปนี้ ใน terminal:
$ cargo --version
ถ้าคุณเห็นเลข version แสดงว่ามี! ถ้าเห็น error อย่าง command not found
ให้ดู documentation ของวิธีติดตั้งที่คุณใช้ เพื่อหาวิธีติดตั้ง Cargo แยก
ต่างหาก
สร้างโปรเจกต์ด้วย Cargo
มาสร้างโปรเจกต์ใหม่ด้วย Cargo แล้วดูว่ามันต่างจากโปรเจกต์ “Hello, world!” เดิมของเราอย่างไร กลับไปที่ directory projects ของคุณ (หรือที่ใดก็ตามที่ คุณตัดสินใจเก็บโค้ด) แล้วบนทุก OS รัน:
$ cargo new hello_cargo
$ cd hello_cargo
คำสั่งแรกสร้าง directory และโปรเจกต์ใหม่ชื่อ hello_cargo เราตั้งชื่อ โปรเจกต์เป็น hello_cargo และ Cargo สร้างไฟล์ของมันใน directory ชื่อ เดียวกัน
เข้าไปใน directory hello_cargo แล้วดูรายการไฟล์ คุณจะเห็นว่า Cargo generate สองไฟล์และหนึ่ง directory ให้: ไฟล์ Cargo.toml และ directory src ที่มีไฟล์ main.rs อยู่ข้างใน
มันยัง initialize Git repository ใหม่พร้อมไฟล์ .gitignore ด้วย ไฟล์ Git
จะไม่ถูก generate ถ้าคุณรัน cargo new ภายใน Git repository ที่มีอยู่
คุณ override พฤติกรรมนี้ได้โดยใช้ cargo new --vcs=git
หมายเหตุ: Git เป็น version control system ที่ใช้กันทั่วไป คุณสามารถ เปลี่ยน
cargo newให้ใช้ version control system อื่น หรือไม่ใช้เลย โดยใช้ flag--vcsรันcargo new --helpเพื่อดู option ที่มี
เปิด Cargo.toml ใน text editor ที่คุณเลือก มันควรหน้าตาคล้ายโค้ดใน Listing 1-2
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2024"
[dependencies]
cargo newไฟล์นี้อยู่ในรูปแบบ TOML (Tom’s Obvious, Minimal Language) ซึ่งเป็นรูปแบบ configuration ของ Cargo
บรรทัดแรก [package] เป็น heading ของ section ที่บ่งบอกว่าบรรทัดที่ตาม
มาเป็นการ config package เมื่อเราเพิ่มข้อมูลในไฟล์นี้ เราจะเพิ่ม section
อื่น ๆ ด้วย
สามบรรทัดถัดมา set ข้อมูล config ที่ Cargo ต้องการสำหรับ compile โปรแกรม
ของคุณ: ชื่อ, version และ edition ของ Rust ที่จะใช้ เราจะพูดถึง key
edition ใน ภาคผนวก E
บรรทัดสุดท้าย [dependencies] คือจุดเริ่มต้นของ section ให้คุณ list
dependency ใด ๆ ของโปรเจกต์ ใน Rust เราเรียก package ของโค้ดว่า crate
เราไม่ต้องการ crate อื่นใดสำหรับโปรเจกต์นี้ แต่จะต้องใช้ใน project แรก
ในบทที่ 2 ดังนั้นเราจะใช้ section dependency ตอนนั้น
ทีนี้เปิด src/main.rs แล้วดู:
Filename: src/main.rs
fn main() {
println!("Hello, world!");
}
Cargo generate โปรแกรม “Hello, world!” ให้คุณแล้ว เหมือนที่เราเขียนใน Listing 1-1! ที่ผ่านมา ความแตกต่างระหว่างโปรเจกต์ของเราและโปรเจกต์ที่ Cargo generate ก็คือ Cargo วางโค้ดไว้ใน directory src และเรามีไฟล์ config Cargo.toml ใน directory บนสุด
Cargo คาดหวังว่าไฟล์ source ของคุณจะอยู่ภายใน directory src ส่วน directory โปรเจกต์ระดับบนสุดมีไว้สำหรับไฟล์ README, ข้อมูล license, ไฟล์ config และอื่น ๆ ที่ไม่เกี่ยวข้องกับโค้ด การใช้ Cargo ช่วยจัด ระเบียบโปรเจกต์ของคุณ มีที่สำหรับทุกอย่าง และทุกอย่างก็อยู่ที่ของมัน
ถ้าคุณเริ่มโปรเจกต์ที่ไม่ได้ใช้ Cargo อย่างที่เราทำกับโปรเจกต์
“Hello, world!” คุณสามารถแปลงมันเป็นโปรเจกต์ที่ใช้ Cargo ได้ ย้ายโค้ด
โปรเจกต์ไปไว้ใน directory src แล้วสร้างไฟล์ Cargo.toml ที่เหมาะสม
วิธีง่าย ๆ ในการได้ไฟล์ Cargo.toml นั้น คือรัน cargo init ซึ่งจะ
สร้างให้คุณอัตโนมัติ
Build และรันโปรเจกต์ Cargo
ทีนี้มาดูว่าอะไรต่างเมื่อเรา build และรันโปรแกรม “Hello, world!” ด้วย Cargo! จาก directory hello_cargo ของคุณ build โปรเจกต์โดยป้อนคำสั่ง ต่อไปนี้:
$ cargo build
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs
คำสั่งนี้สร้างไฟล์ executable ใน target/debug/hello_cargo (หรือ target\debug\hello_cargo.exe บน Windows) แทนที่จะอยู่ใน current directory ของคุณ เพราะ build default เป็น debug build Cargo จึงวาง binary ไว้ใน directory ชื่อ debug คุณรัน executable ได้ด้วยคำสั่งนี้:
$ ./target/debug/hello_cargo # หรือ .\target\debug\hello_cargo.exe บน Windows
Hello, world!
ถ้าทุกอย่างเรียบร้อย Hello, world! ควรพิมพ์ออก terminal การรัน
cargo build ครั้งแรกยังทำให้ Cargo สร้างไฟล์ใหม่ในระดับบนสุด:
Cargo.lock ไฟล์นี้ติดตาม version ที่แน่นอนของ dependency ในโปรเจกต์
ของคุณ โปรเจกต์นี้ไม่มี dependency ดังนั้นไฟล์จึงค่อนข้างว่าง คุณจะไม่
ต้องแก้ไฟล์นี้เอง Cargo จัดการเนื้อหาให้คุณ
เราเพิ่ง build โปรเจกต์ด้วย cargo build แล้วรันด้วย
./target/debug/hello_cargo แต่เราใช้ cargo run เพื่อ compile โค้ดแล้ว
รัน executable ที่ได้ในคำสั่งเดียวก็ได้:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/hello_cargo`
Hello, world!
การใช้ cargo run สะดวกกว่าการต้องจำให้รัน cargo build แล้วใช้ path
ทั้งหมดของ binary ดังนั้นนักพัฒนาส่วนใหญ่ใช้ cargo run
สังเกตว่าครั้งนี้เราไม่เห็น output ที่บ่งบอกว่า Cargo กำลัง compile
hello_cargo Cargo รู้ว่าไฟล์ไม่เปลี่ยน ก็เลยไม่ rebuild แต่รัน binary
อย่างเดียว ถ้าคุณแก้ source code Cargo จะ rebuild โปรเจกต์ก่อนรัน และคุณ
จะเห็น output นี้:
$ cargo run
Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs
Running `target/debug/hello_cargo`
Hello, world!
Cargo ยังมีคำสั่งชื่อ cargo check ด้วย คำสั่งนี้เช็คโค้ดอย่างรวดเร็ว
เพื่อให้แน่ใจว่ามัน compile ได้ แต่ไม่ produce executable:
$ cargo check
Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
ทำไมถึงไม่อยากได้ executable? บ่อยครั้ง cargo check เร็วกว่า
cargo build มาก เพราะข้ามขั้นตอน produce executable ถ้าคุณเช็คงานต่อ
เนื่องระหว่างเขียนโค้ด การใช้ cargo check จะเร่งกระบวนการให้คุณรู้ว่า
โปรเจกต์ยัง compile ผ่านหรือไม่! ด้วยเหตุนี้ Rustacean หลายคนจึงรัน
cargo check เป็นระยะระหว่างเขียนโปรแกรม เพื่อให้แน่ใจว่ามัน compile
ผ่าน แล้วค่อยรัน cargo build ตอนที่พร้อมใช้ executable
มาทบทวนสิ่งที่เราเรียนเกี่ยวกับ Cargo ที่ผ่านมา:
- เราสร้างโปรเจกต์ได้ด้วย
cargo new - เรา build โปรเจกต์ได้ด้วย
cargo build - เรา build และรันโปรเจกต์ในขั้นตอนเดียวได้ด้วย
cargo run - เรา build โปรเจกต์โดยไม่ produce binary เพื่อเช็ค error ได้ด้วย
cargo check - แทนที่จะบันทึกผลของ build ใน directory เดียวกับโค้ด Cargo เก็บมันไว้ ใน directory target/debug
ข้อดีเพิ่มเติมของการใช้ Cargo คือคำสั่งจะเหมือนกันไม่ว่าคุณทำงานบน OS อะไร ดังนั้นจากตรงนี้ เราจะไม่ให้คำแนะนำเฉพาะสำหรับ Linux และ macOS เทียบกับ Windows อีกต่อไป
Build เพื่อ Release
เมื่อโปรเจกต์ของคุณพร้อม release แล้ว คุณใช้ cargo build --release เพื่อ
compile มันพร้อม optimization ได้ คำสั่งนี้จะสร้าง executable ใน
target/release แทน target/debug การ optimize ทำให้โค้ด Rust รันเร็วขึ้น
แต่การเปิดมันใช้เวลา compile นานขึ้น นี่จึงเป็นเหตุผลที่มี profile สอง
แบบที่ต่างกัน: หนึ่งสำหรับ development ตอนที่คุณอยาก rebuild เร็วและบ่อย
อีกหนึ่งสำหรับการ build โปรแกรมสุดท้ายที่คุณจะส่งให้ user ซึ่งจะไม่ถูก
rebuild ซ้ำ ๆ และจะรันเร็วที่สุดเท่าที่ทำได้ ถ้าคุณ benchmark เวลารัน
ของโค้ด อย่าลืมรัน cargo build --release และ benchmark กับ executable
ใน target/release
ใช้ประโยชน์จาก convention ของ Cargo
สำหรับโปรเจกต์ง่าย ๆ Cargo ไม่ให้คุณค่าเพิ่มมากนักเหนือกว่าการใช้แค่
rustc แต่จะพิสูจน์คุณค่าเมื่อโปรแกรมของคุณซับซ้อนขึ้น เมื่อโปรแกรม
เติบโตเป็นหลายไฟล์หรือต้องการ dependency การให้ Cargo coordinate การ
build ก็ง่ายกว่ามาก
แม้โปรเจกต์ hello_cargo จะเรียบง่าย แต่ตอนนี้มันใช้ tooling ส่วนใหญ่ที่
คุณจะใช้จริงในส่วนที่เหลือของ Rust career ของคุณ จริง ๆ แล้ว เพื่อทำงาน
กับโปรเจกต์ที่มีอยู่ คุณใช้คำสั่งต่อไปนี้เพื่อ check out โค้ดด้วย Git
เปลี่ยนเข้า directory ของโปรเจกต์ แล้ว build ได้:
$ git clone example.org/someproject
$ cd someproject
$ cargo build
ดูข้อมูลเพิ่มเติมเกี่ยวกับ Cargo ที่ documentation ของมัน
สรุป
คุณเริ่มต้นการเดินทาง Rust ได้ดีมากแล้ว! ในบทนี้คุณได้เรียน:
- ติดตั้ง Rust version stable ล่าสุดด้วย
rustup - Update เป็น Rust version ใหม่กว่า
- เปิด documentation ที่ติดตั้งไว้ในเครื่อง
- เขียนและรันโปรแกรม “Hello, world!” โดยใช้
rustcตรง ๆ - สร้างและรันโปรเจกต์ใหม่โดยใช้ convention ของ Cargo
นี่เป็นเวลาที่ดีที่จะ build โปรแกรมที่ใหญ่ขึ้น เพื่อทำความคุ้นเคยกับการ อ่านและเขียนโค้ด Rust ดังนั้น ในบทที่ 2 เราจะ build โปรแกรมเกมทายตัวเลข ถ้าคุณอยากเริ่มด้วยการเรียนว่าแนวคิดพื้นฐานของการเขียนโปรแกรมทำงานยังไง ใน Rust ดูบทที่ 3 แล้วค่อยกลับมาที่บทที่ 2
เขียนเกมทายตัวเลข
มากระโดดเข้าสู่ Rust ด้วยการทำโปรเจกต์แบบลงมือทำร่วมกัน! บทนี้แนะนำแนวคิด
Rust ที่ใช้บ่อย ๆ ผ่านการแสดงให้คุณเห็นวิธีใช้มันในโปรแกรมจริง คุณจะได้เรียน
เรื่อง let, match, เมธอด, associated function, external crate และอื่น ๆ!
ในบทถัด ๆ ไป เราจะสำรวจไอเดียเหล่านี้ในรายละเอียดมากขึ้น ในบทนี้คุณแค่ฝึก
พื้นฐาน
เราจะ implement ปัญหา programming คลาสสิกสำหรับมือใหม่: เกมทายตัวเลข มันทำ งานแบบนี้ — โปรแกรมจะ generate จำนวนเต็มสุ่มระหว่าง 1 ถึง 100 จากนั้นจะให้ ผู้เล่นป้อนตัวเลขที่จะทาย หลังจากป้อนคำตอบแล้ว โปรแกรมจะบอกว่าคำตอบสูงไป หรือต่ำไป ถ้าคำตอบถูก เกมจะพิมพ์ข้อความแสดงความยินดีและออก
Setup โปรเจกต์ใหม่
ในการ setup โปรเจกต์ใหม่ ให้ไปที่ directory projects ที่คุณสร้างใน บทที่ 1 แล้วสร้างโปรเจกต์ใหม่ด้วย Cargo ดังนี้:
$ cargo new guessing_game
$ cd guessing_game
คำสั่งแรก cargo new รับชื่อโปรเจกต์ (guessing_game) เป็น argument แรก
คำสั่งที่สองเปลี่ยนเข้าไปใน directory โปรเจกต์ใหม่
ดูไฟล์ Cargo.toml ที่ถูก generate:
Filename: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
[dependencies]
อย่างที่คุณเห็นในบทที่ 1 cargo new generate โปรแกรม “Hello, world!” ให้
คุณ ดูไฟล์ src/main.rs:
Filename: src/main.rs
fn main() {
println!("Hello, world!");
}
ทีนี้มา compile โปรแกรม “Hello, world!” นี้แล้วรันในขั้นตอนเดียวด้วย
คำสั่ง cargo run:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/guessing_game`
Hello, world!
คำสั่ง run มีประโยชน์เมื่อคุณต้องทำซ้ำในโปรเจกต์อย่างรวดเร็ว อย่างที่เรา
จะทำในเกมนี้ ทดสอบแต่ละ iteration อย่างเร็วก่อนไปทำต่อ
เปิดไฟล์ src/main.rs อีกครั้ง คุณจะเขียนโค้ดทั้งหมดในไฟล์นี้
ประมวลผลคำตอบ
ส่วนแรกของโปรแกรมเกมทายตัวเลข จะขอ input จากผู้ใช้ ประมวลผล input นั้น และ เช็คว่า input อยู่ในรูปแบบที่คาดไว้ ในการเริ่มต้น เราจะให้ผู้เล่นป้อน คำตอบ ป้อนโค้ดใน Listing 2-1 ลงใน src/main.rs
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
โค้ดนี้มีข้อมูลเยอะ ฉะนั้นมาอธิบายทีละบรรทัด ในการรับ input จากผู้ใช้แล้ว
พิมพ์ผลออกมา เราต้องนำ library io (input/output) เข้า scope library
io มาจาก standard library ที่รู้จักในชื่อ std:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
โดย default Rust มีชุดของ item ที่ประกาศใน standard library ซึ่งถูกนำเข้า scope ของทุกโปรแกรม ชุดนี้เรียกว่า prelude และคุณดูทุกอย่างในมันได้ ใน documentation ของ standard library
ถ้า type ที่คุณอยากใช้ไม่ได้อยู่ใน prelude คุณต้องนำ type นั้นเข้า scope
แบบ explicit ด้วย statement use การใช้ library std::io ให้ฟีเจอร์ที่
มีประโยชน์หลายอย่าง รวมถึงความสามารถในการรับ input จากผู้ใช้
อย่างที่คุณเห็นในบทที่ 1 ฟังก์ชัน main เป็น entry point ของโปรแกรม:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
Syntax fn ประกาศฟังก์ชันใหม่ วงเล็บ () บ่งบอกว่าไม่มี parameter และ
curly bracket { เริ่ม body ของฟังก์ชัน
อย่างที่คุณเรียนในบทที่ 1 ด้วย println! เป็น macro ที่พิมพ์ string ออก
หน้าจอ:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
โค้ดนี้พิมพ์ prompt บอกว่าเกมคืออะไรและขอ input จากผู้ใช้
เก็บค่าด้วยตัวแปร
ขั้นต่อไป เราจะสร้าง ตัวแปร เพื่อเก็บ input ของผู้ใช้ ดังนี้:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
ทีนี้โปรแกรมเริ่มน่าสนใจแล้ว! มีหลายอย่างเกิดขึ้นในบรรทัดเล็ก ๆ นี้ เราใช้
statement let เพื่อสร้างตัวแปร นี่คืออีกตัวอย่างหนึ่ง:
let apples = 5;
บรรทัดนี้สร้างตัวแปรใหม่ชื่อ apples แล้ว bind มันกับค่า 5 ใน Rust
ตัวแปรเป็น immutable โดย default หมายความว่าเมื่อให้ค่ากับตัวแปรแล้ว ค่า
นั้นจะไม่เปลี่ยน เราจะพูดถึงแนวคิดนี้ในรายละเอียดในส่วน
“ตัวแปรและ mutability” ของ
บทที่ 3 ในการทำให้ตัวแปร mutable เราเพิ่ม mut ก่อนชื่อตัวแปร:
let apples = 5; // immutable
let mut bananas = 5; // mutable
หมายเหตุ: syntax
//เริ่ม comment ที่ดำเนินไปจนจบบรรทัด Rust ละเว้น ทุกอย่างใน comment เราจะพูดถึง comment ในรายละเอียดเพิ่มเติมใน บทที่ 3
กลับมาที่โปรแกรมเกมทายตัวเลข ตอนนี้คุณรู้แล้วว่า let mut guess จะแนะนำ
ตัวแปร mutable ชื่อ guess เครื่องหมายเท่ากับ (=) บอก Rust ว่าเราอยาก
bind บางอย่างกับตัวแปรตอนนี้ ทางขวาของเครื่องหมายเท่ากับคือค่าที่ guess
ถูก bind ด้วย ซึ่งเป็นผลของการเรียก String::new ฟังก์ชันที่ return
instance ใหม่ของ String String เป็น type
string ที่ให้มาโดย standard library เป็น bit ของข้อความที่เติบโตได้ และ
encode แบบ UTF-8
Syntax :: ในบรรทัด ::new บ่งบอกว่า new เป็น associated function ของ
type String associated function คือฟังก์ชันที่ implement บน type ใน
กรณีนี้คือ String ฟังก์ชัน new นี้สร้าง string ว่างใหม่ คุณจะพบ
ฟังก์ชัน new ใน type หลายตัว เพราะมันเป็นชื่อที่ใช้บ่อยสำหรับฟังก์ชัน
ที่สร้างค่าใหม่บางอย่าง
โดยสรุป บรรทัด let mut guess = String::new(); ได้สร้างตัวแปร mutable ที่
ตอนนี้ถูก bind กับ instance ว่างใหม่ของ String เฮ้อ!
รับ input จากผู้ใช้
จำได้ว่าเรา include functionality สำหรับ input/output จาก standard
library ด้วย use std::io; ในบรรทัดแรกของโปรแกรม ทีนี้เราจะเรียกฟังก์ชัน
stdin จาก module io ซึ่งจะให้เราจัดการ input จากผู้ใช้:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
ถ้าเราไม่ได้ import module io ด้วย use std::io; ที่ต้นโปรแกรม เราก็ยัง
ใช้ฟังก์ชันได้โดยเขียนการเรียกฟังก์ชันนี้เป็น std::io::stdin ฟังก์ชัน
stdin return instance ของ std::io::Stdin ซึ่ง
เป็น type ที่แทน handle ของ standard input ของ terminal คุณ
ถัดไป บรรทัด .read_line(&mut guess) เรียกเมธอด
read_line บน standard input handle เพื่อรับ
input จากผู้ใช้ เรายังส่ง &mut guess เป็น argument ให้ read_line เพื่อ
บอกมันว่าจะเก็บ input ของผู้ใช้ใน string ตัวไหน งานเต็ม ๆ ของ read_line
คือรับสิ่งที่ผู้ใช้พิมพ์ลง standard input แล้ว append เข้าไปใน string (โดย
ไม่เขียนทับเนื้อหา) เราจึงส่ง string นั้นเป็น argument argument ที่เป็น
string ต้องเป็น mutable เพื่อให้เมธอดเปลี่ยนเนื้อหาของ string ได้
& บ่งบอกว่า argument นี้เป็น reference ซึ่งให้คุณมีวิธีให้หลายส่วนของ
โค้ดเข้าถึงข้อมูลชิ้นเดียวได้ โดยไม่ต้องคัดลอกข้อมูลนั้นลงในหน่วยความจำ
หลายครั้ง reference เป็นฟีเจอร์ที่ซับซ้อน และหนึ่งในข้อได้เปรียบหลักของ
Rust คือความปลอดภัยและความง่ายของการใช้ reference คุณไม่ต้องรู้รายละเอียด
เหล่านั้นเยอะเพื่อจบโปรแกรมนี้ ตอนนี้สิ่งที่คุณต้องรู้คือ เช่นเดียวกับ
ตัวแปร reference เป็น immutable โดย default ดังนั้นคุณต้องเขียน
&mut guess แทน &guess เพื่อทำให้มัน mutable (บทที่ 4 จะอธิบาย
reference อย่างละเอียดมากขึ้น)
จัดการ failure ที่อาจเกิดขึ้นด้วย Result
เรายังทำงานอยู่กับบรรทัดโค้ดนี้ ตอนนี้เรากำลังพูดถึงบรรทัดที่สามของข้อความ แต่จำไว้ว่ามันยังเป็นส่วนหนึ่งของบรรทัดโค้ดเชิง logic เดียว ส่วนถัดไปคือ เมธอดนี้:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
เราเขียนโค้ดนี้เป็นแบบนี้ก็ได้:
io::stdin().read_line(&mut guess).expect("Failed to read line");
อย่างไรก็ตาม บรรทัดยาว ๆ บรรทัดเดียวอ่านยาก ทางที่ดีที่สุดคือแบ่งมัน บ่อย
ครั้งที่ฉลาดในการแนะนำ newline และ whitespace อื่น ๆ เพื่อช่วยแยกบรรทัด
ยาว ๆ เมื่อคุณเรียกเมธอดด้วย syntax .method_name() ทีนี้มาพูดถึงสิ่งที่
บรรทัดนี้ทำ
อย่างที่กล่าวไว้ก่อนหน้า read_line ใส่สิ่งที่ผู้ใช้ป้อนลงใน string ที่
เราส่งให้ แต่มันยัง return ค่า Result ด้วย Result
เป็น enumeration ที่มักเรียกว่า enum ซึ่งเป็น
type ที่อยู่ในสถานะหนึ่งจากหลายสถานะที่เป็นไปได้ เราเรียกแต่ละสถานะที่
เป็นไปได้ว่า variant
บทที่ 6 จะครอบคลุม enum ในรายละเอียดเพิ่มเติม จุด
ประสงค์ของ type Result เหล่านี้คือ encode ข้อมูลการจัดการ error
variant ของ Result คือ Ok และ Err variant Ok บ่งบอกว่า operation
สำเร็จ และมีค่าที่ generate สำเร็จอยู่ข้างใน variant Err หมายความว่า
operation ล้มเหลว และมีข้อมูลเกี่ยวกับวิธีหรือเหตุผลที่ operation ล้มเหลว
ค่าของ type Result เช่นเดียวกับค่าของ type ใด ๆ มีเมธอดที่ประกาศไว้บน
มัน instance ของ Result มี expect method ที่
คุณเรียกได้ ถ้า instance ของ Result นี้เป็นค่า Err expect จะทำให้
โปรแกรม crash และแสดงข้อความที่คุณส่งเป็น argument ให้ expect ถ้าเมธอด
read_line return Err มันน่าจะเป็นผลของ error ที่มาจาก OS เบื้องล่าง
ถ้า instance ของ Result นี้เป็นค่า Ok expect จะเอาค่าที่ Ok เก็บ
ไว้ แล้ว return แค่ค่านั้นให้คุณ เพื่อให้คุณใช้ได้ ในกรณีนี้ ค่านั้นคือ
จำนวน byte ใน input ของผู้ใช้
ถ้าคุณไม่เรียก expect โปรแกรมจะ compile ได้ แต่คุณจะได้ warning:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
--> src/main.rs:10:5
|
10 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
10 | let _ = io::stdin().read_line(&mut guess);
| +++++++
warning: `guessing_game` (bin "guessing_game") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.59s
Rust เตือนว่าคุณยังไม่ได้ใช้ค่า Result ที่ return จาก read_line บ่ง
บอกว่าโปรแกรมยังไม่ได้จัดการ error ที่อาจเกิดขึ้น
วิธีที่ถูกในการ suppress warning คือเขียนโค้ดจัดการ error จริง ๆ แต่ใน
กรณีของเรา เราแค่อยากให้โปรแกรมนี้ crash เมื่อเกิดปัญหา เราจึงใช้
expect ได้ คุณจะเรียนเรื่อง recover จาก error ใน บทที่ 9
พิมพ์ค่าด้วย placeholder ของ println!
นอกจาก curly bracket ปิด เหลือแค่บรรทัดเดียวที่ต้องพูดถึงในโค้ดที่ผ่านมา:
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
บรรทัดนี้พิมพ์ string ที่ตอนนี้มี input ของผู้ใช้อยู่ ชุด curly bracket
{} คือ placeholder คิดถึง {} เป็นเหมือนก้ามปูเล็ก ๆ ที่จับค่าไว้ที่
เดิม เมื่อพิมพ์ค่าของตัวแปร ชื่อตัวแปรไปอยู่ภายใน curly bracket ได้ เมื่อ
พิมพ์ผลของการประเมิน expression ให้วาง curly bracket ว่างใน format string
แล้วตาม format string ด้วยรายการ expression ที่คั่นด้วย comma เพื่อพิมพ์
ในแต่ละ placeholder curly bracket ว่างในลำดับเดียวกัน การพิมพ์ตัวแปรและ
ผลของ expression ในการเรียก println! ครั้งเดียว จะเป็นแบบนี้:
#![allow(unused)]
fn main() {
let x = 5;
let y = 10;
println!("x = {x} and y + 2 = {}", y + 2);
}
โค้ดนี้จะพิมพ์ x = 5 and y + 2 = 12
ทดสอบส่วนแรก
มาทดสอบส่วนแรกของเกมทายตัวเลขกัน รันมันด้วย cargo run:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.44s
Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6
ณ จุดนี้ ส่วนแรกของเกมเสร็จแล้ว — เรารับ input จาก keyboard แล้วพิมพ์มัน ออกมา
Generate ตัวเลขลับ
ต่อไป เราต้อง generate ตัวเลขลับที่ผู้ใช้จะพยายามทาย ตัวเลขลับควรต่างกันทุก
ครั้ง เพื่อให้เกมสนุกที่จะเล่นมากกว่าหนึ่งครั้ง เราจะใช้ตัวเลขสุ่มระหว่าง 1
ถึง 100 เพื่อให้เกมไม่ยากเกินไป Rust ยังไม่มี functionality สำหรับ random
number ใน standard library อย่างไรก็ตาม ทีม Rust จัดให้มี
crate rand ที่มี functionality ดังกล่าว
เพิ่ม functionality ด้วย crate
จำได้ว่า crate คือชุดของไฟล์ source code ของ Rust โปรเจกต์ที่เรา build อยู่
เป็น binary crate ซึ่งเป็น executable crate rand เป็น library crate ซึ่ง
มีโค้ดที่ตั้งใจให้ใช้ในโปรแกรมอื่น และ execute เองไม่ได้
การ coordinate external crate ของ Cargo เป็นจุดที่ Cargo เปล่งประกายจริง ๆ
ก่อนที่เราจะเขียนโค้ดที่ใช้ rand เราต้องแก้ไฟล์ Cargo.toml เพื่อรวม
crate rand เป็น dependency เปิดไฟล์นั้นแล้วเพิ่มบรรทัดต่อไปนี้ที่ด้านล่าง
ภายใต้ section header [dependencies] ที่ Cargo สร้างให้คุณ อย่าลืมระบุ
rand ให้เป๊ะตามที่เราใส่ไว้ ด้วย version นี้ มิฉะนั้นตัวอย่างโค้ดใน
tutorial นี้อาจไม่ทำงาน:
Filename: Cargo.toml
[dependencies]
rand = "0.8.5"
ในไฟล์ Cargo.toml ทุกอย่างที่ตามมาหลัง header เป็นส่วนหนึ่งของ section
นั้น ที่ดำเนินไปจนกระทั่ง section อื่นเริ่ม ใน [dependencies] คุณบอก
Cargo ว่าโปรเจกต์ของคุณพึ่งพา external crate ตัวไหน และต้องการ version ไหน
ของ crate เหล่านั้น ในกรณีนี้ เราระบุ crate rand ด้วย semantic version
specifier 0.8.5 Cargo เข้าใจ Semantic Versioning
(บางครั้งเรียกว่า SemVer) ซึ่งเป็นมาตรฐานในการเขียนเลข version specifier
0.8.5 จริง ๆ แล้วเป็น shorthand ของ ^0.8.5 ซึ่งหมายถึง version ใด ๆ ที่
อย่างน้อย 0.8.5 แต่ต่ำกว่า 0.9.0
Cargo ถือว่า version เหล่านี้มี API สาธารณะที่เข้ากันได้กับ version 0.8.5 และ specification นี้รับประกันว่าคุณจะได้ patch release ล่าสุดที่ยัง compile กับโค้ดในบทนี้ได้ version 0.9.0 ขึ้นไปไม่รับประกันว่าจะมี API เดียวกับที่ตัวอย่างต่อไปนี้ใช้
ทีนี้ โดยไม่เปลี่ยนโค้ดใด ๆ มา build โปรเจกต์ ตามที่แสดงใน Listing 2-2
$ cargo build
Updating crates.io index
Locking 15 packages to latest Rust 1.85.0 compatible versions
Adding rand v0.8.5 (available: v0.9.0)
Compiling proc-macro2 v1.0.93
Compiling unicode-ident v1.0.17
Compiling libc v0.2.170
Compiling cfg-if v1.0.0
Compiling byteorder v1.5.0
Compiling getrandom v0.2.15
Compiling rand_core v0.6.4
Compiling quote v1.0.38
Compiling syn v2.0.98
Compiling zerocopy-derive v0.7.35
Compiling zerocopy v0.7.35
Compiling ppv-lite86 v0.2.20
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.48s
cargo build หลังเพิ่ม crate rand เป็น dependencyคุณอาจเห็นเลข version ต่างกัน (แต่ทั้งหมดจะเข้ากันได้กับโค้ด ขอบคุณ SemVer!) และบรรทัดต่าง ๆ ก็ต่างกัน (ขึ้นกับ OS) และบรรทัดอาจอยู่ในลำดับต่างกัน
เมื่อเรารวม external dependency Cargo จะ fetch version ล่าสุดของทุกอย่างที่ dependency นั้นต้องการ จาก registry ซึ่งเป็นสำเนาข้อมูลจาก Crates.io Crates.io เป็นที่ที่คนใน ecosystem ของ Rust post โปรเจกต์ Rust แบบ open source ให้คนอื่นใช้
หลัง update registry แล้ว Cargo เช็ค section [dependencies] แล้ว download
crate ใด ๆ ที่ list ไว้ที่ยังไม่ได้ download ในกรณีนี้ แม้เราจะ list แค่
rand เป็น dependency Cargo ก็ดึง crate อื่น ๆ ที่ rand พึ่งพาเพื่อทำงาน
มาด้วย หลัง download crate แล้ว Rust compile พวกมัน แล้ว compile โปรเจกต์
ที่มี dependency พร้อมใช้
ถ้าคุณรัน cargo build อีกครั้งทันทีโดยไม่เปลี่ยนอะไร คุณจะไม่ได้ output
ใด ๆ นอกจากบรรทัด Finished Cargo รู้ว่ามัน download และ compile dependency
แล้ว และคุณไม่ได้เปลี่ยนอะไรเกี่ยวกับพวกมันในไฟล์ Cargo.toml Cargo ยัง
รู้ว่าคุณไม่ได้เปลี่ยนอะไรเกี่ยวกับโค้ดของคุณ ดังนั้นมันไม่ recompile โค้ด
ด้วย เมื่อไม่มีอะไรให้ทำ มันก็แค่ออก
ถ้าคุณเปิดไฟล์ src/main.rs แก้ไขเล็กน้อย แล้วบันทึก แล้ว build ใหม่ คุณจะ เห็นแค่สองบรรทัดของ output:
$ cargo build
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
บรรทัดเหล่านี้แสดงว่า Cargo update build ตามการเปลี่ยนเล็ก ๆ ของคุณในไฟล์ src/main.rs เท่านั้น dependency ของคุณไม่ได้เปลี่ยน Cargo จึงรู้ว่ามันใช้ ของที่ download และ compile ไว้แล้วซ้ำได้
รับประกันการ build ที่ทำซ้ำได้
Cargo มีกลไกที่รับประกันว่าคุณ rebuild artifact เดียวกันได้ทุกครั้งที่คุณ
หรือใครก็ตาม build โค้ดของคุณ — Cargo จะใช้แค่ version ของ dependency ที่
คุณระบุ จนกว่าคุณจะบอกเป็นอย่างอื่น เช่น สมมติว่าสัปดาห์หน้า version 0.8.6
ของ crate rand ออกมา และ version นั้นมี bug fix สำคัญ แต่ก็มี regression
ที่จะทำให้โค้ดของคุณพัง ในการจัดการเรื่องนี้ Rust สร้างไฟล์ Cargo.lock
ครั้งแรกที่คุณรัน cargo build ตอนนี้เราจึงมีไฟล์นี้ใน directory
guessing_game
เมื่อคุณ build โปรเจกต์ครั้งแรก Cargo หา version ของ dependency ทั้งหมดที่ ตรงเกณฑ์ แล้วเขียนลงไฟล์ Cargo.lock เมื่อคุณ build โปรเจกต์ในอนาคต Cargo จะเห็นว่าไฟล์ Cargo.lock มีอยู่ และจะใช้ version ที่ระบุที่นั่น แทนการทำงานหา version อีกครั้ง สิ่งนี้ให้คุณมี build ที่ทำซ้ำได้อัตโนมัติ พูดอีกอย่าง โปรเจกต์ของคุณจะอยู่ที่ 0.8.5 จนกว่าคุณจะ upgrade แบบ explicit ขอบคุณไฟล์ Cargo.lock เพราะไฟล์ Cargo.lock สำคัญสำหรับ build ที่ทำซ้ำ ได้ มันจึงมักถูก check in เข้า source control พร้อมโค้ดที่เหลือในโปรเจกต์ ของคุณ
Update crate เพื่อได้ version ใหม่
เมื่อคุณ อยาก update crate Cargo มีคำสั่ง update ซึ่งจะละเว้นไฟล์
Cargo.lock แล้วหา version ล่าสุดทั้งหมดที่ตรง specification ใน
Cargo.toml จากนั้น Cargo จะเขียน version เหล่านั้นลงไฟล์ Cargo.lock
มิฉะนั้น โดย default Cargo จะมองหาแค่ version ที่มากกว่า 0.8.5 และต่ำกว่า
0.9.0 ถ้า crate rand release version ใหม่สอง version คือ 0.8.6 และ
0.999.0 คุณจะเห็นสิ่งต่อไปนี้ถ้ารัน cargo update:
$ cargo update
Updating crates.io index
Locking 1 package to latest Rust 1.85.0 compatible version
Updating rand v0.8.5 -> v0.8.6 (available: v0.999.0)
Cargo ละเว้น release 0.999.0 ณ จุดนี้ คุณจะสังเกตเห็นการเปลี่ยนแปลงในไฟล์
Cargo.lock ระบุว่า version ของ crate rand ที่คุณใช้ตอนนี้คือ 0.8.6
ถ้าจะใช้ rand version 0.999.0 หรือ version ใด ๆ ใน series 0.999.x คุณ
ต้อง update ไฟล์ Cargo.toml ให้หน้าตาเป็นแบบนี้แทน (อย่าทำการเปลี่ยนนี้
จริง ๆ เพราะตัวอย่างต่อไปนี้สมมติว่าคุณใช้ rand 0.8):
[dependencies]
rand = "0.999.0"
ครั้งถัดไปที่คุณรัน cargo build Cargo จะ update registry ของ crate ที่มี
อยู่ และประเมิน requirement rand ของคุณใหม่ตาม version ใหม่ที่คุณระบุ
มีอะไรอีกเยอะให้พูดถึง Cargo และ ecosystem ของมัน ซึ่งเราจะพูดถึงในบทที่ 14 แต่ตอนนี้แค่นี้ก็พอสำหรับสิ่งที่คุณต้องรู้ Cargo ทำให้การใช้ library ซ้ำ เป็นเรื่องง่ายมาก Rustacean จึงเขียนโปรเจกต์ขนาดเล็กที่ประกอบจาก package หลายตัวได้
Generate ตัวเลขสุ่ม
มาเริ่มใช้ rand เพื่อ generate ตัวเลขให้ทาย ขั้นต่อไปคือ update
src/main.rs ตามที่แสดงใน Listing 2-3
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
ขั้นแรก เราเพิ่มบรรทัด use rand::Rng; trait Rng ประกาศเมธอดที่ random
number generator implement และ trait นี้ต้องอยู่ใน scope เราถึงใช้เมธอด
เหล่านั้นได้ บทที่ 10 จะครอบคลุม trait ในรายละเอียด
ถัดไป เราเพิ่มสองบรรทัดตรงกลาง ในบรรทัดแรก เราเรียกฟังก์ชัน
rand::thread_rng ที่ให้ random number generator เฉพาะที่เราจะใช้: ตัวที่
local กับ thread ของการ execute ปัจจุบัน และถูก seed โดย OS แล้วเราเรียก
เมธอด gen_range บน random number generator เมธอดนี้ถูกประกาศโดย trait
Rng ที่เรานำเข้า scope ด้วย statement use rand::Rng; เมธอด gen_range
รับ range expression เป็น argument แล้ว generate ตัวเลขสุ่มใน range นั้น
range expression ที่เราใช้ที่นี่อยู่ในรูป start..=end และ inclusive ทั้ง
ขอบเขตล่างและบน เราจึงต้องระบุ 1..=100 เพื่อขอตัวเลขระหว่าง 1 ถึง 100
หมายเหตุ: คุณคงไม่รู้เองว่าจะใช้ trait ตัวไหน และเรียกเมธอดและฟังก์ชัน ไหนจาก crate ดังนั้นแต่ละ crate จึงมี documentation พร้อมคำแนะนำการใช้ งาน อีกฟีเจอร์เจ๋ง ๆ ของ Cargo คือการรันคำสั่ง
cargo doc --openจะ build documentation ที่ dependency ทั้งหมดของคุณให้มา และเปิดมันใน browser ของคุณ ถ้าคุณสนใจ functionality อื่นใน craterandเช่น รันcargo doc --openแล้วคลิกrandใน sidebar ทางซ้าย
บรรทัดใหม่ที่สองพิมพ์ตัวเลขลับ ซึ่งมีประโยชน์ระหว่างที่เราพัฒนาโปรแกรม เพื่อทดสอบมัน แต่เราจะลบออกใน version สุดท้าย มันไม่เป็นเกมเลยถ้าโปรแกรม พิมพ์คำตอบทันทีที่เริ่ม!
ลองรันโปรแกรมหลาย ๆ ครั้ง:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4
$ cargo run
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5
คุณควรได้ตัวเลขสุ่มต่างกัน และทั้งหมดควรเป็นตัวเลขระหว่าง 1 ถึง 100 ดีมาก!
เปรียบเทียบคำตอบกับตัวเลขลับ
ตอนนี้เรามี input ของผู้ใช้และตัวเลขสุ่มแล้ว เราเปรียบเทียบมันได้ ขั้นตอน นั้นแสดงใน Listing 2-4 หมายเหตุว่าโค้ดนี้ยัง compile ไม่ได้ ดังที่เราจะ อธิบาย
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
// --snip--
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
ขั้นแรก เราเพิ่ม statement use อีกตัว นำ type ชื่อ std::cmp::Ordering
เข้า scope จาก standard library type Ordering เป็น enum อีกตัว และมี
variant Less, Greater และ Equal นี่คือสามผลลัพธ์ที่เป็นไปได้เมื่อ
คุณเปรียบเทียบสองค่า
จากนั้น เราเพิ่มห้าบรรทัดใหม่ที่ด้านล่างที่ใช้ type Ordering เมธอด cmp
เปรียบเทียบสองค่า และเรียกได้บนอะไรก็ตามที่เปรียบเทียบได้ มันรับ reference
ของอะไรก็ตามที่คุณต้องการเปรียบเทียบด้วย: ที่นี่ มันเปรียบเทียบ guess กับ
secret_number จากนั้นมัน return variant ของ enum Ordering ที่เรานำเข้า
scope ด้วย statement use เราใช้ match expression
เพื่อตัดสินใจว่าจะทำอะไรต่อ ขึ้นกับว่า variant ไหนของ Ordering ถูก return
จากการเรียก cmp ด้วยค่าใน guess และ secret_number
match expression ประกอบด้วย arm arm ประกอบด้วย pattern ที่จะ match
กับ และโค้ดที่ควรรันถ้าค่าที่ให้ match ตรงกับ pattern ของ arm นั้น Rust
เอาค่าที่ให้ match แล้วดูผ่าน pattern ของแต่ละ arm ตามลำดับ pattern และ
โครงสร้าง match เป็นฟีเจอร์ที่ทรงพลังของ Rust — มันให้คุณแสดงสถานการณ์ที่
หลากหลายที่โค้ดของคุณอาจเจอ และทำให้แน่ใจว่าคุณจัดการทั้งหมด ฟีเจอร์เหล่านี้
จะครอบคลุมในรายละเอียดในบทที่ 6 และ 19 ตามลำดับ
มาเดินผ่านตัวอย่างกับ match expression ที่เราใช้ที่นี่ สมมติว่าผู้ใช้ทาย
50 และตัวเลขลับที่ generate แบบสุ่มครั้งนี้คือ 38
เมื่อโค้ดเปรียบเทียบ 50 กับ 38 เมธอด cmp จะ return Ordering::Greater
เพราะ 50 มากกว่า 38 match expression รับค่า Ordering::Greater แล้วเริ่ม
เช็ค pattern ของแต่ละ arm มันดู pattern ของ arm แรก Ordering::Less แล้ว
เห็นว่าค่า Ordering::Greater ไม่ match Ordering::Less มันจึงละเว้นโค้ด
ใน arm นั้นและไปที่ arm ถัดไป pattern ของ arm ถัดไปคือ Ordering::Greater
ซึ่ง match Ordering::Greater! โค้ดที่เกี่ยวข้องใน arm นั้นจะ execute
และพิมพ์ Too big! ออกหน้าจอ match expression จบหลัง match สำเร็จครั้ง
แรก ดังนั้นมันจะไม่ดู arm สุดท้ายในสถานการณ์นี้
อย่างไรก็ตาม โค้ดใน Listing 2-4 ยัง compile ไม่ได้ ลองดู:
$ cargo build
Compiling libc v0.2.86
Compiling getrandom v0.2.2
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.10
Compiling rand_core v0.6.2
Compiling rand_chacha v0.3.0
Compiling rand v0.8.5
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
--> src/main.rs:23:21
|
23 | match guess.cmp(&secret_number) {
| --- ^^^^^^^^^^^^^^ expected `&String`, found `&{integer}`
| |
| arguments to this method are incorrect
|
= note: expected reference `&String`
found reference `&{integer}`
note: method defined here
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/cmp.rs:979:8
For more information about this error, try `rustc --explain E0308`.
error: could not compile `guessing_game` (bin "guessing_game") due to 1 previous error
แก่นของ error บอกว่ามี mismatched types Rust มีระบบ type ที่ strong และ
static อย่างไรก็ตาม มันยังมี type inference เมื่อเราเขียน
let mut guess = String::new() Rust สามารถ infer ได้ว่า guess ควรเป็น
String และไม่ได้บังคับให้เราเขียน type ส่วน secret_number เป็น type
ตัวเลข Rust มี type ตัวเลขไม่กี่ตัวที่มีค่าระหว่าง 1 ถึง 100 ได้ — i32
ตัวเลข 32-bit, u32 ตัวเลข 32-bit แบบไม่มีเครื่องหมาย, i64 ตัวเลข 64-bit
รวมถึงอื่น ๆ ถ้าไม่ระบุเป็นอย่างอื่น Rust default เป็น i32 ซึ่งเป็น type
ของ secret_number เว้นแต่คุณจะเพิ่มข้อมูล type ที่อื่นที่จะทำให้ Rust
infer เป็น type ตัวเลขอื่น เหตุผลของ error คือ Rust เปรียบเทียบ string กับ
type ตัวเลขไม่ได้
สุดท้าย เราอยากแปลง String ที่โปรแกรมอ่านเป็น input ให้เป็น type ตัวเลข
เพื่อให้เปรียบเทียบเชิงตัวเลขกับตัวเลขลับได้ เราทำโดยการเพิ่มบรรทัดนี้เข้า
ใน body ของฟังก์ชัน main:
Filename: src/main.rs
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
บรรทัดนั้นคือ:
let guess: u32 = guess.trim().parse().expect("Please type a number!");
เราสร้างตัวแปรชื่อ guess แต่เดี๋ยวก่อน โปรแกรมไม่ได้มีตัวแปรชื่อ guess
อยู่แล้วเหรอ? ใช่ แต่ Rust ช่วยให้เรา shadow ค่าก่อนหน้าของ guess ด้วยค่า
ใหม่ Shadowing ให้เราใช้ชื่อตัวแปร guess ซ้ำ แทนที่จะถูกบังคับให้สร้าง
ตัวแปรไม่ซ้ำกันสองตัว เช่น guess_str และ guess เราจะครอบคลุมเรื่องนี้
ในรายละเอียดเพิ่มเติมใน บทที่ 3 แต่ตอนนี้
รู้ว่าฟีเจอร์นี้มักใช้เมื่อคุณต้องการแปลงค่าจาก type หนึ่งเป็นอีก type หนึ่ง
เรา bind ตัวแปรใหม่นี้กับ expression guess.trim().parse() guess ใน
expression อ้างถึงตัวแปร guess ตัวเดิมที่มี input เป็น string เมธอด
trim บน instance ของ String จะกำจัด whitespace ใด ๆ ที่ต้นและท้าย ซึ่ง
เราต้องทำก่อนที่จะแปลง string เป็น u32 ที่มีได้แต่ข้อมูลตัวเลข ผู้ใช้
ต้องกด enter เพื่อตอบสนอง read_line และป้อนคำตอบ ซึ่งเพิ่ม
อักขระ newline เข้าไปใน string เช่น ถ้าผู้ใช้พิมพ์ 5 และกด
enter guess จะหน้าตาเป็นแบบนี้: 5\n \n แทน “newline” (บน
Windows การกด enter ให้ผลเป็น carriage return และ newline,
\r\n) เมธอด trim กำจัด \n หรือ \r\n ออก เหลือแค่ 5
เมธอด parse บน string แปลง string ให้เป็น type
อื่น ที่นี่ เราใช้มันแปลงจาก string เป็นตัวเลข เราต้องบอก Rust ถึง type
ตัวเลขที่เราต้องการแบบเป๊ะ ๆ โดยใช้ let guess: u32 colon (:) หลัง
guess บอก Rust ว่าเราจะ annotate type ของตัวแปร Rust มี type ตัวเลข
built-in ไม่กี่ตัว u32 ที่เห็นที่นี่คือจำนวนเต็ม 32-bit แบบไม่มี
เครื่องหมาย เป็น default choice ที่ดีสำหรับตัวเลขบวกขนาดเล็ก คุณจะเรียน
type ตัวเลขอื่น ๆ ใน บทที่ 3
นอกจากนี้ การ annotate u32 ในตัวอย่างโปรแกรมนี้ และการเปรียบเทียบกับ
secret_number หมายความว่า Rust จะ infer ว่า secret_number ควรเป็น u32
ด้วย ดังนั้นตอนนี้การเปรียบเทียบจะเป็นระหว่างค่าสองค่าที่มี type เดียวกัน!
เมธอด parse จะทำงานได้แค่บนอักขระที่เชิง logic แปลงเป็นตัวเลขได้ จึงทำ
ให้เกิด error ได้ง่าย ถ้า เช่น string มี A👍% ก็ไม่มีทางแปลงเป็นตัวเลข
เพราะมันอาจล้มเหลว เมธอด parse จึง return type Result เช่นเดียวกับ
เมธอด read_line (พูดถึงก่อนหน้านี้ใน
“จัดการ failure ที่อาจเกิดขึ้นด้วย Result”)
เราจะปฏิบัติต่อ Result นี้แบบเดียวกันโดยใช้เมธอด expect อีกครั้ง ถ้า
parse return variant Err ของ Result เพราะมันสร้างตัวเลขจาก string
ไม่ได้ การเรียก expect จะ crash เกมและพิมพ์ข้อความที่เราให้มัน ถ้า
parse แปลง string เป็นตัวเลขสำเร็จ มันจะ return variant Ok ของ
Result และ expect จะ return ตัวเลขที่เราต้องการจากค่า Ok
มารันโปรแกรมตอนนี้:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
76
You guessed: 76
Too big!
ดี! แม้จะมี space เพิ่มก่อนคำตอบ โปรแกรมก็ยังรู้ว่าผู้ใช้ทาย 76 รัน โปรแกรมหลาย ๆ ครั้งเพื่อตรวจสอบพฤติกรรมที่ต่างกันด้วย input หลายแบบ — ทาย ตัวเลขถูก ทายตัวเลขที่สูงเกินไป และทายตัวเลขที่ต่ำเกินไป
เรามีเกมส่วนใหญ่ทำงานแล้ว แต่ผู้ใช้ทายได้แค่ครั้งเดียว มาเปลี่ยนเรื่องนั้น ด้วยการเพิ่ม loop!
ให้ทายหลายครั้งด้วย loop
keyword loop สร้าง infinite loop เราจะเพิ่ม loop เพื่อให้ผู้ใช้มีโอกาส
ทายตัวเลขมากกว่า:
Filename: src/main.rs
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
// --snip--
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
// --snip--
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
}
อย่างที่เห็น เราย้ายทุกอย่างตั้งแต่ prompt input คำตอบลงไปใน loop อย่าลืม indent บรรทัดภายใน loop เพิ่มสี่ space และรันโปรแกรมอีกครั้ง โปรแกรมตอนนี้ จะถามคำตอบใหม่ตลอดไป ซึ่งจริง ๆ ก็เกิดปัญหาใหม่ ดูเหมือนผู้ใช้ออกไม่ได้!
ผู้ใช้สามารถ interrupt โปรแกรมได้เสมอด้วย keyboard shortcut
ctrl-C แต่ยังมีอีกวิธีหนึ่งในการหนีจากสัตว์ประหลาด
ที่ไม่รู้จักอิ่มนี้ ดังที่กล่าวไว้ในการพูดถึง parse ใน
“เปรียบเทียบคำตอบกับตัวเลขลับ”
— ถ้าผู้ใช้ป้อนคำตอบที่ไม่ใช่ตัวเลข โปรแกรมจะ crash เราใช้ประโยชน์จากสิ่ง
นั้นเพื่อให้ผู้ใช้ออกได้ ดังที่แสดงที่นี่:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.23s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread 'main' panicked at src/main.rs:28:47:
Please type a number!: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
การพิมพ์ quit จะออกจากเกม แต่อย่างที่คุณจะสังเกต การป้อน input ที่ไม่ใช่
ตัวเลขใด ๆ ก็จะทำเหมือนกัน นี่ไม่ดีที่สุด พูดน้อย ๆ — เราอยากให้เกมหยุดเมื่อ
ทายตัวเลขถูกด้วย
ออกเมื่อทายถูก
มา program ให้เกมออกเมื่อผู้ใช้ชนะ โดยเพิ่ม statement break:
Filename: src/main.rs
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
การเพิ่มบรรทัด break หลัง You win! ทำให้โปรแกรมออกจาก loop เมื่อผู้ใช้
ทายตัวเลขลับถูก การออกจาก loop ก็หมายถึงการออกจากโปรแกรม เพราะ loop เป็น
ส่วนสุดท้ายของ main
จัดการ input ที่ไม่ถูกต้อง
เพื่อปรับปรุงพฤติกรรมของเกมเพิ่ม แทนที่จะ crash โปรแกรมเมื่อผู้ใช้ป้อนสิ่ง
ที่ไม่ใช่ตัวเลข มาทำให้เกมละเว้น input ที่ไม่ใช่ตัวเลข เพื่อให้ผู้ใช้ทาย
ต่อได้ เราทำได้โดยเปลี่ยนบรรทัดที่ guess ถูกแปลงจาก String เป็น u32
ตามที่แสดงใน Listing 2-5
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
// --snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
เราเปลี่ยนจากการเรียก expect มาเป็น match expression เพื่อย้ายจากการ
crash บน error มาเป็นการจัดการ error จำได้ว่า parse return type Result
และ Result เป็น enum ที่มี variant Ok และ Err เราใช้ match
expression ที่นี่ เหมือนที่เราทำกับผล Ordering ของเมธอด cmp
ถ้า parse แปลง string เป็นตัวเลขสำเร็จ มันจะ return ค่า Ok ที่มีตัวเลข
ผลลัพธ์อยู่ ค่า Ok นั้นจะ match pattern ของ arm แรก และ match
expression ก็จะแค่ return ค่า num ที่ parse produce และใส่ใน Ok ตัว
เลขนั้นจะลงเอยตรงที่เราต้องการพอดี ในตัวแปร guess ใหม่ที่เรากำลังสร้าง
ถ้า parse ไม่ สามารถแปลง string เป็นตัวเลข มันจะ return ค่า Err ที่
มีข้อมูลเพิ่มเติมเกี่ยวกับ error ค่า Err ไม่ match pattern Ok(num) ใน
arm match แรก แต่มัน match pattern Err(_) ใน arm ที่สอง underscore
_ คือค่าจับทั้งหมด ในตัวอย่างนี้ เราบอกว่าเราอยาก match ค่า Err
ทั้งหมด ไม่ว่าจะมีข้อมูลอะไรอยู่ข้างใน ดังนั้นโปรแกรมจะ execute โค้ดของ
arm ที่สอง continue ซึ่งบอกโปรแกรมให้ไปที่ iteration ถัดไปของ loop
และขอคำตอบใหม่ จึงทำให้โปรแกรมละเว้น error ทั้งหมดที่ parse อาจเจอ!
ตอนนี้ทุกอย่างในโปรแกรมควรทำงานตามที่คาด ลองดู:
$ cargo run
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!
ยอดเยี่ยม! ด้วยการแก้สุดท้ายเล็ก ๆ เราจะจบเกมทายตัวเลข จำได้ว่าโปรแกรม
ยังพิมพ์ตัวเลขลับอยู่ มันใช้ได้สำหรับการทดสอบ แต่ทำลายความเป็นเกม มาลบ
println! ที่ output ตัวเลขลับ Listing 2-6 แสดงโค้ดสุดท้าย
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
ณ จุดนี้ คุณ build เกมทายตัวเลขสำเร็จแล้ว ขอแสดงความยินดี!
สรุป
โปรเจกต์นี้เป็นวิธีลงมือทำเพื่อแนะนำแนวคิด Rust ใหม่ ๆ ให้คุณ: let,
match, ฟังก์ชัน, การใช้ external crate และอื่น ๆ ในบทถัด ๆ ไป คุณจะเรียน
รู้เกี่ยวกับแนวคิดเหล่านี้ในรายละเอียดเพิ่มเติม บทที่ 3 ครอบคลุมแนวคิดที่
ภาษาโปรแกรมส่วนใหญ่มี เช่น ตัวแปร, ชนิดข้อมูล และฟังก์ชัน และแสดงวิธีใช้
ใน Rust บทที่ 4 สำรวจ ownership ฟีเจอร์ที่ทำให้ Rust ต่างจากภาษาอื่น บทที่
5 พูดถึง struct และ syntax ของเมธอด และบทที่ 6 อธิบายวิธีที่ enum ทำงาน
แนวคิดพื้นฐานของการเขียนโปรแกรม
บทนี้ครอบคลุมแนวคิดที่ปรากฏในแทบทุกภาษาโปรแกรม และวิธีการทำงานของพวกมัน ใน Rust ภาษาโปรแกรมหลายภาษามีสิ่งร่วมกันมากที่แกนกลาง ไม่มีแนวคิดใดในบทนี้ ที่เป็นเอกลักษณ์เฉพาะของ Rust แต่เราจะพูดถึงพวกมันในบริบทของ Rust และ อธิบาย convention ในการใช้งาน
โดยเฉพาะ คุณจะได้เรียนเรื่องตัวแปร, type พื้นฐาน, ฟังก์ชัน, comment และ control flow รากฐานเหล่านี้จะอยู่ในทุกโปรแกรม Rust และการเรียนรู้มันแต่ เนิ่น ๆ จะให้แกนกลางที่แข็งแรงเพื่อเริ่มต้น
Keyword
ภาษา Rust มีชุดของ keyword ที่ถูกสงวนให้ใช้โดยภาษาเท่านั้น เช่นเดียว กับในภาษาอื่น จำไว้ว่าคุณใช้คำเหล่านี้เป็นชื่อตัวแปรหรือฟังก์ชันไม่ได้ keyword ส่วนใหญ่มีความหมายพิเศษ และคุณจะใช้พวกมันทำงานต่าง ๆ ในโปรแกรม Rust ของคุณ บางตัวยังไม่มี functionality ที่ผูกอยู่ในปัจจุบัน แต่ถูก สงวนไว้สำหรับ functionality ที่อาจเพิ่มเข้ามาใน Rust ในอนาคต คุณดูราย ชื่อ keyword ได้ใน ภาคผนวก A
ตัวแปรและ mutability
ตัวแปรและ Mutability
อย่างที่กล่าวไว้ในส่วน “เก็บค่าด้วยตัวแปร” โดย default ตัวแปรเป็น immutable นี่เป็นหนึ่งในหลาย ๆ สัญญาณที่ Rust ให้คุณ เพื่อเขียนโค้ดในแบบที่ใช้ประโยชน์จากความปลอดภัยและ concurrency ที่ง่ายของ Rust อย่างไรก็ตาม คุณยังมีตัวเลือกในการทำให้ตัวแปร mutable มาสำรวจกันว่า ทำไม Rust ถึงสนับสนุนให้คุณเอนเอียงไปทาง immutability และทำไมบางครั้งคุณ อาจอยาก opt out
เมื่อตัวแปรเป็น immutable ทันทีที่ค่าถูก bind กับชื่อ คุณเปลี่ยนค่านั้นไม่
ได้ เพื่อแสดงให้เห็นเรื่องนี้ generate โปรเจกต์ใหม่ชื่อ variables ใน
directory projects ของคุณ ด้วย cargo new variables
จากนั้นใน directory variables ใหม่ของคุณ เปิด src/main.rs แล้วแทนที่ โค้ดของมันด้วยโค้ดต่อไปนี้ ซึ่งจะยัง compile ไม่ผ่าน:
Filename: src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
บันทึกแล้วรันโปรแกรมด้วย cargo run คุณควรได้ error message เกี่ยวกับ
immutability ตามที่แสดงใน output นี้:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
3 | println!("The value of x is: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` (bin "variables") due to 1 previous error
ตัวอย่างนี้แสดงวิธีที่ compiler ช่วยคุณหา error ในโปรแกรม Compiler error อาจทำให้หงุดหงิด แต่จริง ๆ มันแค่หมายความว่าโปรแกรมของคุณยังไม่ได้ทำสิ่ง ที่คุณต้องการอย่างปลอดภัย — มันไม่ได้หมายความว่าคุณเขียนโปรแกรมไม่เก่ง! Rustacean ที่มีประสบการณ์ก็ยังเจอ compiler error
คุณได้รับ error message cannot assign twice to immutable variable `x`
เพราะคุณพยายาม assign ค่าที่สองให้ตัวแปร immutable x
มันสำคัญที่เราจะได้ compile-time error เมื่อพยายามเปลี่ยนค่าที่ถูกกำหนดเป็น immutable เพราะสถานการณ์นี้นี่แหละที่นำไปสู่ bug ได้ ถ้าโค้ดของเราส่วน หนึ่งทำงานบนสมมติฐานว่าค่าจะไม่เปลี่ยน และอีกส่วนหนึ่งของโค้ดเปลี่ยนค่า นั้น เป็นไปได้ที่โค้ดส่วนแรกจะไม่ทำสิ่งที่ออกแบบให้ทำ สาเหตุของ bug แบบนี้ อาจตามหายากหลังเกิดเหตุ โดยเฉพาะเมื่อโค้ดส่วนที่สองเปลี่ยนค่า บางครั้ง เท่านั้น Rust compiler รับประกันว่าเมื่อคุณบอกว่าค่าจะไม่เปลี่ยน มันก็จะ ไม่เปลี่ยนจริง ๆ คุณจึงไม่ต้องตามติดมันเอง โค้ดของคุณจึงคิดตามได้ง่ายขึ้น
แต่ mutability ก็มีประโยชน์มาก และทำให้เขียนโค้ดสะดวกขึ้นได้ แม้ตัวแปรจะ
เป็น immutable โดย default คุณก็ทำให้มัน mutable ได้ โดยเพิ่ม mut ไว้
หน้าชื่อตัวแปรเหมือนที่คุณทำใน
บทที่ 2 การเพิ่ม mut
ยังสื่อเจตนาให้ผู้อ่านโค้ดในอนาคต โดยบ่งบอกว่าส่วนอื่น ๆ ของโค้ดจะ
เปลี่ยนค่าตัวแปรนี้
เช่น มาเปลี่ยน src/main.rs เป็นแบบนี้:
Filename: src/main.rs
fn main() {
let mut x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}
เมื่อรันโปรแกรมตอนนี้ เราจะได้:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
เราได้รับอนุญาตให้เปลี่ยนค่าที่ bind กับ x จาก 5 เป็น 6 เมื่อใช้
mut สุดท้าย การตัดสินใจว่าจะใช้ mutability หรือไม่ ขึ้นกับคุณ และขึ้น
กับสิ่งที่คุณคิดว่าชัดเจนที่สุดในสถานการณ์นั้น ๆ
ประกาศ Constant
เช่นเดียวกับตัวแปร immutable constant คือค่าที่ถูก bind กับชื่อ และไม่ อนุญาตให้เปลี่ยน แต่มีความแตกต่างไม่กี่ข้อระหว่าง constant และตัวแปร
ประการแรก คุณไม่ได้รับอนุญาตให้ใช้ mut กับ constant Constant ไม่ใช่แค่
immutable โดย default — มัน immutable ตลอดเวลา คุณประกาศ constant โดยใช้
keyword const แทน keyword let และ ต้อง annotate type ของค่า เราจะ
ครอบคลุม type และ type annotation ในส่วนถัดไป
“ชนิดข้อมูล” ดังนั้นยังไม่ต้องห่วงรายละเอียด
ตอนนี้ แค่รู้ว่าคุณต้อง annotate type เสมอ
Constant ประกาศได้ใน scope ใด ๆ รวมถึง global scope ซึ่งทำให้มันมีประโยชน์ สำหรับค่าที่โค้ดหลายส่วนต้องรู้
ความแตกต่างสุดท้ายคือ constant ตั้งค่าได้แค่เป็น constant expression ไม่ ใช่ผลของค่าที่คำนวณได้แค่ตอน runtime
นี่คือตัวอย่างการประกาศ constant:
#![allow(unused)]
fn main() {
const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
}
ชื่อของ constant คือ THREE_HOURS_IN_SECONDS และค่าของมันถูกตั้งเป็นผลของ
การคูณ 60 (จำนวนวินาทีในนาที) ด้วย 60 (จำนวนนาทีในชั่วโมง) ด้วย 3 (จำนวน
ชั่วโมงที่เราอยากนับในโปรแกรมนี้) Convention การตั้งชื่อ constant ของ Rust
คือใช้ตัวพิมพ์ใหญ่ทั้งหมด พร้อม underscore ระหว่างคำ Compiler ประเมิน
operation ชุดจำกัดได้ตอน compile time ซึ่งให้เราเลือกเขียนค่านี้ในรูปแบบ
ที่เข้าใจและตรวจสอบได้ง่ายกว่า แทนที่จะตั้ง constant นี้เป็นค่า 10,800 ดู
ส่วน constant evaluation ของ Rust Reference สำหรับข้อมูล
เพิ่มเติมว่า operation ไหนใช้ได้เมื่อประกาศ constant
Constant ใช้งานได้ตลอดเวลาที่โปรแกรมรัน ภายใน scope ที่พวกมันถูกประกาศ คุณสมบัตินี้ทำให้ constant มีประโยชน์สำหรับค่าใน application domain ของ คุณที่หลายส่วนของโปรแกรมอาจต้องรู้ เช่น จำนวนคะแนนสูงสุดที่ผู้เล่นเกม สามารถได้รับ หรือความเร็วแสง
การตั้งชื่อค่า hardcode ที่ใช้ตลอดโปรแกรมเป็น constant มีประโยชน์ในการสื่อ ความหมายของค่านั้นให้ผู้ดูแลโค้ดในอนาคต ยังช่วยให้มีเพียงที่เดียวในโค้ดที่ คุณต้องเปลี่ยน หากค่า hardcode นั้นต้อง update ในอนาคต
Shadowing
อย่างที่คุณเห็นใน tutorial เกมทายตัวเลขใน
บทที่ 2 คุณ
ประกาศตัวแปรใหม่ที่มีชื่อเดียวกับตัวแปรก่อนหน้าได้ Rustacean บอกว่าตัวแปร
แรกถูก shadow ด้วยตัวที่สอง ซึ่งหมายความว่าตัวแปรที่สองคือสิ่งที่
compiler จะเห็น เมื่อคุณใช้ชื่อของตัวแปร ในทางผล ตัวแปรที่สองบดบังตัวแรก
รับการใช้ชื่อตัวแปรมาเป็นของตัวเอง จนกว่าตัวมันเองจะถูก shadow หรือ scope
จบ เรา shadow ตัวแปรได้โดยใช้ชื่อตัวแปรเดียวกัน และเขียน keyword let
ซ้ำ ดังนี้:
Filename: src/main.rs
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("The value of x in the inner scope is: {x}");
}
println!("The value of x is: {x}");
}
โปรแกรมนี้ bind x กับค่า 5 ก่อน จากนั้นสร้างตัวแปร x ใหม่ด้วยการเขียน
let x = ซ้ำ เอาค่าเดิมมาบวก 1 ทำให้ค่าของ x เป็น 6 จากนั้นใน inner
scope ที่สร้างด้วย curly bracket statement let ที่สามก็ shadow x และ
สร้างตัวแปรใหม่ คูณค่าก่อนหน้าด้วย 2 ให้ x มีค่า 12 เมื่อ scope นั้น
จบ inner shadowing สิ้นสุด และ x กลับเป็น 6 เมื่อเรารันโปรแกรมนี้ มัน
จะ output:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
Shadowing ต่างจากการ mark ตัวแปรเป็น mut เพราะเราจะได้ compile-time
error ถ้าเผลอลอง reassign ตัวแปรนี้โดยไม่ใช้ keyword let ด้วยการใช้
let เราทำ transformation ไม่กี่อย่างบนค่าได้ แต่ตัวแปรยังคงเป็น
immutable หลัง transformation เหล่านั้นเสร็จ
ความแตกต่างอีกอย่างระหว่าง mut กับ shadowing คือ เพราะเราเป็นการสร้าง
ตัวแปรใหม่จริง ๆ เมื่อใช้ keyword let อีกครั้ง เราเปลี่ยน type ของค่าได้
แต่ใช้ชื่อเดิม เช่น สมมติว่าโปรแกรมของเราถามผู้ใช้ให้แสดงจำนวน space ที่
ต้องการระหว่างข้อความบางตัว โดยป้อนอักขระ space แล้วเราอยากเก็บ input นั้น
เป็นตัวเลข:
fn main() {
let spaces = " ";
let spaces = spaces.len();
}
ตัวแปร spaces ตัวแรกเป็น type string และตัวแปร spaces ตัวที่สองเป็น
type ตัวเลข Shadowing จึงช่วยให้เราไม่ต้องคิดชื่อต่างกัน เช่น spaces_str
และ spaces_num แต่ใช้ชื่อ spaces ที่ง่ายกว่าซ้ำได้ อย่างไรก็ตาม ถ้า
เราลองใช้ mut สำหรับเรื่องนี้ ดังที่แสดงที่นี่ เราจะได้ compile-time
error:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
Error บอกว่าเราไม่ได้รับอนุญาตให้ mutate type ของตัวแปร:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` (bin "variables") due to 1 previous error
ตอนนี้เราสำรวจวิธีที่ตัวแปรทำงานแล้ว มาดูชนิดข้อมูลที่พวกมันมีได้มากกว่า
ชนิดข้อมูล
ชนิดข้อมูล
ทุกค่าใน Rust เป็น ชนิดข้อมูล (data type) บางอย่าง ซึ่งบอก Rust ว่าเป็น ข้อมูลแบบไหน เพื่อให้รู้วิธีทำงานกับข้อมูลนั้น เราจะดู subset ของชนิด ข้อมูลสองกลุ่ม: scalar และ compound
จำไว้ว่า Rust เป็นภาษาแบบ statically typed ซึ่งหมายความว่ามันต้องรู้
type ของตัวแปรทั้งหมดตอน compile time โดยปกติ compiler infer type ที่
เราต้องการใช้จากค่าและวิธีที่เราใช้มันได้ ในกรณีที่มี type หลายแบบเป็นไป
ได้ เช่นเมื่อเราแปลง String เป็น type ตัวเลขด้วย parse ในส่วน
“เปรียบเทียบคำตอบกับตัวเลขลับ”
ในบทที่ 2 เราต้องเพิ่ม type annotation แบบนี้:
#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}
ถ้าเราไม่เพิ่ม type annotation : u32 ที่แสดงในโค้ดข้างต้น Rust จะแสดง
error ต่อไปนี้ ซึ่งหมายความว่า compiler ต้องการข้อมูลจากเราเพิ่มเพื่อรู้
ว่าจะใช้ type ไหน:
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ ----- type must be known at this point
|
= note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
|
2 | let guess: /* Type */ = "42".parse().expect("Not a number!");
| ++++++++++++
For more information about this error, try `rustc --explain E0284`.
error: could not compile `no_type_annotations` (bin "no_type_annotations") due to 1 previous error
คุณจะเห็น type annotation ที่ต่างกันสำหรับชนิดข้อมูลอื่น ๆ
Scalar Type
Type แบบ scalar แทนค่าเดียว Rust มี scalar type หลัก 4 แบบ: integer, floating-point number, Boolean และ character คุณอาจคุ้นเคยกับสิ่งเหล่านี้ จากภาษาโปรแกรมอื่น มาเริ่มดูว่าพวกมันทำงานยังไงใน Rust
Integer Type
integer คือตัวเลขที่ไม่มีส่วนทศนิยม เราใช้ integer type ตัวหนึ่งในบทที่
2 คือ type u32 การประกาศ type นี้บ่งบอกว่าค่าที่มันผูกอยู่ควรเป็น
unsigned integer (signed integer type ขึ้นต้นด้วย i แทน u) ที่กิน
พื้นที่ 32 bit Table 3-1 แสดง integer type built-in ใน Rust เราใช้ variant
ใด ๆ เหล่านี้ประกาศ type ของค่า integer ได้
Table 3-1: Integer Type ใน Rust
| ความยาว | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| ขึ้นกับ architecture | isize | usize |
แต่ละ variant เป็น signed หรือ unsigned ก็ได้ และมีขนาดชัดเจน Signed และ unsigned อ้างถึงว่าตัวเลขเป็นค่าลบได้หรือไม่ — พูดอีกอย่าง ตัวเลข ต้องมีเครื่องหมายไหม (signed) หรือจะเป็นค่าบวกตลอด และสามารถแทนได้โดยไม่มี เครื่องหมาย (unsigned) เหมือนการเขียนตัวเลขลงกระดาษ — เมื่อเครื่องหมาย สำคัญ ตัวเลขแสดงด้วยเครื่องหมายบวกหรือลบ แต่เมื่อปลอดภัยที่จะสมมติว่า ตัวเลขเป็นบวก ก็แสดงโดยไม่มีเครื่องหมาย Signed number ถูกเก็บโดยใช้รูปแบบ two’s complement
แต่ละ signed variant เก็บตัวเลขจาก −(2n − 1) ถึง 2n −
1 − 1 inclusive ได้ โดยที่ n คือจำนวน bit ที่ variant นั้นใช้ ดังนั้น
i8 เก็บตัวเลขจาก −(27) ถึง 27 − 1 ได้ ซึ่งเท่ากับ
−128 ถึง 127 Unsigned variant เก็บตัวเลขจาก 0 ถึง 2n − 1 ได้
ดังนั้น u8 เก็บตัวเลขจาก 0 ถึง 28 − 1 ได้ ซึ่งเท่ากับ 0 ถึง 255
นอกจากนี้ type isize และ usize ขึ้นกับ architecture ของคอมพิวเตอร์ที่
โปรแกรมรัน: 64 bit ถ้าอยู่บน architecture 64-bit และ 32 bit ถ้าอยู่บน
architecture 32-bit
คุณเขียน integer literal ในรูปแบบใด ๆ ที่แสดงใน Table 3-2 ได้ หมายเหตุว่า
literal ตัวเลขที่เป็นได้หลาย type ตัวเลข อนุญาตให้ใส่ type suffix เช่น
57u8 เพื่อกำหนด type literal ตัวเลขยังใช้ _ เป็นตัวคั่นเชิง visual
เพื่อทำให้ตัวเลขอ่านง่ายขึ้นได้ เช่น 1_000 ซึ่งมีค่าเดียวกับการระบุ
1000
Table 3-2: Integer Literal ใน Rust
| รูปแบบ literal | ตัวอย่าง |
|---|---|
| Decimal | 98_222 |
| Hex | 0xff |
| Octal | 0o77 |
| Binary | 0b1111_0000 |
Byte (u8 เท่านั้น) | b'A' |
แล้วจะรู้ได้ยังไงว่าใช้ integer type ไหน? ถ้าไม่แน่ใจ default ของ Rust
เป็นจุดเริ่มต้นที่ดีโดยทั่วไป — integer type default เป็น i32 สถานการณ์
หลักที่คุณจะใช้ isize หรือ usize คือเมื่อ index collection บางแบบ
Integer Overflow
สมมติว่าคุณมีตัวแปร type u8 ที่เก็บค่าระหว่าง 0 ถึง 255 ได้ ถ้าคุณ
พยายามเปลี่ยนตัวแปรเป็นค่านอก range นั้น เช่น 256 integer overflow
จะเกิดขึ้น ซึ่งให้ผลเป็นพฤติกรรมหนึ่งในสองอย่าง เมื่อคุณ compile ใน
debug mode Rust รวมการเช็ค integer overflow ที่ทำให้โปรแกรม panic
ตอน runtime ถ้าพฤติกรรมนี้เกิดขึ้น Rust ใช้คำว่า panicking เมื่อ
โปรแกรมออกพร้อม error เราจะพูดถึง panic ในรายละเอียดเพิ่มเติมในส่วน
“Unrecoverable Errors with panic!”
ในบทที่ 9
เมื่อคุณ compile ใน release mode ด้วย flag --release Rust ไม่ รวม
การเช็ค integer overflow ที่ทำให้ panic แต่ถ้า overflow เกิดขึ้น Rust
จะทำ two’s complement wrapping พูดสั้น ๆ ค่าที่มากกว่าค่าสูงสุดที่
type เก็บได้ จะ “wrap around” ไปเป็นค่าต่ำสุดที่ type เก็บได้ ในกรณีของ
u8 ค่า 256 กลายเป็น 0, ค่า 257 กลายเป็น 1 และต่อ ๆ ไป โปรแกรมจะไม่
panic แต่ตัวแปรจะมีค่าที่อาจไม่ใช่สิ่งที่คุณคาดหวัง การพึ่งพฤติกรรม
wrapping ของ integer overflow ถือว่าเป็น error
ในการจัดการความเป็นไปได้ของ overflow แบบ explicit คุณใช้ตระกูลเมธอด เหล่านี้ที่ standard library มีให้สำหรับ primitive numeric type:
- Wrap ในทุก mode ด้วยเมธอด
wrapping_*เช่นwrapping_add - Return ค่า
Noneถ้ามี overflow ด้วยเมธอดchecked_* - Return ค่าและ Boolean บ่งบอกว่ามี overflow หรือไม่ ด้วยเมธอด
overflowing_* - Saturate ที่ค่าต่ำสุดหรือสูงสุดของ type ด้วยเมธอด
saturating_*
Floating-Point Type
Rust ยังมี primitive type สำหรับ floating-point number สองแบบ ซึ่งเป็น
ตัวเลขที่มีจุดทศนิยม Floating-point type ของ Rust คือ f32 และ f64 ซึ่ง
มีขนาด 32 bit และ 64 bit ตามลำดับ Default type คือ f64 เพราะบน CPU
สมัยใหม่ มันเร็วพอ ๆ กับ f32 แต่ให้ความแม่นยำมากกว่า Floating-point type
ทั้งหมดเป็น signed
นี่คือตัวอย่างที่แสดง floating-point number ในการใช้งาน:
Filename: src/main.rs
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
Floating-point number ถูกแทนตามมาตรฐาน IEEE-754
Operation ทางตัวเลข
Rust รองรับ operation ทางคณิตศาสตร์พื้นฐานที่คุณคาดหวังสำหรับ type ตัวเลข
ทั้งหมด: บวก, ลบ, คูณ, หาร และ remainder การหาร integer ตัดเศษไปทาง 0 จน
ถึงจำนวนเต็มที่ใกล้ที่สุด โค้ดต่อไปนี้แสดงวิธีใช้ operation ทางตัวเลขแต่ละ
อย่างใน statement let:
Filename: src/main.rs
fn main() {
// addition
let sum = 5 + 10;
// subtraction
let difference = 95.5 - 4.3;
// multiplication
let product = 4 * 30;
// division
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // Results in -1
// remainder
let remainder = 43 % 5;
}
แต่ละ expression ใน statement เหล่านี้ใช้ operator คณิตศาสตร์และประเมิน เป็นค่าเดียว ซึ่งจากนั้น bind กับตัวแปร ภาคผนวก B มีรายการ operator ทั้งหมดที่ Rust มี
Boolean Type
เช่นเดียวกับภาษาโปรแกรมอื่นส่วนใหญ่ Boolean type ใน Rust มีค่าที่เป็นไปได้
สองค่า: true และ false Boolean มีขนาด 1 byte Boolean type ใน Rust
ระบุด้วย bool เช่น:
Filename: src/main.rs
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
วิธีหลักในการใช้ค่า Boolean คือผ่าน conditional เช่น if expression เรา
จะครอบคลุมวิธีที่ if expression ทำงานใน Rust ในส่วน
“Control Flow”
Character Type
Type char ของ Rust เป็น primitive alphabetic type ที่สุดของภาษา นี่คือ
ตัวอย่างการประกาศค่า char:
Filename: src/main.rs
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}
หมายเหตุว่าเราระบุ literal char ด้วย single quotation mark ต่างจาก
string literal ที่ใช้ double quotation mark Type char ของ Rust มีขนาด
4 byte และแทน Unicode scalar value ซึ่งหมายความว่ามันแทนได้มากกว่า ASCII
เยอะ ตัวอักษรที่มีเครื่องหมาย, อักขระจีน-ญี่ปุ่น-เกาหลี, emoji และ zero-
width space เป็นค่า char ที่ valid ใน Rust ทั้งหมด Unicode scalar value
อยู่ในช่วง U+0000 ถึง U+D7FF และ U+E000 ถึง U+10FFFF inclusive
อย่างไรก็ตาม “character” ไม่ได้เป็นแนวคิดใน Unicode จริง ๆ ดังนั้น
สัญชาตญาณของคุณเรื่อง “character” คืออะไร อาจไม่ตรงกับ char ใน Rust เรา
จะพูดถึงหัวข้อนี้ในรายละเอียดใน
“เก็บข้อความ UTF-8 ด้วย String” ในบทที่ 8
Compound Type
Compound type จัดกลุ่มหลายค่าเป็น type เดียวได้ Rust มี primitive compound type สองแบบ: tuple และ array
Tuple Type
tuple คือวิธีทั่วไปในการจัดกลุ่มตัวเลขของค่าหลายค่าที่มี type หลากหลาย ให้เป็น compound type เดียว tuple มีความยาวคงที่ — เมื่อประกาศแล้ว ไม่ สามารถเติบโตหรือหดขนาดได้
เราสร้าง tuple โดยเขียนรายการค่าคั่นด้วย comma ภายในวงเล็บ แต่ละตำแหน่งใน tuple มี type และ type ของค่าต่าง ๆ ใน tuple ไม่จำเป็นต้องเหมือนกัน เรา เพิ่ม type annotation แบบ optional ในตัวอย่างนี้:
Filename: src/main.rs
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
ตัวแปร tup bind กับ tuple ทั้งหมด เพราะ tuple ถือเป็น compound element
เดียว ในการดึงค่าแต่ละค่าออกจาก tuple เราใช้ pattern matching เพื่อ
destructure ค่า tuple ได้ แบบนี้:
Filename: src/main.rs
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
โปรแกรมนี้สร้าง tuple ก่อน แล้ว bind มันกับตัวแปร tup จากนั้นใช้ pattern
กับ let เพื่อเอา tup มาเปลี่ยนเป็นสามตัวแปรแยก: x, y และ z นี่
เรียกว่า destructuring เพราะแยก tuple เดียวออกเป็นสามส่วน สุดท้าย
โปรแกรมพิมพ์ค่าของ y ซึ่งคือ 6.4
เรายังเข้าถึง element ของ tuple ตรง ๆ ได้ โดยใช้ period (.) ตามด้วย
index ของค่าที่อยากเข้าถึง เช่น:
Filename: src/main.rs
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
โปรแกรมนี้สร้าง tuple x แล้วเข้าถึงแต่ละ element ของ tuple ด้วย index
ของพวกมัน เช่นเดียวกับภาษาโปรแกรมส่วนใหญ่ index แรกใน tuple คือ 0
tuple ที่ไม่มีค่าใด ๆ มีชื่อพิเศษว่า unit ค่านี้และ type ของมันเขียนเป็น
() ทั้งคู่ และแทนค่าว่างหรือ return type ว่าง Expression จะ implicitly
return ค่า unit ถ้าไม่ return ค่าอื่นใด
Array Type
อีกวิธีในการมี collection ของหลายค่าคือใช้ array ต่างจาก tuple ทุก element ของ array ต้องมี type เดียวกัน ต่างจาก array ในภาษาอื่นบางภาษา array ใน Rust มีความยาวคงที่
เราเขียนค่าใน array เป็นรายการคั่นด้วย comma ภายใน square bracket:
Filename: src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
}
Array มีประโยชน์เมื่อคุณอยากให้ข้อมูล allocate บน stack เช่นเดียวกับ type อื่น ๆ ที่เราเห็นมา แทนที่จะ allocate บน heap (เราจะพูดถึง stack และ heap มากขึ้นใน บทที่ 4) หรือเมื่อคุณอยากให้ แน่ใจว่ามีจำนวน element คงที่เสมอ Array ไม่ยืดหยุ่นเท่า type vector แต่ vector เป็น collection type ที่คล้ายกัน ที่ standard library มีให้ ซึ่ง อนุญาต ให้เติบโตหรือหดขนาดได้ เพราะเนื้อหาอยู่บน heap ถ้าไม่แน่ใจว่าจะ ใช้ array หรือ vector มีโอกาสสูงที่ควรใช้ vector บทที่ 8 พูดถึง vector ในรายละเอียดเพิ่มเติม
อย่างไรก็ตาม array มีประโยชน์มากกว่า เมื่อคุณรู้ว่าจำนวน element ไม่ต้อง เปลี่ยน เช่น ถ้าคุณกำลังใช้ชื่อเดือนในโปรแกรม คุณคงใช้ array แทน vector เพราะคุณรู้ว่ามันจะมี 12 element เสมอ:
#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
}
คุณเขียน type ของ array ด้วย square bracket พร้อม type ของแต่ละ element, semicolon และจำนวน element ใน array ดังนี้:
#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}
ที่นี่ i32 คือ type ของแต่ละ element หลัง semicolon เลข 5 บ่งบอกว่า
array มี 5 element
คุณยัง initialize array ให้มีค่าเดียวกันสำหรับแต่ละ element ได้ โดยระบุ ค่าเริ่มต้น ตามด้วย semicolon และความยาวของ array ใน square bracket ดัง ที่แสดงที่นี่:
#![allow(unused)]
fn main() {
let a = [3; 5];
}
Array ชื่อ a จะมี 5 element ที่จะถูกตั้งเป็นค่า 3 ทั้งหมดในตอนเริ่มต้น
ซึ่งเหมือนเขียน let a = [3, 3, 3, 3, 3]; แต่กระชับกว่า
เข้าถึง element ของ array
Array คือ chunk เดียวของหน่วยความจำที่มีขนาดรู้และคงที่ ที่ allocate บน stack ได้ คุณเข้าถึง element ของ array ด้วย indexing แบบนี้:
Filename: src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
ในตัวอย่างนี้ ตัวแปรชื่อ first จะได้ค่า 1 เพราะนั่นคือค่าที่ index
[0] ใน array ตัวแปรชื่อ second จะได้ค่า 2 จาก index [1] ใน array
เข้าถึง element ของ array แบบ invalid
มาดูว่าเกิดอะไรขึ้นถ้าคุณพยายามเข้าถึง element ของ array ที่อยู่หลังท้าย array สมมติว่าคุณรันโค้ดนี้ คล้ายเกมทายตัวเลขในบทที่ 2 เพื่อรับ index ของ array จากผู้ใช้:
Filename: src/main.rs
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
โค้ดนี้ compile สำเร็จ ถ้าคุณรันโค้ดนี้ด้วย cargo run แล้วป้อน 0,
1, 2, 3 หรือ 4 โปรแกรมจะพิมพ์ค่าที่ index นั้นใน array ออกมา ถ้า
คุณป้อนตัวเลขหลังท้าย array แทน เช่น 10 คุณจะเห็น output แบบนี้:
thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
โปรแกรมจบลงด้วย runtime error ในจุดที่ใช้ค่า invalid ใน operation
indexing โปรแกรมออกพร้อม error message และไม่ execute statement
println! สุดท้าย เมื่อคุณพยายามเข้าถึง element ด้วย indexing Rust จะ
เช็คว่า index ที่คุณระบุน้อยกว่าความยาว array ถ้า index มากกว่าหรือเท่า
กับความยาว Rust จะ panic การเช็คนี้ต้องเกิดตอน runtime โดยเฉพาะในกรณี
นี้ เพราะ compiler ไม่มีทางรู้ว่าผู้ใช้จะป้อนค่าอะไรเมื่อรันโค้ดทีหลัง
นี่คือตัวอย่างของหลักการ memory safety ของ Rust ในการใช้งาน ในภาษา ระดับต่ำหลายภาษา การเช็คแบบนี้ไม่ได้ทำ และเมื่อคุณให้ index ที่ไม่ถูกต้อง หน่วยความจำ invalid ก็ถูกเข้าถึงได้ Rust ปกป้องคุณจาก error แบบนี้โดย ออกทันที แทนที่จะอนุญาตการเข้าถึงหน่วยความจำและทำงานต่อ บทที่ 9 พูดถึง การจัดการ error ของ Rust เพิ่มเติม และวิธีเขียนโค้ดที่อ่านได้และปลอดภัย ที่ไม่ panic และไม่อนุญาตการเข้าถึงหน่วยความจำ invalid
ฟังก์ชัน
ฟังก์ชัน
ฟังก์ชันมีอยู่ทั่วไปในโค้ด Rust คุณเห็นหนึ่งในฟังก์ชันที่สำคัญที่สุดในภาษา
มาแล้ว — ฟังก์ชัน main ซึ่งเป็น entry point ของโปรแกรมจำนวนมาก คุณยัง
เห็น keyword fn ด้วย ซึ่งให้คุณประกาศฟังก์ชันใหม่
โค้ด Rust ใช้ snake case เป็นสไตล์ convention สำหรับชื่อฟังก์ชันและตัว แปร ซึ่งใช้ตัวพิมพ์เล็กทั้งหมด และคั่นคำด้วย underscore นี่คือโปรแกรมที่มี ตัวอย่างการประกาศฟังก์ชัน:
Filename: src/main.rs
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
เราประกาศฟังก์ชันใน Rust โดยป้อน fn ตามด้วยชื่อฟังก์ชันและชุดวงเล็บ
curly bracket บอก compiler ว่า body ของฟังก์ชันเริ่มและจบที่ไหน
เราเรียกฟังก์ชันใดก็ตามที่เราประกาศไว้ได้ โดยป้อนชื่อตามด้วยชุดวงเล็บ
เพราะ another_function ถูกประกาศในโปรแกรม จึงเรียกจากภายในฟังก์ชัน
main ได้ หมายเหตุว่าเราประกาศ another_function หลัง ฟังก์ชัน
main ใน source code — จะประกาศก่อนก็ได้ Rust ไม่สนใจว่าคุณประกาศ
ฟังก์ชันที่ไหน สนใจแค่ว่ามันถูกประกาศที่ใดที่หนึ่งใน scope ที่ผู้เรียก
เห็นได้
มาเริ่มโปรเจกต์ binary ใหม่ชื่อ functions เพื่อสำรวจฟังก์ชันเพิ่ม วาง
ตัวอย่าง another_function ใน src/main.rs แล้วรัน คุณควรเห็น output
ต่อไปนี้:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
บรรทัด execute ในลำดับที่ปรากฏในฟังก์ชัน main ก่อนอื่นข้อความ “Hello,
world!” ถูกพิมพ์ จากนั้น another_function ถูกเรียกและข้อความของมันถูก
พิมพ์
Parameter
เราประกาศฟังก์ชันให้มี parameter ได้ ซึ่งเป็นตัวแปรพิเศษที่เป็นส่วนหนึ่ง ของ signature ของฟังก์ชัน เมื่อฟังก์ชันมี parameter คุณส่งค่าจริงสำหรับ parameter เหล่านั้นได้ ทางเทคนิค ค่าจริงเรียกว่า argument แต่ในการ สนทนาทั่วไป คนมักใช้คำว่า parameter และ argument แทนกันได้ ทั้งสำหรับ ตัวแปรในการประกาศฟังก์ชัน หรือค่าจริงที่ส่งเข้าตอนเรียกฟังก์ชัน
ใน version นี้ของ another_function เราเพิ่ม parameter:
Filename: src/main.rs
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
ลองรันโปรแกรมนี้ คุณควรได้ output ต่อไปนี้:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
การประกาศ another_function มี parameter หนึ่งตัวชื่อ x Type ของ x
ระบุเป็น i32 เมื่อเราส่ง 5 เข้า another_function macro println!
ใส่ 5 ตรงที่ curly bracket คู่ที่มี x อยู่ใน format string
ใน signature ของฟังก์ชัน คุณ ต้อง ประกาศ type ของแต่ละ parameter นี่ เป็นการตัดสินใจที่ตั้งใจในการออกแบบของ Rust — การกำหนดให้มี type annotation ในการประกาศฟังก์ชัน หมายความว่า compiler แทบไม่ต้องการให้คุณใช้มันที่อื่น ในโค้ดเพื่อหา type ที่คุณหมายถึง Compiler ยังให้ error message ที่มี ประโยชน์มากขึ้นได้ ถ้ามันรู้ว่าฟังก์ชันคาดหวัง type อะไร
เมื่อประกาศ parameter หลายตัว ให้คั่นการประกาศ parameter ด้วย comma ดังนี้:
Filename: src/main.rs
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
ตัวอย่างนี้สร้างฟังก์ชันชื่อ print_labeled_measurement ที่มีสอง
parameter parameter แรกชื่อ value และเป็น i32 ตัวที่สองชื่อ
unit_label และเป็น type char ฟังก์ชันแล้วพิมพ์ข้อความที่มีทั้ง
value และ unit_label
ลองรันโค้ดนี้ แทนที่โปรแกรมที่อยู่ในไฟล์ src/main.rs ของโปรเจกต์
functions ของคุณ ด้วยตัวอย่างก่อนหน้า แล้วรันด้วย cargo run:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
เพราะเราเรียกฟังก์ชันด้วย 5 เป็นค่าสำหรับ value และ 'h' เป็นค่า
สำหรับ unit_label output ของโปรแกรมมีค่าเหล่านั้น
Statement และ Expression
Body ของฟังก์ชันประกอบด้วยชุดของ statement ที่อาจจบด้วย expression ที่ ผ่านมา ฟังก์ชันที่เราครอบคลุมยังไม่ได้รวม expression จบ แต่คุณเห็น expression เป็นส่วนหนึ่งของ statement แล้ว เพราะ Rust เป็นภาษา expression- based นี่เป็นความแตกต่างสำคัญที่ต้องเข้าใจ ภาษาอื่นไม่มีความแตกต่างเดียวกัน มาดูว่า statement และ expression คืออะไร และความแตกต่างกระทบ body ของ ฟังก์ชันยังไง
- Statement คือคำสั่งที่ทำการกระทำบางอย่าง และไม่ return ค่า
- Expression ประเมินเป็นค่าผลลัพธ์
มาดูตัวอย่าง
จริง ๆ แล้วเราใช้ statement และ expression มาแล้ว การสร้างตัวแปรและ
assign ค่าให้มันด้วย keyword let เป็น statement ใน Listing 3-1
let y = 6; เป็น statement
fn main() {
let y = 6;
}
main ที่มี statement หนึ่งตัวการประกาศฟังก์ชันก็เป็น statement เช่นกัน — ตัวอย่างก่อนหน้าทั้งหมดเป็น statement ในตัวมันเอง (อย่างที่เราจะเห็นในไม่ช้า การเรียกฟังก์ชันไม่ใช่ statement)
Statement ไม่ return ค่า ดังนั้นคุณ assign statement let ให้ตัวแปร
อื่นไม่ได้ อย่างที่โค้ดต่อไปนี้พยายามทำ คุณจะได้ error:
Filename: src/main.rs
fn main() {
let x = (let y = 6);
}
เมื่อคุณรันโปรแกรมนี้ error ที่คุณจะได้หน้าตาประมาณนี้:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
statement let y = 6 ไม่ return ค่า จึงไม่มีอะไรให้ x bind ด้วย นี่ต่าง
จากที่เกิดในภาษาอื่น เช่น C และ Ruby ที่การ assign return ค่าของการ assign
ในภาษาเหล่านั้น คุณเขียน x = y = 6 แล้วทั้ง x และ y มีค่า 6 ได้ —
ไม่ใช่กรณีนั้นใน Rust
Expression ประเมินเป็นค่า และประกอบเป็นโค้ดส่วนใหญ่ที่คุณจะเขียนใน Rust
พิจารณา operation คณิตศาสตร์ เช่น 5 + 6 ซึ่งเป็น expression ที่ประเมิน
เป็นค่า 11 Expression เป็นส่วนหนึ่งของ statement ได้ — ใน Listing 3-1
6 ใน statement let y = 6; เป็น expression ที่ประเมินเป็นค่า 6 การ
เรียกฟังก์ชันเป็น expression การเรียก macro เป็น expression block scope
ใหม่ที่สร้างด้วย curly bracket เป็น expression เช่น:
Filename: src/main.rs
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}
Expression นี้:
{
let x = 3;
x + 1
}
เป็น block ที่ในกรณีนี้ประเมินเป็น 4 ค่านั้นถูก bind กับ y เป็นส่วน
หนึ่งของ statement let หมายเหตุว่าบรรทัด x + 1 ไม่มี semicolon ที่
ท้าย ซึ่งต่างจากบรรทัดส่วนใหญ่ที่คุณเห็นมา Expression ไม่รวม semicolon
ปิดท้าย ถ้าคุณเพิ่ม semicolon ที่ท้าย expression คุณเปลี่ยนมันเป็น
statement แล้วมันจะไม่ return ค่า จำเรื่องนี้ไว้ขณะที่คุณสำรวจ return
value ของฟังก์ชันและ expression ต่อไป
ฟังก์ชันที่มี Return Value
ฟังก์ชัน return ค่าให้โค้ดที่เรียกได้ เราไม่ตั้งชื่อ return value แต่ต้อง
ประกาศ type หลังลูกศร (->) ใน Rust return value ของฟังก์ชันเทียบเท่ากับ
ค่าของ expression สุดท้ายใน block ของ body ของฟังก์ชัน คุณ return ก่อนได้
โดยใช้ keyword return และระบุค่า แต่ฟังก์ชันส่วนใหญ่ return expression
สุดท้ายแบบ implicit นี่คือตัวอย่างฟังก์ชันที่ return ค่า:
Filename: src/main.rs
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
ไม่มีการเรียกฟังก์ชัน, macro หรือแม้แต่ statement let ในฟังก์ชัน five
— แค่ตัวเลข 5 ตัวเดียว นั่นเป็นฟังก์ชันที่ valid อย่างสมบูรณ์ใน Rust
หมายเหตุว่า return type ของฟังก์ชันก็ระบุไว้ด้วย เป็น -> i32 ลองรันโค้ด
นี้ output ควรหน้าตาเป็นแบบนี้:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5
5 ใน five เป็น return value ของฟังก์ชัน นี่คือเหตุผลที่ return type
เป็น i32 มาตรวจสอบเรื่องนี้ในรายละเอียดเพิ่มเติม มีสองจุดสำคัญ — ประการ
แรก บรรทัด let x = five(); แสดงว่าเราใช้ return value ของฟังก์ชัน
initialize ตัวแปร เพราะฟังก์ชัน five return 5 บรรทัดนั้นเหมือนกับ
ต่อไปนี้:
#![allow(unused)]
fn main() {
let x = 5;
}
ประการที่สอง ฟังก์ชัน five ไม่มี parameter และประกาศ type ของ return
value แต่ body ของฟังก์ชันเป็น 5 เดียวดายโดยไม่มี semicolon เพราะมัน
เป็น expression ที่เราอยาก return ค่ามัน
มาดูตัวอย่างอีกตัวอย่างหนึ่ง:
Filename: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1
}
การรันโค้ดนี้จะพิมพ์ The value of x is: 6 แต่จะเกิดอะไรขึ้นถ้าเราวาง
semicolon ที่ท้ายบรรทัดที่มี x + 1 เปลี่ยนมันจาก expression เป็น
statement?
Filename: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
การ compile โค้ดนี้จะ produce error ดังนี้:
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` (bin "functions") due to 1 previous error
Error message หลัก mismatched types เผยปัญหาแกนกลางของโค้ดนี้ การประกาศ
ฟังก์ชัน plus_one บอกว่ามันจะ return i32 แต่ statement ไม่ประเมินเป็น
ค่า ซึ่งแสดงด้วย () ที่เป็น unit type ดังนั้นไม่มีอะไรถูก return ซึ่ง
ขัดกับการประกาศฟังก์ชัน และส่งผลให้เกิด error ใน output นี้ Rust ให้
ข้อความที่อาจช่วยแก้ปัญหานี้ — มันแนะนำให้ลบ semicolon ซึ่งจะแก้ error
คอมเมนต์
Comment
โปรแกรมเมอร์ทุกคนพยายามทำให้โค้ดของตนเข้าใจง่าย แต่บางครั้งคำอธิบายเพิ่มเติม ก็จำเป็น ในกรณีเหล่านี้ โปรแกรมเมอร์ทิ้ง comment ไว้ใน source code ของ ตน ซึ่ง compiler จะละเว้น แต่คนที่อ่าน source code อาจพบว่ามีประโยชน์
นี่คือ comment แบบง่าย:
#![allow(unused)]
fn main() {
// hello, world
}
ใน Rust สไตล์ comment ที่ idiomatic เริ่มด้วย slash สองตัว และ comment
ต่อเนื่องจนจบบรรทัด สำหรับ comment ที่ขยายเกินบรรทัดเดียว คุณต้องใส่ //
ในแต่ละบรรทัด แบบนี้:
#![allow(unused)]
fn main() {
// So we're doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what's going on.
}
Comment ยังวางที่ท้ายบรรทัดที่มีโค้ดได้:
Filename: src/main.rs
fn main() {
let lucky_number = 7; // I'm feeling lucky today
}
แต่คุณจะเห็นพวกมันใช้ในรูปแบบนี้บ่อยกว่า โดยมี comment อยู่บรรทัดแยกเหนือ โค้ดที่มัน annotate:
Filename: src/main.rs
fn main() {
// I'm feeling lucky today
let lucky_number = 7;
}
Rust ยังมี comment อีกแบบ คือ documentation comment ซึ่งเราจะพูดถึงใน ส่วน “Publish Crate ไปยัง Crates.io” ของ บทที่ 14
Control Flow
Control Flow
ความสามารถในการรันโค้ดบางอย่างขึ้นกับว่า condition เป็น true หรือไม่
และความสามารถในการรันโค้ดบางอย่างซ้ำ ๆ ขณะที่ condition เป็น true เป็น
building block พื้นฐานในภาษาโปรแกรมส่วนใหญ่ โครงสร้างที่ใช้บ่อยที่สุดที่
ให้คุณควบคุม flow ของการ execute โค้ด Rust คือ if expression และ loop
if Expression
if expression ให้คุณ branch โค้ดขึ้นกับ condition คุณให้ condition แล้ว
บอกว่า “ถ้า condition นี้ตรง รัน block โค้ดนี้ ถ้า condition ไม่ตรง อย่า
รัน block โค้ดนี้”
สร้างโปรเจกต์ใหม่ชื่อ branches ใน directory projects ของคุณ เพื่อ
สำรวจ if expression ในไฟล์ src/main.rs ป้อนต่อไปนี้:
Filename: src/main.rs
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
if expression ทั้งหมดเริ่มด้วย keyword if ตามด้วย condition ในกรณีนี้
condition เช็คว่าตัวแปร number มีค่าน้อยกว่า 5 หรือไม่ เราวาง block ของ
โค้ดที่จะ execute ถ้า condition เป็น true ทันทีหลัง condition ภายใน
curly bracket Block ของโค้ดที่ผูกกับ condition ใน if expression บางครั้ง
เรียกว่า arm เหมือน arm ใน match expression ที่เราพูดถึงในส่วน
“เปรียบเทียบคำตอบกับตัวเลขลับ”
ของบทที่ 2
เราสามารถใส่ else expression แบบ optional ได้ด้วย ซึ่งเราเลือกทำที่นี่
เพื่อให้โปรแกรมมี block โค้ดทางเลือกให้ execute หาก condition ประเมินเป็น
false ถ้าคุณไม่ให้ else expression และ condition เป็น false
โปรแกรมก็จะข้าม block if ไปและไปที่โค้ดถัดไป
ลองรันโค้ดนี้ คุณควรเห็น output ต่อไปนี้:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was true
ลองเปลี่ยนค่าของ number ให้เป็นค่าที่ทำให้ condition เป็น false เพื่อ
ดูว่าเกิดอะไรขึ้น:
fn main() {
let number = 7;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
รันโปรแกรมอีกครั้งและดู output:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
condition was false
ที่ควรหมายเหตุไว้คือ condition ในโค้ดนี้ ต้อง เป็น bool ถ้า condition
ไม่ใช่ bool เราจะได้ error เช่น ลองรันโค้ดต่อไปนี้:
Filename: src/main.rs
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
condition if ประเมินเป็นค่า 3 ครั้งนี้ และ Rust โยน error:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
Error บ่งบอกว่า Rust คาด bool แต่ได้ integer ต่างจากภาษาอย่าง Ruby และ
JavaScript Rust จะไม่พยายามแปลง type ที่ไม่ใช่ Boolean เป็น Boolean
อัตโนมัติ คุณต้อง explicit และให้ if พร้อม Boolean เป็น condition เสมอ
ถ้าเราอยากให้ block โค้ด if รันเฉพาะเมื่อตัวเลขไม่เท่ากับ 0 เช่น เรา
เปลี่ยน if expression เป็นต่อไปนี้ได้:
Filename: src/main.rs
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero");
}
}
การรันโค้ดนี้จะพิมพ์ number was something other than zero
จัดการ Condition หลายตัวด้วย else if
คุณใช้ condition หลายตัวได้โดยรวม if และ else ใน else if expression
เช่น:
Filename: src/main.rs
fn main() {
let number = 6;
if number % 4 == 0 {
println!("number is divisible by 4");
} else if number % 3 == 0 {
println!("number is divisible by 3");
} else if number % 2 == 0 {
println!("number is divisible by 2");
} else {
println!("number is not divisible by 4, 3, or 2");
}
}
โปรแกรมนี้มี path เป็นไปได้ 4 ทาง หลังรันมันแล้ว คุณควรเห็น output ต่อไปนี้:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
number is divisible by 3
เมื่อโปรแกรมนี้ execute มันเช็ค if expression แต่ละตัวตามลำดับ และ
execute body แรกที่ condition ประเมินเป็น true หมายเหตุว่าแม้ 6 หาร 2
ลงตัว เราไม่เห็น output number is divisible by 2 และเราก็ไม่เห็นข้อความ
number is not divisible by 4, 3, or 2 จาก block else นั่นเป็นเพราะ
Rust execute แค่ block ของ condition true ตัวแรก และเมื่อมันเจอตัวหนึ่ง
มันก็ไม่เช็คตัวที่เหลือ
การใช้ else if expression มากเกินไปทำให้โค้ดเลอะเทอะได้ ดังนั้นถ้าคุณมี
มากกว่าหนึ่งตัว คุณอาจอยาก refactor โค้ด บทที่ 6 อธิบายโครงสร้าง branching
ที่ทรงพลังของ Rust ชื่อ match สำหรับกรณีเหล่านี้
ใช้ if ใน Statement let
เพราะ if เป็น expression เราใช้มันทางขวาของ statement let เพื่อ assign
ผลให้ตัวแปรได้ ดังใน Listing 3-2
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
if expression ให้ตัวแปรตัวแปร number จะถูก bind กับค่าตามผลของ if expression รันโค้ดนี้เพื่อ
ดูว่าเกิดอะไรขึ้น:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
The value of number is: 5
จำไว้ว่า block ของโค้ดประเมินเป็น expression สุดท้ายในนั้น และตัวเลขใน
ตัวเองก็เป็น expression ด้วย ในกรณีนี้ ค่าของ if expression ทั้งหมดขึ้น
กับว่า block ของโค้ดไหน execute สิ่งนี้หมายความว่าค่าที่มีศักยภาพเป็นผล
จาก arm แต่ละตัวของ if ต้องมี type เดียวกัน ใน Listing 3-2 ผลของทั้ง
arm if และ arm else คือ integer i32 ถ้า type mismatch ดังตัวอย่าง
ต่อไปนี้ เราจะได้ error:
Filename: src/main.rs
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
เมื่อเราพยายาม compile โค้ดนี้ เราจะได้ error arm if และ else มี type
ของค่าที่เข้ากันไม่ได้ และ Rust ระบุตำแหน่งที่จะหาปัญหาในโปรแกรมเป๊ะ ๆ:
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "six" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` (bin "branches") due to 1 previous error
Expression ใน block if ประเมินเป็น integer และ expression ใน block
else ประเมินเป็น string มันจะไม่ทำงาน เพราะตัวแปรต้องมี type เดียว และ
Rust ต้องรู้แน่นอนตอน compile time ว่า type ของตัวแปร number คืออะไร
การรู้ type ของ number ให้ compiler ตรวจสอบว่า type valid ในทุกที่ที่
เราใช้ number Rust จะไม่สามารถทำแบบนั้นได้ ถ้า type ของ number ถูก
กำหนดแค่ตอน runtime compiler ก็จะซับซ้อนขึ้นและให้การรับประกันเกี่ยวกับ
โค้ดน้อยลง ถ้ามันต้องตามติด type สมมติฐานหลายตัวสำหรับตัวแปรใด ๆ
การทำซ้ำด้วย Loop
มันบ่อยครั้งที่มีประโยชน์ในการ execute block โค้ดมากกว่าหนึ่งครั้ง สำหรับ งานนี้ Rust มี loop หลายแบบ ซึ่งจะรันผ่านโค้ดภายใน body ของ loop จน สุด แล้วเริ่มกลับที่ต้นทันที เพื่อทดลอง loop มาสร้างโปรเจกต์ใหม่ชื่อ loops
Rust มี loop สามแบบ: loop, while และ for ลองดูแต่ละแบบ
ทำซ้ำโค้ดด้วย loop
keyword loop บอก Rust ให้ execute block ของโค้ดซ้ำ ๆ ตลอดไป หรือจน
กว่าคุณบอกให้หยุดแบบ explicit
ตัวอย่าง เปลี่ยนไฟล์ src/main.rs ใน directory loops ของคุณ ให้หน้า ตาเป็นแบบนี้:
Filename: src/main.rs
fn main() {
loop {
println!("again!");
}
}
เมื่อเรารันโปรแกรมนี้ เราจะเห็น again! พิมพ์ซ้ำ ๆ ต่อเนื่อง จนกว่าเรา
หยุดโปรแกรมเอง terminal ส่วนใหญ่รองรับ keyboard shortcut
ctrl-C เพื่อ interrupt โปรแกรมที่ติดอยู่ใน loop
ต่อเนื่อง ลองดู:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
สัญลักษณ์ ^C แทนตำแหน่งที่คุณกด ctrl-C
คุณอาจเห็นหรือไม่เห็นคำ again! พิมพ์หลัง ^C ขึ้นกับว่าโค้ดอยู่ตรงไหน
ใน loop ตอนที่รับ interrupt signal
โชคดีที่ Rust ยังมีวิธี break ออกจาก loop ด้วยโค้ด คุณวาง keyword break
ภายใน loop เพื่อบอกโปรแกรมว่าจะหยุด execute loop เมื่อไหร่ จำได้ว่าเราทำ
แบบนี้ในเกมทายตัวเลขในส่วน
“ออกเมื่อทายถูก” ของ
บทที่ 2 เพื่อออกจากโปรแกรมเมื่อผู้ใช้ชนะเกมโดยทายตัวเลขถูก
เรายังใช้ continue ในเกมทายตัวเลขด้วย ซึ่งใน loop บอกโปรแกรมให้ข้ามโค้ด
ที่เหลือใน iteration นี้ของ loop และไปที่ iteration ถัดไป
Return ค่าจาก Loop
หนึ่งในการใช้ loop คือลอง operation ที่คุณรู้ว่าอาจล้มเหลว เช่น เช็ค
ว่า thread ทำงานเสร็จแล้วหรือไม่ คุณอาจยังต้องส่งผลของ operation นั้นออก
จาก loop ไปยังโค้ดที่เหลือ ในการทำสิ่งนี้ คุณเพิ่มค่าที่คุณอยากให้ return
หลัง expression break ที่ใช้หยุด loop ค่านั้นจะถูก return ออกจาก loop
เพื่อให้คุณใช้ได้ ดังที่แสดงที่นี่:
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {result}");
}
ก่อน loop เราประกาศตัวแปรชื่อ counter แล้ว initialize เป็น 0 จากนั้น
เราประกาศตัวแปรชื่อ result เพื่อเก็บค่าที่ return จาก loop ในทุก
iteration ของ loop เราเพิ่ม 1 ให้ตัวแปร counter แล้วเช็คว่า counter
เท่ากับ 10 หรือไม่ เมื่อเท่า เราใช้ keyword break พร้อมค่า
counter * 2 หลัง loop เราใช้ semicolon จบ statement ที่ assign ค่าให้
result สุดท้าย เราพิมพ์ค่าใน result ซึ่งในกรณีนี้คือ 20
คุณยัง return จากภายใน loop ได้ ขณะที่ break ออกแค่จาก loop ปัจจุบัน
return ออกจากฟังก์ชันปัจจุบันเสมอ
แยกความกำกวมด้วย Loop Label
ถ้าคุณมี loop ภายใน loop break และ continue ใช้กับ loop ในสุดที่จุด
นั้น คุณระบุ loop label บน loop แบบ optional ได้ ซึ่งจากนั้นใช้กับ
break หรือ continue เพื่อระบุว่า keyword เหล่านั้นใช้กับ loop ที่
label แทน loop ในสุด Loop label ต้องเริ่มด้วย single quote นี่คือตัวอย่าง
ที่มี loop ซ้อนสองตัว:
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
Loop ด้านนอกมี label 'counting_up และจะนับขึ้นจาก 0 ถึง 2 Loop ด้านใน
ที่ไม่มี label นับลงจาก 10 ถึง 9 break ตัวแรกที่ไม่ระบุ label จะออกจาก
loop ด้านในเท่านั้น statement break 'counting_up; จะออกจาก loop ด้าน
นอก โค้ดนี้พิมพ์:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
Conditional Loop ที่กระชับด้วย while
บ่อยครั้งโปรแกรมต้องประเมิน condition ภายใน loop ขณะที่ condition เป็น
true loop รัน เมื่อ condition หยุดเป็น true โปรแกรมเรียก break
หยุด loop เป็นไปได้ที่จะ implement พฤติกรรมแบบนี้ด้วยการรวม loop, if,
else และ break คุณลองทำในโปรแกรมตอนนี้ได้ถ้าต้องการ อย่างไรก็ตาม
pattern นี้ใช้บ่อยมาก Rust จึงมีโครงสร้างภาษา built-in สำหรับมัน เรียกว่า
while loop ใน Listing 3-3 เราใช้ while วน loop โปรแกรมสามครั้ง นับ
ถอยหลังทุกครั้ง แล้วหลัง loop พิมพ์ข้อความและออก
fn main() {
let mut number = 3;
while number != 0 {
println!("{number}!");
number -= 1;
}
println!("LIFTOFF!!!");
}
while loop รันโค้ดขณะที่ condition ประเมินเป็น trueโครงสร้างนี้ขจัด nesting หลายชั้นที่จำเป็นถ้าคุณใช้ loop, if, else
และ break และมันชัดเจนกว่า ขณะที่ condition ประเมินเป็น true โค้ดรัน
ไม่อย่างนั้นมันออกจาก loop
วน Loop ผ่าน Collection ด้วย for
คุณเลือกใช้โครงสร้าง while วน loop ผ่าน element ของ collection เช่น
array ได้ เช่น loop ใน Listing 3-4 พิมพ์แต่ละ element ใน array a
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("the value is: {}", a[index]);
index += 1;
}
}
while loopที่นี่ โค้ดนับขึ้นผ่าน element ใน array มันเริ่มที่ index 0 แล้ววน
loop จนถึง index สุดท้ายใน array (นั่นคือเมื่อ index < 5 ไม่ใช่
true อีกต่อไป) การรันโค้ดนี้จะพิมพ์ทุก element ใน array:
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.32s
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
ค่า array ทั้งห้าค่าปรากฏใน terminal ตามที่คาด แม้ index จะถึงค่า 5
ในจุดหนึ่ง loop หยุด execute ก่อนพยายามดึงค่าที่หกจาก array
อย่างไรก็ตาม วิธีนี้เสี่ยง error — เราอาจทำให้โปรแกรม panic ถ้าค่า
index หรือ test condition ไม่ถูกต้อง เช่น ถ้าคุณเปลี่ยนการประกาศ array
a ให้มีสี่ element แต่ลืม update condition เป็น while index < 4
โค้ดจะ panic มันยังช้าด้วย เพราะ compiler เพิ่ม runtime code เพื่อทำการ
เช็ค conditional ว่า index อยู่ภายในขอบเขตของ array ในทุก iteration ของ
loop
ในฐานะทางเลือกที่กระชับกว่า คุณใช้ for loop และ execute โค้ดบางส่วน
สำหรับแต่ละ item ใน collection ได้ for loop หน้าตาเหมือนโค้ดใน
Listing 3-5
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {element}");
}
}
for loopเมื่อเรารันโค้ดนี้ เราจะเห็น output เดียวกับใน Listing 3-4 ที่สำคัญกว่า
คือ ตอนนี้เราเพิ่มความปลอดภัยของโค้ด และขจัดโอกาสของ bug ที่อาจเกิดจาก
การไปเกินท้าย array หรือไปไม่ไกลพอและพลาด item บางตัว Machine code ที่
generate จาก for loop ก็มีประสิทธิภาพมากกว่าได้ด้วย เพราะ index ไม่
ต้องเปรียบเทียบกับความยาว array ทุก iteration
การใช้ for loop คุณไม่ต้องจำเปลี่ยนโค้ดอื่นใด ถ้าคุณเปลี่ยนจำนวนค่าใน
array เหมือนที่คุณจะต้องทำกับวิธีที่ใช้ใน Listing 3-4
ความปลอดภัยและความกระชับของ for loop ทำให้มันเป็นโครงสร้าง loop ที่ใช้
บ่อยที่สุดใน Rust แม้ในสถานการณ์ที่คุณอยากรันโค้ดบางอย่างจำนวนครั้งหนึ่ง
อย่างในตัวอย่างนับถอยหลังที่ใช้ while loop ใน Listing 3-3 Rustacean
ส่วนใหญ่ก็จะใช้ for loop วิธีทำคือใช้ Range ที่ standard library
มีให้ ซึ่ง generate ตัวเลขทั้งหมดในลำดับ เริ่มจากตัวเลขหนึ่งและจบก่อน
อีกตัวเลขหนึ่ง
นี่คือสิ่งที่การนับถอยหลังจะหน้าตาเป็นแบบนี้โดยใช้ for loop และอีก
เมธอดที่เรายังไม่ได้พูดถึง rev เพื่อกลับด้าน range:
Filename: src/main.rs
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
โค้ดนี้ดีขึ้นนิดหน่อย ใช่ไหม?
สรุป
คุณทำได้! นี่เป็นบทขนาดใหญ่ — คุณได้เรียนเรื่องตัวแปร, ชนิดข้อมูล scalar
และ compound, ฟังก์ชัน, comment, if expression และ loop! ในการฝึกกับ
แนวคิดที่พูดถึงในบทนี้ ลอง build โปรแกรมเพื่อทำสิ่งต่อไปนี้:
- แปลงอุณหภูมิระหว่าง Fahrenheit และ Celsius
- Generate ตัวเลข Fibonacci ตัวที่ n
- พิมพ์เนื้อเพลง Christmas carol “The Twelve Days of Christmas” โดยใช้ ประโยชน์จากการทำซ้ำในเพลง
เมื่อคุณพร้อมไปต่อ เราจะพูดถึงแนวคิดใน Rust ที่ ไม่ ค่อยมีในภาษา โปรแกรมอื่น: ownership
ทำความเข้าใจ Ownership
โอนเนอร์ชิป (ownership) เป็นฟีเจอร์ที่เป็นเอกลักษณ์ที่สุดของ Rust และมีนัย ลึกซึ้งต่อส่วนที่เหลือของภาษา มันเปิดทางให้ Rust รับประกัน memory safety ได้โดยไม่ต้องใช้ garbage collector ดังนั้นการเข้าใจว่า ownership ทำงานยัง ไงจึงสำคัญ ในบทนี้เราจะพูดถึง ownership รวมถึงฟีเจอร์ที่เกี่ยวข้องอีกหลาย อย่าง: borrowing, slice และวิธีที่ Rust จัดวางข้อมูลในหน่วยความจำ
Ownership คืออะไร?
Ownership คืออะไร?
Ownership คือชุดของกฎที่กำกับวิธีที่โปรแกรม Rust จัดการหน่วยความจำ ทุกโปรแกรมต้องจัดการวิธีที่ใช้หน่วยความจำของคอมพิวเตอร์ระหว่างรัน บางภาษามี garbage collection ที่คอยดูหน่วยความจำที่ไม่ใช้แล้วเป็นระยะ ระหว่างที่ โปรแกรมรัน ในภาษาอื่น โปรแกรมเมอร์ต้อง allocate และ free หน่วยความจำเอง แบบ explicit Rust ใช้แนวทางที่สาม — หน่วยความจำถูกจัดการผ่านระบบ ownership ที่มีชุดกฎที่ compiler ตรวจสอบ ถ้ามีกฎใดถูกฝ่าฝืน โปรแกรมจะ compile ไม่ ผ่าน ฟีเจอร์ของ ownership ทั้งหมดจะไม่ทำให้โปรแกรมช้าลงระหว่างรัน
เพราะ ownership เป็นแนวคิดใหม่สำหรับโปรแกรมเมอร์หลายคน มันใช้เวลาทำความ คุ้นเคยบ้าง ข่าวดีคือยิ่งคุณมีประสบการณ์กับ Rust และกฎของระบบ ownership มาก ขึ้น คุณจะพบว่ามันง่ายขึ้นที่จะพัฒนาโค้ดที่ปลอดภัยและมีประสิทธิภาพอย่างเป็น ธรรมชาติ สู้ ๆ ครับ!
เมื่อคุณเข้าใจ ownership คุณจะมีรากฐานที่แข็งแรงในการเข้าใจฟีเจอร์ที่ทำให้ Rust มีเอกลักษณ์ ในบทนี้ คุณจะเรียน ownership ผ่านการทำตัวอย่างที่เน้น โครงสร้างข้อมูลที่ใช้บ่อยมาก — string
Stack และ Heap
ภาษาโปรแกรมหลายภาษาไม่ได้บังคับให้คุณคิดเรื่อง stack และ heap บ่อยนัก แต่ ในภาษา systems programming อย่าง Rust การที่ค่าอยู่บน stack หรือ heap ส่งผลต่อพฤติกรรมของภาษา และว่าทำไมคุณต้องตัดสินใจบางอย่าง บางส่วนของ ownership จะถูกอธิบายในความสัมพันธ์กับ stack และ heap ในบทนี้ ดังนั้นนี่ คือคำอธิบายสั้น ๆ เพื่อเตรียมตัว
ทั้ง stack และ heap เป็นส่วนของหน่วยความจำที่โค้ดของคุณใช้ได้ตอน runtime แต่พวกมันมีโครงสร้างต่างกัน Stack เก็บค่าตามลำดับที่ได้รับมา และเอาค่าออก ในลำดับตรงข้าม นี่เรียกว่า last in, first out (LIFO) คิดถึงกองจาน — เมื่อคุณเพิ่มจาน คุณวางไว้บนสุดของกอง และเมื่อต้องการจาน คุณเอาออกจากบนสุด การเพิ่มหรือลบจานจากกลางหรือล่างไม่ work เท่าไหร่! การเพิ่มข้อมูลเรียกว่า push บน stack และการลบข้อมูลเรียกว่า pop ออกจาก stack ข้อมูลทั้งหมดที่ เก็บบน stack ต้องมีขนาดที่รู้และคงที่ ข้อมูลที่ขนาดไม่รู้ตอน compile time หรือขนาดอาจเปลี่ยน ต้องเก็บบน heap แทน
Heap มีระเบียบน้อยกว่า — เมื่อคุณใส่ข้อมูลบน heap คุณขอพื้นที่จำนวนหนึ่ง memory allocator หาที่ว่างใน heap ที่ใหญ่พอ, mark ว่ากำลังใช้, และ return pointer ซึ่งคือ address ของตำแหน่งนั้น กระบวนการนี้เรียกว่า allocate บน heap และบางครั้งย่อเป็นแค่ allocate (การ push ค่าบน stack ไม่ถือว่า เป็น allocate) เพราะ pointer ไปยัง heap มีขนาดรู้และคงที่ คุณจึงเก็บ pointer บน stack ได้ แต่เมื่อคุณต้องการข้อมูลจริง คุณต้องตาม pointer ไป คิดถึงการได้ที่นั่งในร้านอาหาร — เมื่อคุณเข้าไป คุณบอกจำนวนคนในกลุ่ม และ host หาโต๊ะว่างที่จุคนทั้งหมดได้ แล้วพาคุณไป ถ้ามีคนในกลุ่มมาสาย เขาถามได้ ว่าคุณนั่งอยู่ที่ไหน เพื่อหาคุณ
Push บน stack เร็วกว่า allocate บน heap เพราะ allocator ไม่ต้องค้นหาที่ เก็บข้อมูลใหม่ — ตำแหน่งนั้นอยู่บนสุดของ stack เสมอ เมื่อเทียบกัน การ allocate พื้นที่บน heap ต้องการงานมากกว่า เพราะ allocator ต้องหาพื้นที่ที่ ใหญ่พอจะเก็บข้อมูลก่อน แล้วทำ bookkeeping เพื่อเตรียมพร้อมสำหรับการ allocate ครั้งถัดไป
การเข้าถึงข้อมูลใน heap โดยทั่วไปช้ากว่าการเข้าถึงข้อมูลบน stack เพราะ คุณต้องตาม pointer ไป processor สมัยใหม่เร็วกว่าถ้ามันกระโดดในหน่วยความจำ น้อยลง ต่อจากการอุปมา ลองนึกถึง server ในร้านอาหารที่รับ order จากหลาย โต๊ะ มีประสิทธิภาพที่สุดที่จะรับ order ทุกอย่างที่โต๊ะหนึ่งก่อนย้ายไปอีก โต๊ะ การรับ order จากโต๊ะ A แล้ว order จากโต๊ะ B แล้วจาก A อีก แล้วจาก B อีก จะเป็นกระบวนการที่ช้ากว่ามาก ในทำนองเดียวกัน processor ทำงานได้ดีกว่า ถ้าทำงานบนข้อมูลที่อยู่ใกล้ข้อมูลอื่น (อย่างที่อยู่บน stack) มากกว่าไกล ออกไป (อย่างที่อาจอยู่บน heap)
เมื่อโค้ดของคุณเรียกฟังก์ชัน ค่าที่ส่งเข้าฟังก์ชัน (รวมถึง pointer ไป ข้อมูลบน heap ที่อาจมี) และตัวแปร local ของฟังก์ชัน ถูก push บน stack เมื่อฟังก์ชันจบ ค่าเหล่านั้นถูก pop ออกจาก stack
การติดตามว่าส่วนใดของโค้ดใช้ข้อมูลใดบน heap, การลดข้อมูลซ้ำใน heap, และ การ cleanup ข้อมูลที่ไม่ใช้บน heap เพื่อไม่ให้หมดพื้นที่ ทั้งหมดเป็นปัญหาที่ ownership จัดการ เมื่อคุณเข้าใจ ownership คุณจะไม่ต้องคิดเรื่อง stack และ heap บ่อยนัก แต่การรู้ว่าจุดประสงค์หลักของ ownership คือการจัดการข้อมูลบน heap ช่วยอธิบายว่าทำไมมันทำงานในแบบที่ทำ
กฎของ Ownership
ขั้นแรก มาดูกฎของ ownership กัน จำกฎเหล่านี้ไว้ตอนเราทำตัวอย่างที่แสดง ให้เห็น:
- ทุกค่าใน Rust มี owner
- ในเวลาเดียวกันมี owner ได้แค่หนึ่งคน
- เมื่อ owner ออกจาก scope ค่าจะถูก drop
Scope ของตัวแปร
ตอนนี้เราผ่าน syntax พื้นฐานของ Rust แล้ว เราจะไม่รวม fn main() { ทั้งหมด
ในตัวอย่าง ดังนั้นถ้าคุณตามไปด้วย อย่าลืมเอาตัวอย่างต่อไปนี้ใส่ในฟังก์ชัน
main เอง ผลลัพธ์คือ ตัวอย่างของเราจะกระชับขึ้นนิดหน่อย ให้เราโฟกัสที่ราย
ละเอียดจริง ๆ แทนโค้ด boilerplate
ตัวอย่างแรกของ ownership เราจะดู scope ของตัวแปร scope คือช่วงในโปรแกรม ที่ item valid ดูตัวแปรต่อไปนี้:
#![allow(unused)]
fn main() {
let s = "hello";
}
ตัวแปร s อ้างถึง string literal ที่ค่าของ string ถูก hardcode ลงในข้อความ
ของโปรแกรม ตัวแปรนี้ valid ตั้งแต่จุดที่ประกาศจนถึงท้าย scope ปัจจุบัน
Listing 4-1 แสดงโปรแกรมพร้อม comment annotate ตำแหน่งที่ตัวแปร s จะ
valid
fn main() {
{ // s is not valid here, since it's not yet declared
let s = "hello"; // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no longer valid
}
พูดอีกอย่าง มีจุดสำคัญสองจุดในเวลาที่นี่:
- เมื่อ
sเข้ามา ใน scope มัน valid - มันยัง valid อยู่จนกว่าจะออก นอก scope
ณ จุดนี้ ความสัมพันธ์ระหว่าง scope และตอนที่ตัวแปร valid คล้ายกับในภาษา
โปรแกรมอื่น ทีนี้เราจะต่อยอดความเข้าใจนี้ ด้วยการแนะนำ type String
Type String
เพื่อแสดงกฎของ ownership เราต้องมีชนิดข้อมูลที่ซับซ้อนกว่าที่เราครอบคลุมใน
ส่วน “ชนิดข้อมูล” ของบทที่ 3 type ที่ครอบคลุม
ก่อนหน้านี้มีขนาดรู้ เก็บบน stack และ pop ออกจาก stack ได้เมื่อ scope จบ
และคัดลอกให้สร้าง instance ใหม่ที่อิสระได้เร็วและง่าย ถ้าโค้ดส่วนอื่นต้อง
ใช้ค่าเดียวกันใน scope ต่าง แต่เราอยากดูข้อมูลที่เก็บบน heap และสำรวจว่า
Rust รู้เมื่อไหร่ว่าควร cleanup ข้อมูลนั้น และ type String เป็นตัวอย่าง
ที่ดี
เราจะโฟกัสที่ส่วนของ String ที่เกี่ยวกับ ownership แง่มุมเหล่านี้ยังใช้
กับชนิดข้อมูลซับซ้อนอื่น ๆ ด้วย ไม่ว่าจะมาจาก standard library หรือสร้าง
เอง เราจะพูดถึงแง่มุมที่ไม่เกี่ยว ownership ของ String ใน
บทที่ 8
เราเห็น string literal มาแล้ว ที่ค่า string ถูก hardcode ลงในโปรแกรม
String literal สะดวก แต่ไม่เหมาะกับทุกสถานการณ์ที่เราอยากใช้ข้อความ
เหตุผลหนึ่งคือมัน immutable อีกเหตุผลคือไม่ใช่ทุกค่า string จะรู้เมื่อ
เราเขียนโค้ด เช่น ถ้าเราอยากรับ input จากผู้ใช้แล้วเก็บ? สำหรับสถานการณ์
เหล่านี้ Rust จึงมี type String type นี้จัดการข้อมูลที่ allocate บน
heap และสามารถเก็บข้อความปริมาณที่เราไม่รู้ตอน compile time คุณสร้าง
String จาก string literal ได้โดยใช้ฟังก์ชัน from แบบนี้:
#![allow(unused)]
fn main() {
let s = String::from("hello");
}
operator double colon :: ให้เรา namespace ฟังก์ชัน from นี้ภายใต้
type String แทนการใช้ชื่อแบบ string_from เราจะพูดถึง syntax นี้มาก
ขึ้นในส่วน “เมธอด” ของบทที่ 5 และตอนเราพูดถึง
namespace กับ module ใน
“Path สำหรับอ้างถึง item ใน module tree”
ในบทที่ 7
string ชนิดนี้ สามารถ ถูก mutate ได้:
fn main() {
let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{s}"); // this will print `hello, world!`
}
แล้วอะไรคือความแตกต่างที่นี่? ทำไม String ถึง mutate ได้ แต่ literal
ไม่ได้? ความแตกต่างอยู่ที่วิธีที่ type ทั้งสองนี้จัดการหน่วยความจำ
หน่วยความจำและการ Allocate
ในกรณีของ string literal เรารู้เนื้อหาตอน compile time ดังนั้นข้อความถูก hardcode ลงใน executable สุดท้ายโดยตรง นี่คือเหตุผลที่ string literal เร็ว และมีประสิทธิภาพ แต่คุณสมบัติเหล่านี้มาจากการที่ string literal เป็น immutable เท่านั้น น่าเสียดายที่เราไม่สามารถใส่ blob ของหน่วยความจำลงใน binary สำหรับข้อความแต่ละชิ้นที่ขนาดไม่รู้ตอน compile time และขนาดอาจ เปลี่ยนระหว่างรันโปรแกรม
กับ type String เพื่อรองรับชิ้นข้อความที่ mutable และเติบโตได้ เราต้อง
allocate หน่วยความจำจำนวนหนึ่งบน heap, ไม่รู้ตอน compile time, เพื่อเก็บ
เนื้อหา นี่หมายความว่า:
- หน่วยความจำต้องถูกขอจาก memory allocator ตอน runtime
- เราต้องมีวิธี return หน่วยความจำนี้กลับให้ allocator เมื่อเราใช้
Stringเสร็จ
ส่วนแรกนั้นเราทำ — เมื่อเราเรียก String::from การ implement ของมันขอ
หน่วยความจำที่ต้องการ นี่ค่อนข้างเป็นสากลในภาษาโปรแกรม
อย่างไรก็ตาม ส่วนที่สองต่างกัน ในภาษาที่มี garbage collector (GC) GC
ติดตามและ cleanup หน่วยความจำที่ไม่ได้ใช้แล้ว และเราไม่ต้องคิดเรื่องนั้น
ในภาษาส่วนใหญ่ที่ไม่มี GC เป็นหน้าที่ของเราที่จะระบุเมื่อหน่วยความจำไม่
ถูกใช้แล้ว และเรียกโค้ดให้ free มันแบบ explicit เหมือนที่เราทำเพื่อขอมัน
การทำสิ่งนี้ให้ถูก เป็นปัญหา programming ที่ยากมาตลอด ถ้าเราลืม เราเสีย
หน่วยความจำ ถ้าเราทำเร็วเกินไป เรามีตัวแปร invalid ถ้าเราทำสองครั้ง นั่น
ก็เป็น bug ด้วย เราต้องจับคู่ allocate หนึ่งครั้งกับ free หนึ่งครั้ง
เป๊ะ ๆ
Rust เลือกเส้นทางต่าง — หน่วยความจำถูก return อัตโนมัติเมื่อตัวแปรที่ owner
มันออกจาก scope นี่คือ version ของตัวอย่าง scope ของเราจาก Listing 4-1
ที่ใช้ String แทน string literal:
fn main() {
{
let s = String::from("hello"); // s is valid from this point forward
// do stuff with s
} // this scope is now over, and s is no
// longer valid
}
มีจุดธรรมชาติที่เรา return หน่วยความจำที่ String ของเราต้องการกลับให้
allocator ได้ — เมื่อ s ออกจาก scope เมื่อตัวแปรออกจาก scope Rust เรียก
ฟังก์ชันพิเศษให้เรา ฟังก์ชันนี้เรียกว่า drop และเป็นที่ที่ผู้เขียน
String ใส่โค้ดที่ return หน่วยความจำได้ Rust เรียก drop อัตโนมัติที่
curly bracket ปิด
หมายเหตุ: ใน C++ pattern การ deallocate resource ที่ท้าย lifetime ของ item บางครั้งเรียกว่า Resource Acquisition Is Initialization (RAII) ฟังก์ชัน
dropใน Rust จะคุ้นเคยกับคุณถ้าคุณใช้ RAII pattern มาก่อน
pattern นี้มีผลกระทบลึกซึ้งต่อวิธีที่เขียนโค้ด Rust ตอนนี้อาจดูง่าย แต่ พฤติกรรมของโค้ดอาจคาดไม่ถึงในสถานการณ์ที่ซับซ้อนกว่า เมื่อเราอยากให้ตัวแปร หลายตัวใช้ข้อมูลที่เรา allocate บน heap มาสำรวจบางสถานการณ์เหล่านั้นกัน
ตัวแปรและข้อมูลโต้ตอบกันด้วย Move
ตัวแปรหลายตัวโต้ตอบกับข้อมูลเดียวกันได้หลายวิธีใน Rust Listing 4-2 แสดง ตัวอย่างที่ใช้ integer
fn main() {
let x = 5;
let y = x;
}
x ให้ yเราอาจเดาได้ว่ามันทำอะไร — “Bind ค่า 5 กับ x แล้วทำสำเนาของค่าใน x
แล้ว bind กับ y” ตอนนี้เรามีสองตัวแปร x และ y และทั้งคู่เท่ากับ 5
นี่คือสิ่งที่เกิดขึ้นจริง เพราะ integer เป็นค่าง่าย ๆ ที่มีขนาดรู้และคงที่
และค่า 5 สองตัวนี้ถูก push บน stack
ทีนี้มาดู version String:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
}
นี่ดูคล้ายกันมาก เราจึงอาจสมมติว่าวิธีที่มันทำงานจะเหมือนกัน — นั่นคือ
บรรทัดที่สองจะทำสำเนาของค่าใน s1 แล้ว bind กับ s2 แต่นี่ไม่ใช่สิ่งที่
เกิดขึ้นทีเดียว
ดู Figure 4-1 เพื่อเห็นว่าเกิดอะไรขึ้นกับ String เบื้องลึก String
ประกอบด้วยสามส่วน แสดงทางซ้าย — pointer ไปยังหน่วยความจำที่เก็บเนื้อหา
ของ string, ความยาว และความจุ กลุ่มข้อมูลนี้เก็บบน stack ทางขวาคือหน่วย
ความจำบน heap ที่เก็บเนื้อหา
Figure 4-1: ตัวแทนในหน่วยความจำของ String ที่เก็บ
ค่า "hello" bind กับ s1
ความยาวคือจำนวนหน่วยความจำใน byte ที่เนื้อหาของ String ใช้อยู่ปัจจุบัน
ความจุคือจำนวนหน่วยความจำทั้งหมดใน byte ที่ String ได้รับจาก allocator
ความแตกต่างระหว่างความยาวและความจุสำคัญ แต่ไม่ใช่ในบริบทนี้ ดังนั้นตอนนี้
ละเว้นความจุได้
เมื่อเรา assign s1 ให้ s2 ข้อมูล String ถูกคัดลอก ซึ่งหมายความว่าเรา
คัดลอก pointer, ความยาว และความจุที่อยู่บน stack เราไม่คัดลอกข้อมูลบน
heap ที่ pointer อ้างถึง พูดอีกอย่าง ตัวแทนข้อมูลในหน่วยความจำดูเหมือน
Figure 4-2
Figure 4-2: ตัวแทนในหน่วยความจำของตัวแปร s2 ที่มี
สำเนาของ pointer, ความยาว และความจุของ s1
ตัวแทน ไม่ ดูเหมือน Figure 4-3 ซึ่งเป็นสิ่งที่หน่วยความจำจะดูเหมือนถ้า
Rust คัดลอกข้อมูล heap ด้วย ถ้า Rust ทำสิ่งนี้ operation s2 = s1 อาจ
แพงมากในแง่ของ runtime performance ถ้าข้อมูลบน heap ใหญ่
Figure 4-3: ความเป็นไปได้อีกอย่างของสิ่งที่ s2 = s1
อาจทำ ถ้า Rust คัดลอกข้อมูล heap ด้วย
ก่อนหน้านี้ เราบอกว่าเมื่อตัวแปรออกจาก scope Rust เรียกฟังก์ชัน drop
อัตโนมัติและ cleanup หน่วยความจำ heap สำหรับตัวแปรนั้น แต่ Figure 4-2
แสดงทั้ง data pointer ชี้ไปตำแหน่งเดียวกัน นี่เป็นปัญหา — เมื่อ s2 และ
s1 ออกจาก scope ทั้งคู่จะพยายาม free หน่วยความจำเดียวกัน นี่รู้จักใน
ชื่อ double free error และเป็นหนึ่งใน memory safety bug ที่เราพูดถึง
ก่อนหน้านี้ การ free หน่วยความจำสองครั้งอาจนำไปสู่ memory corruption ซึ่ง
อาจนำไปสู่ security vulnerability
เพื่อรับประกัน memory safety หลังบรรทัด let s2 = s1; Rust ถือว่า s1
ไม่ valid อีกต่อไป ดังนั้น Rust ไม่ต้อง free อะไรเมื่อ s1 ออกจาก scope
ดูสิ่งที่เกิดขึ้นเมื่อคุณพยายามใช้ s1 หลังจากสร้าง s2 — มันจะไม่
ทำงาน:
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s1}, world!");
}
คุณจะได้ error แบบนี้ เพราะ Rust ป้องกันคุณจากการใช้ reference ที่ถูก invalidate:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
--> src/main.rs:5:16
|
2 | let s1 = String::from("hello");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 |
5 | println!("{s1}, world!");
| ^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let s2 = s1.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
ถ้าคุณเคยได้ยินคำว่า shallow copy และ deep copy ขณะทำงานกับภาษาอื่น
แนวคิดของการคัดลอก pointer, ความยาว และความจุ โดยไม่คัดลอกข้อมูล น่าจะ
ฟังดูเหมือน shallow copy แต่เพราะ Rust ยัง invalidate ตัวแปรแรกด้วย แทน
ที่จะเรียกว่า shallow copy มันถูกเรียกว่า move ในตัวอย่างนี้ เราจะบอก
ว่า s1 ถูก move ไปเป็น s2 ดังนั้นสิ่งที่เกิดขึ้นจริงแสดงใน
Figure 4-4
Figure 4-4: ตัวแทนในหน่วยความจำหลังจาก s1 ถูก
invalidate
แก้ปัญหาเราแล้ว! ด้วยแค่ s2 ที่ valid เมื่อมันออกจาก scope มันเพียง
ตัวเดียวจะ free หน่วยความจำ และเราเสร็จแล้ว
นอกจากนี้ มีตัวเลือกการออกแบบที่นัยจากนี้ — Rust จะไม่สร้างสำเนาแบบ “deep” ของข้อมูลของคุณอัตโนมัติ ดังนั้น การคัดลอก อัตโนมัติ ใด ๆ สามารถสมมติได้ ว่าไม่แพงในแง่ของ runtime performance
Scope และ Assignment
ตรงข้ามของสิ่งนี้เป็นจริงสำหรับความสัมพันธ์ระหว่าง scope, ownership และ
หน่วยความจำที่ถูก free ผ่านฟังก์ชัน drop ด้วย เมื่อคุณ assign ค่าใหม่
สมบูรณ์ให้ตัวแปรที่มีอยู่ Rust จะเรียก drop และ free หน่วยความจำของค่า
เดิมทันที พิจารณาโค้ดนี้ตัวอย่าง:
fn main() {
let mut s = String::from("hello");
s = String::from("ahoy");
println!("{s}, world!");
}
เราประกาศตัวแปร s ในตอนแรกและ bind กับ String ที่มีค่า "hello" จาก
นั้นเราสร้าง String ใหม่ทันทีที่มีค่า "ahoy" แล้ว assign ให้ s ณ
จุดนี้ ไม่มีอะไรอ้างถึงค่าเดิมบน heap แล้ว Figure 4-5 แสดงข้อมูล stack
และ heap ตอนนี้:
Figure 4-5: ตัวแทนในหน่วยความจำหลังจากค่าเริ่มต้นถูก แทนที่ทั้งหมด
string เดิมจึงออกจาก scope ทันที Rust จะรันฟังก์ชัน drop กับมัน และหน่วย
ความจำของมันจะถูก free ทันที เมื่อเราพิมพ์ค่าตอนท้าย มันจะเป็น
"ahoy, world!"
ตัวแปรและข้อมูลโต้ตอบกันด้วย Clone
ถ้าเรา อยาก คัดลอกข้อมูล heap ของ String แบบ deep ไม่ใช่แค่ข้อมูล
stack เราใช้เมธอดที่ใช้บ่อยชื่อ clone ได้ เราจะพูดถึง syntax ของเมธอด
ในบทที่ 5 แต่เพราะเมธอดเป็นฟีเจอร์ที่ใช้บ่อยในภาษาโปรแกรมหลายภาษา คุณคง
เคยเห็นมาก่อน
นี่คือตัวอย่างเมธอด clone ในการใช้งาน:
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {s1}, s2 = {s2}");
}
นี่ทำงานได้ปกติและ produce พฤติกรรมที่แสดงใน Figure 4-3 แบบ explicit ที่ข้อมูล heap ถูก คัดลอกด้วย
เมื่อคุณเห็นการเรียก clone คุณรู้ว่าโค้ดบางอย่างกำลัง execute และโค้ดนั้น
อาจแพง มันเป็นสัญญาณเชิง visual ว่ามีบางอย่างต่างกำลังเกิดขึ้น
ข้อมูลที่อยู่บน Stack เท่านั้น: Copy
มีอีกประเด็นที่เรายังไม่ได้พูด โค้ดนี้ที่ใช้ integer — ส่วนหนึ่งของมัน แสดงใน Listing 4-2 — ทำงานและ valid:
fn main() {
let x = 5;
let y = x;
println!("x = {x}, y = {y}");
}
แต่โค้ดนี้ดูเหมือนขัดแย้งกับสิ่งที่เราเพิ่งเรียน — เราไม่ได้เรียก clone
แต่ x ยัง valid และไม่ถูก move เข้า y
เหตุผลคือ type อย่าง integer ที่มีขนาดรู้ตอน compile time ถูกเก็บทั้งหมด
บน stack ดังนั้นสำเนาของค่าจริงทำได้เร็ว นั่นหมายความว่าไม่มีเหตุผลที่เรา
จะอยากป้องกัน x จากการ valid หลังจากเราสร้างตัวแปร y พูดอีกอย่าง ไม่
มีความแตกต่างระหว่าง deep และ shallow copy ที่นี่ ดังนั้นการเรียก clone
จะไม่ทำอะไรต่างจาก shallow copy ปกติ และเราละเว้นมันได้
Rust มี annotation พิเศษเรียกว่า trait Copy ที่เราวางบน type ที่เก็บบน
stack ได้ อย่างที่ integer เป็น (เราจะพูดถึง trait มากขึ้นใน
บทที่ 10) ถ้า type implement trait Copy ตัวแปร
ที่ใช้มันจะไม่ move แต่ถูกคัดลอกอย่างง่าย ทำให้มันยัง valid หลัง assign
ให้ตัวแปรอื่น
Rust ไม่ให้เรา annotate type ด้วย Copy ถ้า type หรือส่วนใดของมัน implement
trait Drop ถ้า type ต้องการอะไรพิเศษที่จะเกิดเมื่อค่าออกจาก scope และ
เราเพิ่ม annotation Copy ให้ type นั้น เราจะได้ compile-time error เพื่อ
เรียนรู้เกี่ยวกับวิธีเพิ่ม annotation Copy ให้ type ของคุณเพื่อ implement
trait ดู “Derivable Trait” ใน
ภาคผนวก C
แล้ว type อะไรที่ implement trait Copy? คุณเช็ค documentation ของ type
นั้น ๆ ได้เพื่อให้แน่ใจ แต่กฎทั่วไป กลุ่มของค่า scalar ง่าย ๆ ใด ๆ
implement Copy ได้ และไม่มีอะไรที่ต้องการ allocation หรือเป็น resource
รูปแบบใด ๆ ที่ implement Copy ได้ นี่คือ type บางตัวที่ implement Copy:
- type integer ทั้งหมด เช่น
u32 - type Boolean
boolที่มีค่าtrueและfalse - type floating-point ทั้งหมด เช่น
f64 - type character
char - tuple ถ้ามีแค่ type ที่ implement
Copyด้วย เช่น(i32, i32)implementCopyแต่(i32, String)ไม่
Ownership และฟังก์ชัน
กลไกของการส่งค่าให้ฟังก์ชันคล้ายกับการ assign ค่าให้ตัวแปร การส่งตัวแปรให้ ฟังก์ชันจะ move หรือ copy เหมือนที่การ assign ทำ Listing 4-3 มีตัวอย่าง พร้อม annotation บ้างที่แสดงตำแหน่งที่ตัวแปรเข้าและออก scope
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // Because i32 implements the Copy trait,
// x does NOT move into the function,
// so it's okay to use x afterward.
} // Here, x goes out of scope, then s. However, because s's value was moved,
// nothing special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.
ถ้าเราพยายามใช้ s หลังการเรียก takes_ownership Rust จะโยน compile-time
error การตรวจสอบ static เหล่านี้ปกป้องเราจากความผิดพลาด ลองเพิ่มโค้ดใน
main ที่ใช้ s และ x เพื่อดูตำแหน่งที่คุณใช้พวกมันได้ และตำแหน่งที่
กฎ ownership ป้องกันคุณ
Return Value และ Scope
การ return ค่ายังโอน ownership ได้ด้วย Listing 4-4 แสดงตัวอย่างฟังก์ชันที่ return ค่าบางอย่าง พร้อม annotation คล้ายกับใน Listing 4-3
fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1
let s2 = String::from("hello"); // s2 comes into scope
let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.
fn gives_ownership() -> String { // gives_ownership will move its
// return value into the function
// that calls it
let some_string = String::from("yours"); // some_string comes into scope
some_string // some_string is returned and
// moves out to the calling
// function
}
// This function takes a String and returns a String.
fn takes_and_gives_back(a_string: String) -> String {
// a_string comes into
// scope
a_string // a_string is returned and moves out to the calling function
}
Ownership ของตัวแปรตาม pattern เดียวกันทุกครั้ง — การ assign ค่าให้ตัวแปร
อื่น move มัน เมื่อตัวแปรที่รวมข้อมูลบน heap ออกจาก scope ค่าจะถูก cleanup
โดย drop เว้นแต่ ownership ของข้อมูลถูก move ไปยังตัวแปรอื่น
ขณะที่นี่ทำงานได้ การรับ ownership แล้ว return ownership ทุกฟังก์ชันค่อน ข้างน่าเบื่อ ถ้าเราอยากให้ฟังก์ชันใช้ค่าโดยไม่รับ ownership ล่ะ? มันน่ารำคาญ ที่อะไรก็ตามที่เราส่งเข้า ต้องส่งกลับด้วยถ้าเราอยากใช้อีก เพิ่มเติมจาก ข้อมูลใด ๆ ที่ผลจาก body ของฟังก์ชันที่เราอาจอยาก return ด้วย
Rust ให้เรา return หลายค่าโดยใช้ tuple ได้ ดังที่แสดงใน Listing 4-5
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{s2}' is {len}.");
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() returns the length of a String
(s, length)
}
แต่นี่เป็นพิธีการมากเกินไป และเป็นงานเยอะสำหรับแนวคิดที่ควรใช้บ่อย โชคดี สำหรับเรา Rust มีฟีเจอร์สำหรับการใช้ค่าโดยไม่โอน ownership — reference
Reference และ Borrowing
Reference และ Borrowing
ปัญหากับโค้ด tuple ใน Listing 4-5 คือเราต้อง return String ให้ฟังก์ชัน
ที่เรียก เพื่อให้เรายังใช้ String ได้หลังการเรียก calculate_length
เพราะ String ถูก move เข้า calculate_length แทนนั้น เราให้ reference
ของค่า String ได้ reference คล้าย pointer ตรงที่มันเป็น address ที่เรา
ตามไปเข้าถึงข้อมูลที่เก็บที่ address นั้นได้ ข้อมูลนั้นเป็นเจ้าของโดยตัว
แปรอื่น ต่างจาก pointer reference รับประกันว่าจะชี้ไปยังค่าที่ valid ของ
type หนึ่งตลอด life ของ reference นั้น
นี่คือวิธีที่คุณประกาศและใช้ฟังก์ชัน calculate_length ที่มี reference ของ
object เป็น parameter แทนการรับ ownership ของค่า:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
ขั้นแรก สังเกตว่าโค้ด tuple ทั้งหมดในการประกาศตัวแปรและ return value ของ
ฟังก์ชันหายไป ขั้นที่สอง สังเกตว่าเราส่ง &s1 เข้า calculate_length
และในการประกาศ เรารับ &String แทน String ampersand เหล่านี้แทน
reference และให้คุณอ้างถึงค่าโดยไม่รับ ownership ของมัน Figure 4-6 แสดง
แนวคิดนี้
Figure 4-6: ไดอะแกรมของ &String s ที่ชี้ไปยัง
String s1
หมายเหตุ: ตรงข้ามของการอ้างอิงโดยใช้
&คือ dereference ซึ่งทำได้ด้วย dereference operator*เราจะเห็นการใช้ dereference operator บางตัวใน บทที่ 8 และพูดถึงรายละเอียดของ dereference ในบทที่ 15
มาดูใกล้ ๆ ที่การเรียกฟังก์ชันที่นี่:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize {
s.len()
}
syntax &s1 ให้เราสร้าง reference ที่ อ้างถึง ค่าของ s1 แต่ไม่ owner
มัน เพราะ reference ไม่ owner มัน ค่าที่มันชี้ไปจะไม่ถูก drop เมื่อ
reference หยุดใช้
ในทำนองเดียวกัน signature ของฟังก์ชันใช้ & เพื่อบ่งบอกว่า type ของ
parameter s เป็น reference มาเพิ่ม annotation อธิบายบ้าง:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{s1}' is {len}.");
}
fn calculate_length(s: &String) -> usize { // s is a reference to a String
s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
// it refers to, the String is not dropped.
scope ที่ตัวแปร s valid เหมือนกับ scope ของ parameter ฟังก์ชันใด ๆ แต่
ค่าที่ reference ชี้ไปไม่ถูก drop เมื่อ s หยุดใช้ เพราะ s ไม่มี
ownership เมื่อฟังก์ชันมี reference เป็น parameter แทนค่าจริง เราไม่ต้อง
return ค่าเพื่อคืน ownership เพราะเราไม่เคยมี ownership
เราเรียกการสร้าง reference ว่า borrowing เหมือนในชีวิตจริง ถ้าคนเป็น เจ้าของบางอย่าง คุณยืมจากเขาได้ เมื่อคุณใช้เสร็จ คุณต้องคืน คุณไม่ใช่ เจ้าของ
แล้วเกิดอะไรขึ้นถ้าเราพยายามแก้สิ่งที่กำลังยืมอยู่? ลองโค้ดใน Listing 4-6 Spoiler — มันไม่ทำงาน!
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
นี่คือ error:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
--> src/main.rs:8:5
|
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
7 | fn change(some_string: &mut String) {
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
เช่นเดียวกับที่ตัวแปร immutable โดย default reference ก็เช่นกัน เราไม่ได้ รับอนุญาตให้แก้สิ่งที่เรามี reference ของมัน
Mutable Reference
เราแก้โค้ดจาก Listing 4-6 ให้เราแก้ค่าที่ borrow ได้ ด้วยการแก้เล็กน้อยที่ ใช้ mutable reference แทน:
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
ขั้นแรก เราเปลี่ยน s ให้เป็น mut จากนั้นเราสร้าง mutable reference ด้วย
&mut s ตรงที่เราเรียกฟังก์ชัน change และ update signature ของฟังก์ชัน
ให้รับ mutable reference ด้วย some_string: &mut String นี่ทำให้ชัดเจนมาก
ว่าฟังก์ชัน change จะ mutate ค่าที่มัน borrow
Mutable reference มีข้อจำกัดใหญ่ — ถ้าคุณมี mutable reference ของค่า คุณ
จะมี reference อื่นของค่านั้นไม่ได้ โค้ดนี้ที่พยายามสร้าง mutable
reference สองตัวของ s จะล้มเหลว:
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");
}
นี่คือ error:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> src/main.rs:5:14
|
4 | let r1 = &mut s;
| ------ first mutable borrow occurs here
5 | let r2 = &mut s;
| ^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{r1}, {r2}");
| -- first borrow later used here
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
error นี้บอกว่าโค้ดนี้ invalid เพราะเรา borrow s เป็น mutable มากกว่า
หนึ่งครั้งในเวลาเดียวกันไม่ได้ mutable borrow แรกอยู่ใน r1 และต้องอยู่
จนกว่าจะใช้ใน println! แต่ระหว่างการสร้าง mutable reference นั้นและการ
ใช้มัน เราพยายามสร้าง mutable reference อีกตัวใน r2 ที่ borrow ข้อมูล
เดียวกับ r1
ข้อจำกัดที่ป้องกัน mutable reference หลายตัวของข้อมูลเดียวกันในเวลาเดียวกัน อนุญาตให้ mutate ได้ แต่ในลักษณะที่ควบคุมมาก เป็นสิ่งที่ Rustacean ใหม่ ๆ ต่อสู้ด้วย เพราะภาษาส่วนใหญ่ให้คุณ mutate เมื่อไหร่ก็ได้ ประโยชน์ของการมี ข้อจำกัดนี้คือ Rust ป้องกัน data race ตอน compile time ได้ data race คล้ายกับ race condition และเกิดขึ้นเมื่อสามพฤติกรรมนี้เกิด:
- pointer ตั้งแต่สองตัวขึ้นไปเข้าถึงข้อมูลเดียวกันในเวลาเดียวกัน
- อย่างน้อยหนึ่ง pointer กำลังถูกใช้เขียนข้อมูล
- ไม่มีกลไกที่ถูกใช้ synchronize การเข้าถึงข้อมูล
Data race ทำให้เกิด undefined behavior และวินิจฉัยและแก้ยากเมื่อพยายามตาม หาตอน runtime — Rust ป้องกันปัญหานี้โดยปฏิเสธที่จะ compile โค้ดที่มี data race!
เช่นเคย เราใช้ curly bracket สร้าง scope ใหม่ได้ ให้มี mutable reference หลายตัว แค่ไม่ใช่ พร้อมกัน:
fn main() {
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.
let r2 = &mut s;
}
Rust บังคับใช้กฎคล้ายกันสำหรับการรวม mutable และ immutable reference โค้ด นี้ทำให้เกิด error:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{r1}, {r2}, and {r3}");
}
นี่คือ error:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:6:14
|
4 | let r1 = &s; // no problem
| -- immutable borrow occurs here
5 | let r2 = &s; // no problem
6 | let r3 = &mut s; // BIG PROBLEM
| ^^^^^^ mutable borrow occurs here
7 |
8 | println!("{r1}, {r2}, and {r3}");
| -- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
โอ้! เรา ก็ไม่ สามารถมี mutable reference ขณะที่เรามี immutable หนึ่งของ ค่าเดียวกัน
ผู้ใช้ immutable reference ไม่ได้คาดว่าค่าจะเปลี่ยนใต้พวกเขาทันที! อย่างไรก็ ตาม immutable reference หลายตัวอนุญาต เพราะไม่มีใครที่แค่อ่านข้อมูลที่มี ความสามารถส่งผลต่อการอ่านข้อมูลของคนอื่น
หมายเหตุว่า scope ของ reference เริ่มจากตรงที่ถูกแนะนำ และต่อเนื่องไปจน
ครั้งสุดท้ายที่ reference นั้นถูกใช้ เช่น โค้ดนี้จะ compile ผ่าน เพราะการ
ใช้ครั้งสุดท้ายของ immutable reference อยู่ใน println! ก่อน mutable
reference ถูกแนะนำ:
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {r2}");
// Variables r1 and r2 will not be used after this point.
let r3 = &mut s; // no problem
println!("{r3}");
}
scope ของ immutable reference r1 และ r2 จบหลัง println! ที่พวกมัน
ถูกใช้ครั้งสุดท้าย ซึ่งก่อน mutable reference r3 ถูกสร้าง scope เหล่านี้
ไม่ทับซ้อนกัน โค้ดนี้จึงอนุญาต — compiler บอกได้ว่า reference ไม่ถูกใช้
แล้วในจุดก่อนท้าย scope
แม้ error เรื่อง borrowing อาจทำให้หงุดหงิดบางครั้ง จำไว้ว่ามันคือ Rust compiler ชี้ bug ที่อาจเกิดแต่เนิ่น ๆ (ตอน compile time แทน runtime) และ แสดงให้คุณเห็นตำแหน่งปัญหาเป๊ะ ๆ จากนั้น คุณไม่ต้องตามว่าทำไมข้อมูลไม่ใช่ สิ่งที่คุณคิด
Dangling Reference
ในภาษาที่มี pointer ง่ายที่จะสร้าง dangling pointer — pointer ที่อ้างถึง ตำแหน่งในหน่วยความจำที่อาจถูกให้คนอื่น — โดย free หน่วยความจำขณะที่ยัง รักษา pointer ของหน่วยความจำนั้น ใน Rust ตรงข้าม compiler รับประกันว่า reference จะไม่เป็น dangling reference — ถ้าคุณมี reference ของข้อมูล compiler จะรับประกันว่าข้อมูลจะไม่ออกจาก scope ก่อน reference ของข้อมูล
ลองพยายามสร้าง dangling reference เพื่อดูว่า Rust ป้องกันด้วย compile-time error อย่างไร:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
นี่คือ error:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
--> src/main.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
|
5 | fn dangle() -> &'static String {
| +++++++
help: instead, you are more likely to want to return an owned value
|
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
Error message นี้อ้างถึงฟีเจอร์ที่เรายังไม่ครอบคลุม — lifetime เราจะพูดถึง lifetime ในรายละเอียดในบทที่ 10 แต่ถ้าคุณละเลยส่วนเกี่ยวกับ lifetime ข้อ ความนี้มีกุญแจว่าทำไมโค้ดนี้เป็นปัญหา:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from
มาดูใกล้ ๆ ว่าเกิดอะไรขึ้นในแต่ละ stage ของโค้ด dangle:
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String { // dangle returns a reference to a String
let s = String::from("hello"); // s is a new String
&s // we return a reference to the String, s
} // Here, s goes out of scope and is dropped, so its memory goes away.
// Danger!
เพราะ s ถูกสร้างภายใน dangle เมื่อโค้ดของ dangle เสร็จ s จะถูก
deallocate แต่เราพยายาม return reference ของมัน นั่นหมายความว่า reference
นี้จะชี้ไปยัง String ที่ invalid ไม่ดีเลย! Rust จะไม่ให้เราทำสิ่งนี้
วิธีแก้ที่นี่คือ return String ตรง ๆ:
fn main() {
let string = no_dangle();
}
fn no_dangle() -> String {
let s = String::from("hello");
s
}
นี่ทำงานได้โดยไม่มีปัญหา Ownership ถูก move ออก และไม่มีอะไรถูก deallocate
กฎของ Reference
มาทบทวนสิ่งที่เราพูดถึงเกี่ยวกับ reference:
- ในเวลาใดก็ตาม คุณมีได้ หนึ่ง mutable reference หรือ immutable reference จำนวนเท่าไรก็ได้
- Reference ต้อง valid เสมอ
ต่อไป เราจะดู reference แบบอื่น — slice
Slice Type
Slice Type
Slice ให้คุณอ้างถึงลำดับ element ที่ติดกันใน collection slice เป็น reference ชนิดหนึ่ง ดังนั้นไม่มี ownership
นี่คือปัญหา programming เล็ก ๆ — เขียนฟังก์ชันที่รับ string ของคำที่คั่น ด้วย space แล้ว return คำแรกที่เจอใน string นั้น ถ้าฟังก์ชันไม่เจอ space ใน string ทั้ง string ต้องเป็นหนึ่งคำ ดังนั้นทั้ง string ควรถูก return
หมายเหตุ: เพื่อจุดประสงค์ของการแนะนำ slice เราสมมติแค่ ASCII ในส่วนนี้ การพูดถึง UTF-8 handling อย่างละเอียดอยู่ในส่วน “เก็บข้อความ UTF-8 ด้วย String” ของบทที่ 8
มาทำงานผ่านวิธีเขียน signature ของฟังก์ชันนี้โดยไม่ใช้ slice เพื่อเข้าใจ ปัญหาที่ slice จะแก้:
fn first_word(s: &String) -> ?
ฟังก์ชัน first_word มี parameter type &String เราไม่ต้องการ ownership
ดังนั้นนี่ก็ดี (ใน Rust ที่ idiomatic ฟังก์ชันไม่รับ ownership ของ argument
เว้นแต่ต้องการ และเหตุผลของเรื่องนั้นจะชัดเจนขึ้นเมื่อเราไปต่อ) แต่เรา
ควร return อะไร? เราไม่มีวิธีพูดถึง ส่วน ของ string จริง ๆ อย่างไรก็ตาม
เรา return index ของท้ายคำได้ บ่งบอกด้วย space ลองดู ตามที่แสดงใน
Listing 4-7
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
first_word ที่ return ค่า byte index เข้า parameter Stringเพราะเราต้องไปผ่าน String ทีละ element และเช็คว่าค่าเป็น space หรือไม่
เราจะแปลง String เป็น array ของ byte โดยใช้เมธอด as_bytes
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
ถัดไป เราสร้าง iterator บน array ของ byte โดยใช้เมธอด iter:
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
เราจะพูดถึง iterator ในรายละเอียดใน บทที่ 13 ตอนนี้
รู้ว่า iter เป็นเมธอดที่ return แต่ละ element ใน collection และ
enumerate ห่อผลของ iter แล้ว return แต่ละ element เป็นส่วนหนึ่งของ
tuple แทน element แรกของ tuple ที่ return จาก enumerate คือ index และ
element ที่สองคือ reference ของ element นี้สะดวกกว่าการคำนวณ index เองนิด
หน่อย
เพราะเมธอด enumerate return tuple เราใช้ pattern destructure tuple นั้น
ได้ เราจะพูดถึง pattern มากขึ้นใน บทที่ 6 ใน for
loop เราระบุ pattern ที่มี i สำหรับ index ใน tuple และ &item สำหรับ
single byte ใน tuple เพราะเราได้ reference ของ element จาก
.iter().enumerate() เราใช้ & ใน pattern
ภายใน for loop เราค้นหา byte ที่แทน space โดยใช้ byte literal syntax
ถ้าเราเจอ space เรา return ตำแหน่ง ไม่อย่างนั้น เรา return ความยาวของ
string โดยใช้ s.len()
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {}
ตอนนี้เรามีวิธีหา index ของท้ายคำแรกใน string แต่มีปัญหา เรา return
usize เดี่ยว ๆ แต่มันเป็นตัวเลขที่มีความหมายเฉพาะในบริบทของ &String
พูดอีกอย่าง เพราะมันเป็นค่าที่แยกจาก String ไม่มีการรับประกันว่ามันจะ
ยัง valid ในอนาคต พิจารณาโปรแกรมใน Listing 4-8 ที่ใช้ฟังก์ชัน first_word
จาก Listing 4-7
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s); // word will get the value 5
s.clear(); // this empties the String, making it equal to ""
// word still has the value 5 here, but s no longer has any content that we
// could meaningfully use with the value 5, so word is now totally invalid!
}
first_word แล้วเปลี่ยนเนื้อหา Stringโปรแกรมนี้ compile โดยไม่มี error และจะทำเหมือนกันถ้าเราใช้ word หลังการ
เรียก s.clear() เพราะ word ไม่ได้เชื่อมกับ state ของ s เลย word
ยังมีค่า 5 เราใช้ค่า 5 กับตัวแปร s พยายามดึงคำแรกออกได้ แต่นี่จะเป็น
bug เพราะเนื้อหาของ s เปลี่ยนตั้งแต่เราเซฟ 5 ใน word
การต้องห่วงว่า index ใน word จะหลุดจาก sync กับข้อมูลใน s น่าเบื่อและ
เสี่ยง error! การจัดการ index เหล่านี้ยิ่งเปราะบางถ้าเราเขียนฟังก์ชัน
second_word signature ของมันจะต้องดูเป็นแบบนี้:
fn second_word(s: &String) -> (usize, usize) {
ตอนนี้เรากำลังติดตาม index เริ่ม และ จบ และเรามีค่ามากขึ้นที่คำนวณจาก ข้อมูลใน state เฉพาะ แต่ไม่ผูกกับ state นั้นเลย เรามีตัวแปรไม่เกี่ยวข้อง สามตัวลอยอยู่ที่ต้องเก็บให้ sync กัน
โชคดี Rust มีคำตอบสำหรับปัญหานี้ — string slice
String Slice
string slice คือ reference ของลำดับ element ที่ติดกันของ String และ
หน้าตาเป็นแบบนี้:
fn main() {
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
}
แทนที่จะเป็น reference ของทั้ง String hello เป็น reference ของส่วน
หนึ่งของ String ระบุใน [0..5] ที่เพิ่ม เราสร้าง slice โดยใช้ range
ภายใน square bracket ระบุ [starting_index..ending_index] โดย
starting_index คือตำแหน่งแรกใน slice และ ending_index คือหนึ่ง
มากกว่าตำแหน่งสุดท้ายใน slice ภายใน โครงสร้างข้อมูล slice เก็บตำแหน่งเริ่ม
และความยาวของ slice ซึ่งสอดคล้องกับ ending_index ลบ starting_index
ดังนั้นในกรณีของ let world = &s[6..11]; world จะเป็น slice ที่มี
pointer ไปยัง byte ที่ index 6 ของ s พร้อมค่าความยาว 5
Figure 4-7 แสดงเรื่องนี้ในไดอะแกรม
Figure 4-7: string slice ที่อ้างถึงส่วนหนึ่งของ
String
ด้วย syntax range .. ของ Rust ถ้าคุณอยากเริ่มที่ index 0 คุณ drop ค่า
ก่อน period สองตัวได้ พูดอีกอย่าง เหล่านี้เท่ากัน:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let slice = &s[0..2];
let slice = &s[..2];
}
ในทำนองเดียวกัน ถ้า slice ของคุณรวม byte สุดท้ายของ String คุณ drop ตัว
เลขท้ายได้ นั่นหมายความว่าเหล่านี้เท่ากัน:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let len = s.len();
let slice = &s[3..len];
let slice = &s[3..];
}
คุณยัง drop ทั้งสองค่าได้เพื่อรับ slice ของทั้ง string ดังนั้นเหล่านี้ เท่ากัน:
#![allow(unused)]
fn main() {
let s = String::from("hello");
let len = s.len();
let slice = &s[0..len];
let slice = &s[..];
}
หมายเหตุ: index range ของ string slice ต้องเกิดที่ขอบเขตอักขระ UTF-8 ที่ valid ถ้าคุณพยายามสร้าง string slice ในกลางของอักขระ multibyte โปรแกรม จะออกพร้อม error
ด้วยข้อมูลทั้งหมดนี้ในใจ มาเขียน first_word ใหม่เพื่อ return slice
type ที่บ่งบอก “string slice” เขียนเป็น &str:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {}
เราได้ index สำหรับท้ายคำแบบเดียวกับที่เราทำใน Listing 4-7 โดยมองหาการ เกิดครั้งแรกของ space เมื่อเราเจอ space เรา return string slice โดยใช้ ต้น string และ index ของ space เป็น index เริ่มและจบ
ตอนนี้เมื่อเราเรียก first_word เราได้ค่าเดียวกลับมาที่ผูกกับข้อมูล
ต้นทาง ค่าประกอบด้วย reference ของจุดเริ่มของ slice และจำนวน element ใน
slice
การ return slice ก็ใช้ได้สำหรับฟังก์ชัน second_word:
fn second_word(s: &String) -> &str {
ตอนนี้เรามี API ที่ตรงไปตรงมาที่ทำพังยากกว่ามาก เพราะ compiler จะรับ
ประกันว่า reference เข้า String ยัง valid จำ bug ในโปรแกรมใน
Listing 4-8 ได้ไหม ตอนที่เราได้ index ท้ายคำแรก แต่แล้ว clear string
จน index invalid? โค้ดนั้นเชิง logic ผิด แต่ไม่แสดง error ใด ๆ ทันที
ปัญหาจะแสดงทีหลังถ้าเรายังพยายามใช้ index คำแรกกับ string ที่ว่างเปล่า
Slice ทำให้ bug นี้เป็นไปไม่ได้ และให้เรารู้เร็วกว่ามากว่าเรามีปัญหากับ
โค้ด การใช้ version slice ของ first_word จะโยน compile-time error:
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {word}");
}
นี่คือ compiler error:
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> src/main.rs:18:5
|
16 | let word = first_word(&s);
| -- immutable borrow occurs here
17 |
18 | s.clear(); // error!
| ^^^^^^^^^ mutable borrow occurs here
19 |
20 | println!("the first word is: {word}");
| ---- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error
จำจากกฎการ borrow ได้ว่าถ้าเรามี immutable reference ของบางอย่าง เราก็จะ
รับ mutable reference เพิ่มไม่ได้ เพราะ clear ต้อง truncate String
มันต้องรับ mutable reference println! หลังการเรียก clear ใช้
reference ใน word ดังนั้น immutable reference ต้องยัง active ณ จุดนั้น
Rust ไม่อนุญาตให้ mutable reference ใน clear และ immutable reference
ใน word มีอยู่ในเวลาเดียวกัน และ compile ล้มเหลว ไม่ใช่แค่ Rust ทำให้
API ของเราใช้ง่ายขึ้น มันยังกำจัด error ทั้งกลุ่มที่ compile time ด้วย!
String Literal คือ Slice
จำได้ว่าเราพูดถึง string literal ถูกเก็บภายใน binary ตอนนี้เรารู้เรื่อง slice เราเข้าใจ string literal ได้อย่างถูกต้อง:
#![allow(unused)]
fn main() {
let s = "Hello, world!";
}
type ของ s ที่นี่คือ &str — มันเป็น slice ที่ชี้ไปยังจุดเฉพาะของ
binary นี่ก็เป็นเหตุผลที่ string literal เป็น immutable — &str เป็น
immutable reference
String Slice เป็น Parameter
การรู้ว่าคุณรับ slice ของ literal และค่า String ได้ นำเราไปสู่การปรับ
ปรุงอีกอย่างใน first_word คือ signature ของมัน:
fn first_word(s: &String) -> &str {
Rustacean ที่มีประสบการณ์มากกว่าจะเขียน signature ที่แสดงใน Listing 4-9
แทน เพราะมันให้เราใช้ฟังก์ชันเดียวกันกับทั้งค่า &String และค่า &str
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole.
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s.
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or
// whole.
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
first_word โดยใช้ string slice เป็น type ของ parameter sถ้าเรามี string slice เราส่งตรง ๆ ได้ ถ้าเรามี String เราส่ง slice ของ
String หรือ reference ของ String ได้ ความยืดหยุ่นนี้ใช้ประโยชน์จาก
deref coercion ฟีเจอร์ที่เราจะครอบคลุมในส่วน
“ใช้ Deref Coercion ในฟังก์ชันและเมธอด”
ของบทที่ 15
การประกาศฟังก์ชันให้รับ string slice แทน reference ของ String ทำให้ API
ของเราทั่วไปและมีประโยชน์มากขึ้นโดยไม่เสียฟังก์ชันใด ๆ:
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// `first_word` works on slices of `String`s, whether partial or whole.
let word = first_word(&my_string[0..6]);
let word = first_word(&my_string[..]);
// `first_word` also works on references to `String`s, which are equivalent
// to whole slices of `String`s.
let word = first_word(&my_string);
let my_string_literal = "hello world";
// `first_word` works on slices of string literals, whether partial or
// whole.
let word = first_word(&my_string_literal[0..6]);
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
Slice อื่น ๆ
String slice อย่างที่คุณนึกได้ เฉพาะกับ string แต่ยังมี slice type ที่ ทั่วไปกว่าด้วย พิจารณา array นี้:
#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}
เหมือนที่เราอาจอยากอ้างถึงส่วนหนึ่งของ string เราอาจอยากอ้างถึงส่วนหนึ่ง ของ array เราทำได้แบบนี้:
#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
}
slice นี้มี type &[i32] มันทำงานแบบเดียวกับ string slice ทำ โดยเก็บ
reference ของ element แรกและความยาว คุณจะใช้ slice ชนิดนี้สำหรับ
collection อื่น ๆ ทุกประเภท เราจะพูดถึง collection เหล่านี้ในรายละเอียด
เมื่อเราพูดถึง vector ในบทที่ 8
สรุป
แนวคิดของ ownership, borrowing และ slice รับประกัน memory safety ใน โปรแกรม Rust ที่ compile time ภาษา Rust ให้คุณควบคุมการใช้หน่วยความจำใน แบบเดียวกับภาษา systems programming อื่น แต่การมี owner ของข้อมูล cleanup ข้อมูลอัตโนมัติเมื่อ owner ออกจาก scope หมายความว่าคุณไม่ต้องเขียนและ debug โค้ดเพิ่มเพื่อได้การควบคุมนี้
Ownership ส่งผลต่อวิธีที่หลายส่วนอื่นของ Rust ทำงาน เราจะพูดถึงแนวคิดเหล่า
นี้ต่อตลอดส่วนที่เหลือของหนังสือ มาไปบทที่ 5 และดูการรวมชิ้นข้อมูลเข้าด้วย
กันใน struct
ใช้ Struct จัดโครงสร้างข้อมูล
struct หรือ structure คือชนิดข้อมูลที่กำหนดเอง ที่ให้คุณห่อและตั้งชื่อ ค่าหลายค่าที่เกี่ยวข้องกันให้เป็นกลุ่มที่มีความหมาย ถ้าคุณคุ้นเคยกับภาษา object-oriented struct เหมือน data attribute ของ object ในบทนี้เราจะ เปรียบเทียบ tuple กับ struct เพื่อต่อยอดจากสิ่งที่คุณรู้แล้ว และแสดงเมื่อ struct เป็นวิธีจัดกลุ่มข้อมูลที่ดีกว่า
เราจะแสดงวิธีประกาศและสร้าง instance ของ struct เราจะพูดถึงวิธีประกาศ associated function โดยเฉพาะ associated function ชนิดที่เรียกว่า เมธอด (method) เพื่อระบุพฤติกรรมที่ผูกกับ struct type Struct และ enum (พูดถึง ในบทที่ 6) เป็น building block สำหรับสร้าง type ใหม่ใน domain ของโปรแกรม คุณ เพื่อใช้ประโยชน์เต็มที่จากการตรวจสอบ type ตอน compile time ของ Rust
การประกาศและสร้าง struct
การประกาศและสร้าง Struct
Struct คล้ายกับ tuple ที่พูดถึงในส่วน “Tuple Type” ตรงที่ทั้งคู่เก็บค่าหลายค่าที่เกี่ยวข้องกัน เช่นเดียวกับ tuple ชิ้นของ struct เป็น type ต่างกันได้ ต่างจาก tuple ใน struct คุณตั้งชื่อแต่ละชิ้น ข้อมูล จึงชัดเจนว่าค่าหมายถึงอะไร การเพิ่มชื่อเหล่านี้หมายความว่า struct ยืดหยุ่นกว่า tuple — คุณไม่ต้องพึ่งลำดับของข้อมูลเพื่อระบุหรือเข้าถึงค่า ของ instance
ในการประกาศ struct เราป้อน keyword struct แล้วตั้งชื่อ struct ทั้งหมด
ชื่อของ struct ควรอธิบายความสำคัญของชิ้นข้อมูลที่จัดกลุ่มเข้าด้วยกัน
จากนั้น ภายใน curly bracket เราประกาศชื่อและ type ของชิ้นข้อมูล ซึ่งเรา
เรียกว่า field เช่น Listing 5-1 แสดง struct ที่เก็บข้อมูลเกี่ยวกับ
บัญชีผู้ใช้
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {}
Userในการใช้ struct หลังประกาศแล้ว เราสร้าง instance ของ struct นั้นโดย
ระบุค่าที่เป็นรูปธรรมให้แต่ละ field เราสร้าง instance โดยระบุชื่อ struct
แล้วเพิ่ม curly bracket ที่มีคู่ key: value โดย key คือชื่อ field และ
value คือข้อมูลที่อยากเก็บใน field เหล่านั้น เราไม่ต้องระบุ field ในลำดับ
เดียวกับที่ประกาศใน struct พูดอีกอย่าง การประกาศ struct เหมือน template
ทั่วไปสำหรับ type และ instance เติม template นั้นด้วยข้อมูลเฉพาะเพื่อสร้าง
ค่าของ type เช่น เราประกาศผู้ใช้คนหนึ่งได้ตามที่แสดงใน Listing 5-2
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};
}
Userในการได้ค่าเฉพาะจาก struct เราใช้ dot notation เช่น ในการเข้าถึง email
ของผู้ใช้นี้ เราใช้ user1.email ถ้า instance เป็น mutable เราเปลี่ยน
ค่าได้โดยใช้ dot notation และ assign ลงใน field เฉพาะ Listing 5-3 แสดง
วิธีเปลี่ยนค่าใน field email ของ instance User ที่ mutable
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let mut user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};
user1.email = String::from("[email protected]");
}
email ของ instance Userหมายเหตุว่าทั้ง instance ต้องเป็น mutable — Rust ไม่ให้เรา mark แค่บาง field เป็น mutable เช่นเดียวกับ expression ใด ๆ เราสร้าง instance ใหม่ ของ struct เป็น expression สุดท้ายใน body ของฟังก์ชันได้ เพื่อ implicitly return instance ใหม่นั้น
Listing 5-4 แสดงฟังก์ชัน build_user ที่ return instance User ด้วย
email และ username ที่ให้มา field active ได้ค่า true และ
sign_in_count ได้ค่า 1
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username: username,
email: email,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("[email protected]"),
String::from("someusername123"),
);
}
build_user ที่รับ email และ username แล้ว return instance Userมันสมเหตุสมผลที่จะตั้งชื่อ parameter ของฟังก์ชันให้เหมือนกับชื่อ field
ของ struct แต่การต้องเขียนชื่อ field email และ username และตัวแปร
ซ้ำน่าเบื่อนิดหน่อย ถ้า struct มี field มากขึ้น การเขียนชื่อแต่ละตัวซ้ำ
จะยิ่งน่ารำคาญ โชคดีมี shorthand ที่สะดวก!
ใช้ Field Init Shorthand
เพราะชื่อ parameter และชื่อ field ของ struct เหมือนกันเป๊ะใน Listing 5-4
เราใช้ syntax field init shorthand เขียน build_user ใหม่ได้ ให้มัน
ทำงานเหมือนกันเป๊ะ แต่ไม่มีการเขียน username และ email ซ้ำ ดังที่
แสดงใน Listing 5-5
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn build_user(email: String, username: String) -> User {
User {
active: true,
username,
email,
sign_in_count: 1,
}
}
fn main() {
let user1 = build_user(
String::from("[email protected]"),
String::from("someusername123"),
);
}
build_user ที่ใช้ field init shorthand เพราะ parameter username และ email มีชื่อเหมือน field ของ structที่นี่ เรากำลังสร้าง instance ใหม่ของ struct User ซึ่งมี field ชื่อ
email เราอยาก set ค่าของ field email เป็นค่าใน parameter email ของ
ฟังก์ชัน build_user เพราะ field email และ parameter email มีชื่อ
เหมือนกัน เราเขียนแค่ email แทน email: email
สร้าง Instance ด้วย Struct Update Syntax
บ่อยครั้งที่มีประโยชน์ในการสร้าง instance ใหม่ของ struct ที่มีค่าส่วนใหญ่ จาก instance อื่นของ type เดียวกัน แต่เปลี่ยนบางค่า คุณทำได้โดยใช้ struct update syntax
ขั้นแรก ใน Listing 5-6 เราแสดงวิธีสร้าง instance User ใหม่ใน user2
ในแบบปกติ โดยไม่มี update syntax เรา set ค่าใหม่สำหรับ email แต่ใช้ค่า
เดิมจาก user1 ที่เราสร้างใน Listing 5-2 สำหรับที่เหลือ
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("[email protected]"),
sign_in_count: user1.sign_in_count,
};
}
User ใหม่ที่ใช้ค่าทั้งหมดยกเว้นหนึ่งจาก user1ใช้ struct update syntax เราได้ผลแบบเดียวกันด้วยโค้ดน้อยกว่า ดังที่แสดง
ใน Listing 5-7 syntax .. ระบุว่า field ที่เหลือที่ไม่ set แบบ explicit
ควรมีค่าเหมือน field ใน instance ที่ให้
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// --snip--
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
let user2 = User {
email: String::from("[email protected]"),
..user1
};
}
email ใหม่สำหรับ instance User แต่ใช้ค่าที่เหลือจาก user1โค้ดใน Listing 5-7 ก็สร้าง instance ใน user2 ที่มีค่าต่างสำหรับ email
แต่มีค่าเหมือนสำหรับ field username, active และ sign_in_count จาก
user1 ..user1 ต้องมาท้ายสุด เพื่อระบุว่า field ที่เหลือใด ๆ ควรได้ค่า
จาก field ที่สอดคล้องใน user1 แต่เราเลือกระบุค่าสำหรับ field กี่ตัวก็ได้
ในลำดับใดก็ได้ ไม่ว่าลำดับของ field ใน struct จะเป็นอย่างไร
หมายเหตุว่า struct update syntax ใช้ = เหมือน assignment — นี่เพราะมัน
move ข้อมูล เหมือนที่เราเห็นในส่วน
“ตัวแปรและข้อมูลโต้ตอบกันด้วย Move” ในตัวอย่างนี้
เราใช้ user1 ไม่ได้แล้วหลังสร้าง user2 เพราะ String ใน field
username ของ user1 ถูก move เข้า user2 ถ้าเราให้ user2 ค่า
String ใหม่ทั้ง email และ username และจึงใช้แค่ค่า active และ
sign_in_count จาก user1 user1 จะยัง valid หลังสร้าง user2 ทั้ง
active และ sign_in_count เป็น type ที่ implement trait Copy ดังนั้น
พฤติกรรมที่เราพูดถึงในส่วน
“ข้อมูลที่อยู่บน Stack เท่านั้น: Copy” ใช้ได้ เรา
ยังใช้ user1.email ในตัวอย่างนี้ได้ เพราะค่าของมันไม่ถูก move ออกจาก
user1
สร้าง Type ต่างกันด้วย Tuple Struct
Rust ยังรองรับ struct ที่ดูคล้าย tuple เรียกว่า tuple struct Tuple struct มีความหมายเพิ่มที่ชื่อ struct ให้ แต่ไม่มีชื่อผูกกับ field — มี แค่ type ของ field Tuple struct มีประโยชน์เมื่อคุณอยากให้ทั้ง tuple มี ชื่อ และทำให้ tuple เป็น type ต่างจาก tuple อื่น และเมื่อการตั้งชื่อแต่ละ field เหมือน struct ปกติจะยาวหรือซ้ำ
ในการประกาศ tuple struct เริ่มด้วย keyword struct และชื่อ struct ตาม
ด้วย type ใน tuple เช่น ที่นี่เราประกาศและใช้ tuple struct สองตัวชื่อ
Color และ Point:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}
หมายเหตุว่าค่า black และ origin เป็น type ต่างกัน เพราะเป็น instance
ของ tuple struct ต่างกัน แต่ละ struct ที่คุณประกาศเป็น type ของตัวมันเอง
แม้ field ภายใน struct อาจมี type เดียวกัน เช่น ฟังก์ชันที่รับ parameter
type Color รับ Point เป็น argument ไม่ได้ แม้ทั้งสอง type ประกอบจาก
ค่า i32 สามตัว ไม่อย่างนั้น instance ของ tuple struct คล้าย tuple ตรงที่
คุณ destructure เป็นชิ้น ๆ ได้ และใช้ . ตามด้วย index เพื่อเข้าถึงค่า
แต่ละค่าได้ ต่างจาก tuple ตรงที่ tuple struct บังคับให้คุณตั้งชื่อ type
ของ struct เมื่อ destructure เช่น เราเขียน let Point(x, y, z) = origin;
เพื่อ destructure ค่าใน origin point เข้าตัวแปรชื่อ x, y และ z
ประกาศ Unit-Like Struct
คุณยังประกาศ struct ที่ไม่มี field ใด ๆ ได้! เหล่านี้เรียกว่า unit-like
struct เพราะพฤติกรรมคล้าย () ที่เป็น unit type ที่เราเอ่ยใน
“Tuple Type” Unit-like struct มีประโยชน์เมื่อ
คุณต้อง implement trait บน type บางตัว แต่ไม่มีข้อมูลที่อยากเก็บใน type
เอง เราจะพูดถึง trait ในบทที่ 10 นี่คือตัวอย่างการประกาศและสร้าง
instance ของ unit struct ชื่อ AlwaysEqual:
struct AlwaysEqual;
fn main() {
let subject = AlwaysEqual;
}
ในการประกาศ AlwaysEqual เราใช้ keyword struct ชื่อที่อยากได้ แล้ว
semicolon ไม่ต้องใช้ curly bracket หรือวงเล็บ! จากนั้นเราได้ instance
ของ AlwaysEqual ในตัวแปร subject แบบเดียวกัน — ใช้ชื่อที่เราประกาศ
ไว้ โดยไม่มี curly bracket หรือวงเล็บ ลองนึกว่าทีหลังเราจะ implement
พฤติกรรมสำหรับ type นี้ ว่าทุก instance ของ AlwaysEqual เท่ากับทุก
instance ของ type อื่นใดเสมอ บางทีเพื่อมีผลที่รู้สำหรับจุดประสงค์การ
ทดสอบ เราไม่ต้องการข้อมูลใด ๆ เพื่อ implement พฤติกรรมนั้น! คุณจะเห็น
ในบทที่ 10 วิธีประกาศ trait และ implement บน type ใด ๆ รวมถึง unit-like
struct
Ownership ของข้อมูล Struct
ในการประกาศ struct User ใน Listing 5-1 เราใช้ type String ที่ owned
แทน type &str ที่เป็น string slice นี่เป็นตัวเลือกที่ตั้งใจ เพราะเรา
อยากให้แต่ละ instance ของ struct นี้ own ข้อมูลทั้งหมดของมัน และให้ข้อมูล
นั้น valid ตราบที่ทั้ง struct valid
เป็นไปได้ที่ struct จะเก็บ reference ของข้อมูลที่ owned โดยอย่างอื่น แต่การทำเช่นนั้นต้องใช้ lifetime ฟีเจอร์ของ Rust ที่เราจะพูดถึงในบทที่ 10 Lifetime รับประกันว่าข้อมูลที่ struct อ้างถึง valid ตราบที่ struct valid ลองว่าคุณพยายามเก็บ reference ใน struct โดยไม่ระบุ lifetime ดัง ต่อไปนี้ใน src/main.rs — มันจะไม่ทำงาน:
struct User {
active: bool,
username: &str,
email: &str,
sign_in_count: u64,
}
fn main() {
let user1 = User {
active: true,
username: "someusername123",
email: "[email protected]",
sign_in_count: 1,
};
}
Compiler จะบ่นว่าต้อง lifetime specifier:
$ cargo run
Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
--> src/main.rs:3:15
|
3 | username: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 ~ username: &'a str,
|
error[E0106]: missing lifetime specifier
--> src/main.rs:4:12
|
4 | email: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | active: bool,
3 | username: &str,
4 ~ email: &'a str,
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` (bin "structs") due to 2 previous errors
ในบทที่ 10 เราจะพูดถึงวิธีแก้ error เหล่านี้ เพื่อให้คุณเก็บ reference
ใน struct ได้ แต่ตอนนี้เราจะแก้ error แบบนี้โดยใช้ type ที่ owned อย่าง
String แทน reference อย่าง &str
ตัวอย่างโปรแกรมที่ใช้ struct
ตัวอย่างโปรแกรมที่ใช้ 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 ของเรา
เมธอด
เมธอด
เมธอดคล้ายกับฟังก์ชัน — เราประกาศพวกมันด้วย keyword fn และชื่อ พวกมัน
มี parameter และ return value ได้ และมีโค้ดที่รันเมื่อเมธอดถูกเรียกจาก
ที่อื่น ต่างจากฟังก์ชัน เมธอดถูกประกาศภายในบริบทของ struct (หรือ enum
หรือ trait object ซึ่งเราครอบคลุมใน บทที่ 6 และ
บทที่ 18 ตามลำดับ) และ parameter แรกของ
พวกมันคือ self เสมอ ซึ่งแทน instance ของ struct ที่เมธอดถูกเรียกใน
Method Syntax
มาเปลี่ยนฟังก์ชัน area ที่มี instance Rectangle เป็น parameter และ
แทนนั้นทำเป็นเมธอด area ที่ประกาศบน struct Rectangle ดังที่แสดงใน
Listing 5-13
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
area บน struct Rectangleในการประกาศฟังก์ชันภายในบริบทของ Rectangle เราเริ่ม block impl
(implementation) สำหรับ Rectangle ทุกอย่างภายใน block impl นี้จะ
ผูกกับ type Rectangle จากนั้นเราย้ายฟังก์ชัน area เข้า curly bracket
ของ impl และเปลี่ยน parameter แรก (และในกรณีนี้คือเดียว) ให้เป็น
self ใน signature และทุกที่ภายใน body ใน main ที่เราเรียกฟังก์ชัน
area และส่ง rect1 เป็น argument เราใช้ method syntax เรียกเมธอด
area บน instance Rectangle ของเราได้แทน method syntax ไปหลัง instance
— เราเพิ่ม dot ตามด้วยชื่อเมธอด วงเล็บ และ argument ใด ๆ
ใน signature ของ area เราใช้ &self แทน rectangle: &Rectangle
&self จริง ๆ เป็น shorthand ของ self: &Self ภายใน block impl type
Self เป็น alias ของ type ที่ block impl เป็น เมธอดต้องมี parameter
ชื่อ self ของ type Self เป็น parameter แรก Rust จึงให้คุณย่อด้วยแค่
ชื่อ self ในตำแหน่ง parameter แรก หมายเหตุว่าเรายังต้องใช้ & หน้า
shorthand self เพื่อบ่งบอกว่าเมธอดนี้ borrow instance Self เหมือนที่
เราทำใน rectangle: &Rectangle เมธอดรับ ownership ของ self, borrow
self แบบ immutable อย่างที่เราทำที่นี่, หรือ borrow self แบบ mutable
ได้ เหมือนที่ทำกับ parameter อื่นใด
เราเลือก &self ที่นี่ด้วยเหตุผลเดียวกับที่เราใช้ &Rectangle ใน version
ฟังก์ชัน — เราไม่อยากรับ ownership และเราแค่อยากอ่านข้อมูลใน struct ไม่
ใช่เขียน ถ้าเราอยากเปลี่ยน instance ที่เราเรียกเมธอดด้วยเป็นส่วนหนึ่งของ
สิ่งที่เมธอดทำ เราจะใช้ &mut self เป็น parameter แรก การมีเมธอดที่รับ
ownership ของ instance โดยใช้แค่ self เป็น parameter แรกเป็นเรื่องหา
ยาก เทคนิคนี้มักใช้เมื่อเมธอด transform self เป็นอย่างอื่น และคุณอยาก
ป้องกัน caller ใช้ instance เดิมหลัง transformation
เหตุผลหลักของการใช้เมธอดแทนฟังก์ชัน นอกจากให้ method syntax และไม่ต้อง
เขียน type ของ self ซ้ำใน signature ของทุกเมธอด คือเพื่อการจัดระเบียบ
เราใส่ทุกสิ่งที่เราทำได้กับ instance ของ type ใน block impl เดียว แทน
การให้ user ในอนาคตของโค้ดเราหา capability ของ Rectangle ในที่ต่าง ๆ
ใน library ที่เราให้
หมายเหตุว่าเราเลือกให้เมธอดมีชื่อเหมือนหนึ่งใน field ของ struct ได้ เช่น
เราประกาศเมธอดบน Rectangle ที่ชื่อ width ด้วยได้:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}
ที่นี่ เราเลือกให้เมธอด width return true ถ้าค่าใน field width ของ
instance มากกว่า 0 และ false ถ้าค่าเป็น 0 — เราใช้ field ภายในเมธอด
ที่ชื่อเดียวกันได้สำหรับจุดประสงค์ใด ๆ ใน main เมื่อเราตาม rect1.width
ด้วยวงเล็บ Rust รู้ว่าเราหมายถึงเมธอด width เมื่อเราไม่ใช้วงเล็บ Rust
รู้ว่าเราหมายถึง field width
บ่อยครั้ง แต่ไม่เสมอ เมื่อเราให้เมธอดมีชื่อเหมือน field เราอยากให้มัน return แค่ค่าใน field และไม่ทำอะไรอื่น เมธอดแบบนี้เรียกว่า getter และ Rust ไม่ implement พวกมันอัตโนมัติสำหรับ field ของ struct เหมือนภาษาอื่น บางตัวทำ Getter มีประโยชน์เพราะคุณทำให้ field เป็น private ได้ แต่เมธอด เป็น public และจึงเปิดทางให้เข้าถึงแบบ read-only ของ field นั้นเป็นส่วน หนึ่งของ public API ของ type เราจะพูดถึงว่า public และ private คืออะไร และวิธีกำหนด field หรือเมธอดเป็น public หรือ private ใน บทที่ 7
Operator -> อยู่ไหน?
ใน C และ C++ มี operator ต่างกันสองตัวที่ใช้เรียกเมธอด — คุณใช้ . ถ้า
กำลังเรียกเมธอดบน object ตรง ๆ และ -> ถ้ากำลังเรียกเมธอดบน pointer
ของ object และต้อง dereference pointer ก่อน พูดอีกอย่าง ถ้า object
เป็น pointer object->something() คล้ายกับ (*object).something()
Rust ไม่มีสิ่งเทียบเท่า operator -> แทนนั้น Rust มีฟีเจอร์ชื่อ
automatic referencing และ dereferencing การเรียกเมธอดเป็นหนึ่งใน
ไม่กี่ที่ใน Rust ที่มีพฤติกรรมนี้
นี่คือวิธีที่มันทำงาน — เมื่อคุณเรียกเมธอดด้วย object.something()
Rust เพิ่ม &, &mut หรือ * อัตโนมัติ เพื่อให้ object match
signature ของเมธอด พูดอีกอย่าง สิ่งต่อไปนี้เหมือนกัน:
#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
x: f64,
y: f64,
}
impl Point {
fn distance(&self, other: &Point) -> f64 {
let x_squared = f64::powi(other.x - self.x, 2);
let y_squared = f64::powi(other.y - self.y, 2);
f64::sqrt(x_squared + y_squared)
}
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}
ตัวแรกดูสะอาดกว่ามาก พฤติกรรมการ reference อัตโนมัตินี้ทำงานได้ เพราะ
เมธอดมี receiver ที่ชัดเจน — type ของ self เมื่อมี receiver และชื่อ
ของเมธอด Rust รู้แน่นอนได้ว่าเมธอดกำลังอ่าน (&self), mutate
(&mut self) หรือ consume (self) ข้อเท็จจริงที่ว่า Rust ทำให้
borrowing เป็น implicit สำหรับ method receiver เป็นส่วนใหญ่ที่ทำให้
ownership ใช้สะดวกในทางปฏิบัติ
เมธอดที่มี Parameter เพิ่มเติม
มาฝึกใช้เมธอดโดย implement เมธอดที่สองบน struct Rectangle ครั้งนี้เรา
อยากให้ instance ของ Rectangle รับ instance อื่นของ Rectangle แล้ว
return true ถ้า Rectangle ที่สองพอดีอย่างสมบูรณ์ภายใน self
(Rectangle แรก) ไม่อย่างนั้นควร return false นั่นคือ เมื่อเราประกาศ
เมธอด can_hold แล้ว เราอยากเขียนโปรแกรมที่แสดงใน Listing 5-14 ได้
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
can_hold ที่ยังไม่เขียนOutput ที่คาดจะหน้าตาเป็นต่อไปนี้ เพราะมิติของ rect2 ทั้งคู่เล็กกว่า
มิติของ rect1 แต่ rect3 กว้างกว่า rect1:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
เรารู้ว่าอยากประกาศเมธอด มันจึงอยู่ใน block impl Rectangle ชื่อเมธอด
จะเป็น can_hold และจะรับ immutable borrow ของ Rectangle อีกตัวเป็น
parameter เราบอกได้ว่า type ของ parameter จะเป็นอะไรโดยดูที่โค้ดที่เรียก
เมธอด — rect1.can_hold(&rect2) ส่ง &rect2 ซึ่งเป็น immutable borrow
ของ rect2, instance ของ Rectangle นี่สมเหตุสมผล เพราะเราต้องการแค่
อ่าน rect2 (แทนการเขียน ซึ่งจะหมายความว่าเราต้องการ mutable borrow)
และเราอยากให้ main เก็บ ownership ของ rect2 ไว้ เพื่อเราใช้ได้อีกหลัง
เรียกเมธอด can_hold Return value ของ can_hold จะเป็น Boolean และ
implementation จะเช็คว่า width และ height ของ self มากกว่า width และ
height ของ Rectangle อีกตัวตามลำดับ มาเพิ่มเมธอด can_hold ใหม่ใน
block impl จาก Listing 5-13 แสดงใน Listing 5-15
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
can_hold บน Rectangle ที่รับ instance Rectangle อีกตัวเป็น parameterเมื่อเรารันโค้ดนี้ด้วยฟังก์ชัน main ใน Listing 5-14 เราจะได้ output ที่
ต้องการ เมธอดรับ parameter หลายตัวที่เราเพิ่มใน signature หลัง parameter
self ได้ และ parameter เหล่านั้นทำงานเหมือน parameter ในฟังก์ชัน
Associated Function
ฟังก์ชันทั้งหมดที่ประกาศภายใน block impl เรียกว่า associated function
เพราะพวกมันผูกกับ type ที่ตั้งชื่อหลัง impl เราประกาศ associated function
ที่ไม่มี self เป็น parameter แรกได้ (และจึงไม่ใช่เมธอด) เพราะพวกมันไม่
ต้องการ instance ของ type ที่จะทำงานด้วย เราใช้ฟังก์ชันแบบนี้มาแล้ว — ฟังก์ชัน
String::from ที่ประกาศบน type String
Associated function ที่ไม่ใช่เมธอด มักใช้สำหรับ constructor ที่จะ return
instance ใหม่ของ struct มักเรียกว่า new แต่ new ไม่ใช่ชื่อพิเศษและไม่
ได้ built-in ในภาษา เช่น เราเลือกให้ associated function ชื่อ square ที่
มี parameter มิติหนึ่งตัว และใช้นั้นเป็นทั้ง width และ height จึงทำให้ง่าย
ขึ้นที่จะสร้าง Rectangle รูปสี่เหลี่ยมจัตุรัส แทนการต้องระบุค่าเดียวกัน
สองครั้ง:
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3);
}
Keyword Self ใน return type และใน body ของฟังก์ชัน เป็น alias ของ type
ที่ปรากฏหลัง keyword impl ซึ่งในกรณีนี้คือ Rectangle
ในการเรียก associated function นี้ เราใช้ syntax :: กับชื่อ struct —
let sq = Rectangle::square(3); เป็นตัวอย่าง ฟังก์ชันนี้อยู่ใน namespace
ของ struct — syntax :: ใช้สำหรับทั้ง associated function และ namespace
ที่สร้างโดย module เราจะพูดถึง module ใน บทที่ 7
Block impl หลายตัว
แต่ละ struct อนุญาตให้มี block impl หลายตัว เช่น Listing 5-15 เทียบเท่า
กับโค้ดที่แสดงใน Listing 5-16 ซึ่งมีแต่ละเมธอดใน block impl ของตัวเอง
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
impl หลายตัวไม่มีเหตุผลที่จะแยกเมธอดเหล่านี้เป็น block impl หลายตัวที่นี่ แต่นี่เป็น
syntax ที่ valid เราจะเห็นกรณีที่ block impl หลายตัวมีประโยชน์ในบทที่
10 ที่เราพูดถึง generic type และ trait
สรุป
Struct ให้คุณสร้าง type custom ที่มีความหมายสำหรับ domain ของคุณ ด้วยการ
ใช้ struct คุณเก็บชิ้นข้อมูลที่ผูกกันให้เชื่อมโยงกัน และตั้งชื่อแต่ละชิ้น
เพื่อทำให้โค้ดของคุณชัดเจน ใน block impl คุณประกาศฟังก์ชันที่ผูกกับ
type ของคุณได้ และเมธอดเป็น associated function ชนิดหนึ่งที่ให้คุณระบุ
พฤติกรรมที่ instance ของ struct ของคุณมี
แต่ struct ไม่ใช่วิธีเดียวที่คุณสร้าง type custom ได้ — มาดูฟีเจอร์ enum ของ Rust เพื่อเพิ่มเครื่องมืออีกตัวให้กล่องเครื่องมือของคุณ
Enum และ Pattern Matching
ในบทนี้เราจะดู enumeration หรือที่เรียกว่า enum Enum ให้คุณประกาศ type
โดยระบุ variant ที่เป็นไปได้ของมัน ก่อนอื่นเราจะประกาศและใช้ enum เพื่อ
แสดงว่า enum encode ความหมายพร้อมข้อมูลได้อย่างไร ถัดไปเราจะสำรวจ enum ที่
มีประโยชน์เป็นพิเศษ ชื่อ Option ซึ่งแสดงว่าค่าเป็นบางอย่างหรือไม่มีอะไร
ก็ได้ จากนั้นเราจะดูว่า pattern matching ใน match expression ทำให้ง่าย
ที่จะรันโค้ดต่างกันสำหรับค่าต่างกันของ enum ยังไง สุดท้ายเราจะครอบคลุมว่า
โครงสร้าง if let เป็นอีก idiom ที่สะดวกและกระชับสำหรับจัดการ enum ใน
โค้ดของคุณ
การประกาศ enum
ประกาศ Enum
ที่ struct ให้คุณวิธีจัดกลุ่ม field และข้อมูลที่ผูกกัน อย่าง Rectangle
กับ width และ height ของมัน enum ให้คุณวิธีบอกว่าค่าเป็นหนึ่งในชุดของ
ค่าที่เป็นไปได้ เช่น เราอาจอยากบอกว่า Rectangle เป็นหนึ่งในชุดของรูปร่าง
ที่เป็นไปได้ ที่รวม Circle และ Triangle ด้วย ในการทำสิ่งนี้ Rust ให้
เรา encode ความเป็นไปได้เหล่านี้เป็น enum
มาดูสถานการณ์ที่เราอาจอยากแสดงในโค้ด และดูว่าทำไม enum มีประโยชน์และเหมาะ สมกว่า struct ในกรณีนี้ สมมติว่าเราต้องทำงานกับ IP address ปัจจุบันมี มาตรฐานหลักสองตัวที่ใช้สำหรับ IP address — version สี่และ version หก เพราะ นี่คือความเป็นไปได้เดียวสำหรับ IP address ที่โปรแกรมเราจะเจอ เรา enumerate variant ที่เป็นไปได้ทั้งหมดได้ ซึ่งเป็นที่มาของชื่อ enumeration
IP address ใด ๆ เป็นได้ทั้ง version สี่หรือ version หก แต่ไม่ใช่ทั้งคู่ ในเวลาเดียวกัน คุณสมบัตินั้นของ IP address ทำให้โครงสร้างข้อมูล enum เหมาะสม เพราะค่า enum เป็นได้แค่หนึ่งใน variant ของมัน ทั้ง address version สี่และ version หกยังเป็น IP address โดยพื้นฐาน ดังนั้นพวกมัน ควรถูกปฏิบัติเป็น type เดียวเมื่อโค้ดจัดการสถานการณ์ที่ใช้กับ IP address ชนิดใดก็ได้
เราแสดงแนวคิดนี้ในโค้ดได้ โดยประกาศ enumeration IpAddrKind และระบุชนิด
ที่เป็นไปได้ที่ IP address เป็น V4 และ V6 เหล่านี้คือ variant ของ
enum:
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
IpAddrKind ตอนนี้เป็น type custom data type ที่เราใช้ที่อื่นในโค้ดได้
ค่า Enum
เราสร้าง instance ของแต่ละ variant ทั้งสองของ IpAddrKind ได้แบบนี้:
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
หมายเหตุว่า variant ของ enum อยู่ใน namespace ภายใต้ identifier ของมัน และ
เราใช้ double colon คั่นทั้งสอง นี่มีประโยชน์ เพราะตอนนี้ทั้งค่า
IpAddrKind::V4 และ IpAddrKind::V6 เป็น type เดียวกัน — IpAddrKind
เราจึงประกาศฟังก์ชันที่รับ IpAddrKind ใด ๆ ได้ เช่น:
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
และเราเรียกฟังก์ชันนี้ด้วย variant ใดก็ได้:
enum IpAddrKind {
V4,
V6,
}
fn main() {
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
route(IpAddrKind::V4);
route(IpAddrKind::V6);
}
fn route(ip_kind: IpAddrKind) {}
การใช้ enum มีข้อดีอีกมาก คิดเพิ่มเกี่ยวกับ type IP address ของเรา ตอนนี้ เราไม่มีวิธีเก็บ ข้อมูล IP address จริง ๆ — เรารู้แค่ว่ามันเป็น ชนิด อะไร เมื่อคุณเพิ่งเรียนเรื่อง struct ในบทที่ 5 คุณอาจอยากจัดการปัญหานี้ ด้วย struct ดังที่แสดงใน Listing 6-1
fn main() {
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind,
address: String,
}
let home = IpAddr {
kind: IpAddrKind::V4,
address: String::from("127.0.0.1"),
};
let loopback = IpAddr {
kind: IpAddrKind::V6,
address: String::from("::1"),
};
}
IpAddrKind ของ IP address โดยใช้ structที่นี่ เราประกาศ struct IpAddr ที่มีสอง field — field kind ที่เป็น
type IpAddrKind (enum ที่เราประกาศก่อนหน้า) และ field address ที่เป็น
type String เรามีสอง instance ของ struct นี้ ตัวแรกคือ home และมีค่า
IpAddrKind::V4 เป็น kind พร้อมข้อมูล address ที่ผูกอยู่คือ 127.0.0.1
instance ที่สองคือ loopback มี variant อื่นของ IpAddrKind เป็นค่า
kind คือ V6 และมี address ::1 ผูกอยู่ เราใช้ struct ห่อค่า kind
และ address เข้าด้วยกัน ดังนั้นตอนนี้ variant ผูกกับค่า
อย่างไรก็ตาม การแสดงแนวคิดเดียวกันโดยใช้แค่ enum กระชับกว่า — แทนที่จะมี
enum ภายใน struct เราใส่ข้อมูลลงในแต่ละ variant ของ enum ตรง ๆ ได้
การประกาศ enum IpAddr ใหม่นี้บอกว่าทั้ง variant V4 และ V6 จะมีค่า
String ผูกอยู่:
fn main() {
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
}
เราติดข้อมูลกับแต่ละ variant ของ enum ตรง ๆ ดังนั้นไม่ต้องมี struct เพิ่ม
ที่นี่ยังเห็นรายละเอียดอีกอย่างของวิธีที่ enum ทำงานง่ายขึ้น — ชื่อของ
แต่ละ variant ของ enum ที่เราประกาศ ยังกลายเป็นฟังก์ชันที่สร้าง instance
ของ enum นั่นคือ IpAddr::V4() เป็นการเรียกฟังก์ชันที่รับ argument
String แล้ว return instance ของ type IpAddr เราได้ฟังก์ชัน constructor
นี้อัตโนมัติเป็นผลของการประกาศ enum
มีข้อดีอีกอย่างของการใช้ enum แทน struct — แต่ละ variant มี type และจำนวน
ข้อมูลที่ผูกต่างกันได้ Address IP version สี่จะมี component ตัวเลขสี่ตัว
ที่มีค่าระหว่าง 0 ถึง 255 เสมอ ถ้าเราอยากเก็บ address V4 เป็นค่า u8
สี่ตัว แต่ยังแสดง address V6 เป็นค่า String หนึ่งค่า เราทำไม่ได้กับ
struct Enum จัดการกรณีนี้ได้สบาย ๆ:
fn main() {
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
}
เราแสดงหลายวิธีในการประกาศโครงสร้างข้อมูลเพื่อเก็บ IP address version สี่
และ version หก อย่างไรก็ตาม ปรากฏว่าการอยากเก็บ IP address และ encode
ชนิดเป็นเรื่องใช้บ่อยมาก standard library มี definition ที่เราใช้ได้!
มาดูว่า standard library ประกาศ IpAddr อย่างไร มันมี enum และ variant
เป๊ะ ๆ เหมือนที่เราประกาศและใช้ แต่ฝัง address data ภายใน variant ในรูป
ของ struct สองตัวที่ต่างกัน ซึ่งประกาศต่างกันสำหรับแต่ละ variant:
#![allow(unused)]
fn main() {
struct Ipv4Addr {
// --snip--
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
}
โค้ดนี้แสดงว่าคุณใส่ข้อมูลชนิดใดก็ได้ภายใน variant ของ enum — string, type ตัวเลข หรือ struct เป็นต้น คุณรวม enum อื่นเข้าไปก็ยังได้! standard library type ก็มักไม่ซับซ้อนกว่าสิ่งที่คุณคิดได้เองมากนัก
หมายเหตุว่าแม้ standard library จะมี definition ของ IpAddr เรายังสร้าง
และใช้ definition ของเราเองได้โดยไม่ขัดกัน เพราะเราไม่ได้นำ definition
ของ standard library เข้า scope เราจะพูดถึงการนำ type เข้า scope มากขึ้น
ในบทที่ 7
มาดูตัวอย่างอีกตัวอย่างของ enum ใน Listing 6-2 — ตัวนี้มี type หลากหลาย ฝังใน variant
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {}
Message ที่แต่ละ variant เก็บจำนวนและ type ของค่าต่างกันenum นี้มีสี่ variant ที่มี type ต่างกัน:
Quit: ไม่มีข้อมูลผูกอยู่เลยMove: มี field ที่ตั้งชื่อ เหมือน struct ทำWrite: รวมStringหนึ่งตัวChangeColor: รวมค่าi32สามตัว
การประกาศ enum ที่มี variant อย่างใน Listing 6-2 คล้ายกับการประกาศ struct
หลายชนิด ยกเว้น enum ไม่ใช้ keyword struct และ variant ทั้งหมดถูกจัด
กลุ่มภายใต้ type Message struct ต่อไปนี้จะเก็บข้อมูลเดียวกับที่ variant
ของ enum ก่อนหน้าเก็บ:
struct QuitMessage; // unit struct
struct MoveMessage {
x: i32,
y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct
fn main() {}
แต่ถ้าเราใช้ struct ต่างกัน ที่แต่ละตัวมี type ของตนเอง เราจะไม่สามารถ
ประกาศฟังก์ชันให้รับ message ชนิดใด ๆ เหล่านี้ได้ง่ายเท่า เหมือนที่เราทำ
ได้กับ enum Message ที่ประกาศใน Listing 6-2 ซึ่งเป็น type เดียว
มีอีกความคล้ายระหว่าง enum และ struct — เหมือนที่เราประกาศเมธอดบน struct
ได้ด้วย impl เราประกาศเมธอดบน enum ได้ด้วย นี่คือเมธอดชื่อ call ที่
เราประกาศบน enum Message ของเราได้:
fn main() {
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// method body would be defined here
}
}
let m = Message::Write(String::from("hello"));
m.call();
}
Body ของเมธอดจะใช้ self เพื่อรับค่าที่เราเรียกเมธอด ในตัวอย่างนี้ เรา
สร้างตัวแปร m ที่มีค่า Message::Write(String::from("hello")) และนั่น
คือสิ่งที่ self จะเป็นใน body ของเมธอด call เมื่อ m.call() รัน
มาดู enum อีกตัวใน standard library ที่ใช้บ่อยและมีประโยชน์มาก — Option
Enum Option
ส่วนนี้สำรวจ case study ของ Option ซึ่งเป็น enum อีกตัวที่ประกาศโดย
standard library Type Option encode สถานการณ์ที่ใช้บ่อยมาก ที่ค่าอาจ
เป็นบางอย่าง หรืออาจไม่มีอะไร
เช่น ถ้าคุณขอ item แรกใน list ที่ไม่ว่าง คุณจะได้ค่า ถ้าคุณขอ item แรกใน list ที่ว่าง คุณจะไม่ได้อะไร การแสดงแนวคิดนี้ในรูปของระบบ type หมายความว่า compiler เช็คได้ว่าคุณจัดการทุก case ที่ควรจัดการหรือยัง — functionality นี้ป้องกัน bug ที่ใช้บ่อยมากในภาษาโปรแกรมอื่นได้
การออกแบบภาษาโปรแกรมมักคิดในแง่ของฟีเจอร์ที่คุณรวม แต่ฟีเจอร์ที่คุณยกเว้น ก็สำคัญ Rust ไม่มีฟีเจอร์ null ที่ภาษาอื่นหลายภาษามี Null คือค่าที่ หมายถึงไม่มีค่าที่นั่น ในภาษาที่มี null ตัวแปรอยู่ในหนึ่งในสอง state เสมอ — null หรือไม่ null
ในการนำเสนอปี 2009 “Null References: The Billion Dollar Mistake” Tony Hoare ผู้คิดค้น null บอกไว้:
ผมเรียกมันว่าความผิดพลาดมูลค่าพันล้านดอลลาร์ของผม ในตอนนั้น ผมกำลังออกแบบ ระบบ type ที่ครอบคลุมตัวแรกสำหรับ reference ในภาษา object-oriented เป้า หมายของผมคือรับประกันว่าการใช้ reference ทุกครั้งต้องปลอดภัยอย่างสมบูรณ์ ด้วยการเช็คที่ทำโดย compiler อัตโนมัติ แต่ผมต้านทานความล่อใจไม่ได้ที่จะ ใส่ null reference เพียงเพราะมัน implement ง่ายมาก สิ่งนี้นำไปสู่ error, vulnerability และ system crash นับไม่ถ้วน ซึ่งอาจทำให้เกิดความเจ็บปวด และความเสียหายมูลค่าพันล้านดอลลาร์ในสี่สิบปีที่ผ่านมา
ปัญหากับค่า null คือ ถ้าคุณพยายามใช้ค่า null เป็นค่าที่ไม่ใช่ null คุณจะ ได้ error บางชนิด เพราะคุณสมบัติ null หรือไม่ null นี้แพร่ขยายไปทั่ว มัน ง่ายมากที่จะทำ error แบบนี้
อย่างไรก็ตาม แนวคิดที่ null พยายามแสดงยังมีประโยชน์ — null คือค่าที่ตอนนี้ invalid หรือไม่มีด้วยเหตุผลบางอย่าง
ปัญหาไม่ใช่อยู่ที่แนวคิดจริง ๆ แต่อยู่ที่ implementation เฉพาะ ดังนั้น
Rust ไม่มี null แต่มี enum ที่ encode แนวคิดของค่าที่มีอยู่หรือไม่มีได้
enum นี้คือ Option<T> และ ประกาศโดย standard library
ดังนี้:
#![allow(unused)]
fn main() {
enum Option<T> {
None,
Some(T),
}
}
enum Option<T> มีประโยชน์มากจนรวมใน prelude ด้วย คุณไม่ต้องนำเข้า scope
แบบ explicit variant ของมันก็รวมใน prelude ด้วย — คุณใช้ Some และ
None ตรง ๆ ได้โดยไม่มี prefix Option:: enum Option<T> ยังเป็นแค่
enum ปกติ และ Some(T) กับ None ยังเป็น variant ของ type Option<T>
syntax <T> เป็นฟีเจอร์ของ Rust ที่เรายังไม่พูดถึง มันเป็น generic type
parameter และเราจะครอบคลุม generic ในรายละเอียดในบทที่ 10 ตอนนี้สิ่งที่
คุณต้องรู้คือ <T> หมายความว่า variant Some ของ enum Option เก็บ
ข้อมูลหนึ่งชิ้นของ type ใดก็ได้ และแต่ละ type คอนกรีตที่ใช้แทน T ทำให้
type Option<T> โดยรวมเป็น type ต่างกัน นี่คือตัวอย่างการใช้ค่า Option
เก็บ type ตัวเลขและ type char:
fn main() {
let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;
}
type ของ some_number คือ Option<i32> type ของ some_char คือ
Option<char> ซึ่งเป็น type ต่างกัน Rust infer type เหล่านี้ได้ เพราะ
เราระบุค่าภายใน variant Some สำหรับ absent_number Rust กำหนดให้เรา
annotate type Option โดยรวม — compiler infer type ที่ variant Some
ที่สอดคล้องจะเก็บไม่ได้ โดยดูแค่ค่า None ที่นี่ เราบอก Rust ว่าเรา
หมายถึง absent_number เป็น type Option<i32>
เมื่อเรามีค่า Some เรารู้ว่ามีค่าอยู่ และค่าถูกเก็บภายใน Some เมื่อ
เรามีค่า None ในแง่หนึ่งหมายเหมือน null — เราไม่มีค่าที่ valid แล้ว
ทำไม Option<T> ถึงดีกว่า null?
สั้น ๆ คือ เพราะ Option<T> และ T (โดย T เป็น type ใดก็ได้) เป็น
type ต่างกัน compiler ไม่ให้เราใช้ค่า Option<T> ราวกับว่ามันแน่นอนเป็น
ค่าที่ valid เช่น โค้ดนี้จะไม่ compile เพราะมันพยายามบวก i8 กับ
Option<i8>:
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
}
ถ้าเรารันโค้ดนี้ เราได้ error message แบบนี้:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
`&i8` implements `Add<i8>`
`&i8` implements `Add`
`i8` implements `Add<&i8>`
`i8` implements `Add`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error
แรง! ผลคือ error message นี้หมายความว่า Rust ไม่เข้าใจวิธีบวก i8 กับ
Option<i8> เพราะเป็น type ต่างกัน เมื่อเรามีค่า type อย่าง i8 ใน
Rust compiler จะรับประกันว่าเรามีค่าที่ valid เสมอ เราดำเนินต่อไปอย่าง
มั่นใจได้โดยไม่ต้องเช็ค null ก่อนใช้ค่านั้น เฉพาะเมื่อเรามี Option<i8>
(หรือ type ของค่าที่กำลังทำงานด้วย) เราต้องห่วงว่าอาจไม่มีค่า และ compiler
จะรับประกันว่าเราจัดการกรณีนั้นก่อนใช้ค่า
พูดอีกอย่าง คุณต้องแปลง Option<T> เป็น T ก่อนคุณทำ operation T กับ
มันได้ โดยทั่วไป สิ่งนี้ช่วยจับหนึ่งในปัญหาที่ใช้บ่อยที่สุดของ null — การ
สมมติว่าอะไรบางอย่างไม่ใช่ null ทั้งที่จริงเป็น
การกำจัดความเสี่ยงของการสมมติค่าที่ไม่ใช่ null อย่างไม่ถูกต้อง ช่วยให้คุณ
มั่นใจมากขึ้นในโค้ด เพื่อให้มีค่าที่อาจเป็น null คุณต้อง opt in แบบ
explicit โดยทำให้ type ของค่านั้นเป็น Option<T> จากนั้น เมื่อคุณใช้ค่า
นั้น คุณถูกบังคับให้จัดการ case เมื่อค่าเป็น null แบบ explicit ทุกที่ที่
ค่ามี type ที่ไม่ใช่ Option<T> คุณ สามารถ สมมติได้อย่างปลอดภัยว่าค่า
ไม่ใช่ null นี่เป็นการตัดสินใจออกแบบที่ตั้งใจของ Rust เพื่อจำกัดการแพร่
ขยายของ null และเพิ่มความปลอดภัยของโค้ด Rust
แล้วคุณดึงค่า T ออกจาก variant Some ยังไง เมื่อคุณมีค่า type
Option<T> เพื่อให้คุณใช้ค่านั้นได้? enum Option<T> มีเมธอดจำนวนมากที่
มีประโยชน์ในสถานการณ์ต่าง ๆ คุณดูได้ใน documentation ของมัน
การคุ้นเคยกับเมธอดบน Option<T> จะมีประโยชน์มากในการเดินทางกับ Rust ของ
คุณ
โดยทั่วไป เพื่อใช้ค่า Option<T> คุณอยากมีโค้ดที่จัดการแต่ละ variant
คุณอยากมีโค้ดที่รันเฉพาะเมื่อคุณมีค่า Some(T) และโค้ดนี้ใช้ T ภายในได้
คุณอยากมีโค้ดอื่นรันเฉพาะถ้าคุณมีค่า None และโค้ดนั้นไม่มีค่า T ให้ใช้
match expression คือโครงสร้าง control flow ที่ทำสิ่งนี้พอดี เมื่อใช้กับ
enum — มันจะรันโค้ดต่างกันขึ้นกับว่ามี variant ไหนของ enum และโค้ดนั้นใช้
ข้อมูลภายในค่าที่ match ได้
โครงสร้างควบคุม match
โครงสร้าง Control Flow match
Rust มีโครงสร้าง control flow ที่ทรงพลังมากชื่อ match ที่ให้คุณเปรียบ
เทียบค่ากับชุดของ pattern แล้ว execute โค้ดตามว่า pattern ไหน match
Pattern ประกอบจากค่า literal, ชื่อตัวแปร, wildcard และอื่น ๆ อีกมาก
บทที่ 19 ครอบคลุม pattern ชนิดต่าง ๆ
ทั้งหมดและสิ่งที่พวกมันทำ พลังของ match มาจากความสามารถในการแสดงออกของ
pattern และข้อเท็จจริงที่ว่า compiler ยืนยันว่าทุกกรณีที่เป็นไปได้ถูก
จัดการ
คิดถึง match expression เหมือนเครื่องคัดแยกเหรียญ — เหรียญไหลลงรางที่มีรู
ขนาดต่างกัน และแต่ละเหรียญตกผ่านรูแรกที่เจอที่มันลงพอดี ในแบบเดียวกัน
ค่าผ่านแต่ละ pattern ใน match และที่ pattern แรกที่ค่า “พอดี” ค่าตกลงใน
block โค้ดที่ผูกอยู่เพื่อถูกใช้ระหว่างการ execute
พูดถึงเหรียญ มาใช้พวกมันเป็นตัวอย่างกับ match! เราเขียนฟังก์ชันที่รับ
เหรียญสหรัฐที่ไม่รู้จัก และในแบบคล้ายกับเครื่องคัดแยก กำหนดว่าเป็นเหรียญ
อะไร แล้ว return ค่าเป็นเซ็นต์ ดังที่แสดงใน Listing 6-3
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
match expression ที่มี variant ของ enum เป็น patternมาแยก match ในฟังก์ชัน value_in_cents ก่อน เราใส่ keyword match ตาม
ด้วย expression ซึ่งในกรณีนี้คือค่า coin ดูคล้าย conditional expression
ที่ใช้กับ if แต่มีความแตกต่างใหญ่ — กับ if condition ต้องประเมินเป็น
ค่า Boolean แต่ที่นี่เป็น type ใดก็ได้ Type ของ coin ในตัวอย่างนี้คือ
enum Coin ที่เราประกาศในบรรทัดแรก
ถัดไปคือ arm ของ match arm มีสองส่วน — pattern และโค้ดบางตัว arm แรก
ที่นี่มี pattern ที่เป็นค่า Coin::Penny แล้ว operator => ที่คั่น
pattern และโค้ดที่จะรัน โค้ดในกรณีนี้คือแค่ค่า 1 แต่ละ arm ถูกคั่นจาก
ตัวถัดไปด้วย comma
เมื่อ match expression execute มันเปรียบเทียบค่าผลลัพธ์กับ pattern ของ
แต่ละ arm ตามลำดับ ถ้า pattern match ค่า โค้ดที่ผูกกับ pattern นั้นถูก
execute ถ้า pattern นั้นไม่ match ค่า การ execute ดำเนินต่อไปยัง arm ถัด
ไป เหมือนในเครื่องคัดแยกเหรียญ เรามี arm ได้มากเท่าที่ต้องการ — ใน
Listing 6-3 match ของเรามีสี่ arm
โค้ดที่ผูกกับแต่ละ arm เป็น expression และค่าผลลัพธ์ของ expression ใน arm
ที่ match คือค่าที่ถูก return สำหรับทั้ง match expression
โดยทั่วไปเราไม่ใช้ curly bracket ถ้าโค้ดของ match arm สั้น เหมือนใน
Listing 6-3 ที่แต่ละ arm แค่ return ค่า ถ้าคุณอยากรันโค้ดหลายบรรทัดใน
match arm คุณต้องใช้ curly bracket และ comma ที่ตาม arm จึงเป็น optional
เช่น โค้ดต่อไปนี้พิมพ์ “Lucky penny!” ทุกครั้งที่เมธอดถูกเรียกด้วย
Coin::Penny แต่ยัง return ค่าสุดท้ายของ block คือ 1:
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {}
Pattern ที่ Bind กับค่า
ฟีเจอร์ที่มีประโยชน์อีกอย่างของ match arm คือพวกมัน bind กับส่วนของค่าที่ match pattern ได้ นี่คือวิธีที่เราดึงค่าออกจาก variant ของ enum
เป็นตัวอย่าง ลองเปลี่ยน variant หนึ่งของ enum เพื่อเก็บข้อมูลภายใน ตั้งแต่
ปี 1999 ถึง 2008 สหรัฐผลิตเหรียญ quarter ที่มีการออกแบบต่างกันสำหรับ
แต่ละรัฐ 50 รัฐที่ด้านหนึ่ง เหรียญอื่นไม่มีการออกแบบรัฐ เฉพาะเหรียญ
quarter มีค่าเพิ่มเติมนี้ เราเพิ่มข้อมูลนี้เข้า enum ของเราได้ โดย
เปลี่ยน variant Quarter ให้รวมค่า UsState ที่เก็บภายใน ซึ่งเราทำใน
Listing 6-4
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {}
Coin ที่ variant Quarter เก็บค่า UsState ด้วยลองจินตนาการว่าเพื่อนกำลังพยายามรวบรวมเหรียญ quarter ของรัฐทั้ง 50 รัฐ ขณะที่เราคัดเศษเหรียญตามชนิด เราจะพูดชื่อของรัฐที่ผูกกับแต่ละ quarter ด้วย เพื่อถ้าเป็นตัวที่เพื่อนยังไม่มี เขาจะเพิ่มเข้าสะสมของเขาได้
ใน match expression สำหรับโค้ดนี้ เราเพิ่มตัวแปรชื่อ state ใน pattern
ที่ match ค่าของ variant Coin::Quarter เมื่อ Coin::Quarter match ตัว
แปร state จะ bind กับค่ารัฐของ quarter นั้น จากนั้นเราใช้ state ใน
โค้ดของ arm นั้นได้ ดังนี้:
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}
fn main() {
value_in_cents(Coin::Quarter(UsState::Alaska));
}
ถ้าเราเรียก value_in_cents(Coin::Quarter(UsState::Alaska)) coin จะ
เป็น Coin::Quarter(UsState::Alaska) เมื่อเราเปรียบเทียบค่านั้นกับ match
arm แต่ละตัว ไม่มีตัวไหน match จนกระทั่งเราถึง Coin::Quarter(state) ณ
จุดนั้น binding ของ state จะเป็นค่า UsState::Alaska เราใช้ binding
นั้นใน println! expression ได้ จึงดึงค่ารัฐภายในออกจาก variant Coin
สำหรับ Quarter
match Pattern ของ Option<T>
ในส่วนก่อนหน้า เราอยากดึงค่า T ภายในออกจากกรณี Some เมื่อใช้
Option<T> เรายังจัดการ Option<T> โดยใช้ match ได้ เหมือนที่เราทำกับ
enum Coin! แทนการเปรียบเทียบเหรียญ เราจะเปรียบเทียบ variant ของ
Option<T> แต่วิธีที่ match expression ทำงานยังเหมือนเดิม
สมมติเราอยากเขียนฟังก์ชันที่รับ Option<i32> และ ถ้ามีค่าภายใน บวก 1
กับค่านั้น ถ้าไม่มีค่าภายใน ฟังก์ชันควร return ค่า None และไม่พยายามทำ
operation ใด ๆ
ฟังก์ชันนี้เขียนง่ายมาก ขอบคุณ match และจะดูเหมือน Listing 6-5
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
match expression บน Option<i32>มาตรวจสอบการ execute ครั้งแรกของ plus_one ในรายละเอียดมากขึ้น เมื่อเรา
เรียก plus_one(five) ตัวแปร x ใน body ของ plus_one จะมีค่า
Some(5) จากนั้นเราเปรียบเทียบกับแต่ละ match arm:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
ค่า Some(5) ไม่ match pattern None เราจึงดำเนินต่อไปยัง arm ถัดไป:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
Some(5) match Some(i) ไหม? match! เรามี variant เดียวกัน i bind
กับค่าภายใน Some ดังนั้น i รับค่า 5 โค้ดใน match arm จากนั้น
execute เราจึงบวก 1 กับค่าของ i และสร้างค่า Some ใหม่กับยอดรวม 6
ภายใน
ทีนี้พิจารณาการเรียก plus_one ครั้งที่สองใน Listing 6-5 ที่ x คือ
None เราเข้า match และเปรียบเทียบกับ arm แรก:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
มัน match! ไม่มีค่าให้บวก โปรแกรมจึงหยุดและ return ค่า None ทางขวาของ
=> เพราะ arm แรก match ไม่มี arm อื่นถูกเปรียบเทียบ
การรวม match และ enum มีประโยชน์ในหลายสถานการณ์ คุณจะเห็น pattern นี้
มากในโค้ด Rust — match กับ enum, bind ตัวแปรกับข้อมูลภายใน แล้ว execute
โค้ดตามนั้น มันยากนิดหน่อยตอนแรก แต่เมื่อคุณคุ้นแล้ว คุณจะอยากให้มีในทุก
ภาษา มันเป็นที่ชื่นชอบของ user อย่างต่อเนื่อง
Match ต้องครอบคลุมทุกกรณี
มีอีกแง่มุมของ match ที่เราต้องพูดถึง — pattern ของ arm ต้องครอบคลุม
ความเป็นไปได้ทั้งหมด พิจารณา version นี้ของฟังก์ชัน plus_one ของเรา
ซึ่งมี bug และจะ compile ไม่ผ่าน:
fn main() {
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
}
เราไม่ได้จัดการกรณี None โค้ดนี้จึงทำให้เกิด bug โชคดี มันเป็น bug ที่
Rust รู้วิธีจับ ถ้าเราลอง compile โค้ดนี้ เราจะได้ error นี้:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
note: `Option<i32>` defined here
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
|
= note: not covered
= note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
|
4 ~ Some(i) => Some(i + 1),
5 ~ None => todo!(),
|
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` (bin "enums") due to 1 previous error
Rust รู้ว่าเราไม่ได้ครอบคลุมทุกกรณีที่เป็นไปได้ และยังรู้ด้วยว่าเราลืม
pattern ไหน! Match ใน Rust เป็น exhaustive — เราต้อง exhaust ทุก
ความเป็นไปได้สุดท้าย เพื่อให้โค้ด valid โดยเฉพาะในกรณีของ Option<T> เมื่อ
Rust ป้องกันเราจากการลืมจัดการกรณี None แบบ explicit มันปกป้องเราจากการ
สมมติว่าเรามีค่าเมื่อเราอาจมี null จึงทำให้ความผิดพลาดมูลค่าพันล้านดอลลาร์
ที่พูดถึงก่อนหน้าเป็นไปไม่ได้
Pattern จับทั้งหมดและ Placeholder _
ใช้ enum เรายังทำ action พิเศษสำหรับค่าเฉพาะบางตัวได้ แต่สำหรับค่าอื่น
ทั้งหมดทำ action default หนึ่งตัว ลองนึกว่าเรากำลัง implement เกมที่ ถ้า
คุณ roll dice ได้ 3 player ของคุณไม่เคลื่อนที่ แต่ได้หมวกใหม่หรูแทน ถ้า
คุณ roll ได้ 7 player ของคุณเสียหมวกหรู สำหรับค่าอื่นทั้งหมด player
ของคุณเคลื่อนที่จำนวนช่องนั้นบนกระดานเกม นี่คือ match ที่ implement
logic นั้น ด้วยผลของการ roll dice ที่ hardcode แทนค่าสุ่ม และ logic อื่น
ทั้งหมดแทนด้วยฟังก์ชันที่ไม่มี body เพราะการ implement พวกมันจริงอยู่นอก
ขอบเขตของตัวอย่างนี้:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
}
สำหรับ arm สองตัวแรก pattern คือค่า literal 3 และ 7 สำหรับ arm สุดท้าย
ที่ครอบคลุมค่าอื่นที่เป็นไปได้ทั้งหมด pattern คือตัวแปรที่เราเลือกตั้งชื่อ
other โค้ดที่รันสำหรับ arm other ใช้ตัวแปรโดยส่งให้ฟังก์ชัน
move_player
โค้ดนี้ compile ผ่าน แม้เราจะไม่ได้ list ค่าที่เป็นไปได้ทั้งหมดที่ u8
มีได้ เพราะ pattern สุดท้ายจะ match ค่าทั้งหมดที่ไม่ได้ list เฉพาะ
Pattern จับทั้งหมดนี้ตรงตามข้อกำหนดที่ match ต้อง exhaustive หมายเหตุว่า
เราต้องวาง arm จับทั้งหมดท้ายสุด เพราะ pattern ถูกประเมินตามลำดับ ถ้าเรา
วาง arm จับทั้งหมดก่อนหน้านี้ arm อื่นจะไม่ได้รัน Rust จะเตือนเราถ้าเรา
เพิ่ม arm หลังจับทั้งหมด!
Rust ยังมี pattern ที่เราใช้ได้เมื่อเราอยากมีจับทั้งหมด แต่ไม่อยาก ใช้
ค่าใน pattern จับทั้งหมด — _ เป็น pattern พิเศษที่ match ค่าใด ๆ และไม่
bind กับค่านั้น สิ่งนี้บอก Rust ว่าเราจะไม่ใช้ค่า Rust จึงไม่เตือนเรา
เกี่ยวกับตัวแปรที่ไม่ใช้
มาเปลี่ยนกฎของเกม — ตอนนี้ถ้าคุณ roll อะไรที่ไม่ใช่ 3 หรือ 7 คุณต้อง roll
อีก เราไม่ต้องใช้ค่าจับทั้งหมดแล้ว เราจึงเปลี่ยนโค้ดให้ใช้ _ แทนตัวแปร
ชื่อ other:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
}
ตัวอย่างนี้ก็ตรงตามข้อกำหนด exhaustiveness ด้วย เพราะเราละเว้นค่าอื่น ทั้งหมดใน arm สุดท้ายแบบ explicit — เราไม่ได้ลืมอะไร
สุดท้าย เราจะเปลี่ยนกฎของเกมอีกครั้ง ให้ไม่มีอะไรอื่นเกิดในตาของคุณ ถ้า
คุณ roll อะไรที่ไม่ใช่ 3 หรือ 7 เราแสดงสิ่งนั้นได้โดยใช้ค่า unit (empty
tuple type ที่เราเอ่ยในส่วน “Tuple Type”) เป็น
โค้ดที่ไปกับ arm _:
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}
fn add_fancy_hat() {}
fn remove_fancy_hat() {}
}
ที่นี่ เรากำลังบอก Rust แบบ explicit ว่าเราจะไม่ใช้ค่าอื่นใดที่ไม่ match pattern ใน arm ก่อนหน้า และเราไม่อยากรันโค้ดในกรณีนี้
มีอีกมากเกี่ยวกับ pattern และ matching ที่เราจะครอบคลุมใน
บทที่ 19 ตอนนี้ เราจะไปต่อที่ syntax
if let ซึ่งมีประโยชน์ในสถานการณ์ที่ match expression ยาวเกินไปนิด
หน่อย
Control flow สั้น ๆ ด้วย if let และ let...else
Control Flow ที่กระชับด้วย if let และ let...else
syntax if let ให้คุณรวม if และ let เป็นวิธีจัดการค่าที่ match
pattern เดียวที่กระชับกว่า ขณะละเว้นค่าที่เหลือ พิจารณาโปรแกรมใน
Listing 6-6 ที่ match ค่า Option<u8> ในตัวแปร config_max แต่อยาก
execute โค้ดเฉพาะถ้าค่าเป็น variant Some
fn main() {
let config_max = Some(3u8);
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
}
match ที่สนใจแค่การ execute โค้ดเมื่อค่าเป็น Someถ้าค่าเป็น Some เราพิมพ์ค่าใน variant Some โดย bind ค่ากับตัวแปร max
ใน pattern เราไม่อยากทำอะไรกับค่า None เพื่อ satisfy match expression
เราต้องเพิ่ม _ => () หลังประมวลผลแค่ variant เดียว ซึ่งเป็น boilerplate
ที่น่ารำคาญที่ต้องเพิ่ม
แทนนั้น เราเขียนนี้ในแบบสั้นกว่าโดยใช้ if let ได้ โค้ดต่อไปนี้ทำงาน
เหมือนกับ match ใน Listing 6-6:
fn main() {
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}
}
syntax if let รับ pattern และ expression ที่คั่นด้วยเครื่องหมายเท่ากับ
มันทำงานแบบเดียวกับ match ที่ expression ให้กับ match และ pattern
คือ arm แรกของมัน ในกรณีนี้ pattern คือ Some(max) และ max bind กับ
ค่าภายใน Some เราใช้ max ใน body ของ block if let ได้แบบเดียวกับที่
เราใช้ max ใน match arm ที่สอดคล้อง โค้ดใน block if let รันเฉพาะถ้าค่า
match pattern
การใช้ if let หมายถึงพิมพ์น้อยลง indent น้อยลง และ boilerplate น้อยลง
อย่างไรก็ตาม คุณเสียการเช็ค exhaustive ที่ match บังคับใช้ ที่รับประกัน
ว่าคุณไม่ลืมจัดการกรณีใด ๆ การเลือกระหว่าง match และ if let ขึ้นกับ
สิ่งที่คุณทำในสถานการณ์ของคุณ และว่าการได้ความกระชับเป็น trade-off ที่
เหมาะสมสำหรับการเสียการเช็ค exhaustive
พูดอีกอย่าง คุณคิดถึง if let เป็น syntax sugar ของ match ที่รันโค้ด
เมื่อค่า match pattern หนึ่งแล้วละเว้นค่าอื่นทั้งหมดได้
เรารวม else กับ if let ได้ block โค้ดที่ไปกับ else เหมือนกับ block
โค้ดที่จะไปกับกรณี _ ใน match expression ที่เทียบเท่ากับ if let
และ else จำการประกาศ enum Coin ใน Listing 6-4 ได้ ที่ variant
Quarter เก็บค่า UsState ด้วย ถ้าเราอยากนับเหรียญที่ไม่ใช่ quarter
ทั้งหมดที่เราเห็น พร้อมประกาศรัฐของ quarter ด้วย เราทำได้ด้วย match
expression แบบนี้:
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {
let coin = Coin::Penny;
let mut count = 0;
match coin {
Coin::Quarter(state) => println!("State quarter from {state:?}!"),
_ => count += 1,
}
}
หรือเราใช้ if let และ else expression แบบนี้ได้:
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// --snip--
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn main() {
let coin = Coin::Penny;
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {state:?}!");
} else {
count += 1;
}
}
อยู่บน “Happy Path” ด้วย let...else
Pattern ที่ใช้บ่อยคือทำการคำนวณบางอย่างเมื่อค่ามีอยู่ และ return ค่า
default ไม่อย่างนั้น ต่อจากตัวอย่างของเรา เหรียญที่มีค่า UsState ถ้า
เราอยากพูดอะไรตลก ๆ ขึ้นกับว่ารัฐบนเหรียญ quarter เก่าแค่ไหน เราอาจ
แนะนำเมธอดบน UsState เพื่อเช็คอายุของรัฐ ดังนี้:
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
if let Coin::Quarter(state) = coin {
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
} else {
None
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
จากนั้น เราอาจใช้ if let match บนชนิดของเหรียญ แนะนำตัวแปร state
ภายใน body ของ condition เหมือนใน Listing 6-7
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
if let Coin::Quarter(state) = coin {
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
} else {
None
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
if letนั่นได้งาน แต่มันผลักงานเข้า body ของ statement if let และถ้างานที่ทำ
ซับซ้อนกว่า มันอาจตามยากว่า branch ระดับบนสุดผูกกันอย่างไร เราใช้ประโยชน์
จากข้อเท็จจริงที่ว่า expression produce ค่า ได้ ทั้งเพื่อ produce state
จาก if let หรือ return เร็ว เหมือนใน Listing 6-8 (คุณทำคล้ายกันกับ
match ก็ได้)
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
let state = if let Coin::Quarter(state) = coin {
state
} else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
if let เพื่อ produce ค่าหรือ return เร็วอันนี้ก็ตามได้ยากในแบบของมันเอง! Branch หนึ่งของ if let produce ค่า
และอีกอันคืน return จากฟังก์ชันทั้งหมด
เพื่อทำให้ pattern ที่ใช้บ่อยนี้แสดงออกได้ดีขึ้น Rust มี let...else
syntax let...else รับ pattern ทางซ้ายและ expression ทางขวา คล้าย
if let มาก แต่ไม่มี branch if มีแค่ branch else ถ้า pattern match
มันจะ bind ค่าจาก pattern ใน scope ภายนอก ถ้า pattern ไม่ match
โปรแกรมจะไหลเข้า arm else ซึ่งต้อง return จากฟังก์ชัน
ใน Listing 6-9 คุณเห็นว่า Listing 6-8 หน้าตาเป็นอย่างไรเมื่อใช้
let...else แทน if let
#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
Alabama,
Alaska,
// --snip--
}
impl UsState {
fn existed_in(&self, year: u16) -> bool {
match self {
UsState::Alabama => year >= 1819,
UsState::Alaska => year >= 1959,
// -- snip --
}
}
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn describe_state_quarter(coin: Coin) -> Option<String> {
let Coin::Quarter(state) = coin else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}
fn main() {
if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
println!("{desc}");
}
}
let...else เพื่อทำให้ flow ผ่านฟังก์ชันชัดเจนสังเกตว่ามันอยู่บน “happy path” ใน body หลักของฟังก์ชันแบบนี้ โดยไม่มี
control flow ที่ต่างกันอย่างมีนัยสำหรับสอง branch แบบที่ if let ทำ
ถ้าคุณมีสถานการณ์ที่โปรแกรมของคุณมี logic ที่ยาวเกินไปที่จะแสดงด้วย
match จำไว้ว่า if let และ let...else ก็อยู่ในกล่องเครื่องมือ Rust
ของคุณด้วย
สรุป
ตอนนี้เราครอบคลุมวิธีใช้ enum เพื่อสร้าง type custom ที่เป็นได้หนึ่งใน
ชุดของค่า enumerate แล้ว เราแสดงว่า type Option<T> ของ standard
library ช่วยคุณใช้ระบบ type ป้องกัน error อย่างไร เมื่อค่า enum มีข้อมูล
ภายใน คุณใช้ match หรือ if let ดึงและใช้ค่าเหล่านั้นได้ ขึ้นกับว่ามี
กี่กรณีที่คุณต้องจัดการ
โปรแกรม Rust ของคุณตอนนี้แสดงแนวคิดใน domain ของคุณโดยใช้ struct และ enum ได้ การสร้าง type custom เพื่อใช้ใน API ของคุณรับประกัน type safety — compiler จะรับประกันว่าฟังก์ชันของคุณได้รับเฉพาะค่าของ type ที่แต่ละฟังก์ชันคาด
เพื่อให้ API ที่จัดระเบียบดีกับ user ของคุณ ที่ตรงไปตรงมาในการใช้ และ เปิดเผยเฉพาะสิ่งที่ user ของคุณต้องการ ทีนี้มาดู module ของ Rust กัน
Package, Crate และ Module
เมื่อคุณเขียนโปรแกรมขนาดใหญ่ การจัดระเบียบโค้ดจะสำคัญขึ้นเรื่อย ๆ ด้วยการ จัดกลุ่ม functionality ที่เกี่ยวข้อง และแยกโค้ดที่มีฟีเจอร์เฉพาะ คุณจะ ทำให้ชัดเจนว่าจะหาโค้ดที่ implement ฟีเจอร์เฉพาะที่ไหน และจะไปที่ไหนเพื่อ เปลี่ยนวิธีที่ฟีเจอร์ทำงาน
โปรแกรมที่เราเขียนมาที่ผ่านมาอยู่ใน module เดียวในไฟล์เดียว เมื่อโปรเจกต์ เติบโต คุณควรจัดระเบียบโค้ดโดยแบ่งมันเป็นหลาย module แล้วเป็นหลายไฟล์ package มี binary crate หลายตัวและ library crate หนึ่งตัวแบบ optional ได้ เมื่อ package เติบโต คุณดึงส่วนเป็น crate แยกที่กลายเป็น dependency ภายนอกได้ บทนี้ครอบคลุมเทคนิคทั้งหมดเหล่านี้ สำหรับโปรเจกต์ใหญ่มากที่ ประกอบด้วยชุดของ package ที่ผูกกันที่พัฒนาไปด้วยกัน Cargo มี workspace ซึ่งเราจะครอบคลุมใน “Cargo Workspace” ใน บทที่ 14
เราจะพูดถึงการ encapsulate รายละเอียด implementation ด้วย ซึ่งให้คุณใช้ โค้ดซ้ำในระดับสูงกว่า — เมื่อคุณ implement operation แล้ว โค้ดอื่นเรียก โค้ดของคุณผ่าน public interface ของมันได้ โดยไม่ต้องรู้ว่า implementation ทำงานอย่างไร วิธีที่คุณเขียนโค้ดกำหนดว่าส่วนไหนเป็น public ให้โค้ดอื่นใช้ และส่วนไหนเป็นรายละเอียด implementation private ที่คุณสงวนสิทธิ์ที่จะ เปลี่ยน นี่เป็นอีกวิธีในการจำกัดจำนวนรายละเอียดที่คุณต้องเก็บในหัว
แนวคิดที่เกี่ยวข้องคือ scope — บริบทซ้อนที่โค้ดถูกเขียนมีชุดของชื่อที่ ประกาศว่าอยู่ “ใน scope” เมื่ออ่าน เขียน และ compile โค้ด โปรแกรมเมอร์ และ compiler ต้องรู้ว่าชื่อเฉพาะที่จุดเฉพาะอ้างถึงตัวแปร ฟังก์ชัน struct enum module constant หรือ item อื่น และ item นั้นหมายถึงอะไร คุณสร้าง scope และเปลี่ยนว่าชื่อไหนอยู่ใน scope หรือออกนอก scope ได้ คุณมี item สองตัวที่ชื่อเดียวกันใน scope เดียวไม่ได้ มีเครื่องมือให้แก้ความขัดแย้ง ของชื่อ
Rust มีฟีเจอร์หลายตัวที่ให้คุณจัดการการจัดระเบียบโค้ด รวมถึงรายละเอียดไหน ที่เปิดเผย รายละเอียดไหนเป็น private และชื่อไหนอยู่ใน scope แต่ละตัวใน โปรแกรมคุณ ฟีเจอร์เหล่านี้ บางครั้งเรียกรวมกันว่า ระบบ module รวม:
- Package: ฟีเจอร์ Cargo ที่ให้คุณ build, test และแชร์ crate
- Crate: tree ของ module ที่ produce library หรือ executable
- Module และ use: ให้คุณควบคุมการจัดระเบียบ, scope และ privacy ของ path
- Path: วิธีการตั้งชื่อ item เช่น struct, ฟังก์ชัน หรือ module
ในบทนี้ เราจะครอบคลุมฟีเจอร์ทั้งหมดเหล่านี้ พูดถึงวิธีที่พวกมันโต้ตอบกัน และอธิบายวิธีใช้พวกมันจัดการ scope ในที่สุด คุณควรมีความเข้าใจแน่นเรื่อง ระบบ module และทำงานกับ scope ได้แบบมืออาชีพ!
Package และ Crate
Package และ Crate
ส่วนแรกของระบบ module ที่เราจะครอบคลุมคือ package และ crate
crate คือจำนวนน้อยที่สุดของโค้ดที่ Rust compiler พิจารณาในเวลาหนึ่ง
แม้ว่าคุณรัน rustc แทน cargo และส่งไฟล์ source code เดียว (อย่างที่เรา
ทำเมื่อนานมาแล้วใน “พื้นฐานโปรแกรม Rust” ในบทที่
- compiler ถือว่าไฟล์นั้นเป็น crate Crate มี module ได้ และ module ประกาศในไฟล์อื่นที่ compile กับ crate ได้ อย่างที่เราจะเห็นในส่วนถัด ๆ ไป
Crate เป็นได้ในสองรูปแบบ — binary crate หรือ library crate Binary crate
คือโปรแกรมที่คุณ compile เป็น executable ที่คุณรันได้ เช่น โปรแกรม
command line หรือ server แต่ละตัวต้องมีฟังก์ชันชื่อ main ที่ประกาศว่า
เกิดอะไรขึ้นเมื่อ executable รัน Crate ทั้งหมดที่เราสร้างที่ผ่านมาเป็น
binary crate
Library crate ไม่มีฟังก์ชัน main และไม่ compile เป็น executable แทน
นั้น พวกมันประกาศ functionality ที่ตั้งใจให้แชร์กับหลายโปรเจกต์ เช่น
crate rand ที่เราใช้ใน บทที่ 2 มี functionality
ที่ generate ตัวเลขสุ่ม โดยส่วนใหญ่เมื่อ Rustacean พูดว่า “crate” พวกเขา
หมายถึง library crate และใช้ “crate” แทน concept “library” ทั่วไปในการ
เขียนโปรแกรม
crate root คือไฟล์ source ที่ Rust compiler เริ่มจาก และประกอบเป็น module root ของ crate ของคุณ (เราจะอธิบาย module อย่างละเอียดใน “ควบคุม Scope และ Privacy ด้วย Module”)
package คือ bundle ของ crate หนึ่งหรือมากกว่าที่ให้ชุดของ functionality package มีไฟล์ Cargo.toml ที่บรรยายวิธี build crate เหล่านั้น Cargo จริง ๆ เป็น package ที่มี binary crate สำหรับเครื่องมือ command line ที่คุณ ใช้ build โค้ดของคุณ Package Cargo ยังมี library crate ที่ binary crate พึ่งพา โปรเจกต์อื่นพึ่ง library crate ของ Cargo เพื่อใช้ logic เดียวกับ เครื่องมือ command line ของ Cargo ได้
Package มี binary crate ได้เท่าที่คุณอยาก แต่ library crate มากที่สุด หนึ่งตัว Package ต้องมีอย่างน้อยหนึ่ง crate ไม่ว่าจะเป็น library หรือ binary crate
มาเดินผ่านสิ่งที่เกิดเมื่อเราสร้าง package ก่อน เราป้อนคำสั่ง
cargo new my-project:
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
หลังเรา run cargo new my-project เราใช้ ls เพื่อดูสิ่งที่ Cargo สร้าง
ใน directory my-project มีไฟล์ Cargo.toml ให้ package กับเรา ยังมี
directory src ที่มี main.rs เปิด Cargo.toml ใน text editor และ
หมายเหตุว่าไม่มีการเอ่ยถึง src/main.rs Cargo ตาม convention ที่
src/main.rs เป็น crate root ของ binary crate ที่มีชื่อเหมือนกับ
package ในทำนองเดียวกัน Cargo รู้ว่าถ้า directory package มี
src/lib.rs, package มี library crate ที่ชื่อเหมือนกับ package และ
src/lib.rs คือ crate root ของมัน Cargo ส่งไฟล์ crate root ให้ rustc
เพื่อ build library หรือ binary
ที่นี่ เรามี package ที่มีแค่ src/main.rs หมายความว่ามันมีแค่ binary
crate ชื่อ my-project ถ้า package มี src/main.rs และ src/lib.rs
มันมีสอง crate — binary และ library ทั้งคู่ชื่อเหมือนกับ package
Package มี binary crate หลายตัวได้โดยวางไฟล์ใน directory src/bin —
แต่ละไฟล์จะเป็น binary crate แยก
ควบคุม scope และ privacy ด้วย module
ควบคุม Scope และ Privacy ด้วย Module
ในส่วนนี้ เราจะพูดถึง module และส่วนอื่นของระบบ module ได้แก่ path ที่
ให้คุณตั้งชื่อ item, keyword use ที่นำ path เข้า scope และ keyword pub
เพื่อทำให้ item เป็น public เราจะพูดถึง keyword as, package ภายนอก และ
glob operator ด้วย
Cheat Sheet ของ Module
ก่อนเราเข้าสู่รายละเอียดของ module และ path ที่นี่เราให้ reference ด่วน
เกี่ยวกับวิธีที่ module, path, keyword use และ keyword pub ทำงานใน
compiler และวิธีที่นักพัฒนาส่วนใหญ่จัดระเบียบโค้ดของพวกเขา เราจะผ่าน
ตัวอย่างของกฎเหล่านี้แต่ละข้อตลอดบทนี้ แต่นี่เป็นที่ดีที่จะอ้างถึงเพื่อ
เตือนความจำว่า module ทำงานยังไง
- เริ่มจาก crate root: เมื่อ compile crate compiler มองในไฟล์ crate root ก่อน (โดยปกติ src/lib.rs สำหรับ library crate และ src/main.rs สำหรับ binary crate) สำหรับโค้ดที่จะ compile
- ประกาศ module: ในไฟล์ crate root คุณประกาศ module ใหม่ได้ สมมติคุณ
ประกาศ module “garden” ด้วย
mod garden;Compiler จะมองหาโค้ดของ module ในที่เหล่านี้:- Inline ภายใน curly bracket ที่แทน semicolon ตาม
mod garden - ในไฟล์ src/garden.rs
- ในไฟล์ src/garden/mod.rs
- Inline ภายใน curly bracket ที่แทน semicolon ตาม
- ประกาศ submodule: ในไฟล์ใด ๆ นอกจาก crate root คุณประกาศ submodule
ได้ เช่น คุณอาจประกาศ
mod vegetables;ใน src/garden.rs Compiler จะมองหาโค้ดของ submodule ภายใน directory ที่ตั้งชื่อตาม module พ่อ ใน ที่เหล่านี้:- Inline ตามด้วย
mod vegetablesตรง ๆ ภายใน curly bracket แทน semicolon - ในไฟล์ src/garden/vegetables.rs
- ในไฟล์ src/garden/vegetables/mod.rs
- Inline ตามด้วย
- Path ไปยังโค้ดใน module: เมื่อ module เป็นส่วนหนึ่งของ crate ของคุณ
คุณอ้างถึงโค้ดใน module นั้นจากที่อื่นใดใน crate เดียวกันได้ ตราบที่กฎ
privacy อนุญาต โดยใช้ path ไปยังโค้ด เช่น type
Asparagusใน module garden vegetables จะถูกหาที่crate::garden::vegetables::Asparagus - Private vs. public: โค้ดภายใน module เป็น private จาก module พ่อ
โดย default ในการทำให้ module เป็น public ประกาศด้วย
pub modแทนmodในการทำให้ item ภายใน module public เป็น public ด้วย ใช้pubก่อนการประกาศ - Keyword
use: ภายใน scope keyworduseสร้าง shortcut ของ item เพื่อลดการเขียน path ยาว ๆ ซ้ำ ใน scope ใดก็ตามที่อ้างถึงcrate::garden::vegetables::Asparagusได้ คุณสร้าง shortcut ด้วยuse crate::garden::vegetables::Asparagus;ได้ และจากนั้นคุณเขียนแค่Asparagusเพื่อใช้ type นั้นใน scope ก็พอ
ที่นี่ เราสร้าง binary crate ชื่อ backyard ที่แสดงกฎเหล่านี้ Directory
ของ crate ก็ชื่อ backyard มีไฟล์และ directory เหล่านี้:
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rs
ไฟล์ crate root ในกรณีนี้คือ src/main.rs และมี:
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
}
บรรทัด pub mod garden; บอก compiler ให้ include โค้ดที่มันเจอใน
src/garden.rs ซึ่งคือ:
pub mod vegetables;
ที่นี่ pub mod vegetables; หมายความว่าโค้ดใน src/garden/vegetables.rs
ถูก include ด้วย โค้ดนั้นคือ:
#[derive(Debug)]
pub struct Asparagus {}
ทีนี้มาเข้าสู่รายละเอียดของกฎเหล่านี้และแสดงพวกมันในการใช้งาน!
จัดกลุ่มโค้ดที่ผูกกันใน Module
Module ให้เราจัดระเบียบโค้ดภายใน crate สำหรับความอ่านง่ายและการใช้ซ้ำ ได้ง่าย Module ยังให้เราควบคุม privacy ของ item ด้วย เพราะโค้ดภายใน module เป็น private โดย default Private item เป็นรายละเอียด implementation ภายในที่ไม่มีให้ภายนอกใช้ เราเลือกทำให้ module และ item ภายในเป็น public ได้ ซึ่งเปิดเผยพวกมันให้โค้ดภายนอกใช้และพึ่งพา
เป็นตัวอย่าง มาเขียน library crate ที่ให้ functionality ของร้านอาหาร เรา จะประกาศ signature ของฟังก์ชัน แต่ทิ้ง body ว่างไว้ เพื่อโฟกัสที่การจัด ระเบียบของโค้ดมากกว่าการ implement ร้านอาหาร
ในอุตสาหกรรมร้านอาหาร บางส่วนของร้านอาหารถูกอ้างถึงเป็น front of house และอื่น ๆ เป็น back of house Front of house คือที่ที่ลูกค้าอยู่ ซึ่งรวม ที่ที่ host จัดที่นั่งลูกค้า, server รับ order และเงิน, และ bartender ทำ เครื่องดื่ม Back of house คือที่ที่ chef และพ่อครัวทำงานในครัว, คนล้าง จานทำความสะอาด และผู้จัดการทำงาน administrative
ในการ structure crate ของเราในแบบนี้ เราจัดระเบียบฟังก์ชันของมันเป็น
module ซ้อน สร้าง library ใหม่ชื่อ restaurant โดยรัน
cargo new restaurant --lib จากนั้นป้อนโค้ดใน Listing 7-1 เข้า
src/lib.rs เพื่อประกาศ module และ signature ฟังก์ชัน — โค้ดนี้คือส่วน
front of house
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
front_of_house ที่มี module อื่นที่จากนั้นมีฟังก์ชันเราประกาศ module ด้วย keyword mod ตามด้วยชื่อของ module (ในกรณีนี้
front_of_house) body ของ module จากนั้นไปภายใน curly bracket ภายใน
module เราวาง module อื่นได้ เหมือนในกรณีนี้กับ module hosting และ
serving Module ยังเก็บ definition สำหรับ item อื่นได้ เช่น struct,
enum, constant, trait และเหมือนใน Listing 7-1 ฟังก์ชัน
ด้วยการใช้ module เราจัดกลุ่ม definition ที่ผูกกันเข้าด้วยกัน และตั้งชื่อ ว่าทำไมพวกมันผูกกัน โปรแกรมเมอร์ที่ใช้โค้ดนี้นำทางโค้ดตามกลุ่ม แทนต้อง อ่านผ่าน definition ทั้งหมด ทำให้ง่ายขึ้นที่จะหา definition ที่เกี่ยวข้อง กับพวกเขา โปรแกรมเมอร์ที่เพิ่ม functionality ใหม่ให้โค้ดนี้รู้ว่าจะวาง โค้ดที่ไหน เพื่อให้โปรแกรมจัดระเบียบ
ก่อนหน้านี้ เราเอ่ยว่า src/main.rs และ src/lib.rs เรียกว่า crate
root เหตุผลของชื่อพวกมันคือเนื้อหาของหนึ่งในสองไฟล์เหล่านี้ ประกอบ
เป็น module ชื่อ crate ที่ root ของโครงสร้าง module ของ crate รู้จัก
ในชื่อ module tree
Listing 7-2 แสดง module tree สำหรับโครงสร้างใน Listing 7-1
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
tree นี้แสดงว่า module บางตัวซ้อนภายใน module อื่นอย่างไร เช่น hosting
ซ้อนภายใน front_of_house tree ยังแสดงว่า module บางตัวเป็น sibling
หมายความว่าพวกมันประกาศใน module เดียวกัน — hosting และ serving เป็น
sibling ที่ประกาศภายใน front_of_house ถ้า module A ถูกบรรจุภายใน module
B เราบอกว่า module A เป็น ลูก ของ module B และ module B เป็น พ่อ ของ
module A สังเกตว่าทั้ง module tree มี root อยู่ภายใต้ module implicit ที่
ชื่อ crate
module tree อาจเตือนคุณถึง directory tree ของ filesystem บนคอมพิวเตอร์ ของคุณ — นี่เป็นการเปรียบเทียบที่เหมาะมาก! เหมือนกับ directory ใน filesystem คุณใช้ module จัดระเบียบโค้ดของคุณ และเหมือนกับไฟล์ใน directory เราต้องการวิธีหา module ของเรา
Path สำหรับอ้างถึง item ใน module tree
Path สำหรับอ้างถึง Item ใน Module Tree
ในการแสดงให้ Rust รู้ว่าจะหา item ใน module tree ที่ไหน เราใช้ path ใน แบบเดียวกับที่เราใช้ path เมื่อ navigate filesystem ในการเรียกฟังก์ชัน เราต้องรู้ path ของมัน
Path เป็นได้สองรูปแบบ:
- absolute path คือ path เต็มที่เริ่มจาก crate root สำหรับโค้ดจาก
external crate absolute path เริ่มด้วยชื่อ crate และสำหรับโค้ดจาก crate
ปัจจุบัน มันเริ่มด้วย literal
crate - relative path เริ่มจาก module ปัจจุบัน และใช้
self,superหรือ identifier ใน module ปัจจุบัน
ทั้ง absolute และ relative path ตามด้วย identifier หนึ่งหรือมากกว่าที่
คั่นด้วย double colon (::)
กลับไปที่ Listing 7-1 สมมติเราอยากเรียกฟังก์ชัน add_to_waitlist นี่
เหมือนกับถามว่า — path ของฟังก์ชัน add_to_waitlist คืออะไร? Listing 7-3
มี Listing 7-1 ที่ลบ module และฟังก์ชันบางตัวออก
เราจะแสดงสองวิธีในการเรียกฟังก์ชัน add_to_waitlist จากฟังก์ชันใหม่
eat_at_restaurant ที่ประกาศใน crate root path เหล่านี้ถูก แต่มีปัญหา
อีกอันที่จะป้องกันไม่ให้ตัวอย่างนี้ compile ได้ตามที่เป็น เราจะอธิบายว่า
ทำไมในไม่ช้า
ฟังก์ชัน eat_at_restaurant เป็นส่วนหนึ่งของ public API ของ library
crate เรา เราจึง mark มันด้วย keyword pub ในส่วน
“เปิดเผย Path ด้วย Keyword pub” เราจะลงรายละเอียด
มากขึ้นเรื่อง pub
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
add_to_waitlist ด้วย absolute และ relative pathครั้งแรกที่เราเรียกฟังก์ชัน add_to_waitlist ใน eat_at_restaurant เรา
ใช้ absolute path ฟังก์ชัน add_to_waitlist ประกาศใน crate เดียวกับ
eat_at_restaurant ซึ่งหมายความว่าเราใช้ keyword crate เริ่ม absolute
path ได้ จากนั้นเรารวมแต่ละ module ที่ต่อกันจนกระทั่งเราเดินไปถึง
add_to_waitlist คุณนึกถึง filesystem ที่มีโครงสร้างเดียวกันได้ — เรา
ระบุ path /front_of_house/hosting/add_to_waitlist เพื่อรันโปรแกรม
add_to_waitlist การใช้ชื่อ crate เริ่มจาก crate root เหมือนการใช้ /
เริ่มจาก filesystem root ใน shell ของคุณ
ครั้งที่สองที่เราเรียก add_to_waitlist ใน eat_at_restaurant เราใช้
relative path path เริ่มด้วย front_of_house ชื่อของ module ที่ประกาศ
ในระดับเดียวกันของ module tree กับ eat_at_restaurant ที่นี่สิ่งเทียบ
เท่า filesystem จะเป็นการใช้ path front_of_house/hosting/add_to_waitlist
การเริ่มด้วยชื่อ module หมายความว่า path เป็น relative
การเลือกใช้ relative หรือ absolute path เป็นการตัดสินใจที่คุณจะทำตาม
โปรเจกต์ของคุณ และขึ้นกับว่าคุณมีโอกาสมากกว่าที่จะย้ายโค้ดการประกาศ item
แยกจากหรือพร้อมกับโค้ดที่ใช้ item เช่น ถ้าเราย้าย module front_of_house
และฟังก์ชัน eat_at_restaurant เข้า module ชื่อ customer_experience
เราต้อง update absolute path ไปยัง add_to_waitlist แต่ relative path
จะยัง valid อย่างไรก็ตาม ถ้าเราย้ายฟังก์ชัน eat_at_restaurant แยกเข้า
module ชื่อ dining absolute path ไปยังการเรียก add_to_waitlist จะ
ยังเหมือนเดิม แต่ relative path จะต้อง update ความชอบของเราโดยทั่วไปคือ
ระบุ absolute path เพราะมีโอกาสมากกว่าที่เราจะอยากย้ายการประกาศโค้ดและ
การเรียก item อย่างอิสระจากกัน
ลอง compile Listing 7-3 และดูว่าทำไมมันยัง compile ไม่ได้! Error ที่เรา ได้แสดงใน Listing 7-4
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
error[E0603]: module `hosting` is private
--> src/lib.rs:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
Error message บอกว่า module hosting เป็น private พูดอีกอย่าง เรามี
path ที่ถูกต้องสำหรับ module hosting และฟังก์ชัน add_to_waitlist แต่
Rust ไม่ให้เราใช้พวกมัน เพราะมันไม่มีการเข้าถึงส่วน private ใน Rust item
ทั้งหมด (ฟังก์ชัน, เมธอด, struct, enum, module และ constant) เป็น
private จาก module พ่อโดย default ถ้าคุณอยากทำให้ item อย่างฟังก์ชันหรือ
struct เป็น private คุณใส่มันใน module
Item ใน module พ่อใช้ private item ภายใน module ลูกไม่ได้ แต่ item ใน module ลูกใช้ item ใน module บรรพบุรุษได้ นี่เพราะ module ลูกห่อและซ่อน รายละเอียด implementation ของพวกมัน แต่ module ลูกเห็นบริบทที่พวกมันถูก ประกาศได้ ต่อกับการเปรียบเทียบของเรา คิดถึงกฎ privacy เหมือน back office ของร้านอาหาร — สิ่งที่เกิดในนั้นเป็น private ต่อลูกค้าร้านอาหาร แต่ผู้ จัดการ office เห็นและทำทุกอย่างในร้านอาหารที่พวกเขาดำเนินการ
Rust เลือกให้ระบบ module ทำงานในแบบนี้ เพื่อให้การซ่อนรายละเอียด
implementation ภายในเป็น default แบบนั้น คุณรู้ว่าส่วนไหนของโค้ดภายในที่
คุณเปลี่ยนได้โดยไม่ทำให้โค้ดภายนอกเสีย อย่างไรก็ตาม Rust ให้ตัวเลือกคุณ
ในการเปิดเผยส่วนภายในของโค้ด module ลูกให้ module บรรพบุรุษภายนอก โดยใช้
keyword pub เพื่อทำให้ item เป็น public
เปิดเผย Path ด้วย Keyword pub
มากลับไปที่ error ใน Listing 7-4 ที่บอกเราว่า module hosting เป็น
private เราอยากให้ฟังก์ชัน eat_at_restaurant ใน module พ่อมีการเข้าถึง
ฟังก์ชัน add_to_waitlist ใน module ลูก เราจึง mark module hosting
ด้วย keyword pub ดังที่แสดงใน Listing 7-5
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
// -- snip --
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
hosting เป็น pub เพื่อใช้จาก eat_at_restaurantน่าเสียดาย โค้ดใน Listing 7-5 ยังคงให้ compiler error ดังที่แสดงใน Listing 7-6
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:10:37
|
10 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:13:30
|
13 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
เกิดอะไรขึ้น? การเพิ่ม keyword pub หน้า mod hosting ทำให้ module เป็น
public ด้วยการเปลี่ยนนี้ ถ้าเราเข้าถึง front_of_house ได้ เราเข้าถึง
hosting ได้ แต่ เนื้อหา ของ hosting ยังเป็น private — การทำให้
module เป็น public ไม่ทำให้เนื้อหาของมันเป็น public Keyword pub บน
module เพียงให้โค้ดใน module บรรพบุรุษอ้างถึงมัน ไม่ให้เข้าถึงโค้ดภายใน
เพราะ module เป็น container ไม่มีอะไรมากที่เราทำได้โดยทำให้แค่ module
เป็น public — เราต้องไปไกลกว่าและเลือกทำให้ item หนึ่งหรือมากกว่าภายใน
module เป็น public ด้วย
Error ใน Listing 7-6 บอกว่าฟังก์ชัน add_to_waitlist เป็น private กฎ
privacy ใช้กับ struct, enum, ฟังก์ชันและเมธอด รวมถึง module
มาทำให้ฟังก์ชัน add_to_waitlist เป็น public ด้วย โดยเพิ่ม keyword
pub ก่อนการประกาศของมัน เหมือนใน Listing 7-7
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// -- snip --
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
pub ให้ mod hosting และ fn add_to_waitlist ให้เราเรียกฟังก์ชันจาก eat_at_restaurantตอนนี้โค้ดจะ compile ผ่าน! เพื่อดูว่าทำไมการเพิ่ม keyword pub ให้เราใช้
path เหล่านี้ใน eat_at_restaurant ตามกฎ privacy ได้ มาดู absolute และ
relative path
ใน absolute path เราเริ่มด้วย crate ที่ root ของ module tree ของ crate
เรา module front_of_house ประกาศใน crate root ขณะที่ front_of_house
ไม่ public แต่เพราะฟังก์ชัน eat_at_restaurant ประกาศใน module เดียวกับ
front_of_house (นั่นคือ eat_at_restaurant และ front_of_house เป็น
sibling) เราอ้างถึง front_of_house จาก eat_at_restaurant ได้ ถัดไปคือ
module hosting ที่ mark ด้วย pub เราเข้าถึง module พ่อของ hosting
ได้ จึงเข้าถึง hosting ได้ สุดท้าย ฟังก์ชัน add_to_waitlist ถูก mark
ด้วย pub และเราเข้าถึง module พ่อของมันได้ การเรียกฟังก์ชันนี้จึงทำงาน!
ใน relative path logic เหมือน absolute path ยกเว้นขั้นแรก — แทนการเริ่ม
จาก crate root path เริ่มจาก front_of_house Module front_of_house
ประกาศภายใน module เดียวกันกับ eat_at_restaurant relative path ที่เริ่ม
จาก module ที่ eat_at_restaurant ประกาศจึงทำงาน จากนั้น เพราะ hosting
และ add_to_waitlist ถูก mark ด้วย pub ส่วนที่เหลือของ path ทำงาน
และการเรียกฟังก์ชันนี้ valid!
ถ้าคุณวางแผนแชร์ library crate เพื่อให้โปรเจกต์อื่นใช้โค้ดของคุณ public API ของคุณคือสัญญากับ user ของ crate ของคุณ ที่กำหนดวิธีที่พวกเขาโต้ตอบ กับโค้ดของคุณ มีข้อพิจารณาเยอะเรื่องการจัดการการเปลี่ยนใน public API เพื่อทำให้ง่ายขึ้นที่คนจะพึ่ง crate ของคุณ ข้อพิจารณาเหล่านี้อยู่นอก ขอบเขตของหนังสือนี้ ถ้าคุณสนใจหัวข้อนี้ ดู Rust API Guidelines
Best Practice สำหรับ Package ที่มีทั้ง Binary และ Library
เราเอ่ยว่า package มีทั้ง src/main.rs binary crate root และ src/lib.rs library crate root ได้ และทั้งสอง crate จะมีชื่อของ package โดย default โดยปกติ package ที่มี pattern นี้ของการมีทั้ง library และ binary crate จะมีโค้ดใน binary crate เพียงพอที่จะเริ่ม executable ที่เรียกโค้ดที่ประกาศใน library crate สิ่งนี้ให้โปรเจกต์อื่น ได้รับประโยชน์จาก functionality ส่วนใหญ่ที่ package ให้ เพราะโค้ดของ library crate แชร์ได้
Module tree ควรประกาศใน src/lib.rs จากนั้น public item ใด ๆ ใช้ใน binary crate ได้ โดยเริ่ม path ด้วยชื่อ package Binary crate กลายเป็น user ของ library crate เหมือนกับที่ external crate สมบูรณ์จะใช้ library crate — มันใช้ได้แค่ public API เท่านั้น สิ่งนี้ช่วยให้คุณ ออกแบบ API ที่ดี — ไม่ใช่แค่คุณเป็นผู้เขียน คุณยังเป็น client ด้วย!
ใน บทที่ 12 เราจะแสดงการปฏิบัติการจัดระเบียบนี้ กับโปรแกรม command line ที่มีทั้ง binary crate และ library crate
เริ่ม Relative Path ด้วย super
เราสร้าง relative path ที่เริ่มใน module พ่อ แทน module ปัจจุบันหรือ crate
root ได้ โดยใช้ super ที่ต้น path นี่เหมือนการเริ่ม filesystem path
ด้วย syntax .. ที่หมายถึงไป directory พ่อ การใช้ super ให้เราอ้างถึง
item ที่เรารู้ว่าอยู่ใน module พ่อ ซึ่งทำให้การจัดเรียง module tree ใหม่
ง่ายขึ้นเมื่อ module ผูกใกล้กับพ่อ แต่พ่ออาจถูกย้ายไปที่อื่นใน module
tree วันหนึ่ง
พิจารณาโค้ดใน Listing 7-8 ที่ model สถานการณ์ที่ chef แก้ order ที่ผิด
และเอามาให้ลูกค้าด้วยตัวเอง ฟังก์ชัน fix_incorrect_order ที่ประกาศใน
module back_of_house เรียกฟังก์ชัน deliver_order ที่ประกาศใน module
พ่อ โดยระบุ path ไปยัง deliver_order เริ่มด้วย super
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
superฟังก์ชัน fix_incorrect_order อยู่ใน module back_of_house เราจึงใช้
super ไป module พ่อของ back_of_house ได้ ซึ่งในกรณีนี้คือ crate
root จากตรงนั้น เรามองหา deliver_order และเจอมัน สำเร็จ! เราคิดว่า
module back_of_house และฟังก์ชัน deliver_order มีโอกาสจะอยู่ในความ
สัมพันธ์เดียวกันกับกัน และถูกย้ายไปด้วยกัน ถ้าเราตัดสินใจจัดระเบียบ
module tree ของ crate ใหม่ ดังนั้นเราใช้ super เพื่อจะมีที่น้อยกว่าให้
update โค้ดในอนาคต ถ้าโค้ดนี้ถูกย้ายไปที่ module อื่น
ทำให้ Struct และ Enum เป็น Public
เรายังใช้ pub กำหนด struct และ enum เป็น public ได้ แต่มีรายละเอียด
เพิ่มเติมเรื่องการใช้ pub กับ struct และ enum ถ้าเราใช้ pub ก่อนการ
ประกาศ struct เราทำให้ struct เป็น public แต่ field ของ struct จะยัง
เป็น private เราทำให้แต่ละ field เป็น public หรือไม่ได้แบบ case-by-case
ใน Listing 7-9 เราประกาศ struct back_of_house::Breakfast ที่ public
ที่มี field toast public แต่ field seasonal_fruit private สิ่งนี้
model กรณีในร้านอาหารที่ลูกค้าเลือกชนิดขนมปังที่มากับมื้อได้ แต่ chef
ตัดสินใจว่าผลไม้ตัวไหนคู่กับมื้อ ขึ้นกับว่าอะไรอยู่ในฤดูและในสต็อก ผลไม้
ที่มีเปลี่ยนเร็ว ลูกค้าจึงเลือกผลไม้ไม่ได้ และเห็นไม่ได้แม้จะได้ผลไม้
ตัวไหน
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}
pub fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast.
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like.
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal.
// meal.seasonal_fruit = String::from("blueberries");
}
เพราะ field toast ใน struct back_of_house::Breakfast เป็น public ใน
eat_at_restaurant เราเขียนและอ่าน field toast โดยใช้ dot notation ได้
สังเกตว่าเราใช้ field seasonal_fruit ใน eat_at_restaurant ไม่ได้
เพราะ seasonal_fruit เป็น private ลอง uncomment บรรทัดที่แก้ค่า field
seasonal_fruit เพื่อดูว่าคุณได้ error อะไร!
หมายเหตุว่า เพราะ back_of_house::Breakfast มี field private struct ต้อง
ให้ associated function public ที่สร้าง instance ของ Breakfast (เรา
ตั้งชื่อมัน summer ที่นี่) ถ้า Breakfast ไม่มีฟังก์ชันแบบนี้ เราสร้าง
instance ของ Breakfast ใน eat_at_restaurant ไม่ได้ เพราะเรา set ค่า
ของ field seasonal_fruit private ใน eat_at_restaurant ไม่ได้
ตรงข้าม ถ้าเราทำให้ enum เป็น public variant ทั้งหมดของมันจะเป็น public
แล้ว เราต้องการแค่ pub ก่อน keyword enum ดังที่แสดงใน Listing 7-10
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
เพราะเราทำให้ enum Appetizer เป็น public เราใช้ variant Soup และ
Salad ใน eat_at_restaurant ได้
Enum ไม่มีประโยชน์มากเว้นแต่ variant ของมันเป็น public มันจะน่ารำคาญที่
ต้อง annotate variant ของ enum ทั้งหมดด้วย pub ในทุกกรณี default
สำหรับ variant ของ enum จึงเป็น public Struct มักมีประโยชน์โดยที่ field
ไม่ public ดังนั้น field ของ struct ตามกฎทั่วไปที่ทุกอย่างเป็น private
โดย default เว้นแต่ annotate ด้วย pub
มีอีกสถานการณ์ที่เกี่ยวกับ pub ที่เราไม่ได้ครอบคลุม และนั่นคือฟีเจอร์
ระบบ module สุดท้ายของเรา — keyword use เราจะครอบคลุม use เอง ๆ ก่อน
แล้วเราจะแสดงวิธีรวม pub และ use
นำ path เข้า scope ด้วย keyword use
นำ Path เข้า Scope ด้วย Keyword use
การต้องเขียน path เพื่อเรียกฟังก์ชันรู้สึกไม่สะดวกและซ้ำซ้อนได้ ใน
Listing 7-7 ไม่ว่าเราเลือก absolute หรือ relative path ไปยังฟังก์ชัน
add_to_waitlist ทุกครั้งที่เราอยากเรียก add_to_waitlist เราต้องระบุ
front_of_house และ hosting ด้วย โชคดี มีวิธีที่จะทำให้กระบวนการนี้
ง่ายขึ้น — เราสร้าง shortcut ของ path ด้วย keyword use ครั้งเดียว แล้ว
ใช้ชื่อสั้นกว่าทุกที่อื่นใน scope ได้
ใน Listing 7-11 เรานำ module crate::front_of_house::hosting เข้าใน
scope ของฟังก์ชัน eat_at_restaurant เพื่อให้เราต้องระบุแค่
hosting::add_to_waitlist เพื่อเรียกฟังก์ชัน add_to_waitlist ใน
eat_at_restaurant
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
useการเพิ่ม use และ path ใน scope คล้ายกับการสร้าง symbolic link ใน
filesystem ด้วยการเพิ่ม use crate::front_of_house::hosting ใน crate
root hosting ตอนนี้เป็นชื่อ valid ใน scope นั้น เหมือนกับว่า module
hosting ประกาศใน crate root Path ที่นำเข้า scope ด้วย use ก็เช็ค
privacy ด้วย เหมือน path อื่นใด
หมายเหตุว่า use สร้าง shortcut เฉพาะสำหรับ scope ที่ use เกิดเท่านั้น
Listing 7-12 ย้ายฟังก์ชัน eat_at_restaurant เข้า module ลูกใหม่ชื่อ
customer ซึ่งจึงเป็น scope ต่างจาก statement use body ของฟังก์ชันจะ
compile ไม่ผ่าน
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
use ใช้ได้แค่ใน scope ที่มันอยู่Compiler error แสดงว่า shortcut ใช้ไม่ได้แล้วภายใน module customer:
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0433]: failed to resolve: use of unresolved module or unlinked crate `hosting`
--> src/lib.rs:11:9
|
11 | hosting::add_to_waitlist();
| ^^^^^^^ use of unresolved module or unlinked crate `hosting`
|
= help: if you wanted to use a crate named `hosting`, use `cargo add hosting` to add it to your `Cargo.toml`
help: consider importing this module through its public re-export
|
10 + use crate::hosting;
|
warning: unused import: `crate::front_of_house::hosting`
--> src/lib.rs:7:5
|
7 | use crate::front_of_house::hosting;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
For more information about this error, try `rustc --explain E0433`.
warning: `restaurant` (lib) generated 1 warning
error: could not compile `restaurant` (lib) due to 1 previous error; 1 warning emitted
สังเกตว่ายังมี warning ว่า use ไม่ถูกใช้แล้วใน scope ของมัน! ในการแก้
ปัญหานี้ ย้าย use เข้า module customer ด้วย หรืออ้างถึง shortcut ใน
module พ่อด้วย super::hosting ภายใน module customer ลูก
สร้าง Path use ที่ Idiomatic
ใน Listing 7-11 คุณอาจสงสัยว่าทำไมเราระบุ use crate::front_of_house::hosting
แล้วเรียก hosting::add_to_waitlist ใน eat_at_restaurant แทนการระบุ
path use ตลอดจนถึงฟังก์ชัน add_to_waitlist เพื่อให้ผลเดียวกัน เหมือนใน
Listing 7-13
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting::add_to_waitlist;
pub fn eat_at_restaurant() {
add_to_waitlist();
}
add_to_waitlist เข้า scope ด้วย use ซึ่งไม่ idiomaticแม้ทั้ง Listing 7-11 และ Listing 7-13 ทำงานเดียวกัน Listing 7-11 เป็น
วิธี idiomatic ในการนำฟังก์ชันเข้า scope ด้วย use การนำ module พ่อของ
ฟังก์ชันเข้า scope ด้วย use หมายความว่าเราต้องระบุ module พ่อเมื่อเรียก
ฟังก์ชัน การระบุ module พ่อเมื่อเรียกฟังก์ชันทำให้ชัดเจนว่าฟังก์ชันไม่
ประกาศ local ขณะลดการเขียน path เต็มซ้ำ โค้ดใน Listing 7-13 ไม่ชัดเจนว่า
add_to_waitlist ประกาศที่ไหน
ตรงข้าม เมื่อนำ struct, enum และ item อื่นด้วย use มัน idiomatic ที่จะ
ระบุ path เต็ม Listing 7-14 แสดงวิธี idiomatic ในการนำ struct HashMap
ของ standard library เข้า scope ของ binary crate
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
HashMap เข้า scope ในแบบ idiomaticไม่มีเหตุผลแข็งแรงเบื้องหลัง idiom นี้ — มันแค่ convention ที่เกิดขึ้น และคนคุ้นเคยกับการอ่านและเขียนโค้ด Rust ในแบบนี้
ข้อยกเว้นของ idiom นี้คือถ้าเรานำสอง item ที่มีชื่อเดียวกันเข้า scope
ด้วย statement use เพราะ Rust ไม่อนุญาตอย่างนั้น Listing 7-15 แสดงวิธี
นำสอง type Result ที่มีชื่อเดียวกันแต่ module พ่อต่างกันเข้า scope และ
วิธีอ้างถึงพวกมัน
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
Ok(())
}
fn function2() -> io::Result<()> {
// --snip--
Ok(())
}
อย่างที่คุณเห็น การใช้ module พ่อแยก type Result สองตัว ถ้าแทนเราระบุ
use std::fmt::Result และ use std::io::Result เราจะมี type Result
สองตัวใน scope เดียว และ Rust จะไม่รู้ว่าเราหมายถึงตัวไหนเมื่อเราใช้
Result
ให้ชื่อใหม่ด้วย Keyword as
มีอีกคำตอบสำหรับปัญหาของการนำสอง type ที่ชื่อเดียวกันเข้า scope เดียวกัน
ด้วย use — หลัง path เราระบุ as และชื่อ local ใหม่ หรือ alias ของ
type ได้ Listing 7-16 แสดงอีกวิธีในการเขียนโค้ดใน Listing 7-15 โดย
เปลี่ยนชื่อหนึ่งใน type Result สองตัวด้วย as
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
Ok(())
}
fn function2() -> IoResult<()> {
// --snip--
Ok(())
}
asใน statement use ที่สอง เราเลือกชื่อใหม่ IoResult สำหรับ type
std::io::Result ซึ่งจะไม่ขัดกับ Result จาก std::fmt ที่เราก็นำเข้า
scope ด้วย Listing 7-15 และ Listing 7-16 ถือเป็น idiomatic ดังนั้นการ
เลือกอยู่ที่คุณ!
Re-export ชื่อด้วย pub use
เมื่อเรานำชื่อเข้า scope ด้วย keyword use ชื่อเป็น private ต่อ scope
ที่เรา import เข้า ในการเปิดทางให้โค้ดนอก scope นั้นอ้างถึงชื่อนั้น
ราวกับว่ามันประกาศใน scope นั้น เรารวม pub และ use ได้ เทคนิคนี้
เรียกว่า re-export เพราะเรากำลังนำ item เข้า scope แต่ก็ทำให้ item นั้น
มีให้คนอื่นนำเข้า scope ของพวกเขาด้วย
Listing 7-17 แสดงโค้ดใน Listing 7-11 ที่เปลี่ยน use ใน root module
เป็น pub use
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
pub useก่อนการเปลี่ยนนี้ โค้ดภายนอกจะต้องเรียกฟังก์ชัน add_to_waitlist โดยใช้
path restaurant::front_of_house::hosting::add_to_waitlist() ซึ่งจะ
ต้องการให้ module front_of_house ถูก mark เป็น pub ด้วย ตอนนี้ pub use นี้ได้ re-export module hosting จาก root module โค้ดภายนอกใช้
path restaurant::hosting::add_to_waitlist() แทนได้
Re-export มีประโยชน์เมื่อโครงสร้างภายในของโค้ดของคุณต่างจากที่โปรแกรมเมอร์
ที่เรียกโค้ดของคุณจะคิดเรื่อง domain เช่น ในการเปรียบเทียบร้านอาหารนี้
คนที่ดำเนินร้านอาหารคิดเรื่อง “front of house” และ “back of house” แต่
ลูกค้าที่เยี่ยมร้านอาหารคงไม่คิดเรื่องส่วนของร้านอาหารในคำเหล่านั้น ด้วย
pub use เราเขียนโค้ดของเราด้วยโครงสร้างหนึ่ง แต่เปิดเผยโครงสร้างต่าง
ได้ การทำอย่างนั้นทำให้ library ของเราจัดระเบียบดีสำหรับโปรแกรมเมอร์ที่
ทำงานบน library และโปรแกรมเมอร์ที่เรียก library เราจะดูตัวอย่างอีกของ
pub use และวิธีที่มันส่งผลต่อ documentation ของ crate ของคุณใน
“Export Public API ที่สะดวก” ในบทที่ 14
ใช้ Package ภายนอก
ในบทที่ 2 เราเขียนโปรเจกต์เกมทายตัวเลขที่ใช้ external package ชื่อ rand
เพื่อรับตัวเลขสุ่ม ในการใช้ rand ในโปรเจกต์ของเรา เราเพิ่มบรรทัดนี้ใน
Cargo.toml:
rand = "0.8.5"
การเพิ่ม rand เป็น dependency ใน Cargo.toml บอก Cargo ให้ download
package rand และ dependency ใด ๆ จาก crates.io
และทำให้ rand มีให้โปรเจกต์ของเรา
จากนั้น ในการนำ definition ของ rand เข้า scope ของ package ของเรา เรา
เพิ่มบรรทัด use เริ่มด้วยชื่อ crate rand และ list item ที่เราอยากนำ
เข้า scope จำได้ว่าใน “Generate ตัวเลขสุ่ม” ในบทที่
2 เรานำ trait Rng เข้า scope และเรียกฟังก์ชัน rand::thread_rng:
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
}
สมาชิกของ Rust community ทำ package หลายตัวให้ที่
crates.io และการดึงพวกมันใด ๆ เข้า package ของคุณ
เกี่ยวข้องกับขั้นตอนเดียวกัน — list พวกมันในไฟล์ Cargo.toml ของ package
ของคุณ และใช้ use นำ item จาก crate ของพวกมันเข้า scope
หมายเหตุว่า library std มาตรฐานก็เป็น crate ที่ external ต่อ package
ของเรา เพราะ standard library มากับภาษา Rust เราไม่ต้องเปลี่ยน
Cargo.toml เพื่อรวม std แต่เราต้องอ้างถึงมันด้วย use เพื่อนำ item
จากตรงนั้นเข้า scope ของ package เรา เช่น กับ HashMap เราจะใช้บรรทัด
นี้:
#![allow(unused)]
fn main() {
use std::collections::HashMap;
}
นี่เป็น absolute path ที่เริ่มด้วย std ชื่อของ standard library crate
ใช้ Nested Path เพื่อทำความสะอาด List use
ถ้าเราใช้ item หลายตัวที่ประกาศใน crate หรือ module เดียวกัน การ list
แต่ละ item บนบรรทัดของตัวเองอาจกินที่แนวตั้งเยอะในไฟล์ของเรา เช่น
statement use สองตัวที่เรามีในเกมทายตัวเลขใน Listing 2-4 นำ item จาก
std เข้า scope:
use rand::Rng;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
แทน เราใช้ nested path นำ item เดียวกันเข้า scope ในบรรทัดเดียวได้ เราทำ สิ่งนี้โดยระบุส่วนที่เหมือนกันของ path ตามด้วย colon สองตัว แล้ว curly bracket รอบ list ของส่วนของ path ที่ต่างกัน ดังที่แสดงใน Listing 7-18
use rand::Rng;
// --snip--
use std::{cmp::Ordering, io};
// --snip--
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!");
println!("You guessed: {guess}");
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
ในโปรแกรมใหญ่ขึ้น การนำ item หลายตัวเข้า scope จาก crate หรือ module
เดียวกันโดยใช้ nested path ลดจำนวน statement use แยกที่ต้องการได้เยอะ!
เราใช้ nested path ในระดับใดของ path ได้ ซึ่งมีประโยชน์เมื่อรวม statement
use สองตัวที่แชร์ subpath เช่น Listing 7-19 แสดง statement use สอง
ตัว — ตัวหนึ่งที่นำ std::io เข้า scope และตัวหนึ่งที่นำ std::io::Write
เข้า scope
use std::io;
use std::io::Write;
use สองตัวที่ตัวหนึ่งเป็น subpath ของอีกตัวส่วนที่เหมือนกันของสอง path นี้คือ std::io และนั่นคือ path แรกสมบูรณ์
ในการรวมสอง path นี้เป็น statement use เดียว เราใช้ self ใน nested
path ได้ ดังที่แสดงใน Listing 7-20
use std::io::{self, Write};
use เดียวบรรทัดนี้นำ std::io และ std::io::Write เข้า scope
Import Item ด้วย Glob Operator
ถ้าเราอยากนำ public item ทั้งหมด ที่ประกาศใน path เข้า scope เราระบุ
path นั้นตามด้วย glob operator *:
#![allow(unused)]
fn main() {
use std::collections::*;
}
statement use นี้นำ public item ทั้งหมดที่ประกาศใน std::collections
เข้า scope ปัจจุบัน ระวังเมื่อใช้ glob operator! Glob ทำให้ยากขึ้นที่จะ
บอกว่าชื่อไหนอยู่ใน scope และที่ที่ชื่อที่ใช้ในโปรแกรมของคุณถูกประกาศ
นอกจากนี้ ถ้า dependency เปลี่ยน definition สิ่งที่คุณ import เปลี่ยน
ด้วย ซึ่งอาจนำไปสู่ compiler error เมื่อคุณ upgrade dependency ถ้า
dependency เพิ่ม definition ที่มีชื่อเหมือน definition ของคุณใน scope
เดียวกัน เช่น
glob operator มักใช้เมื่อทำการทดสอบ เพื่อนำทุกอย่างภายใต้การทดสอบเข้า
module tests เราจะพูดถึงเรื่องนั้นใน
“วิธีเขียนเทส” ในบทที่ 11 glob operator
บางครั้งก็ใช้เป็นส่วนหนึ่งของ prelude pattern ดู
documentation ของ standard library
สำหรับข้อมูลเพิ่มเติมเรื่อง pattern นั้น
แยก module ไปคนละไฟล์
แยก Module ไปคนละไฟล์
ที่ผ่านมา ตัวอย่างทั้งหมดในบทนี้ประกาศ module หลายตัวในไฟล์เดียว เมื่อ module ใหญ่ คุณอาจอยากย้าย definition ของพวกมันไปไฟล์แยก เพื่อทำให้โค้ด ง่ายต่อการ navigate
เช่น เริ่มจากโค้ดใน Listing 7-17 ที่มี module ร้านอาหารหลายตัว เราจะดึง module เข้าไฟล์ แทนการมี module ทั้งหมดประกาศในไฟล์ crate root ในกรณีนี้ ไฟล์ crate root คือ src/lib.rs แต่ขั้นตอนนี้ก็ใช้ได้กับ binary crate ที่ ไฟล์ crate root คือ src/main.rs
ก่อนอื่น เราจะดึง module front_of_house ออกไปไฟล์ของมันเอง ลบโค้ดภายใน
curly bracket สำหรับ module front_of_house เหลือแค่การประกาศ
mod front_of_house; เพื่อให้ src/lib.rs มีโค้ดที่แสดงใน Listing 7-21
หมายเหตุว่านี่จะ compile ไม่ผ่านจนกว่าเราจะสร้างไฟล์ src/front_of_house.rs
ใน Listing 7-22
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
front_of_house ที่ body จะอยู่ใน src/front_of_house.rsถัดไป วางโค้ดที่อยู่ใน curly bracket เข้าไฟล์ใหม่ชื่อ
src/front_of_house.rs ดังที่แสดงใน Listing 7-22 Compiler รู้ว่าจะมอง
ในไฟล์นี้ เพราะมันเจอการประกาศ module ใน crate root ที่ชื่อ
front_of_house
pub mod hosting {
pub fn add_to_waitlist() {}
}
front_of_house ใน src/front_of_house.rsหมายเหตุว่าคุณต้อง load ไฟล์โดยใช้การประกาศ mod ครั้งเดียว ใน module
tree ของคุณ เมื่อ compiler รู้ว่าไฟล์เป็นส่วนของโปรเจกต์ (และรู้ว่าโค้ด
อยู่ที่ไหนใน module tree เพราะคุณวาง statement mod ที่ไหน) ไฟล์อื่นใน
โปรเจกต์ของคุณควรอ้างถึงโค้ดของไฟล์ที่ load โดยใช้ path ไปยังตำแหน่งที่มัน
ถูกประกาศ ดังที่ครอบคลุมในส่วน
“Path สำหรับอ้างถึง Item ใน Module Tree” พูดอีก
อย่าง mod ไม่ใช่ operation “include” ที่คุณอาจเคยเห็นในภาษาโปรแกรม
อื่น
ถัดไป เราจะดึง module hosting ออกไปไฟล์ของมันเอง กระบวนการต่างกันนิด
หน่อย เพราะ hosting เป็น module ลูกของ front_of_house ไม่ใช่ของ
module root เราจะวางไฟล์สำหรับ hosting ใน directory ใหม่ที่จะตั้งชื่อ
ตามบรรพบุรุษใน module tree ในกรณีนี้ src/front_of_house
ในการเริ่มย้าย hosting เราเปลี่ยน src/front_of_house.rs ให้มีแค่การ
ประกาศของ module hosting:
pub mod hosting;
จากนั้น เราสร้าง directory src/front_of_house และไฟล์ hosting.rs
เพื่อมี definition ที่ทำใน module hosting:
pub fn add_to_waitlist() {}
ถ้าแทนเราใส่ hosting.rs ใน directory src compiler จะคาดว่าโค้ด
hosting.rs อยู่ใน module hosting ที่ประกาศใน crate root และไม่
ประกาศเป็นลูกของ module front_of_house กฎของ compiler สำหรับไฟล์ไหนที่
จะเช็คสำหรับโค้ดของ module ไหน หมายความว่า directory และไฟล์ match กับ
module tree ใกล้กว่า
Path ไฟล์ทางเลือก
ที่ผ่านมาเราครอบคลุม path ไฟล์ที่ idiomatic ที่สุดที่ Rust compiler ใช้
แต่ Rust รองรับสไตล์ path ไฟล์เก่ากว่าด้วย สำหรับ module ชื่อ
front_of_house ที่ประกาศใน crate root compiler จะมองหาโค้ดของ module
ใน:
- src/front_of_house.rs (สิ่งที่เราครอบคลุม)
- src/front_of_house/mod.rs (สไตล์เก่ากว่า ยังรองรับ path)
สำหรับ module ชื่อ hosting ที่เป็น submodule ของ front_of_house
compiler จะมองหาโค้ดของ module ใน:
- src/front_of_house/hosting.rs (สิ่งที่เราครอบคลุม)
- src/front_of_house/hosting/mod.rs (สไตล์เก่ากว่า ยังรองรับ path)
ถ้าคุณใช้ทั้งสองสไตล์สำหรับ module เดียวกัน คุณจะได้ compiler error การใช้สไตล์ผสมกันสำหรับ module ต่างกันในโปรเจกต์เดียวกันอนุญาต แต่อาจ ทำให้คนที่ navigate โปรเจกต์ของคุณสับสน
ข้อเสียหลักของสไตล์ที่ใช้ไฟล์ชื่อ mod.rs คือโปรเจกต์ของคุณอาจจบลงด้วย ไฟล์ชื่อ mod.rs หลายตัว ซึ่งอาจสับสนเมื่อคุณเปิดพวกมันใน editor พร้อม กัน
เราย้ายโค้ดของแต่ละ module ไปไฟล์แยก และ module tree ยังเหมือนเดิม การ
เรียกฟังก์ชันใน eat_at_restaurant จะทำงานได้โดยไม่ต้องแก้ใด ๆ แม้
definition อยู่ในไฟล์ต่างกัน เทคนิคนี้ให้คุณย้าย module ไปไฟล์ใหม่เมื่อ
พวกมันใหญ่ขึ้น
หมายเหตุว่า statement pub use crate::front_of_house::hosting ใน
src/lib.rs ก็ไม่ได้เปลี่ยน และ use ไม่มีผลต่อไฟล์ไหนที่ถูก compile
เป็นส่วนของ crate Keyword mod ประกาศ module และ Rust มองในไฟล์ที่ชื่อ
เดียวกับ module สำหรับโค้ดที่ไปใน module นั้น
สรุป
Rust ให้คุณแบ่ง package เป็นหลาย crate และ crate เป็นหลาย module เพื่อ
ให้คุณอ้างถึง item ที่ประกาศใน module หนึ่งจากอีก module ได้ คุณทำได้โดย
ระบุ absolute หรือ relative path Path เหล่านี้นำเข้า scope ด้วย statement
use ได้ เพื่อให้คุณใช้ path สั้นกว่าสำหรับการใช้ item หลายครั้งใน scope
นั้น โค้ด module เป็น private โดย default แต่คุณทำให้ definition เป็น
public ได้ โดยเพิ่ม keyword pub
ในบทถัดไป เราจะดูโครงสร้างข้อมูล collection ใน standard library ที่คุณใช้ ในโค้ดที่จัดระเบียบดีของคุณได้
Collection ที่ใช้บ่อย
Standard library ของ Rust รวมโครงสร้างข้อมูลที่มีประโยชน์มากหลายตัวที่ เรียกว่า collection Type ข้อมูลอื่นส่วนใหญ่แทนค่าเฉพาะหนึ่งค่า แต่ collection มีค่าหลายค่าได้ ต่างจาก type array และ tuple built-in ข้อมูล ที่ collection เหล่านี้ชี้ไป ถูกเก็บบน heap ซึ่งหมายความว่าจำนวนข้อมูล ไม่ต้องรู้ตอน compile time และเติบโตหรือหดได้เมื่อโปรแกรมรัน แต่ละชนิด ของ collection มี capability และต้นทุนต่างกัน และการเลือกตัวที่เหมาะสม สำหรับสถานการณ์ปัจจุบันของคุณเป็นทักษะที่คุณจะพัฒนาตามเวลา ในบทนี้เราจะ พูดถึง collection สามตัวที่ใช้บ่อยมากในโปรแกรม Rust:
- vector ให้คุณเก็บจำนวนค่าที่ไม่คงที่ติดกัน
- string คือ collection ของอักขระ เราเอ่ยถึง type
Stringก่อนหน้า แต่ ในบทนี้เราจะพูดถึงอย่างละเอียด - hash map ให้คุณผูกค่ากับ key เฉพาะ เป็น implementation เฉพาะของ โครงสร้างข้อมูลทั่วไปกว่าที่เรียกว่า map
ในการเรียนรู้เกี่ยวกับ collection ชนิดอื่นที่ standard library มี ดู documentation
เราจะพูดถึงวิธีสร้างและ update vector, string และ hash map รวมถึงสิ่งที่ ทำให้แต่ละตัวพิเศษ
เก็บรายการค่าด้วย vector
เก็บ List ของค่าด้วย Vector
Collection type แรกที่เราจะดูคือ Vec<T> หรือที่รู้จักในชื่อ vector
Vector ให้คุณเก็บค่ามากกว่าหนึ่งค่าในโครงสร้างข้อมูลเดียวที่ใส่ค่าทั้งหมด
ติดกันในหน่วยความจำ Vector เก็บได้แค่ค่าของ type เดียวกัน พวกมันมีประโยชน์
เมื่อคุณมี list ของ item เช่น บรรทัดข้อความในไฟล์ หรือราคาของ item ใน
รถเข็นช็อปปิ้ง
สร้าง Vector ใหม่
ในการสร้าง vector ว่างใหม่ เราเรียกฟังก์ชัน Vec::new ดังที่แสดงใน
Listing 8-1
fn main() {
let v: Vec<i32> = Vec::new();
}
i32หมายเหตุว่าเราเพิ่ม type annotation ที่นี่ เพราะเราไม่ได้แทรกค่าใด ๆ เข้า
vector นี้ Rust ไม่รู้ว่าเราตั้งใจเก็บ element ชนิดอะไร นี่เป็นจุดสำคัญ
Vector ถูก implement โดยใช้ generic เราจะครอบคลุมวิธีใช้ generic กับ type
ของคุณเองในบทที่ 10 ตอนนี้ รู้ว่า type Vec<T> ที่ standard library ให้
เก็บ type ใดก็ได้ เมื่อเราสร้าง vector เพื่อเก็บ type เฉพาะ เราระบุ type
ภายใน angle bracket ใน Listing 8-1 เราบอก Rust ว่า Vec<T> ใน v จะ
เก็บ element ของ type i32
บ่อยกว่านั้น คุณจะสร้าง Vec<T> ด้วยค่าเริ่มต้น และ Rust จะ infer type
ของค่าที่คุณอยากเก็บ คุณจึงไม่ค่อยต้องทำ type annotation นี้ Rust สะดวก
ให้ macro vec! ซึ่งจะสร้าง vector ใหม่ที่เก็บค่าที่คุณให้ Listing 8-2
สร้าง Vec<i32> ใหม่ที่เก็บค่า 1, 2 และ 3 integer type คือ i32
เพราะนั่นคือ default integer type ดังที่เราพูดถึงในส่วน
“ชนิดข้อมูล” ของบทที่ 3
fn main() {
let v = vec![1, 2, 3];
}
เพราะเราให้ค่า i32 เริ่มต้น Rust infer ได้ว่า type ของ v คือ
Vec<i32> และไม่จำเป็นต้องมี type annotation ถัดไป เราจะดูวิธีแก้
vector
Update Vector
ในการสร้าง vector แล้วเพิ่ม element ให้มัน เราใช้เมธอด push ได้ ดังที่
แสดงใน Listing 8-3
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
push เพื่อเพิ่มค่าให้ vectorเช่นเดียวกับตัวแปรใด ๆ ถ้าเราอยากเปลี่ยนค่า เราต้องทำให้มัน mutable โดย
ใช้ keyword mut ดังที่พูดถึงในบทที่ 3 ตัวเลขที่เราใส่ภายในเป็น type
i32 ทั้งหมด และ Rust infer สิ่งนี้จากข้อมูล เราจึงไม่ต้องมี annotation
Vec<i32>
อ่าน Element ของ Vector
มีสองวิธีในการอ้างถึงค่าที่เก็บใน vector — ผ่าน indexing หรือใช้เมธอด
get ในตัวอย่างต่อไปนี้ เรา annotate type ของค่าที่ return จากฟังก์ชัน
เหล่านี้เพื่อความชัดเจนเพิ่ม
Listing 8-4 แสดงทั้งสองเมธอดของการเข้าถึงค่าใน vector ด้วย indexing
syntax และเมธอด get
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
}
get เข้าถึง item ใน vectorหมายเหตุรายละเอียดบางอย่างที่นี่ เราใช้ค่า index 2 เพื่อรับ element ที่
สาม เพราะ vector ถูก index ด้วยตัวเลข เริ่มที่ศูนย์ การใช้ & และ []
ให้ reference ของ element ที่ค่า index เมื่อเราใช้เมธอด get ด้วย index
ที่ส่งเป็น argument เราได้ Option<&T> ที่เราใช้กับ match ได้
Rust ให้สองวิธีในการอ้างถึง element เพื่อให้คุณเลือกว่าโปรแกรมทำอย่างไร เมื่อคุณพยายามใช้ค่า index นอก range ของ element ที่มีอยู่ ตัวอย่าง มาดู ว่าเกิดอะไรขึ้นเมื่อเรามี vector ห้า element แล้วเราพยายามเข้าถึง element ที่ index 100 ด้วยแต่ละเทคนิค ดังที่แสดงใน Listing 8-5
fn main() {
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
}
เมื่อเรารันโค้ดนี้ เมธอด [] แรกจะทำให้โปรแกรม panic เพราะมันอ้างถึง
element ที่ไม่มี เมธอดนี้ใช้ดีที่สุดเมื่อคุณอยากให้โปรแกรมของคุณ crash
ถ้ามีการพยายามเข้าถึง element หลังท้าย vector
เมื่อเมธอด get ถูกส่ง index ที่อยู่นอก vector มัน return None โดยไม่
panic คุณจะใช้เมธอดนี้ถ้าการเข้าถึง element นอก range ของ vector อาจ
เกิดเป็นครั้งคราวภายใต้สถานการณ์ปกติ จากนั้นโค้ดของคุณจะมี logic จัดการ
การมี Some(&element) หรือ None ดังที่พูดถึงในบทที่ 6 เช่น index
อาจมาจากคนที่ป้อนตัวเลข ถ้าเขาเผลอป้อนตัวเลขที่ใหญ่เกินไป และโปรแกรมได้
ค่า None คุณบอก user ได้ว่ามีกี่ item ใน vector ปัจจุบัน และให้โอกาส
อีกครั้งในการป้อนค่าที่ valid นั่นจะเป็นมิตรกับ user มากกว่าการ crash
โปรแกรมเพราะ typo!
เมื่อโปรแกรมมี reference ที่ valid borrow checker บังคับใช้กฎ ownership และ borrowing (ครอบคลุมในบทที่ 4) เพื่อรับประกันว่า reference นี้และ reference อื่นใดของเนื้อหา vector ยัง valid จำกฎที่ระบุว่าคุณมี mutable และ immutable reference ใน scope เดียวไม่ได้ กฎนั้นใช้ใน Listing 8-6 ที่ เราถือ immutable reference ของ element แรกใน vector และพยายามเพิ่ม element ที่ท้าย โปรแกรมนี้จะไม่ทำงานถ้าเราพยายามอ้างถึง element นั้นทีหลัง ในฟังก์ชันด้วย
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}
การ compile โค้ดนี้จะให้ error นี้:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {first}");
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error
โค้ดใน Listing 8-6 อาจดูเหมือนน่าจะทำงานได้ — ทำไม reference ของ element แรกควรห่วงเรื่องการเปลี่ยนที่ท้ายของ vector? Error นี้เกิดจากวิธีที่ vector ทำงาน — เพราะ vector ใส่ค่าติดกันในหน่วยความจำ การเพิ่ม element ใหม่ที่ท้าย vector อาจต้องการ allocate หน่วยความจำใหม่ และคัดลอก element เก่าไปยังพื้นที่ใหม่ ถ้าไม่มีที่พอที่จะใส่ element ทั้งหมดติดกันที่ vector ถูกเก็บปัจจุบัน ในกรณีนั้น reference ของ element แรกจะชี้ไปยัง หน่วยความจำที่ deallocate กฎ borrowing ป้องกันโปรแกรมจากการจบลงในสถานการณ์ นั้น
หมายเหตุ: สำหรับข้อมูลเพิ่มเติมเรื่องรายละเอียด implementation ของ type
Vec<T>ดู “The Rustonomicon”
Iterate ผ่านค่าใน Vector
ในการเข้าถึงแต่ละ element ใน vector ทีละตัว เราจะ iterate ผ่าน element
ทั้งหมด แทนการใช้ index เข้าถึงทีละตัว Listing 8-7 แสดงวิธีใช้ for loop
เพื่อรับ immutable reference ของแต่ละ element ใน vector ของค่า i32
และพิมพ์พวกมัน
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
for loopเรายัง iterate ผ่าน mutable reference ของแต่ละ element ใน vector
mutable เพื่อทำการเปลี่ยนกับ element ทั้งหมดได้ for loop ใน Listing 8-8
จะเพิ่ม 50 ให้แต่ละ element
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}
ในการเปลี่ยนค่าที่ mutable reference อ้างถึง เราต้องใช้ dereference
operator * เพื่อรับค่าใน i ก่อนเราใช้ operator += ได้ เราจะพูดถึง
dereference operator เพิ่มในส่วน
“ตาม Reference ไปยังค่า” ของบทที่ 15
การ iterate ผ่าน vector ไม่ว่า immutable หรือ mutable ปลอดภัยเพราะกฎ
ของ borrow checker ถ้าเราพยายามแทรกหรือลบ item ใน body ของ for loop
ใน Listing 8-7 และ Listing 8-8 เราจะได้ compiler error คล้ายกับที่เราได้
กับโค้ดใน Listing 8-6 reference ของ vector ที่ for loop ถือ ป้องกัน
การแก้ทั้ง vector พร้อมกัน
ใช้ Enum เก็บ Type หลายตัว
Vector เก็บได้แค่ค่าของ type เดียวกัน นี่อาจไม่สะดวก มีแน่นอน use case ที่ต้องการเก็บ list ของ item ที่ต่างกัน โชคดี variant ของ enum ถูก ประกาศใต้ enum type เดียวกัน ดังนั้นเมื่อเราต้องการ type หนึ่งแทน element ของ type ต่างกัน เราประกาศและใช้ enum ได้!
เช่น สมมติเราอยากรับค่าจาก row ใน spreadsheet ที่บาง column ใน row มี integer บางอันมี floating-point number และบางอันมี string เราประกาศ enum ที่ variant จะเก็บ value type ต่างกันได้ และ variant ของ enum ทั้งหมดจะถือเป็น type เดียวกัน คือของ enum จากนั้นเราสร้าง vector เก็บ enum นั้น และสุดท้ายเก็บ type ต่างกันได้ เราแสดงสิ่งนี้ใน Listing 8-9
fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
Rust ต้องรู้ว่า type อะไรจะอยู่ใน vector ตอน compile time เพื่อให้รู้ว่า
ต้องการหน่วยความจำเท่าไรบน heap ในการเก็บแต่ละ element เราต้อง explicit
ด้วยว่า type อะไรอนุญาตใน vector นี้ ถ้า Rust อนุญาตให้ vector เก็บ type
ใด ๆ จะมีโอกาสที่ type หนึ่งหรือมากกว่าจะทำให้เกิด error กับ operation
ที่ทำบน element ของ vector การใช้ enum บวก match expression หมายความ
ว่า Rust จะรับประกันที่ compile time ว่าทุกกรณีที่เป็นไปได้ถูกจัดการ ดัง
ที่พูดถึงในบทที่ 6
ถ้าคุณไม่รู้ชุด exhaustive ของ type ที่โปรแกรมจะได้ตอน runtime เพื่อเก็บ ใน vector เทคนิค enum จะไม่ทำงาน แทน คุณใช้ trait object ได้ ซึ่งเราจะ ครอบคลุมในบทที่ 18
ตอนนี้เราพูดถึงวิธีที่ใช้ vector บ่อยที่สุดบางตัวแล้ว อย่าลืมทบทวน
API documentation สำหรับเมธอดที่มีประโยชน์
มากมายทั้งหมดที่ประกาศบน Vec<T> โดย standard library เช่น เพิ่มเติม
จาก push เมธอด pop ลบและ return element สุดท้าย
การ Drop Vector ทำให้ Drop Element ของมัน
เหมือนกับ struct อื่นใด vector ถูก free เมื่อมันออกจาก scope ดังที่
annotate ใน Listing 8-10
fn main() {
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
}
เมื่อ vector ถูก drop เนื้อหาทั้งหมดของมันก็ถูก drop ด้วย หมายความว่า integer ที่มันเก็บจะถูก cleanup borrow checker รับประกันว่า reference ใด ๆ ของเนื้อหา vector ถูกใช้แค่ขณะที่ vector เองยัง valid
มาไปที่ collection type ถัดไป — String!
เก็บข้อความ UTF-8 ด้วย string
เก็บข้อความ UTF-8 ด้วย String
เราพูดถึง string ในบทที่ 4 แต่เราจะดูในรายละเอียดเพิ่มเติมตอนนี้ Rustacean ใหม่มักติดที่ string ด้วยเหตุผลสามอย่างรวมกัน — แนวโน้มของ Rust ในการเปิดเผย error ที่เป็นไปได้, string เป็นโครงสร้างข้อมูลที่ซับซ้อนกว่า โปรแกรมเมอร์หลายคนให้เครดิตมัน, และ UTF-8 ปัจจัยเหล่านี้รวมกันในแบบที่ดู ยากเมื่อคุณมาจากภาษาโปรแกรมอื่น
เราพูดถึง string ในบริบทของ collection เพราะ string ถูก implement เป็น
collection ของ byte บวกบางเมธอดเพื่อให้ functionality ที่มีประโยชน์เมื่อ
byte เหล่านั้นถูกตีความเป็นข้อความ ในส่วนนี้เราจะพูดถึง operation บน
String ที่ทุก collection type มี เช่นการสร้าง, update และอ่าน เราจะ
พูดถึงวิธีที่ String ต่างจาก collection อื่น คือวิธีที่การ index เข้า
String ซับซ้อนเพราะความต่างระหว่างวิธีที่คนและคอมพิวเตอร์ตีความข้อมูล
String
นิยาม String
เราจะนิยามก่อนว่าเราหมายถึงอะไรด้วยคำว่า string Rust มี type string
เดียวในภาษาแกน ซึ่งคือ string slice str ที่มักเห็นในรูป borrow ของมัน
&str ในบทที่ 4 เราพูดถึง string slice ซึ่งเป็น reference ของข้อมูล
string ที่ encode UTF-8 ที่เก็บที่อื่น String literal ถูกเก็บใน binary
ของโปรแกรม จึงเป็น string slice
Type String ที่ standard library ของ Rust ให้ ไม่ใช่ถูก code เข้าไป
ภาษาแกน เป็น string type ที่เติบโตได้, mutable, owned และ encode UTF-8
เมื่อ Rustacean อ้างถึง “string” ใน Rust พวกเขาอาจหมายถึง type String
หรือ string slice &str ไม่ใช่แค่ตัวเดียว แม้ส่วนนี้ส่วนใหญ่เกี่ยวกับ
String ทั้งสอง type ใช้บ่อยใน standard library ของ Rust และทั้ง
String และ string slice encode UTF-8
สร้าง String ใหม่
operation จำนวนมากที่เหมือนกันที่มีกับ Vec<T> มีกับ String ด้วย
เพราะ String จริง ๆ ถูก implement เป็น wrapper รอบ vector ของ byte
พร้อมการรับประกัน, ข้อจำกัด และ capability เพิ่มเติม ตัวอย่างฟังก์ชันที่
ทำงานแบบเดียวกันกับ Vec<T> และ String คือฟังก์ชัน new เพื่อสร้าง
instance ดังที่แสดงใน Listing 8-11
fn main() {
let mut s = String::new();
}
String ว่างใหม่บรรทัดนี้สร้าง string ว่างใหม่ชื่อ s ที่เรา load ข้อมูลเข้าได้ บ่อย
ครั้งเราจะมีข้อมูลเริ่มต้นที่เราอยากเริ่ม string ด้วย สำหรับนั้น เราใช้
เมธอด to_string ซึ่งมีให้บน type ใดที่ implement trait Display
เหมือนที่ string literal ทำ Listing 8-12 แสดงสองตัวอย่าง
fn main() {
let data = "initial contents";
let s = data.to_string();
// The method also works on a literal directly:
let s = "initial contents".to_string();
}
to_string สร้าง String จาก string literalโค้ดนี้สร้าง string ที่มี initial contents
เรายังใช้ฟังก์ชัน String::from สร้าง String จาก string literal ได้
โค้ดใน Listing 8-13 เทียบเท่ากับโค้ดใน Listing 8-12 ที่ใช้ to_string
fn main() {
let s = String::from("initial contents");
}
String::from สร้าง String จาก string literalเพราะ string ใช้สำหรับหลายสิ่ง เราใช้ generic API ต่าง ๆ มากมายสำหรับ
string ได้ ให้ตัวเลือกเราเยอะ บางอันอาจดูซ้ำซ้อน แต่ทั้งหมดมีที่ของมัน!
ในกรณีนี้ String::from และ to_string ทำสิ่งเดียวกัน เลือกตัวไหนจึง
เป็นเรื่องสไตล์และความอ่านง่าย
จำไว้ว่า string encode UTF-8 เรารวมข้อมูลที่ encode ถูกต้องใด ๆ ในนั้น ได้ ดังที่แสดงใน Listing 8-14
fn main() {
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שלום");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
}
ทั้งหมดเหล่านี้เป็นค่า String ที่ valid
Update String
String เติบโตในขนาดและเนื้อหาของมันเปลี่ยนได้ เหมือนเนื้อหาของ Vec<T>
ถ้าคุณ push ข้อมูลเพิ่มเข้ามัน นอกจากนี้ คุณใช้ operator + หรือ macro
format! เพื่อต่อค่า String ได้สะดวก
ต่อท้ายด้วย push_str หรือ push
เราขยาย String โดยใช้เมธอด push_str เพื่อต่อ string slice ดังที่แสดง
ใน Listing 8-15
fn main() {
let mut s = String::from("foo");
s.push_str("bar");
}
String ด้วยเมธอด push_strหลังสองบรรทัดนี้ s จะมี foobar เมธอด push_str รับ string slice
เพราะเราไม่จำเป็นต้องรับ ownership ของ parameter เช่น ในโค้ดใน Listing
8-16 เราอยากใช้ s2 หลังต่อเนื้อหาของมันเข้า s1
fn main() {
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {s2}");
}
Stringถ้าเมธอด push_str รับ ownership ของ s2 เราจะพิมพ์ค่าในบรรทัดสุดท้าย
ไม่ได้ อย่างไรก็ตาม โค้ดนี้ทำงานตามที่เราคาด!
เมธอด push รับอักขระเดียวเป็น parameter และเพิ่มมันเข้า String
Listing 8-17 เพิ่มตัวอักษร l เข้า String โดยใช้เมธอด push
fn main() {
let mut s = String::from("lo");
s.push('l');
}
String โดยใช้ pushผลลัพธ์ s จะมี lol
ต่อด้วย + หรือ format!
บ่อยครั้งคุณจะอยากรวม string สองตัวที่มีอยู่ วิธีหนึ่งในการทำคือใช้
operator + ดังที่แสดงใน Listing 8-18
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // note s1 has been moved here and can no longer be used
}
+ รวมค่า String สองตัวเป็นค่า String ใหม่string s3 จะมี Hello, world! เหตุผลที่ s1 ไม่ valid อีกหลังการบวก
และเหตุผลที่เราใช้ reference ของ s2 เกี่ยวกับ signature ของเมธอดที่
ถูกเรียกเมื่อเราใช้ operator + Operator + ใช้เมธอด add ซึ่ง
signature ดูประมาณนี้:
fn add(self, s: &str) -> String {
ใน standard library คุณจะเห็น add ประกาศโดยใช้ generic และ associated
type ที่นี่เราแทนด้วย type คอนกรีต ซึ่งเป็นสิ่งที่เกิดขึ้นเมื่อเราเรียก
เมธอดนี้ด้วยค่า String เราจะพูดถึง generic ในบทที่ 10 Signature นี้ให้
เบาะแสที่เราต้องการเพื่อเข้าใจส่วนยาก ๆ ของ operator +
ขั้นแรก s2 มี & หมายความว่าเรากำลังเพิ่ม reference ของ string ที่สอง
เข้า string แรก นี่เพราะ parameter s ในฟังก์ชัน add — เราเพิ่มได้แค่
string slice เข้า String เราเพิ่มค่า String สองตัวเข้าด้วยกันไม่ได้
แต่เดี๋ยว — type ของ &s2 คือ &String ไม่ใช่ &str ตามที่ระบุใน
parameter ที่สองของ add แล้วทำไม Listing 8-18 compile ผ่าน?
เหตุผลที่เราใช้ &s2 ในการเรียก add ได้คือ compiler บีบ argument
&String เป็น &str ได้ เมื่อเราเรียกเมธอด add Rust ใช้ deref
coercion ซึ่งที่นี่เปลี่ยน &s2 เป็น &s2[..] เราจะพูดถึง deref
coercion ในรายละเอียดเพิ่มเติมในบทที่ 15 เพราะ add ไม่รับ ownership
ของ parameter s s2 จะยังเป็น String valid หลัง operation นี้
ขั้นที่สอง เราเห็นใน signature ว่า add รับ ownership ของ self เพราะ
self ไม่ มี & นี่หมายความว่า s1 ใน Listing 8-18 จะถูก move เข้า
การเรียก add และจะไม่ valid หลังจากนั้น ดังนั้นแม้ let s3 = s1 + &s2;
ดูเหมือนจะคัดลอกทั้งสอง string และสร้างใหม่ statement นี้จริง ๆ รับ
ownership ของ s1 ต่อสำเนาเนื้อหาของ s2 แล้ว return ownership ของผล
พูดอีกอย่าง มันดูเหมือนทำสำเนาเยอะ แต่ไม่ — implementation มีประสิทธิภาพ
กว่าการคัดลอก
ถ้าเราต้องต่อ string หลายตัว พฤติกรรมของ operator + กลายเป็นไม่สะดวก:
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
}
ณ จุดนี้ s จะเป็น tic-tac-toe ด้วยอักขระ + และ " ทั้งหมด ยากที่
จะเห็นสิ่งที่เกิดขึ้น สำหรับการรวม string ในแบบที่ซับซ้อนกว่า เราใช้
macro format! แทนได้:
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
}
โค้ดนี้ก็ set s เป็น tic-tac-toe macro format! ทำงานเหมือน
println! แต่แทนการพิมพ์ output ออกหน้าจอ มัน return String ที่มี
เนื้อหา version ของโค้ดที่ใช้ format! อ่านง่ายกว่ามาก และโค้ดที่
generate โดย macro format! ใช้ reference การเรียกนี้จึงไม่รับ
ownership ของ parameter ใด ๆ
Index เข้า String
ในภาษาโปรแกรมอื่นหลายภาษา การเข้าถึงอักขระแต่ละตัวใน string โดยอ้างถึง
ด้วย index เป็น operation ที่ valid และใช้บ่อย อย่างไรก็ตาม ถ้าคุณ
พยายามเข้าถึงส่วนของ String โดยใช้ indexing syntax ใน Rust คุณจะได้
error พิจารณาโค้ด invalid ใน Listing 8-19
fn main() {
let s1 = String::from("hi");
let h = s1[0];
}
Stringโค้ดนี้จะให้ error ต่อไปนี้:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `str` cannot be indexed by `{integer}`
--> src/main.rs:3:16
|
3 | let h = s1[0];
| ^ string indices are ranges of `usize`
|
= help: the trait `SliceIndex<str>` is not implemented for `{integer}`
= note: you can use `.chars().nth()` or `.bytes().nth()`
for more information, see chapter 8 in The Book: <https://doc.rust-lang.org/book/ch08-02-strings.html#indexing-into-strings>
= help: the following other types implement trait `SliceIndex<T>`:
`usize` implements `SliceIndex<ByteStr>`
`usize` implements `SliceIndex<[T]>`
= note: required for `String` to implement `Index<{integer}>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` (bin "collections") due to 1 previous error
Error บอกเรื่อง — string ของ Rust ไม่รองรับ indexing แต่ทำไม? ในการตอบ คำถามนั้น เราต้องพูดถึงวิธีที่ Rust เก็บ string ในหน่วยความจำ
Representation ภายใน
String คือ wrapper ของ Vec<u8> มาดู string ตัวอย่างที่ encode UTF-8
อย่างถูกต้องของเราจาก Listing 8-14 ก่อน ตัวนี้:
fn main() {
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שלום");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
}
ในกรณีนี้ len จะเป็น 4 หมายความว่า vector ที่เก็บ string "Hola"
ยาว 4 byte แต่ละตัวอักษรเหล่านี้กิน 1 byte เมื่อ encode ใน UTF-8 อย่างไร
ก็ตาม บรรทัดต่อไปนี้อาจทำให้คุณประหลาดใจ (หมายเหตุว่า string นี้ขึ้นต้น
ด้วยตัวอักษร Cyrillic capital Ze ไม่ใช่เลข 3):
fn main() {
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שלום");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
}
ถ้าถามว่า string ยาวเท่าไร คุณอาจตอบ 12 จริง ๆ คำตอบของ Rust คือ 24 — นั่นคือจำนวน byte ที่ใช้ encode “Здравствуйте” ใน UTF-8 เพราะแต่ละ Unicode scalar value ใน string นั้นกิน 2 byte ของ storage ดังนั้น index เข้า byte ของ string จะไม่สอดคล้องกับ Unicode scalar value ที่ valid เสมอ เพื่อแสดง พิจารณาโค้ด Rust invalid นี้:
let hello = "Здравствуйте";
let answer = &hello[0];
คุณรู้แล้วว่า answer จะไม่ใช่ З ตัวอักษรแรก เมื่อ encode ใน UTF-8
byte แรกของ З คือ 208 และที่สองคือ 151 จึงดูเหมือน answer ควร
เป็น 208 จริง ๆ แต่ 208 ไม่ใช่อักขระ valid เอง ๆ การ return 208
คงไม่ใช่สิ่งที่ user อยากได้ถ้าเขาขอตัวอักษรแรกของ string นี้ แต่นั่น
คือข้อมูลเดียวที่ Rust มีที่ byte index 0 user โดยทั่วไปไม่อยากให้ค่า
byte return แม้ string มีแค่ตัวอักษร Latin — ถ้า &"hi"[0] เป็นโค้ด
valid ที่ return ค่า byte มันจะ return 104 ไม่ใช่ h
คำตอบจึงคือ เพื่อหลีกเลี่ยงการ return ค่าที่ไม่คาด และทำให้เกิด bug ที่ อาจไม่ค้นพบทันที Rust ไม่ compile โค้ดนี้เลย และป้องกันความเข้าใจผิดแต่ เนิ่นในกระบวนการพัฒนา
Byte, Scalar Value และ Grapheme Cluster
อีกจุดเรื่อง UTF-8 คือมีจริง ๆ สามวิธีที่เกี่ยวข้องในการดู string จาก มุมมองของ Rust — เป็น byte, scalar value และ grapheme cluster (สิ่งที่ ใกล้ที่สุดกับสิ่งที่เราจะเรียก ตัวอักษร)
ถ้าเราดูคำฮินดี “नमस्ते” ที่เขียนใน Devanagari script มันถูกเก็บเป็น
vector ของค่า u8 ที่ดูเป็นแบบนี้:
[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]
นั่น 18 byte และเป็นวิธีที่คอมพิวเตอร์เก็บข้อมูลนี้ในที่สุด ถ้าเราดูพวก
มันเป็น Unicode scalar value ซึ่งเป็นสิ่งที่ type char ของ Rust เป็น
byte เหล่านั้นดูเป็นแบบนี้:
['न', 'म', 'स', '्', 'त', 'े']
มีค่า char หกตัวที่นี่ แต่ตัวที่สี่และหกไม่ใช่ตัวอักษร — พวกมันเป็น
diacritic ที่ไม่มีความหมายเอง ๆ สุดท้าย ถ้าเราดูพวกมันเป็น grapheme
cluster เราจะได้สิ่งที่คนจะเรียกตัวอักษรสี่ตัวที่ประกอบเป็นคำฮินดี:
["न", "म", "स्", "ते"]
Rust ให้วิธีต่างกันในการตีความข้อมูล string ดิบที่คอมพิวเตอร์เก็บ เพื่อ ให้แต่ละโปรแกรมเลือกการตีความที่ต้องการ ไม่ว่าข้อมูลเป็นภาษามนุษย์ใด
เหตุผลสุดท้ายที่ Rust ไม่ให้เรา index เข้า String เพื่อรับอักขระคือ
operation indexing คาดว่าจะใช้เวลา constant เสมอ (O(1)) แต่เป็นไปไม่ได้
ที่จะรับประกัน performance นั้นกับ String เพราะ Rust จะต้องเดินผ่าน
เนื้อหาจากต้นถึง index เพื่อกำหนดว่ามีกี่อักขระ valid
Slice String
การ index เข้า string มักเป็นความคิดไม่ดี เพราะไม่ชัดเจนว่า return type ของ operation string-indexing ควรเป็น — byte value, อักขระ, grapheme cluster หรือ string slice ถ้าคุณต้องการใช้ index จริง ๆ เพื่อสร้าง string slice ดังนั้น Rust ขอให้คุณเฉพาะมากขึ้น
แทนการ index โดยใช้ [] กับตัวเลขเดียว คุณใช้ [] กับ range เพื่อสร้าง
string slice ที่มี byte เฉพาะได้:
#![allow(unused)]
fn main() {
let hello = "Здравствуйте";
let s = &hello[0..4];
}
ที่นี่ s จะเป็น &str ที่มี byte 4 ตัวแรกของ string ก่อนหน้านี้เราเอ่ย
ว่าแต่ละอักขระเหล่านี้คือ 2 byte ซึ่งหมายความว่า s จะเป็น Зд
ถ้าเราลอง slice แค่ส่วนของ byte ของอักขระด้วยอย่าง &hello[0..1] Rust
จะ panic ตอน runtime ในแบบเดียวกับถ้า index invalid ถูกเข้าถึงใน vector:
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/collections`
thread 'main' panicked at src/main.rs:4:19:
byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
คุณควรระวังเมื่อสร้าง string slice ด้วย range เพราะการทำเช่นนั้นอาจ crash โปรแกรมของคุณ
Iterate ผ่าน String
วิธีดีที่สุดในการ operate บนชิ้นของ string คือ explicit ว่าคุณอยากได้
อักขระหรือ byte สำหรับ Unicode scalar value แต่ละค่า ใช้เมธอด chars
การเรียก chars บน “Зд” แยกและ return ค่าสองค่าของ type char และคุณ
iterate ผ่านผลเพื่อเข้าถึงแต่ละ element ได้:
#![allow(unused)]
fn main() {
for c in "Зд".chars() {
println!("{c}");
}
}
โค้ดนี้จะพิมพ์ต่อไปนี้:
З
д
ทางเลือก เมธอด bytes return แต่ละ byte ดิบ ซึ่งอาจเหมาะกับ domain ของ
คุณ:
#![allow(unused)]
fn main() {
for b in "Зд".bytes() {
println!("{b}");
}
}
โค้ดนี้จะพิมพ์ 4 byte ที่ประกอบเป็น string นี้:
208
151
208
180
แต่อย่าลืมจำว่า Unicode scalar value ที่ valid อาจประกอบจาก byte มากกว่า 1 ตัว
การรับ grapheme cluster จาก string เหมือนกับ Devanagari script ซับซ้อน functionality นี้จึงไม่ให้โดย standard library Crate มีให้ที่ crates.io ถ้าคุณต้องการ functionality นี้
จัดการความซับซ้อนของ String
สรุป string ซับซ้อน ภาษาโปรแกรมต่างกันเลือกต่างกันในการนำเสนอความซับซ้อน
นี้ให้โปรแกรมเมอร์ Rust เลือกทำให้การจัดการข้อมูล String ที่ถูกต้องเป็น
พฤติกรรม default สำหรับโปรแกรม Rust ทั้งหมด ซึ่งหมายความว่าโปรแกรมเมอร์
ต้องคิดมากขึ้นเรื่องการจัดการข้อมูล UTF-8 ตั้งแต่ต้น Trade-off นี้เปิด
เผยความซับซ้อนของ string มากกว่าที่เห็นในภาษาโปรแกรมอื่น แต่มันป้องกัน
คุณจากการต้องจัดการ error ที่เกี่ยวกับอักขระไม่ใช่ ASCII ทีหลังในวงจรการ
พัฒนาของคุณ
ข่าวดีคือ standard library มี functionality มากมายที่สร้างจาก type
String และ &str เพื่อช่วยจัดการสถานการณ์ซับซ้อนเหล่านี้อย่างถูกต้อง
อย่าลืมเช็ค documentation สำหรับเมธอดที่มีประโยชน์อย่าง contains สำหรับ
ค้นหาใน string และ replace สำหรับแทนที่ส่วนของ string ด้วย string อื่น
มาเปลี่ยนไปสิ่งที่ซับซ้อนน้อยกว่านิดหน่อย — hash map!
เก็บคู่ key-value ด้วย hash map
เก็บ Key พร้อมค่าที่ผูกกันด้วย Hash Map
Collection ที่ใช้บ่อยตัวสุดท้ายของเราคือ hash map type HashMap<K, V>
เก็บการ map ของ key type K กับค่า type V โดยใช้ hashing function
ซึ่งกำหนดว่ามันวาง key และค่าเหล่านี้ในหน่วยความจำอย่างไร ภาษาโปรแกรม
หลายภาษารองรับโครงสร้างข้อมูลชนิดนี้ แต่มักใช้ชื่อต่าง เช่น hash,
map, object, hash table, dictionary หรือ associative array
เป็นต้น
Hash map มีประโยชน์เมื่อคุณอยาก lookup ข้อมูล ไม่ใช่โดยใช้ index อย่างที่ คุณทำได้กับ vector แต่โดยใช้ key ที่เป็น type ใดก็ได้ เช่น ในเกม คุณติด ตามคะแนนของแต่ละทีมใน hash map ได้ ที่แต่ละ key เป็นชื่อทีมและค่าเป็น คะแนนของแต่ละทีม เมื่อให้ชื่อทีม คุณดึงคะแนนได้
เราจะดู API พื้นฐานของ hash map ในส่วนนี้ แต่ของดีอีกมากซ่อนในฟังก์ชันที่
ประกาศบน HashMap<K, V> โดย standard library เช่นเคย เช็ค documentation
ของ standard library สำหรับข้อมูลเพิ่มเติม
สร้าง Hash Map ใหม่
วิธีหนึ่งในการสร้าง hash map ว่างคือใช้ new และเพิ่ม element ด้วย
insert ใน Listing 8-20 เรากำลังติดตามคะแนนของสองทีมที่ชื่อ Blue และ
Yellow ทีม Blue เริ่มที่ 10 คะแนน และทีม Yellow เริ่มที่ 50
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}
หมายเหตุว่าเราต้อง use HashMap จากส่วน collection ของ standard
library ก่อน จาก collection ที่ใช้บ่อยสามตัวของเรา ตัวนี้ใช้น้อยที่สุด
มันจึงไม่รวมในฟีเจอร์ที่นำเข้า scope อัตโนมัติใน prelude Hash map ยังมี
support จาก standard library น้อยกว่า — ไม่มี macro built-in สำหรับ
สร้างพวกมัน เช่น
เหมือนกับ vector hash map เก็บข้อมูลบน heap HashMap นี้มี key type
String และค่า type i32 เหมือนกับ vector hash map เป็น homogeneous —
key ทั้งหมดต้องมี type เดียวกัน และค่าทั้งหมดต้องมี type เดียวกัน
เข้าถึงค่าใน Hash Map
เราดึงค่าออกจาก hash map ได้โดยให้ key ของมันกับเมธอด get ดังที่แสดง
ใน Listing 8-21
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
}
ที่นี่ score จะมีค่าที่ผูกกับทีม Blue และผลจะเป็น 10 เมธอด get
return Option<&V> — ถ้าไม่มีค่าสำหรับ key นั้นใน hash map get จะ
return None โปรแกรมนี้จัดการ Option โดยเรียก copied เพื่อรับ
Option<i32> แทน Option<&i32> แล้ว unwrap_or set score เป็นศูนย์
ถ้า scores ไม่มี entry สำหรับ key
เรา iterate ผ่านแต่ละคู่ key-value ใน hash map ได้ในแบบคล้ายที่เราทำกับ
vector โดยใช้ for loop:
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{key}: {value}");
}
}
โค้ดนี้จะพิมพ์แต่ละคู่ในลำดับสุ่ม:
Yellow: 50
Blue: 10
จัดการ Ownership ใน Hash Map
สำหรับ type ที่ implement trait Copy อย่าง i32 ค่าถูกคัดลอกเข้า hash
map สำหรับค่าที่ owned อย่าง String ค่าจะถูก move และ hash map จะเป็น
owner ของค่าเหล่านั้น ดังที่แสดงใน Listing 8-22
fn main() {
use std::collections::HashMap;
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name and field_value are invalid at this point, try using them and
// see what compiler error you get!
}
เราใช้ตัวแปร field_name และ field_value ไม่ได้หลังจากพวกมันถูก move
เข้า hash map ด้วยการเรียก insert
ถ้าเราแทรก reference ของค่าเข้า hash map ค่าจะไม่ถูก move เข้า hash map ค่าที่ reference ชี้ไปต้อง valid อย่างน้อยตราบที่ hash map valid เราจะ พูดถึงปัญหาเหล่านี้เพิ่มใน “ตรวจสอบ Reference ด้วย Lifetime” ในบทที่ 10
Update Hash Map
แม้จำนวนคู่ key และค่าจะเติบโตได้ แต่ละ key ที่ไม่ซ้ำมีค่าผูกอยู่ได้แค่
ค่าเดียวในเวลาเดียว (แต่ไม่ตรงข้าม — เช่น ทั้งทีม Blue และทีม Yellow มี
ค่า 10 เก็บใน hash map scores ได้)
เมื่อคุณอยากเปลี่ยนข้อมูลใน hash map คุณต้องตัดสินใจวิธีจัดการกรณีที่ key มีค่า assign อยู่แล้ว คุณแทนค่าเดิมด้วยค่าใหม่ ละเลยค่าเดิมโดย สมบูรณ์ได้ คุณเก็บค่าเดิมและละเว้นค่าใหม่ เพิ่มค่าใหม่เฉพาะถ้า key ยังไม่ มีค่าได้ หรือคุณรวมค่าเดิมและค่าใหม่ได้ มาดูวิธีทำแต่ละอย่าง!
เขียนทับค่า
ถ้าเราแทรก key และค่าเข้า hash map แล้วแทรก key เดียวกันด้วยค่าต่าง ค่า
ที่ผูกกับ key นั้นจะถูกแทน แม้โค้ดใน Listing 8-23 เรียก insert สอง
ครั้ง hash map จะมีแค่หนึ่งคู่ key-value เพราะเรากำลังแทรกค่าสำหรับ key
ของทีม Blue ทั้งสองครั้ง
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{scores:?}");
}
โค้ดนี้จะพิมพ์ {"Blue": 25} ค่าเดิม 10 ถูกเขียนทับ
เพิ่ม Key และค่าเฉพาะถ้า Key ไม่มีอยู่
มันใช้บ่อยที่จะเช็คว่า key เฉพาะมีอยู่แล้วใน hash map พร้อมค่า แล้วทำ action ต่อไปนี้ — ถ้า key มีอยู่ใน hash map ค่าที่มีอยู่ควรอยู่เหมือนเดิม ถ้า key ไม่มี แทรกมันและค่าสำหรับมัน
Hash map มี API พิเศษสำหรับเรื่องนี้ชื่อ entry ที่รับ key ที่คุณอยาก
เช็คเป็น parameter Return value ของเมธอด entry คือ enum ชื่อ Entry
ที่แทนค่าที่อาจมีหรือไม่มี สมมติเราอยากเช็คว่า key สำหรับทีม Yellow มี
ค่าผูกกับมันไหม ถ้าไม่ เราอยากแทรกค่า 50 และเหมือนกันสำหรับทีม Blue
ด้วย API entry โค้ดดูเหมือน Listing 8-24
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{scores:?}");
}
entry เพื่อแทรกเฉพาะถ้า key ยังไม่มีค่าเมธอด or_insert บน Entry ประกาศให้ return mutable reference ของค่า
สำหรับ key Entry ที่สอดคล้องถ้า key นั้นมีอยู่ และถ้าไม่ มันแทรก
parameter เป็นค่าใหม่สำหรับ key นี้ และ return mutable reference ของ
ค่าใหม่ เทคนิคนี้สะอาดกว่าการเขียน logic เองมาก และเพิ่มเติม เล่นได้ดี
ขึ้นกับ borrow checker
การรันโค้ดใน Listing 8-24 จะพิมพ์ {"Yellow": 50, "Blue": 10} การเรียก
entry ครั้งแรกจะแทรก key สำหรับทีม Yellow ด้วยค่า 50 เพราะทีม Yellow
ยังไม่มีค่า การเรียก entry ครั้งที่สองจะไม่เปลี่ยน hash map เพราะทีม
Blue มีค่า 10 อยู่แล้ว
Update ค่าตามค่าเดิม
อีก use case ที่ใช้บ่อยสำหรับ hash map คือ lookup ค่าของ key แล้ว update
ตามค่าเดิม เช่น Listing 8-25 แสดงโค้ดที่นับว่าแต่ละคำปรากฏกี่ครั้งใน
ข้อความ เราใช้ hash map ที่มีคำเป็น key และ increment ค่าเพื่อติดตามว่า
เราเห็นคำนั้นกี่ครั้ง ถ้าเป็นครั้งแรกที่เห็นคำ เราจะแทรกค่า 0 ก่อน
fn main() {
use std::collections::HashMap;
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{map:?}");
}
โค้ดนี้จะพิมพ์ {"world": 2, "hello": 1, "wonderful": 1} คุณอาจเห็นคู่
key-value เดียวกันพิมพ์ในลำดับต่างกัน — จำได้จาก
“เข้าถึงค่าใน Hash Map” ว่าการ iterate ผ่าน
hash map เกิดในลำดับสุ่ม
เมธอด split_whitespace return iterator บน subslice ที่คั่นด้วย
whitespace ของค่าใน text เมธอด or_insert return mutable reference
(&mut V) ของค่าสำหรับ key ที่ระบุ ที่นี่เราเก็บ mutable reference นั้น
ในตัวแปร count ดังนั้นในการ assign ให้ค่านั้น เราต้อง dereference
count โดยใช้ asterisk (*) ก่อน Mutable reference ออกจาก scope ที่ท้าย
for loop การเปลี่ยนเหล่านี้ทั้งหมดจึงปลอดภัยและกฎ borrowing อนุญาต
Hashing Function
โดย default HashMap ใช้ hashing function ชื่อ SipHash ที่ให้การ
ต้านทาน denial-of-service (DoS) attack ที่เกี่ยวกับ hash
table1 นี่ไม่ใช่ hashing algorithm ที่เร็วที่สุด
แต่ trade-off สำหรับ security ที่ดีกว่าที่มากับการลด performance คุ้มค่า
ถ้าคุณ profile โค้ดและพบว่า default hash function ช้าเกินไปสำหรับจุด
ประสงค์ของคุณ คุณเปลี่ยนเป็นฟังก์ชันอื่นได้โดยระบุ hasher ต่าง hasher
คือ type ที่ implement trait BuildHasher เราจะพูดถึง trait และวิธี
implement ใน บทที่ 10 คุณไม่จำเป็นต้อง implement
hasher ของคุณเองจากศูนย์ crates.io
มี library ที่แชร์โดย user Rust อื่นที่ให้ hasher ที่ implement hashing
algorithm ที่ใช้บ่อยหลายตัว
สรุป
Vector, string และ hash map จะให้ functionality มากที่จำเป็นในโปรแกรม เมื่อคุณต้องเก็บ เข้าถึง และแก้ข้อมูล นี่คือแบบฝึกหัดที่ตอนนี้คุณควร แก้ได้:
- ให้ list ของ integer ใช้ vector และ return median (เมื่อเรียง ค่าใน ตำแหน่งกลาง) และ mode (ค่าที่เกิดบ่อยที่สุด — hash map จะช่วยตรงนี้) ของ list
- แปลง string เป็น Pig Latin พยัญชนะแรกของแต่ละคำถูกย้ายไปท้ายคำ และ เพิ่ม ay ดังนั้น first กลายเป็น irst-fay คำที่ขึ้นต้นด้วยสระ เพิ่ม hay ที่ท้ายแทน (apple กลายเป็น apple-hay) จำรายละเอียด เกี่ยวกับ UTF-8 encoding ไว้!
- ใช้ hash map และ vector สร้าง text interface ให้ user เพิ่มชื่อพนัก งานเข้าแผนกในบริษัทได้ เช่น “Add Sally to Engineering” หรือ “Add Amir to Sales” จากนั้นให้ user ดึง list ของคนทั้งหมดในแผนก หรือคน ทั้งหมดในบริษัทแยกตามแผนก เรียงตามตัวอักษร
API documentation ของ standard library อธิบายเมธอดที่ vector, string และ hash map มี ที่จะช่วยกับแบบฝึกหัดเหล่านี้!
เรากำลังเข้าสู่โปรแกรมที่ซับซ้อนขึ้นที่ operation ล้มเหลวได้ ดังนั้นเป็น เวลาที่สมบูรณ์แบบที่จะพูดถึงการจัดการ error เราจะทำเรื่องนั้นต่อไป!
การจัดการ Error
Error เป็นข้อเท็จจริงของชีวิตใน software ดังนั้น Rust จึงมีฟีเจอร์หลาย ตัวสำหรับจัดการสถานการณ์ที่อะไรบางอย่างผิดพลาด ในหลายกรณี Rust บังคับให้ คุณยอมรับความเป็นไปได้ของ error และทำ action บางอย่างก่อนโค้ดของคุณจะ compile ผ่าน ข้อกำหนดนี้ทำให้โปรแกรมของคุณแข็งแกร่งขึ้น โดยรับประกันว่า คุณจะค้นพบ error และจัดการอย่างเหมาะสมก่อน deploy โค้ดของคุณไป production!
Rust จัดกลุ่ม error เป็นสองหมวดหลัก — recoverable และ unrecoverable error สำหรับ recoverable error เช่น error file not found เราคงแค่ อยากรายงานปัญหาให้ user และลอง operation ใหม่ Unrecoverable error มักเป็นอาการของ bug เช่น พยายามเข้าถึงตำแหน่งหลังท้าย array เราจึงอยาก หยุดโปรแกรมทันที
ภาษาส่วนใหญ่ไม่แยก error สองชนิดนี้และจัดการทั้งคู่ในแบบเดียวกัน โดยใช้
กลไกอย่าง exception Rust ไม่มี exception แทน มันมี type Result<T, E>
สำหรับ recoverable error และ macro panic! ที่หยุด execution เมื่อโปรแกรม
เจอ unrecoverable error บทนี้ครอบคลุมการเรียก panic! ก่อน แล้วพูดถึง
การ return ค่า Result<T, E> นอกจากนี้ เราจะสำรวจข้อพิจารณาเมื่อตัดสิน
ใจว่าจะลอง recover จาก error หรือหยุด execution
Unrecoverable error ด้วย panic!
Unrecoverable Error ด้วย panic!
บางครั้งสิ่งร้ายเกิดในโค้ดของคุณ และไม่มีอะไรคุณทำได้เกี่ยวกับมัน ใน
กรณีเหล่านี้ Rust มี macro panic! มีสองวิธีที่ทำให้เกิด panic ในทาง
ปฏิบัติ — โดยทำ action ที่ทำให้โค้ดเรา panic (เช่นเข้าถึง array หลังท้าย)
หรือเรียก macro panic! แบบ explicit ในทั้งสองกรณี เราทำให้เกิด panic
ในโปรแกรมของเรา โดย default panic เหล่านี้จะพิมพ์ failure message,
unwind, cleanup stack และออก ผ่าน environment variable คุณยังให้ Rust
แสดง call stack เมื่อ panic เกิดได้ เพื่อทำให้ง่ายขึ้นในการตามหาแหล่งของ
panic
Unwind Stack หรือ Abort เมื่อเกิด Panic
โดย default เมื่อ panic เกิด โปรแกรมเริ่ม unwind ซึ่งหมายความว่า Rust เดินกลับขึ้น stack และ cleanup ข้อมูลจากแต่ละฟังก์ชันที่มันเจอ อย่างไรก็ตาม การเดินกลับและ cleanup เป็นงานเยอะ Rust จึงให้คุณเลือก ทางเลือกของการ abort ทันที ซึ่งจบโปรแกรมโดยไม่ cleanup
หน่วยความจำที่โปรแกรมใช้จะต้องถูก cleanup โดย OS ถ้าในโปรเจกต์ของคุณ
ต้องทำให้ binary ผลลัพธ์เล็กที่สุดเท่าที่ทำได้ คุณเปลี่ยนจาก unwind
เป็น abort เมื่อ panic ได้ โดยเพิ่ม panic = 'abort' เข้าใน section
[profile] ที่เหมาะสมในไฟล์ Cargo.toml ของคุณ เช่น ถ้าคุณอยาก abort
ตอน panic ใน release mode เพิ่มนี้:
[profile.release]
panic = 'abort'
ลองเรียก panic! ในโปรแกรมง่าย ๆ:
fn main() {
panic!("crash and burn");
}
เมื่อคุณรันโปรแกรม คุณจะเห็นสิ่งคล้ายนี้:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
การเรียก panic! ทำให้เกิด error message ที่มีในสองบรรทัดสุดท้าย บรรทัด
แรกแสดง panic message ของเราและตำแหน่งใน source code ที่ panic เกิด —
src/main.rs:2:5 บ่งบอกว่ามันคือบรรทัดที่ 2 อักขระที่ 5 ของไฟล์
src/main.rs ของเรา
ในกรณีนี้ บรรทัดที่ระบุเป็นส่วนของโค้ดเรา และถ้าเราไปที่บรรทัดนั้น เราเห็น
การเรียก panic! macro ในกรณีอื่น การเรียก panic! อาจอยู่ในโค้ดที่โค้ด
เราเรียก และชื่อไฟล์และเลขบรรทัดที่ error message รายงาน จะเป็นโค้ดของคน
อื่นที่ panic! macro ถูกเรียก ไม่ใช่บรรทัดของโค้ดเราที่นำไปสู่การเรียก
panic! ในที่สุด
เราใช้ backtrace ของฟังก์ชันที่การเรียก panic! มาจาก เพื่อหาส่วนของ
โค้ดเราที่เป็นต้นเหตุได้ ในการเข้าใจวิธีใช้ backtrace ของ panic! มาดู
ตัวอย่างอีกตัวอย่างหนึ่ง และดูว่าเป็นอย่างไรเมื่อ panic! มาจาก library
เพราะ bug ในโค้ดเรา แทนที่โค้ดของเราเรียก macro ตรง ๆ Listing 9-1 มี
โค้ดที่พยายามเข้าถึง index ใน vector นอก range ของ index ที่ valid
fn main() {
let v = vec![1, 2, 3];
v[99];
}
panic!ที่นี่ เรากำลังพยายามเข้าถึง element ที่ 100 ของ vector (ที่ index 99
เพราะ indexing เริ่มที่ศูนย์) แต่ vector มีแค่สาม element ในสถานการณ์นี้
Rust จะ panic การใช้ [] ควรจะ return element แต่ถ้าคุณส่ง index invalid
ไม่มี element ที่ Rust ใน return ที่นี่ที่ถูกต้อง
ใน C การพยายามอ่านหลังท้ายของโครงสร้างข้อมูลเป็น undefined behavior คุณ อาจได้อะไรก็ตามที่อยู่ในตำแหน่งหน่วยความจำที่จะสอดคล้องกับ element นั้น ในโครงสร้างข้อมูล แม้หน่วยความจำจะไม่เป็นของโครงสร้างนั้น นี่เรียกว่า buffer overread และนำไปสู่ security vulnerability ได้ ถ้า attacker จัดการ index ในแบบที่อ่านข้อมูลที่ไม่ควรได้รับอนุญาตที่เก็บหลังโครงสร้าง ข้อมูล
เพื่อปกป้องโปรแกรมของคุณจาก vulnerability ชนิดนี้ ถ้าคุณลองอ่าน element ที่ index ไม่มี Rust จะหยุด execution และปฏิเสธที่จะดำเนินต่อ ลองดู:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Error นี้ชี้ที่บรรทัดที่ 4 ของ main.rs ของเรา ที่เราพยายามเข้าถึง index
99 ของ vector ใน v
บรรทัด note: บอกว่าเราตั้ง environment variable RUST_BACKTRACE เพื่อ
รับ backtrace ของสิ่งที่เกิดขึ้นเป๊ะ ๆ ที่ทำให้เกิด error ได้ backtrace
คือ list ของฟังก์ชันทั้งหมดที่ถูกเรียกเพื่อมาถึงจุดนี้ Backtrace ใน Rust
ทำงานเหมือนในภาษาอื่น — กุญแจของการอ่าน backtrace คือเริ่มจากด้านบนและ
อ่านจนคุณเห็นไฟล์ที่คุณเขียน นั่นคือจุดที่ปัญหาเริ่ม บรรทัดเหนือจุดนั้น
คือโค้ดที่โค้ดของคุณเรียก บรรทัดข้างใต้คือโค้ดที่เรียกโค้ดของคุณ บรรทัด
ก่อน-หลังเหล่านี้อาจรวมโค้ดแกน Rust, โค้ด standard library หรือ crate
ที่คุณใช้ ลองรับ backtrace โดย set environment variable RUST_BACKTRACE
เป็นค่าใด ๆ ยกเว้น 0 Listing 9-2 แสดง output คล้ายกับที่คุณจะเห็น
$ RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: rust_begin_unwind
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
1: core::panicking::panic_fmt
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
2: core::panicking::panic_bounds_check
at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
panic! แสดงเมื่อ environment variable RUST_BACKTRACE ถูกตั้งนั่นเป็น output เยอะ! output ที่คุณเห็นอาจต่างกันขึ้นกับ OS และ Rust
version ในการรับ backtrace พร้อมข้อมูลนี้ debug symbol ต้องเปิด Debug
symbol เปิดโดย default เมื่อใช้ cargo build หรือ cargo run โดยไม่มี
flag --release อย่างที่เราทำที่นี่
ใน output ใน Listing 9-2 บรรทัดที่ 6 ของ backtrace ชี้ที่บรรทัดในโปรเจกต์ เราที่เป็นต้นเหตุ — บรรทัดที่ 4 ของ src/main.rs ถ้าเราไม่อยากให้ โปรแกรม panic เราควรเริ่มการสืบสวนที่ตำแหน่งที่ชี้โดยบรรทัดแรกที่กล่าวถึง ไฟล์ที่เราเขียน ใน Listing 9-1 ที่เราเขียนโค้ดตั้งใจให้ panic วิธีแก้ panic คือไม่ขอ element นอก range ของ index ของ vector เมื่อโค้ดของคุณ panic ในอนาคต คุณจะต้องคิดว่าโค้ดกำลังทำ action อะไรกับค่าอะไรที่ทำให้ panic และโค้ดควรทำอะไรแทน
เราจะกลับไปที่ panic! และเมื่อเราควรและไม่ควรใช้ panic! จัดการเงื่อนไข
error ในส่วน
“panic! หรือไม่ panic!”
ทีหลังในบทนี้ ถัดไป เราจะดูวิธี recover จาก error โดยใช้ Result
Recoverable error ด้วย Result
Recoverable Error ด้วย Result
Error ส่วนใหญ่ไม่จริงจังพอที่จะต้องการให้โปรแกรมหยุดทั้งหมด บางครั้งเมื่อ ฟังก์ชันล้มเหลว เป็นเพราะเหตุผลที่คุณตีความและตอบสนองได้ง่าย เช่น ถ้าคุณ ลองเปิดไฟล์ และ operation นั้นล้มเหลวเพราะไฟล์ไม่มี คุณอาจอยากสร้างไฟล์ แทนการจบ process
จำจาก “จัดการ failure ที่อาจเกิดขึ้นด้วย Result”
ในบทที่ 2 ว่า enum Result ประกาศโดยมีสอง variant Ok และ Err ดังนี้:
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
T และ E เป็น generic type parameter — เราจะพูดถึง generic ในราย
ละเอียดเพิ่มเติมในบทที่ 10 สิ่งที่คุณต้องรู้ตอนนี้คือ T แทน type ของ
ค่าที่จะ return ในกรณีสำเร็จภายใน variant Ok และ E แทน type ของ
error ที่จะ return ในกรณีล้มเหลวภายใน variant Err เพราะ Result มี
generic type parameter เหล่านี้ เราใช้ type Result และฟังก์ชันที่
ประกาศบนมันในหลายสถานการณ์ที่ค่าสำเร็จและค่า error ที่เราอยาก return
อาจต่างกันได้
ลองเรียกฟังก์ชันที่ return ค่า Result เพราะฟังก์ชันอาจล้มเหลว ใน
Listing 9-3 เราพยายามเปิดไฟล์
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
}
Return type ของ File::open คือ Result<T, E> Generic parameter T
ถูกเติมโดย implementation ของ File::open ด้วย type ของค่าสำเร็จ
std::fs::File ซึ่งเป็น file handle Type ของ E ที่ใช้ในค่า error คือ
std::io::Error Return type นี้หมายความว่าการเรียก File::open อาจ
สำเร็จและ return file handle ที่เราอ่านหรือเขียนได้ การเรียกฟังก์ชันอาจ
ล้มเหลวด้วย — เช่น ไฟล์อาจไม่มี หรือเราอาจไม่มี permission เข้าถึงไฟล์
ฟังก์ชัน File::open ต้องมีวิธีบอกเราว่ามันสำเร็จหรือล้มเหลว และในเวลา
เดียวกันให้เราทั้ง file handle หรือข้อมูล error ข้อมูลนี้เป๊ะคือสิ่งที่
enum Result สื่อ
ในกรณีที่ File::open สำเร็จ ค่าในตัวแปร greeting_file_result จะเป็น
instance ของ Ok ที่มี file handle ในกรณีที่ล้มเหลว ค่าใน
greeting_file_result จะเป็น instance ของ Err ที่มีข้อมูลเพิ่มเติม
เกี่ยวกับชนิดของ error ที่เกิด
เราต้องเพิ่มในโค้ดใน Listing 9-3 เพื่อทำ action ต่างกันขึ้นกับค่าที่
File::open return Listing 9-4 แสดงวิธีหนึ่งในการจัดการ Result โดยใช้
เครื่องมือพื้นฐาน — match expression ที่เราพูดถึงในบทที่ 6
use std::fs::File;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => panic!("Problem opening the file: {error:?}"),
};
}
match expression จัดการ variant Result ที่อาจ returnหมายเหตุว่า เหมือนกับ enum Option enum Result และ variant ของมันถูก
นำเข้า scope โดย prelude เราจึงไม่ต้องระบุ Result:: ก่อน variant Ok
และ Err ใน match arm
เมื่อผลคือ Ok โค้ดนี้จะ return ค่า file ภายในออกจาก variant Ok และ
เราจากนั้น assign ค่า file handle นั้นให้ตัวแปร greeting_file หลัง
match เราใช้ file handle สำหรับอ่านหรือเขียนได้
arm อีกตัวของ match จัดการกรณีที่เราได้ค่า Err จาก File::open ใน
ตัวอย่างนี้ เราเลือกเรียก panic! macro ถ้าไม่มีไฟล์ชื่อ hello.txt
ใน directory ปัจจุบันของเรา และเรารันโค้ดนี้ เราจะเห็น output ต่อไปนี้
จาก panic! macro:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
ตามปกติ output นี้บอกเราเป๊ะ ๆ ว่าอะไรผิดพลาด
Match บน Error ต่างกัน
โค้ดใน Listing 9-4 จะ panic! ไม่ว่าทำไม File::open ล้มเหลว อย่างไรก็
ตาม เราอยากทำ action ต่างกันสำหรับเหตุผลล้มเหลวต่างกัน ถ้า File::open
ล้มเหลวเพราะไฟล์ไม่มี เราอยากสร้างไฟล์และ return handle ให้ไฟล์ใหม่ ถ้า
File::open ล้มเหลวด้วยเหตุผลอื่น — เช่น เราไม่มี permission เปิดไฟล์ —
เรายังอยากให้โค้ด panic! ในแบบเดียวกับที่ทำใน Listing 9-4 สำหรับเรื่อง
นี้ เราเพิ่ม match expression ภายใน แสดงใน Listing 9-5
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}
Type ของค่าที่ File::open return ภายใน variant Err คือ io::Error
ซึ่งเป็น struct ที่ standard library ให้ struct นี้มีเมธอด kind ที่เรา
เรียกเพื่อรับค่า io::ErrorKind ได้ enum io::ErrorKind ที่ standard
library ให้ มี variant ที่แทน error ชนิดต่างกันที่อาจเป็นผลจาก io
operation Variant ที่เราอยากใช้คือ ErrorKind::NotFound ซึ่งบ่งบอกว่า
ไฟล์ที่เราพยายามเปิดยังไม่มี ดังนั้น เรา match บน greeting_file_result
แต่เรายังมี inner match บน error.kind()
เงื่อนไขที่เราอยากเช็คใน inner match คือว่าค่าที่ return โดย error.kind()
เป็น variant NotFound ของ enum ErrorKind ไหม ถ้าใช่ เราลองสร้างไฟล์
ด้วย File::create อย่างไรก็ตาม เพราะ File::create อาจล้มเหลวด้วย เรา
ต้องการ arm ที่สองใน inner match expression เมื่อไฟล์สร้างไม่ได้
error message ต่างถูกพิมพ์ arm ที่สองของ outer match ยังเหมือนเดิม
โปรแกรมจึง panic บน error อื่นนอกจาก error ไฟล์ไม่มี
ทางเลือกแทนการใช้ match กับ Result<T, E>
นั่นเป็น match เยอะ! match expression มีประโยชน์มากแต่ก็เป็น
primitive มาก ในบทที่ 13 คุณจะเรียนเกี่ยวกับ closure ซึ่งใช้กับเมธอด
หลายตัวที่ประกาศบน Result<T, E> เมธอดเหล่านี้กระชับกว่าการใช้ match
ในการจัดการค่า Result<T, E> ในโค้ดของคุณได้
เช่น นี่คืออีกวิธีเขียน logic เดียวกับที่แสดงใน Listing 9-5 ครั้งนี้
โดยใช้ closure และเมธอด unwrap_or_else:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {error:?}");
})
} else {
panic!("Problem opening the file: {error:?}");
}
});
}
แม้โค้ดนี้มีพฤติกรรมเดียวกับ Listing 9-5 มันไม่มี match expression
ใด ๆ และสะอาดในการอ่าน กลับมาที่ตัวอย่างนี้หลังคุณอ่านบทที่ 13 และ
lookup เมธอด unwrap_or_else ใน documentation ของ standard library
เมธอดเหล่านี้อีกมากทำความสะอาด match expression ที่ใหญ่และซ้อนได้
เมื่อคุณจัดการ error
Shortcut สำหรับ Panic บน Error
การใช้ match ทำงานได้ดีพอ แต่อาจยาวนิดหน่อยและไม่สื่อเจตนาดีเสมอ Type
Result<T, E> มีเมธอด helper หลายตัวประกาศบนมัน เพื่อทำงานต่าง ๆ ที่
เฉพาะเจาะจงมากขึ้น เมธอด unwrap คือเมธอด shortcut ที่ implement
เหมือนกับ match expression ที่เราเขียนใน Listing 9-4 ถ้าค่า Result
เป็น variant Ok unwrap จะ return ค่าภายใน Ok ถ้า Result เป็น
variant Err unwrap จะเรียก panic! macro ให้เรา นี่คือตัวอย่างของ
unwrap ในการใช้งาน:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt").unwrap();
}
ถ้าเรารันโค้ดนี้โดยไม่มีไฟล์ hello.txt เราจะเห็น error message จากการ
เรียก panic! ที่เมธอด unwrap ทำ:
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
ในทำนองเดียวกัน เมธอด expect ให้เราเลือก error message ของ panic!
ด้วย การใช้ expect แทน unwrap และให้ error message ที่ดี สื่อเจตนา
ของคุณและทำให้การตามหาแหล่งของ panic ง่ายขึ้น syntax ของ expect ดู
แบบนี้:
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")
.expect("hello.txt should be included in this project");
}
เราใช้ expect ในแบบเดียวกับ unwrap — เพื่อ return file handle หรือ
เรียก panic! macro Error message ที่ใช้โดย expect ในการเรียก panic!
จะเป็น parameter ที่เราส่งให้ expect แทน panic! message default ที่
unwrap ใช้ นี่คือสิ่งที่ดูเหมือน:
thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }
ในโค้ดคุณภาพ production Rustacean ส่วนใหญ่เลือก expect แทน unwrap
และให้บริบทเพิ่มเติมว่าทำไม operation คาดว่าจะสำเร็จเสมอ แบบนั้น ถ้า
สมมติฐานของคุณถูกพิสูจน์ผิด คุณมีข้อมูลเพิ่มเติมใช้ในการ debug
Propagate Error
เมื่อ implementation ของฟังก์ชันเรียกบางอย่างที่อาจล้มเหลว แทนการจัดการ error ภายในฟังก์ชันเอง คุณ return error ให้โค้ดที่เรียก เพื่อให้มันตัดสิน ใจได้ว่าจะทำอะไร นี่รู้จักในชื่อ propagate error และให้การควบคุมเพิ่ม ให้โค้ดที่เรียก ที่อาจมีข้อมูลหรือ logic เพิ่มเติมที่กำหนดวิธีจัดการ error มากกว่าสิ่งที่คุณมีในบริบทของโค้ดคุณ
เช่น Listing 9-6 แสดงฟังก์ชันที่อ่าน username จากไฟล์ ถ้าไฟล์ไม่มีหรืออ่าน ไม่ได้ ฟังก์ชันนี้จะ return error เหล่านั้นให้โค้ดที่เรียกฟังก์ชัน
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let username_file_result = File::open("hello.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
}
matchฟังก์ชันนี้เขียนในแบบสั้นกว่ามากได้ แต่เราจะเริ่มโดยทำส่วนใหญ่ด้วยตนเอง
เพื่อสำรวจการจัดการ error ในที่สุด เราจะแสดงวิธีสั้นกว่า มาดู return type
ของฟังก์ชันก่อน — Result<String, io::Error> นี่หมายความว่าฟังก์ชัน
return ค่า type Result<T, E> ที่ generic parameter T ถูกเติมด้วย
type คอนกรีต String และ generic type E ถูกเติมด้วย type คอนกรีต
io::Error
ถ้าฟังก์ชันนี้สำเร็จโดยไม่มีปัญหา โค้ดที่เรียกฟังก์ชันนี้จะได้รับค่า Ok
ที่เก็บ String — username ที่ฟังก์ชันนี้อ่านจากไฟล์ ถ้าฟังก์ชันนี้
เจอปัญหา โค้ดที่เรียกจะได้รับค่า Err ที่เก็บ instance ของ io::Error
ที่มีข้อมูลเพิ่มเติมเกี่ยวกับปัญหา เราเลือก io::Error เป็น return type
ของฟังก์ชันนี้ เพราะเกิดเป็น type ของค่า error ที่ return จาก operation
สองตัวที่เราเรียกใน body ของฟังก์ชันนี้ที่อาจล้มเหลว — ฟังก์ชัน
File::open และเมธอด read_to_string
Body ของฟังก์ชันเริ่มโดยเรียกฟังก์ชัน File::open จากนั้นเราจัดการค่า
Result ด้วย match คล้ายกับ match ใน Listing 9-4 ถ้า File::open
สำเร็จ file handle ในตัวแปร pattern file กลายเป็นค่าในตัวแปร mutable
username_file และฟังก์ชันดำเนินต่อ ในกรณี Err แทนการเรียก panic!
เราใช้ keyword return เพื่อ return ก่อนออกจากฟังก์ชันทั้งหมด และส่ง
ค่า error จาก File::open ตอนนี้ในตัวแปร pattern e กลับให้โค้ดที่เรียก
เป็นค่า error ของฟังก์ชันนี้
ดังนั้น ถ้าเรามี file handle ใน username_file ฟังก์ชันจากนั้นสร้าง
String ใหม่ในตัวแปร username และเรียกเมธอด read_to_string บน file
handle ใน username_file เพื่ออ่านเนื้อหาของไฟล์เข้า username เมธอด
read_to_string ก็ return Result ด้วยเพราะอาจล้มเหลว แม้ File::open
สำเร็จ ดังนั้น เราต้อง match อีกตัวจัดการ Result นั้น — ถ้า
read_to_string สำเร็จ ฟังก์ชันของเราสำเร็จ และเรา return username จาก
ไฟล์ที่ตอนนี้อยู่ใน username ห่อใน Ok ถ้า read_to_string ล้มเหลว
เรา return ค่า error ในแบบเดียวกับที่เรา return ค่า error ใน match
ที่จัดการ return value ของ File::open อย่างไรก็ตาม เราไม่ต้อง explicit
ว่า return เพราะนี่เป็น expression สุดท้ายในฟังก์ชัน
โค้ดที่เรียกโค้ดนี้จะจัดการการได้ค่า Ok ที่มี username หรือค่า Err
ที่มี io::Error ขึ้นอยู่กับโค้ดที่เรียกที่จะตัดสินใจว่าจะทำอะไรกับค่า
เหล่านั้น ถ้าโค้ดที่เรียกได้ค่า Err มันเรียก panic! และ crash โปรแกรม
ใช้ username default หรือ lookup username จากที่อื่นนอกจากไฟล์ เป็นต้น
ก็ได้ เราไม่มีข้อมูลพอเรื่องสิ่งที่โค้ดที่เรียกพยายามทำจริง ๆ เราจึง
propagate ข้อมูลสำเร็จหรือ error ทั้งหมดขึ้นไป ให้มันจัดการอย่างเหมาะสม
pattern การ propagate error นี้ใช้บ่อยมากใน Rust จน Rust ให้ question
mark operator ? เพื่อทำให้สิ่งนี้ง่ายขึ้น
Shortcut Operator ?
Listing 9-7 แสดง implementation ของ read_username_from_file ที่มี
functionality เดียวกับใน Listing 9-6 แต่ implementation นี้ใช้ operator
?
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username_file = File::open("hello.txt")?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
}
?? ที่วางหลังค่า Result ประกาศให้ทำงานในแบบเกือบเหมือน match
expression ที่เราประกาศจัดการค่า Result ใน Listing 9-6 ถ้าค่าของ
Result เป็น Ok ค่าภายใน Ok จะถูก return จาก expression นี้ และ
โปรแกรมจะดำเนินต่อ ถ้าค่าเป็น Err Err จะถูก return จากทั้งฟังก์ชัน
เหมือนเราใช้ keyword return ดังนั้นค่า error ถูก propagate ไปยังโค้ดที่
เรียก
มีความแตกต่างระหว่างสิ่งที่ match expression จาก Listing 9-6 ทำ และ
สิ่งที่ operator ? ทำ — ค่า error ที่ operator ? ถูกเรียกบนพวกมัน
ผ่านฟังก์ชัน from ที่ประกาศใน trait From ใน standard library ซึ่งใช้
แปลงค่าจาก type หนึ่งเป็นอีก type หนึ่ง เมื่อ operator ? เรียกฟังก์ชัน
from type error ที่ได้รับถูกแปลงเป็น type error ที่ประกาศใน return
type ของฟังก์ชันปัจจุบัน นี่มีประโยชน์เมื่อฟังก์ชัน return type error
หนึ่งตัวที่แทนวิธีทั้งหมดที่ฟังก์ชันอาจล้มเหลว แม้ส่วนต่าง ๆ อาจล้มเหลว
ด้วยเหตุผลต่างกัน
เช่น เราเปลี่ยนฟังก์ชัน read_username_from_file ใน Listing 9-7 ให้
return type error custom ชื่อ OurError ที่เราประกาศได้ ถ้าเรายังประกาศ
impl From<io::Error> for OurError เพื่อสร้าง instance ของ OurError
จาก io::Error operator ? ที่เรียกใน body ของ read_username_from_file
จะเรียก from และแปลง error type โดยไม่ต้องเพิ่มโค้ดเพิ่มเติมในฟังก์ชัน
ในบริบทของ Listing 9-7 ? ที่ท้ายการเรียก File::open จะ return ค่า
ภายใน Ok ให้ตัวแปร username_file ถ้า error เกิด operator ? จะ
return ก่อนออกจากทั้งฟังก์ชัน และให้ค่า Err ใด ๆ ให้โค้ดที่เรียก สิ่ง
เดียวกันใช้กับ ? ที่ท้ายการเรียก read_to_string
Operator ? ขจัด boilerplate เยอะและทำให้ implementation ของฟังก์ชันนี้
ง่ายขึ้น เราย่อโค้ดนี้ลงไปอีกได้โดย chain การเรียกเมธอดทันทีหลัง ?
ดังที่แสดงใน Listing 9-8
#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> Result<String, io::Error> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
}
?เราย้ายการสร้าง String ใหม่ใน username ไปต้นฟังก์ชัน ส่วนนั้นไม่
เปลี่ยน แทนการสร้างตัวแปร username_file เรา chain การเรียก
read_to_string ตรงกับผลของ File::open("hello.txt")? เรายังมี ? ที่
ท้ายการเรียก read_to_string และเรายัง return ค่า Ok ที่มี username
เมื่อทั้ง File::open และ read_to_string สำเร็จ แทนการ return error
Functionality ยังเหมือนกับใน Listing 9-6 และ Listing 9-7 — นี่แค่วิธี
เขียนที่ต่างและ ergonomic กว่า
Listing 9-9 แสดงวิธีทำให้นี่สั้นกว่าอีก โดยใช้ fs::read_to_string
#![allow(unused)]
fn main() {
use std::fs;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
}
fs::read_to_string แทนการเปิดแล้วอ่านไฟล์การอ่านไฟล์เข้า string เป็น operation ที่ใช้บ่อย standard library จึงให้
ฟังก์ชัน fs::read_to_string ที่สะดวก ที่เปิดไฟล์ สร้าง String ใหม่
อ่านเนื้อหาของไฟล์ ใส่เนื้อหาเข้า String นั้น และ return มัน แน่นอน
การใช้ fs::read_to_string ไม่ให้โอกาสเราอธิบายการจัดการ error ทั้งหมด
เราจึงทำแบบยาวกว่าก่อน
ใช้ Operator ? ที่ไหน
Operator ? ใช้ได้แค่ในฟังก์ชันที่ return type เข้ากับค่าที่ ? ใช้บน
มัน นี่เพราะ operator ? ประกาศให้ทำ early return ของค่าออกจากฟังก์ชัน
ในแบบเดียวกับ match expression ที่เราประกาศใน Listing 9-6 ใน Listing
9-6 match ใช้ค่า Result และ arm early return return ค่า Err(e)
Return type ของฟังก์ชันต้องเป็น Result เพื่อให้เข้ากับ return นี้
ใน Listing 9-10 มาดู error ที่เราจะได้ถ้าใช้ operator ? ในฟังก์ชัน
main ที่ return type ไม่เข้ากับ type ของค่าที่เราใช้ ? บน
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
? ในฟังก์ชัน main ที่ return () จะ compile ไม่ผ่านโค้ดนี้เปิดไฟล์ ซึ่งอาจล้มเหลว Operator ? ตามค่า Result ที่ return
โดย File::open แต่ฟังก์ชัน main นี้มี return type () ไม่ใช่
Result เมื่อเรา compile โค้ดนี้ เราได้ error message ต่อไปนี้:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let greeting_file = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 | let greeting_file = File::open("hello.txt")?;
5 + Ok(())
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error
Error นี้ชี้ว่าเราได้รับอนุญาตให้ใช้ operator ? แค่ในฟังก์ชันที่ return
Result, Option หรือ type อื่นที่ implement FromResidual เท่านั้น
ในการแก้ error คุณมีสองตัวเลือก ตัวเลือกหนึ่งคือเปลี่ยน return type ของ
ฟังก์ชันให้เข้ากับค่าที่คุณใช้ operator ? บน ตราบที่ไม่มีข้อจำกัดห้าม
ทำเช่นนั้น ตัวเลือกอื่นคือใช้ match หรือหนึ่งในเมธอด Result<T, E>
จัดการ Result<T, E> ในแบบที่เหมาะสม
Error message ยังเอ่ยว่า ? ใช้กับค่า Option<T> ได้ด้วย เหมือนกับการ
ใช้ ? บน Result คุณใช้ ? บน Option ได้แค่ในฟังก์ชันที่ return
Option พฤติกรรมของ operator ? เมื่อเรียกบน Option<T> คล้ายกับ
พฤติกรรมเมื่อเรียกบน Result<T, E> — ถ้าค่าเป็น None None จะถูก
return ก่อนจากฟังก์ชัน ณ จุดนั้น ถ้าค่าเป็น Some ค่าภายใน Some เป็น
ค่าผลของ expression และฟังก์ชันดำเนินต่อ Listing 9-11 มีตัวอย่างของ
ฟังก์ชันที่หาอักขระสุดท้ายของบรรทัดแรกใน text ที่ให้
fn last_char_of_first_line(text: &str) -> Option<char> {
text.lines().next()?.chars().last()
}
fn main() {
assert_eq!(
last_char_of_first_line("Hello, world\nHow are you today?"),
Some('d')
);
assert_eq!(last_char_of_first_line(""), None);
assert_eq!(last_char_of_first_line("\nhi"), None);
}
? บนค่า Option<T>ฟังก์ชันนี้ return Option<char> เพราะเป็นไปได้ที่มีอักขระตรงนั้น แต่ก็
เป็นไปได้ที่ไม่มี โค้ดนี้รับ argument string slice text และเรียกเมธอด
lines บนมัน ซึ่ง return iterator ผ่านบรรทัดใน string เพราะฟังก์ชันนี้
อยากตรวจสอบบรรทัดแรก มันเรียก next บน iterator เพื่อรับค่าแรกจาก
iterator ถ้า text เป็น string ว่าง การเรียก next นี้จะ return None
ซึ่งในกรณีนั้นเราใช้ ? หยุดและ return None จาก
last_char_of_first_line ถ้า text ไม่ใช่ string ว่าง next จะ return
ค่า Some ที่มี string slice ของบรรทัดแรกใน text
? ดึง string slice และเราเรียก chars บน string slice นั้นเพื่อรับ
iterator ของอักขระ เราสนใจอักขระสุดท้ายในบรรทัดแรกนี้ เราจึงเรียก last
เพื่อ return item สุดท้ายใน iterator นี่เป็น Option เพราะเป็นไปได้ที่
บรรทัดแรกเป็น string ว่าง เช่น ถ้า text ขึ้นต้นด้วยบรรทัดเปล่า แต่มี
อักขระบนบรรทัดอื่น อย่างใน "\nhi" อย่างไรก็ตาม ถ้ามีอักขระสุดท้ายบน
บรรทัดแรก จะถูก return ใน variant Some operator ? ตรงกลางให้เราวิธี
กระชับในการแสดง logic นี้ ให้เรา implement ฟังก์ชันในบรรทัดเดียวได้ ถ้า
เราใช้ operator ? บน Option ไม่ได้ เราจะต้อง implement logic นี้
โดยใช้การเรียกเมธอดเพิ่มเติมหรือ match expression
หมายเหตุว่าคุณใช้ operator ? บน Result ในฟังก์ชันที่ return Result
และคุณใช้ operator ? บน Option ในฟังก์ชันที่ return Option ได้
แต่คุณผสมและ match ไม่ได้ Operator ? จะไม่แปลง Result เป็น Option
หรือตรงกันข้ามอัตโนมัติ ในกรณีเหล่านั้น คุณใช้เมธอดอย่าง ok บน
Result หรือ ok_or บน Option ทำการแปลงแบบ explicit ได้
ที่ผ่านมา ฟังก์ชัน main ทั้งหมดที่เราใช้ return () ฟังก์ชัน main
พิเศษเพราะมันเป็น entry point และ exit point ของโปรแกรม executable และมี
ข้อจำกัดเรื่อง return type ของมัน เพื่อให้โปรแกรมทำงานตามที่คาด
โชคดี main ยัง return Result<(), E> ได้ Listing 9-12 มีโค้ดจาก
Listing 9-10 แต่เราเปลี่ยน return type ของ main เป็น
Result<(), Box<dyn Error>> และเพิ่ม return value Ok(()) ที่ท้าย
โค้ดนี้จะ compile ผ่านตอนนี้
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let greeting_file = File::open("hello.txt")?;
Ok(())
}
main ให้ return Result<(), E> อนุญาตให้ใช้ operator ? บนค่า ResultType Box<dyn Error> เป็น trait object ซึ่งเราจะพูดถึงใน
“ใช้ Trait Object เพื่อนามธรรมพฤติกรรมร่วม”
ในบทที่ 18 ตอนนี้ คุณอ่าน Box<dyn Error> ว่าหมายถึง “error ชนิดใดก็ได้”
การใช้ ? บนค่า Result ในฟังก์ชัน main ที่มี error type
Box<dyn Error> อนุญาต เพราะมันให้ค่า Err ใด ๆ ถูก return ก่อน แม้
body ของฟังก์ชัน main นี้จะ return แค่ error type std::io::Error
โดยระบุ Box<dyn Error> signature นี้จะยังถูกต้องแม้โค้ดที่ return
error อื่นถูกเพิ่มเข้า body ของ main
เมื่อฟังก์ชัน main return Result<(), E> executable จะออกด้วยค่า 0
ถ้า main return Ok(()) และจะออกด้วยค่าที่ไม่ใช่ศูนย์ถ้า main
return ค่า Err Executable ที่เขียนใน C return integer เมื่อออก —
โปรแกรมที่ออกสำเร็จ return integer 0 และโปรแกรมที่ error return
integer ที่ไม่ใช่ 0 Rust ก็ return integer จาก executable เพื่อให้
เข้ากับ convention นี้
ฟังก์ชัน main return type ใด ๆ ที่ implement
trait std::process::Termination ได้ ซึ่ง
มีฟังก์ชัน report ที่ return ExitCode ปรึกษา documentation ของ
standard library สำหรับข้อมูลเพิ่มเติมเรื่องการ implement trait
Termination สำหรับ type ของคุณเอง
ตอนนี้เราพูดถึงรายละเอียดของการเรียก panic! หรือ return Result แล้ว
กลับไปที่หัวข้อของวิธีตัดสินใจว่าตัวไหนเหมาะใช้ในกรณีไหน
panic! หรือไม่ panic!
panic! หรือไม่ panic!
แล้วคุณตัดสินใจเมื่อไหร่ควรเรียก panic! และเมื่อไหร่ควร return
Result? เมื่อโค้ด panic ไม่มีวิธี recover คุณเรียก panic! สำหรับ
สถานการณ์ error ใด ๆ ได้ ไม่ว่าจะมีวิธี recover ที่เป็นไปได้หรือไม่ แต่
จากนั้นคุณกำลังตัดสินใจว่าสถานการณ์เป็น unrecoverable แทนโค้ดที่เรียก
เมื่อคุณเลือก return ค่า Result คุณให้ตัวเลือกแก่โค้ดที่เรียก โค้ดที่
เรียกเลือกพยายาม recover ในแบบที่เหมาะสำหรับสถานการณ์ของมัน หรือมัน
ตัดสินใจว่าค่า Err ในกรณีนี้เป็น unrecoverable แล้วเรียก panic! และ
เปลี่ยน recoverable error ของคุณเป็น unrecoverable ได้ ดังนั้น การ
return Result เป็นตัวเลือก default ที่ดี เมื่อคุณประกาศฟังก์ชันที่อาจ
ล้มเหลว
ในสถานการณ์เช่นตัวอย่าง, prototype code และ test เหมาะกว่าที่จะเขียนโค้ด
ที่ panic แทน return Result มาสำรวจว่าทำไม แล้วพูดถึงสถานการณ์ที่
compiler บอกไม่ได้ว่าความล้มเหลวเป็นไปไม่ได้ แต่คุณในฐานะมนุษย์บอกได้
บทจะจบด้วย guideline ทั่วไปเรื่องวิธีตัดสินใจว่าจะ panic ในโค้ด library
หรือไม่
ตัวอย่าง, Prototype Code และ Test
เมื่อคุณเขียนตัวอย่างเพื่อแสดงแนวคิด การรวมโค้ดจัดการ error ที่แข็งแกร่ง
อาจทำให้ตัวอย่างชัดเจนน้อยลง ในตัวอย่าง เข้าใจว่าการเรียกเมธอดอย่าง
unwrap ที่อาจ panic ตั้งใจเป็น placeholder ของวิธีที่คุณจะอยากให้
application ของคุณจัดการ error ซึ่งต่างกันได้ขึ้นกับสิ่งที่โค้ดที่เหลือ
ทำ
ในทำนองเดียวกัน เมธอด unwrap และ expect สะดวกมากเมื่อคุณ prototype
และคุณยังไม่พร้อมตัดสินใจวิธีจัดการ error พวกมันทิ้ง marker ชัดเจนในโค้ด
ของคุณ เมื่อคุณพร้อมทำให้โปรแกรมแข็งแกร่งขึ้น
ถ้าการเรียกเมธอดล้มเหลวใน test คุณจะอยากให้ทั้ง test ล้มเหลว แม้เมธอด
นั้นไม่ใช่ functionality ภายใต้การทดสอบ เพราะ panic! เป็นวิธีที่ test
ถูก mark เป็นความล้มเหลว การเรียก unwrap หรือ expect คือสิ่งที่เป๊ะ
ที่ควรเกิด
เมื่อคุณมีข้อมูลมากกว่า Compiler
ก็เหมาะที่จะเรียก expect เมื่อคุณมี logic อื่นที่รับประกันว่า Result
จะมีค่า Ok แต่ logic นั้นไม่ใช่สิ่งที่ compiler เข้าใจ คุณจะยังมีค่า
Result ที่ต้องจัดการ — operation ใดก็ตามที่คุณเรียกยังมีความเป็นไปได้
ของความล้มเหลวโดยทั่วไป แม้จะเป็นไปไม่ได้เชิง logic ในสถานการณ์เฉพาะของ
คุณ ถ้าคุณรับประกันได้โดยตรวจสอบโค้ดด้วยตนเองว่าคุณจะไม่มี variant Err
เลย มันยอมรับได้ที่จะเรียก expect และ document เหตุผลที่คุณคิดว่าคุณ
จะไม่มี variant Err ใน argument text นี่คือตัวอย่าง:
fn main() {
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1"
.parse()
.expect("Hardcoded IP address should be valid");
}
เรากำลังสร้าง instance IpAddr โดย parse string ที่ hardcode เราเห็นได้
ว่า 127.0.0.1 เป็น IP address ที่ valid มันจึงยอมรับได้ที่จะใช้
expect ที่นี่ อย่างไรก็ตาม การมี string ที่ hardcode และ valid ไม่
เปลี่ยน return type ของเมธอด parse — เรายังได้ค่า Result และ
compiler จะยังบังคับให้เราจัดการ Result ราวกับว่า variant Err เป็น
ความเป็นไปได้ เพราะ compiler ไม่ฉลาดพอที่จะเห็นว่า string นี้เป็น IP
address ที่ valid เสมอ ถ้า string IP address มาจาก user แทนการ hardcode
เข้าโปรแกรม และจึง มี ความเป็นไปได้ของความล้มเหลว เราจะอยากจัดการ
Result ในแบบที่แข็งแกร่งกว่าแน่นอน การเอ่ยถึงสมมติฐานว่า IP address นี้
ถูก hardcode จะเตือนเราให้เปลี่ยน expect เป็นโค้ดจัดการ error ที่ดีกว่า
ถ้าในอนาคต เราต้องการรับ IP address จากแหล่งอื่นแทน
Guideline สำหรับการจัดการ Error
แนะนำให้โค้ดของคุณ panic เมื่อเป็นไปได้ที่โค้ดของคุณอาจจบในสถานะแย่ ใน บริบทนี้ สถานะแย่ คือเมื่อสมมติฐาน, การรับประกัน, สัญญา หรือ invariant บางอย่างถูกทำลาย เช่นเมื่อค่า invalid, ค่าขัดแย้ง หรือค่าหาย ถูกส่งให้ โค้ดของคุณ — บวกหนึ่งหรือมากกว่าของต่อไปนี้:
- สถานะแย่เป็นสิ่งที่ไม่คาด ตรงข้ามกับสิ่งที่อาจเกิดเป็นครั้งคราว เช่น user ป้อนข้อมูลในรูปแบบผิด
- โค้ดของคุณหลังจุดนี้ต้องพึ่งการไม่อยู่ในสถานะแย่นี้ แทนการเช็คปัญหาในทุก ขั้นตอน
- ไม่มีวิธีที่ดีที่จะ encode ข้อมูลนี้ใน type ที่คุณใช้ เราจะทำงานผ่าน ตัวอย่างของสิ่งที่เราหมายถึงใน “Encode State และพฤติกรรมเป็น Type” ในบทที่ 18
ถ้ามีคนเรียกโค้ดของคุณและส่งค่าที่ไม่สมเหตุสมผล ดีที่สุดที่จะ return
error ถ้าทำได้ เพื่อให้ user ของ library ตัดสินใจว่าจะทำอะไรในกรณีนั้น
อย่างไรก็ตาม ในกรณีที่การดำเนินต่ออาจ insecure หรือเป็นอันตราย ตัวเลือก
ดีที่สุดอาจเป็นเรียก panic! และเตือนคนที่ใช้ library ของคุณถึง bug ใน
โค้ดของเขา เพื่อให้เขาแก้ระหว่าง development ในทำนองเดียวกัน panic!
มักเหมาะถ้าคุณเรียก external code ที่อยู่นอกการควบคุมของคุณและ return
สถานะ invalid ที่คุณไม่มีวิธีแก้
อย่างไรก็ตาม เมื่อความล้มเหลวคาดได้ เหมาะกว่าที่จะ return Result
แทนการเรียก panic! ตัวอย่างรวมถึง parser ที่ได้ข้อมูลผิดรูปแบบ หรือ
HTTP request ที่ return สถานะบ่งบอกว่าคุณชน rate limit ในกรณีเหล่านี้
การ return Result บ่งบอกว่าความล้มเหลวเป็นความเป็นไปได้ที่คาด ที่โค้ด
ที่เรียกต้องตัดสินใจวิธีจัดการ
เมื่อโค้ดของคุณทำ operation ที่อาจทำให้ user เสี่ยง ถ้ามันถูกเรียกด้วยค่า
invalid โค้ดของคุณควรตรวจสอบค่าว่า valid ก่อน และ panic ถ้าค่าไม่ valid
นี่ส่วนใหญ่เพื่อเหตุผลด้าน safety — การพยายาม operate บนข้อมูล invalid
อาจเปิดเผยโค้ดของคุณกับ vulnerability นี่คือเหตุผลหลักที่ standard
library จะเรียก panic! ถ้าคุณพยายามเข้าถึงหน่วยความจำที่ out-of-bounds
— การพยายามเข้าถึงหน่วยความจำที่ไม่เป็นของโครงสร้างข้อมูลปัจจุบันเป็น
ปัญหา security ที่ใช้บ่อย ฟังก์ชันมักมี สัญญา — พฤติกรรมรับประกันเฉพาะ
ถ้า input ตรงตามข้อกำหนดเฉพาะ การ panic เมื่อสัญญาถูกฝ่าฝืนสมเหตุสมผล
เพราะการฝ่าฝืนสัญญามักบ่งบอก bug ของฝั่ง caller และไม่ใช่ชนิดของ error
ที่คุณอยากให้โค้ดที่เรียกต้องจัดการแบบ explicit จริง ๆ ไม่มีวิธีสมเหตุสม
ผลให้โค้ดที่เรียก recover — โปรแกรมเมอร์ ที่เรียกต้องแก้โค้ด สัญญา
สำหรับฟังก์ชัน โดยเฉพาะเมื่อการฝ่าฝืนจะทำให้ panic ควรอธิบายใน API
documentation สำหรับฟังก์ชัน
อย่างไรก็ตาม การมีการเช็ค error เยอะในฟังก์ชันทั้งหมดของคุณจะยาวและน่ารำ
คาญ โชคดี คุณใช้ระบบ type ของ Rust (และจึงการตรวจสอบ type ที่ทำโดย
compiler) ทำการเช็คหลายอย่างให้คุณได้ ถ้าฟังก์ชันของคุณมี type เฉพาะเป็น
parameter คุณดำเนินด้วย logic ของโค้ดของคุณ โดยรู้ว่า compiler ได้รับ
ประกันแล้วว่าคุณมีค่า valid เช่น ถ้าคุณมี type แทน Option โปรแกรมของ
คุณคาดว่าจะมี บางอย่าง แทน ไม่มีอะไร โค้ดของคุณจึงไม่ต้องจัดการสอง
กรณีสำหรับ variant Some และ None — มันจะมีแค่กรณีเดียวสำหรับการมีค่า
แน่นอน โค้ดที่พยายามส่งไม่มีอะไรเข้าฟังก์ชันของคุณจะ compile ไม่ผ่าน
ฟังก์ชันของคุณจึงไม่ต้องเช็คกรณีนั้นตอน runtime ตัวอย่างอีก คือใช้
unsigned integer type อย่าง u32 ซึ่งรับประกันว่า parameter ไม่เคยเป็น
ค่าลบ
Type Custom สำหรับ Validation
มาเอาไอเดียของการใช้ระบบ type ของ Rust รับประกันว่าเรามีค่า valid ก้าวต่อ ไป และดูการสร้าง type custom สำหรับ validation จำเกมทายตัวเลขในบทที่ 2 ที่โค้ดของเราถาม user ทายตัวเลขระหว่าง 1 และ 100 เราไม่เคย validate ว่า การทายของ user อยู่ระหว่างตัวเลขเหล่านั้น ก่อนเช็คกับตัวเลขลับของเรา — เรา validate แค่ว่าการทายเป็นบวก ในกรณีนี้ ผลกระทบไม่รุนแรงมาก — output ของเรา “Too high” หรือ “Too low” จะยังถูก แต่จะเป็นการปรับปรุงที่มีประโยชน์ ที่จะแนะนำ user ไปสู่การทายที่ valid และมีพฤติกรรมต่างเมื่อ user ทายเลข นอก range เทียบกับเมื่อ user พิมพ์ตัวอักษรแทน เป็นต้น
วิธีหนึ่งในการทำคือ parse การทายเป็น i32 แทนแค่ u32 เพื่ออนุญาตตัวเลข
ลบที่อาจเป็นไปได้ และเพิ่มการเช็คตัวเลขอยู่ใน range ดังนี้:
use rand::Rng;
use std::cmp::Ordering;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
loop {
// --snip--
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
if expression เช็คว่าค่าของเราอยู่นอก range ไหม, บอก user เรื่องปัญหา
และเรียก continue เริ่ม iteration ถัดไปของ loop และขอการทายอีก หลัง
if expression เราดำเนินด้วยการเปรียบเทียบระหว่าง guess และตัวเลขลับ
โดยรู้ว่า guess อยู่ระหว่าง 1 และ 100
อย่างไรก็ตาม นี่ไม่ใช่คำตอบในอุดมคติ — ถ้ามันสำคัญมากที่โปรแกรม operate แค่บนค่าระหว่าง 1 และ 100 และมีฟังก์ชันมากที่มีข้อกำหนดนี้ การมีการเช็ค แบบนี้ในทุกฟังก์ชันจะน่าเบื่อ (และอาจกระทบ performance)
แทน เราทำ type ใหม่ใน module เฉพาะและใส่ validation ในฟังก์ชันสร้าง
instance ของ type แทนการเขียน validation ซ้ำทุกที่ แบบนั้น ฟังก์ชัน
ปลอดภัยที่จะใช้ type ใหม่ใน signature และมั่นใจใช้ค่าที่ได้รับ Listing
9-13 แสดงวิธีหนึ่งในการประกาศ type Guess ที่จะสร้าง instance ของ
Guess แค่ถ้าฟังก์ชัน new ได้รับค่าระหว่าง 1 และ 100
#![allow(unused)]
fn main() {
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
}
Guess ที่จะดำเนินแค่ด้วยค่าระหว่าง 1 และ 100หมายเหตุว่าโค้ดนี้ใน src/guessing_game.rs พึ่งการเพิ่มการประกาศ module
mod guessing_game; ใน src/lib.rs ที่เราไม่ได้แสดงที่นี่ ภายในไฟล์ของ
module ใหม่นี้ เราประกาศ struct ชื่อ Guess ที่มี field ชื่อ value
ที่เก็บ i32 นี่คือที่ที่ตัวเลขจะเก็บ
จากนั้นเรา implement associated function ชื่อ new บน Guess ที่สร้าง
instance ของค่า Guess ฟังก์ชัน new ประกาศให้มี parameter หนึ่งตัว
ชื่อ value type i32 และ return Guess โค้ดใน body ของฟังก์ชัน new
ทดสอบ value ให้แน่ใจว่าอยู่ระหว่าง 1 และ 100 ถ้า value ไม่ผ่านทดสอบ
นี้ เราเรียก panic! ซึ่งจะเตือนโปรแกรมเมอร์ที่กำลังเขียนโค้ดที่เรียก
ว่าเขามี bug ที่ต้องแก้ เพราะการสร้าง Guess ด้วย value นอก range นี้
จะฝ่าฝืนสัญญาที่ Guess::new พึ่งอยู่ เงื่อนไขที่ Guess::new อาจ panic
ควรพูดถึงใน public-facing API documentation — เราจะครอบคลุม convention
documentation ที่บ่งบอกความเป็นไปได้ของ panic! ใน API documentation
ที่คุณสร้างในบทที่ 14 ถ้า value ผ่านทดสอบ เราสร้าง Guess ใหม่ที่
field value ตั้งเป็น parameter value และ return Guess
ถัดไป เรา implement เมธอดชื่อ value ที่ borrow self ไม่มี parameter
อื่น และ return i32 เมธอดชนิดนี้บางครั้งเรียก getter เพราะจุดประสงค์
คือรับข้อมูลจาก field และ return เมธอด public นี้จำเป็น เพราะ field
value ของ struct Guess เป็น private สำคัญที่ field value เป็น
private เพื่อโค้ดที่ใช้ struct Guess ไม่อนุญาตให้ set value ตรง ๆ —
โค้ดนอก module guessing_game ต้อง ใช้ฟังก์ชัน Guess::new สร้าง
instance ของ Guess จึงรับประกันว่าไม่มีวิธีให้ Guess มี value ที่
ไม่ถูกเช็คโดยเงื่อนไขในฟังก์ชัน Guess::new
ฟังก์ชันที่มี parameter หรือ return แค่ตัวเลขระหว่าง 1 และ 100 ประกาศใน
signature ของมันว่ามันรับหรือ return Guess แทน i32 ได้ และไม่ต้อง
ทำการเช็คเพิ่มเติมใน body
สรุป
ฟีเจอร์การจัดการ error ของ Rust ออกแบบเพื่อช่วยคุณเขียนโค้ดที่แข็งแกร่ง
ขึ้น panic! macro ส่งสัญญาณว่าโปรแกรมของคุณอยู่ในสถานะที่จัดการไม่ได้
และให้คุณบอก process ให้หยุด แทนการพยายามดำเนินด้วยค่า invalid หรือไม่
ถูกต้อง enum Result ใช้ระบบ type ของ Rust บ่งบอกว่า operation อาจล้ม
เหลวในแบบที่โค้ดของคุณ recover ได้ คุณใช้ Result บอกโค้ดที่เรียกโค้ด
ของคุณว่าต้องจัดการความสำเร็จหรือความล้มเหลวที่อาจเป็นไปได้ด้วย การใช้
panic! และ Result ในสถานการณ์ที่เหมาะสม จะทำให้โค้ดของคุณน่าเชื่อถือ
ขึ้นในการเผชิญปัญหาที่หลีกเลี่ยงไม่ได้
ตอนนี้คุณเห็นวิธีที่มีประโยชน์ที่ standard library ใช้ generic กับ enum
Option และ Result แล้ว เราจะพูดถึงวิธีที่ generic ทำงานและวิธีคุณใช้
ในโค้ดของคุณ
Generic Type, Trait และ Lifetime
ทุกภาษาโปรแกรมมีเครื่องมือสำหรับจัดการการซ้ำของแนวคิดอย่างมีประสิทธิภาพ ใน Rust เครื่องมือหนึ่งคือ generic — ตัวแทนนามธรรมของ type คอนกรีตหรือ คุณสมบัติอื่น เราแสดงพฤติกรรมของ generic หรือวิธีที่พวกมันสัมพันธ์กับ generic อื่นได้ โดยไม่ต้องรู้ว่าอะไรจะอยู่ในที่ของมันเมื่อ compile และ รันโค้ด
ฟังก์ชันรับ parameter ของ generic type บางตัว แทน type คอนกรีตอย่าง
i32 หรือ String ได้ ในแบบเดียวกับที่รับ parameter ที่มีค่าไม่รู้
เพื่อรันโค้ดเดียวกันบนค่าคอนกรีตหลายตัว จริง ๆ เราใช้ generic แล้วในบทที่
6 กับ Option<T> ในบทที่ 8 กับ Vec<T> และ HashMap<K, V> และในบทที่
9 กับ Result<T, E> ในบทที่นี้ คุณจะสำรวจวิธีประกาศ type, ฟังก์ชัน และ
เมธอดของคุณเองด้วย generic!
ก่อนอื่น เราจะทบทวนวิธีดึงฟังก์ชันออกเพื่อลดการซ้ำของโค้ด แล้วเราจะใช้ เทคนิคเดียวกันสร้าง generic function จากสองฟังก์ชันที่ต่างกันแค่ที่ type ของ parameter เราจะอธิบายวิธีใช้ generic type ใน definition ของ struct และ enum ด้วย
จากนั้น คุณจะเรียนวิธีใช้ trait ประกาศพฤติกรรมในแบบ generic คุณรวม trait กับ generic type เพื่อจำกัด generic type ให้รับเฉพาะ type ที่มีพฤติกรรม เฉพาะ ตรงข้ามกับแค่ type ใด ๆ ได้
สุดท้าย เราจะพูดถึง lifetime — generic หลากหลายที่ให้ข้อมูล compiler ว่า reference สัมพันธ์กันอย่างไร Lifetime ให้เราให้ข้อมูล compiler เพียง พอเรื่องค่าที่ borrow เพื่อให้มันรับประกันได้ว่า reference จะ valid ใน สถานการณ์มากกว่าที่ทำได้โดยไม่มีความช่วยเหลือของเรา
ลดการซ้ำโดยดึงฟังก์ชัน
Generic ให้เราแทน type เฉพาะด้วย placeholder ที่แทน type หลายตัว เพื่อ ลดการซ้ำของโค้ด ก่อนลงลึก syntax ของ generic มาดูวิธีลดการซ้ำในแบบที่ไม่ เกี่ยวกับ generic type ก่อน โดยดึงฟังก์ชันที่แทนค่าเฉพาะด้วย placeholder ที่แทนค่าหลายค่า แล้วเราจะใช้เทคนิคเดียวกันดึง generic function! ด้วยการ ดูวิธีจับโค้ดที่ซ้ำที่คุณดึงเป็นฟังก์ชันได้ คุณจะเริ่มจับโค้ดที่ซ้ำที่ ใช้ generic ได้
เราจะเริ่มด้วยโปรแกรมสั้นใน Listing 10-1 ที่หาตัวเลขที่ใหญ่ที่สุดใน list
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
assert_eq!(*largest, 100);
}
เราเก็บ list ของ integer ในตัวแปร number_list และวาง reference ของ
ตัวเลขแรกใน list ในตัวแปรชื่อ largest จากนั้นเรา iterate ผ่านตัวเลข
ทั้งหมดใน list และถ้าตัวเลขปัจจุบันมากกว่าตัวเลขที่เก็บใน largest เรา
แทน reference ในตัวแปรนั้น อย่างไรก็ตาม ถ้าตัวเลขปัจจุบันน้อยกว่าหรือ
เท่ากับตัวเลขที่ใหญ่ที่สุดที่เห็นมา ตัวแปรไม่เปลี่ยน และโค้ดไปยังตัวเลข
ถัดไปใน list หลังพิจารณาตัวเลขทั้งหมดใน list largest ควรอ้างถึงตัวเลข
ที่ใหญ่ที่สุด ซึ่งในกรณีนี้คือ 100
ตอนนี้เราได้รับมอบหมายให้หาตัวเลขที่ใหญ่ที่สุดใน list ตัวเลขสอง list ที่ ต่างกัน ในการทำเช่นนั้น เราเลือกที่จะคัดลอกโค้ดใน Listing 10-1 และใช้ logic เดียวกันในสองที่ในโปรแกรม ดังที่แสดงใน Listing 10-2
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let mut largest = &number_list[0];
for number in &number_list {
if number > largest {
largest = number;
}
}
println!("The largest number is {largest}");
}
แม้โค้ดนี้ทำงาน การคัดลอกโค้ดน่าเบื่อและเสี่ยง error เรายังต้องจำที่จะ update โค้ดในหลายที่เมื่อเราอยากเปลี่ยน
เพื่อกำจัดการซ้ำนี้ เราจะสร้าง abstraction โดยประกาศฟังก์ชันที่ operate บน list ของ integer ใด ๆ ที่ส่งเข้าเป็น parameter คำตอบนี้ทำให้โค้ดของ เราชัดเจนขึ้นและให้เราแสดงแนวคิดของการหาตัวเลขที่ใหญ่ที่สุดใน list อย่าง เป็นนามธรรม
ใน Listing 10-3 เราดึงโค้ดที่หาตัวเลขที่ใหญ่ที่สุดเข้าฟังก์ชันชื่อ
largest จากนั้น เราเรียกฟังก์ชันหาตัวเลขที่ใหญ่ที่สุดในสอง list จาก
Listing 10-2 เรายังใช้ฟังก์ชันบน list ของค่า i32 อื่นใดที่เราอาจมีใน
อนาคตได้
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 100);
let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];
let result = largest(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 6000);
}
ฟังก์ชัน largest มี parameter ชื่อ list ซึ่งแทน slice คอนกรีตใด ๆ
ของค่า i32 ที่เราอาจส่งเข้าฟังก์ชัน ผลคือ เมื่อเราเรียกฟังก์ชัน โค้ด
รันบนค่าเฉพาะที่เราส่งเข้า
สรุป นี่คือขั้นตอนที่เราทำเพื่อเปลี่ยนโค้ดจาก Listing 10-2 เป็น Listing 10-3:
- ระบุโค้ดที่ซ้ำ
- ดึงโค้ดที่ซ้ำเข้า body ของฟังก์ชัน และระบุ input และ return value ของ โค้ดนั้นใน signature ของฟังก์ชัน
- Update สอง instance ของโค้ดที่ซ้ำให้เรียกฟังก์ชันแทน
ถัดไป เราจะใช้ขั้นตอนเดียวกันเหล่านี้กับ generic ลดการซ้ำของโค้ด ในแบบ
เดียวกับที่ body ของฟังก์ชัน operate บน list นามธรรมแทนค่าเฉพาะได้
generic ให้โค้ด operate บน type นามธรรม
เช่น สมมติเรามีสองฟังก์ชัน — ตัวหนึ่งหา item ที่ใหญ่ที่สุดใน slice ของ
ค่า i32 และตัวหนึ่งหา item ที่ใหญ่ที่สุดใน slice ของค่า char เราจะ
กำจัดการซ้ำนั้นยังไง? มาดูกัน!
Generic data type
Generic Data Type
เราใช้ generic สร้าง definition สำหรับ item อย่าง signature ฟังก์ชันหรือ struct ซึ่งเราใช้กับ type ข้อมูลคอนกรีตหลายตัวได้ ก่อนอื่นมาดูวิธีประกาศ ฟังก์ชัน struct enum และเมธอดโดยใช้ generic จากนั้น เราจะพูดถึงวิธีที่ generic กระทบ performance ของโค้ด
ในการประกาศฟังก์ชัน
เมื่อประกาศฟังก์ชันที่ใช้ generic เราวาง generic ใน signature ของฟังก์ชัน ตรงที่โดยปกติเราจะระบุ type ข้อมูลของ parameter และ return value การทำ เช่นนั้นทำให้โค้ดของเรายืดหยุ่นขึ้นและให้ functionality เพิ่มแก่ผู้เรียก ฟังก์ชันของเรา ขณะป้องกันการซ้ำของโค้ด
ต่อจากฟังก์ชัน largest ของเรา Listing 10-4 แสดงสองฟังก์ชันที่ทั้งคู่หา
ค่าที่ใหญ่ที่สุดใน slice เราจะรวมพวกมันเป็นฟังก์ชันเดียวที่ใช้ generic
fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> &char {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {result}");
assert_eq!(*result, 100);
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {result}");
assert_eq!(*result, 'y');
}
ฟังก์ชัน largest_i32 คือตัวที่เราดึงใน Listing 10-3 ที่หา i32 ที่
ใหญ่ที่สุดใน slice ฟังก์ชัน largest_char หา char ที่ใหญ่ที่สุดใน
slice body ของฟังก์ชันมีโค้ดเดียวกัน มากำจัดการซ้ำโดยแนะนำ generic type
parameter ในฟังก์ชันเดียว
ในการ parameterize type ในฟังก์ชันเดียวใหม่ เราต้องตั้งชื่อ type parameter
เช่นเดียวกับที่เราทำสำหรับ value parameter ของฟังก์ชัน คุณใช้ identifier
ใดเป็นชื่อ type parameter ได้ แต่เราจะใช้ T เพราะตาม convention ชื่อ
type parameter ใน Rust สั้น มักแค่ตัวอักษรเดียว และ convention การตั้งชื่อ
type ของ Rust คือ UpperCamelCase ย่อจาก type T เป็นตัวเลือก default
ของโปรแกรมเมอร์ Rust ส่วนใหญ่
เมื่อเราใช้ parameter ใน body ของฟังก์ชัน เราต้องประกาศชื่อ parameter ใน
signature เพื่อให้ compiler รู้ว่าชื่อนั้นหมายถึงอะไร ในทำนองเดียวกัน
เมื่อเราใช้ชื่อ type parameter ใน signature ฟังก์ชัน เราต้องประกาศชื่อ
type parameter ก่อนเราใช้ ในการประกาศฟังก์ชัน largest แบบ generic เรา
วางการประกาศชื่อ type ภายใน angle bracket <> ระหว่างชื่อฟังก์ชันและ
list parameter ดังนี้:
fn largest<T>(list: &[T]) -> &T {
เราอ่าน definition นี้ว่า “ฟังก์ชัน largest เป็น generic บน type T
บางตัว” ฟังก์ชันนี้มี parameter หนึ่งตัวชื่อ list ซึ่งเป็น slice ของ
ค่า type T ฟังก์ชัน largest จะ return reference ของค่า type T
เดียวกัน
Listing 10-5 แสดง definition ฟังก์ชัน largest ที่รวมโดยใช้ generic data
type ใน signature listing ยังแสดงวิธีเราเรียกฟังก์ชันด้วย slice ของค่า
i32 หรือค่า char หมายเหตุว่าโค้ดนี้จะยัง compile ไม่ผ่าน
fn largest<T>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {result}");
}
largest ที่ใช้ generic type parameter — ยัง compile ไม่ผ่านถ้าเรา compile โค้ดนี้ตอนนี้ เราจะได้ error นี้:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
--> src/main.rs:5:17
|
5 | if item > largest {
| ---- ^ ------- &T
| |
| &T
|
help: consider restricting type parameter `T` with trait `PartialOrd`
|
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
| ++++++++++++++++++++++
For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
ข้อความ help เอ่ย std::cmp::PartialOrd ซึ่งเป็น trait และเราจะพูดถึง
trait ในส่วนถัดไป ตอนนี้ รู้ว่า error นี้ระบุว่า body ของ largest จะ
ไม่ทำงานสำหรับ type ที่เป็นไปได้ทั้งหมดที่ T เป็น เพราะเราอยาก
เปรียบเทียบค่า type T ใน body เราใช้ได้แค่ type ที่ค่าของพวกมัน
เรียงลำดับได้ เพื่อเปิดทางการเปรียบเทียบ standard library มี trait
std::cmp::PartialOrd ที่คุณ implement บน type ได้ (ดูภาคผนวก C สำหรับ
ข้อมูลเพิ่มเติมเรื่อง trait นี้) ในการแก้ Listing 10-5 เราตามคำแนะนำของ
ข้อความ help และจำกัด type ที่ valid สำหรับ T ให้เป็นแค่ตัวที่
implement PartialOrd Listing จะ compile ผ่านตอนนั้น เพราะ standard
library implement PartialOrd บนทั้ง i32 และ char
ในการประกาศ Struct
เรายังประกาศ struct ให้ใช้ generic type parameter ในหนึ่งหรือมากกว่าหนึ่ง
field โดยใช้ syntax <> ได้ Listing 10-6 ประกาศ struct Point<T> เพื่อ
เก็บค่าพิกัด x และ y ของ type ใด ๆ
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
Point<T> ที่เก็บค่า x และ y ของ type Tsyntax สำหรับใช้ generic ใน definition struct คล้ายกับที่ใช้ใน definition ฟังก์ชัน ก่อนอื่น เราประกาศชื่อ type parameter ภายใน angle bracket หลัง ชื่อ struct จากนั้น เราใช้ generic type ใน definition struct ตรงที่ มิฉะนั้นเราจะระบุ type ข้อมูลคอนกรีต
หมายเหตุว่าเพราะเราใช้แค่ generic type หนึ่งตัวประกาศ Point<T>
definition นี้บอกว่า struct Point<T> เป็น generic บน type T บางตัว
และ field x และ y ทั้งคู่ เป็น type เดียวกัน ไม่ว่า type นั้นจะ
เป็นอะไร ถ้าเราสร้าง instance ของ Point<T> ที่มีค่า type ต่างกัน
อย่างใน Listing 10-7 โค้ดของเราจะ compile ไม่ผ่าน
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
x และ y ต้องเป็น type เดียวกันเพราะทั้งคู่มี generic data type T เดียวกันในตัวอย่างนี้ เมื่อเรา assign ค่า integer 5 ให้ x เราให้ compiler
รู้ว่า generic type T จะเป็น integer สำหรับ instance นี้ของ Point<T>
จากนั้น เมื่อเราระบุ 4.0 สำหรับ y ซึ่งเราประกาศให้มี type เดียวกับ
x เราจะได้ error type mismatch แบบนี้:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
--> src/main.rs:7:38
|
7 | let wont_work = Point { x: 5, y: 4.0 };
| ^^^ expected integer, found floating-point number
For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
ในการประกาศ struct Point ที่ x และ y เป็น generic ทั้งคู่ แต่มี
type ต่างกันได้ เราใช้ generic type parameter หลายตัวได้ เช่น ใน
Listing 10-8 เราเปลี่ยน definition ของ Point ให้เป็น generic บน type
T และ U ที่ x เป็น type T และ y เป็น type U
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
Point<T, U> generic บนสอง type เพื่อให้ x และ y เป็นค่าของ type ต่างกันได้ตอนนี้ instance ทั้งหมดของ Point ที่แสดงอนุญาตแล้ว! คุณใช้ generic
type parameter มากเท่าที่อยากใน definition ได้ แต่ใช้มากกว่าไม่กี่ตัวทำ
ให้โค้ดของคุณอ่านยาก ถ้าคุณพบว่าต้องการ generic type หลายตัวในโค้ด มัน
อาจบ่งบอกว่าโค้ดต้องการการ restructure เป็นชิ้นเล็ก
ในการประกาศ Enum
อย่างที่เราทำกับ struct เราประกาศ enum ให้เก็บ generic data type ใน
variant ได้ ลองมาดู enum Option<T> ที่ standard library ให้อีกครั้ง
ซึ่งเราใช้ในบทที่ 6:
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T),
None,
}
}
Definition นี้ควรสมเหตุสมผลมากขึ้นกับคุณตอนนี้ อย่างที่คุณเห็น enum
Option<T> เป็น generic บน type T และมีสอง variant — Some ที่เก็บ
ค่าหนึ่งของ type T และ variant None ที่ไม่เก็บค่าใด ๆ ด้วยการใช้
enum Option<T> เราแสดงแนวคิดนามธรรมของค่าทางเลือกได้ และเพราะ
Option<T> เป็น generic เราใช้ abstraction นี้ได้ไม่ว่า type ของค่า
ทางเลือกจะเป็นอะไร
Enum ใช้ generic type หลายตัวได้ด้วย Definition ของ enum Result ที่เรา
ใช้ในบทที่ 9 เป็นตัวอย่างหนึ่ง:
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
Enum Result เป็น generic บนสอง type T และ E และมีสอง variant —
Ok ที่เก็บค่าของ type T และ Err ที่เก็บค่าของ type E Definition
นี้ทำให้สะดวกในการใช้ enum Result ที่ใดก็ตามที่เรามี operation ที่อาจ
สำเร็จ (return ค่าของ type T) หรือล้มเหลว (return error ของ type E)
จริง ๆ นี่คือสิ่งที่เราใช้เปิดไฟล์ใน Listing 9-3 ที่ T ถูกเติมด้วย
type std::fs::File เมื่อไฟล์ถูกเปิดสำเร็จ และ E ถูกเติมด้วย type
std::io::Error เมื่อมีปัญหาเปิดไฟล์
เมื่อคุณจับสถานการณ์ในโค้ดของคุณที่มี definition struct หรือ enum หลาย ตัวที่ต่างกันแค่ที่ type ของค่าที่พวกมันเก็บ คุณหลีกเลี่ยงการซ้ำได้โดย ใช้ generic type แทน
ในการประกาศเมธอด
เรา implement เมธอดบน struct และ enum ได้ (อย่างที่เราทำในบทที่ 5) และ
ใช้ generic type ใน definition ของพวกมันด้วย Listing 10-9 แสดง struct
Point<T> ที่เราประกาศใน Listing 10-6 พร้อมเมธอดชื่อ x ที่ implement
บนมัน
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
x บน struct Point<T> ที่จะ return reference ของ field x type Tที่นี่ เราประกาศเมธอดชื่อ x บน Point<T> ที่ return reference ของ
ข้อมูลใน field x
หมายเหตุว่าเราต้องประกาศ T หลัง impl ตรง ๆ เพื่อให้เราใช้ T ระบุ
ว่าเรากำลัง implement เมธอดบน type Point<T> โดยประกาศ T เป็น generic
type หลัง impl Rust ระบุได้ว่า type ใน angle bracket ใน Point เป็น
generic type แทน type คอนกรีต เราเลือกชื่อต่างสำหรับ generic parameter
นี้ที่ต่างจาก generic parameter ที่ประกาศใน definition struct ได้ แต่
ใช้ชื่อเดียวกันเป็น convention ถ้าคุณเขียนเมธอดภายใน impl ที่ประกาศ
generic type เมธอดนั้นจะถูกประกาศบน instance ใด ๆ ของ type ไม่ว่า type
คอนกรีตอะไรลงเอยแทน generic type
เรายังระบุข้อจำกัดบน generic type เมื่อประกาศเมธอดบน type ได้ เช่น เรา
implement เมธอดแค่บน instance Point<f32> แทนบน instance Point<T>
ด้วย generic type ใดก็ได้ ใน Listing 10-10 เราใช้ type คอนกรีต f32
หมายความว่าเราไม่ประกาศ type ใด ๆ หลัง impl
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
impl ที่ใช้แค่กับ struct ที่มี type คอนกรีตเฉพาะสำหรับ generic type parameter Tโค้ดนี้หมายความว่า type Point<f32> จะมีเมธอด distance_from_origin
instance อื่นของ Point<T> ที่ T ไม่ใช่ type f32 จะไม่มีเมธอดนี้
ประกาศ เมธอดวัดว่าจุดของเราอยู่ห่างจากจุดที่พิกัด (0.0, 0.0) เท่าไร และ
ใช้ operation คณิตศาสตร์ที่มีให้แค่สำหรับ type floating-point
Generic type parameter ใน definition struct ไม่ใช่ตัวเดียวกับที่คุณใช้
ใน signature เมธอดของ struct นั้นเสมอ Listing 10-11 ใช้ generic type
X1 และ Y1 สำหรับ struct Point และ X2 และ Y2 สำหรับ signature
เมธอด mixup เพื่อทำให้ตัวอย่างชัดเจนขึ้น เมธอดสร้าง instance Point
ใหม่ด้วยค่า x จาก self Point (type X1) และค่า y จาก Point
ที่ส่งเข้า (type Y2)
struct Point<X1, Y1> {
x: X1,
y: Y1,
}
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
ใน main เราประกาศ Point ที่มี i32 สำหรับ x (ค่า 5) และ f64
สำหรับ y (ค่า 10.4) ตัวแปร p2 เป็น struct Point ที่มี string
slice สำหรับ x (ค่า "Hello") และ char สำหรับ y (ค่า c) การเรียก
mixup บน p1 ด้วย argument p2 ให้เรา p3 ซึ่งจะมี i32 สำหรับ x
เพราะ x มาจาก p1 ตัวแปร p3 จะมี char สำหรับ y เพราะ y มาจาก
p2 การเรียก println! macro จะพิมพ์ p3.x = 5, p3.y = c
จุดประสงค์ของตัวอย่างนี้คือแสดงสถานการณ์ที่ generic parameter บางตัว
ประกาศกับ impl และบางตัวประกาศกับ definition เมธอด ที่นี่ generic
parameter X1 และ Y1 ประกาศหลัง impl เพราะพวกมันไปกับ definition
struct generic parameter X2 และ Y2 ประกาศหลัง fn mixup เพราะพวก
มันเกี่ยวข้องแค่กับเมธอด
Performance ของโค้ดที่ใช้ Generic
คุณอาจสงสัยว่ามีต้นทุน runtime เมื่อใช้ generic type parameter ไหม ข่าวดีคือการใช้ generic type จะไม่ทำให้โปรแกรมของคุณรันช้ากว่าที่จะรัน ด้วย type คอนกรีต
Rust ทำสิ่งนี้สำเร็จโดยทำ monomorphization ของโค้ดที่ใช้ generic ตอน compile time Monomorphization คือกระบวนการเปลี่ยนโค้ด generic เป็น โค้ดเฉพาะโดยเติม type คอนกรีตที่ใช้เมื่อ compile ในกระบวนการนี้ compiler ทำตรงข้ามกับขั้นตอนที่เราใช้สร้าง generic function ใน Listing 10-5 — compiler ดูทุกที่ที่ generic code ถูกเรียก และ generate โค้ดสำหรับ type คอนกรีตที่ generic code ถูกเรียกด้วย
มาดูว่ามันทำงานยังไงโดยใช้ enum Option<T> generic ของ standard library:
#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}
เมื่อ Rust compile โค้ดนี้ มันทำ monomorphization ระหว่างกระบวนการนั้น
compiler อ่านค่าที่ถูกใช้ใน instance Option<T> และระบุสองชนิดของ
Option<T> — หนึ่งคือ i32 และอีกหนึ่งคือ f64 ดังนั้น มันขยาย
definition generic ของ Option<T> เป็นสอง definition พิเศษสำหรับ i32
และ f64 จึงแทน definition generic ด้วยตัวเฉพาะ
version monomorphize ของโค้ดดูคล้ายต่อไปนี้ (compiler ใช้ชื่อต่างจากที่ เราใช้ที่นี่เพื่อแสดง):
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
Option<T> generic ถูกแทนด้วย definition เฉพาะที่ compiler สร้าง เพราะ
Rust compile generic code เป็นโค้ดที่ระบุ type ในแต่ละ instance เราจ่าย
ต้นทุน runtime ไม่มีสำหรับการใช้ generic เมื่อโค้ดรัน มันทำเหมือนเรา
คัดลอกแต่ละ definition ด้วยมือ กระบวนการ monomorphization ทำให้ generic
ของ Rust มีประสิทธิภาพมากตอน runtime
นิยามพฤติกรรมร่วมด้วย trait
นิยามพฤติกรรมร่วมด้วย Trait
trait ประกาศ functionality ที่ type เฉพาะมีและแชร์กับ type อื่นได้ เรา ใช้ trait ประกาศพฤติกรรมร่วมในแบบนามธรรมได้ เราใช้ trait bound ระบุว่า generic type เป็น type ใด ๆ ที่มีพฤติกรรมเฉพาะได้
หมายเหตุ: Trait คล้ายกับฟีเจอร์ที่มักเรียกว่า interface ในภาษาอื่น แม้มีความแตกต่างบางอย่าง
ประกาศ Trait
พฤติกรรมของ type ประกอบด้วยเมธอดที่เราเรียกบน type นั้นได้ Type ต่างกัน แชร์พฤติกรรมเดียวกันถ้าเราเรียกเมธอดเดียวกันบน type เหล่านั้นได้ทั้งหมด Definition trait เป็นวิธีจัดกลุ่ม signature เมธอดเข้าด้วยกัน เพื่อประกาศ ชุดของพฤติกรรมที่จำเป็นเพื่อบรรลุจุดประสงค์บางอย่าง
เช่น สมมติเรามี struct หลายตัวที่เก็บชนิดและปริมาณข้อความต่าง ๆ — struct
NewsArticle ที่เก็บข่าวสารที่ยื่นในตำแหน่งเฉพาะ และ SocialPost ที่
มีอักขระมากที่สุด 280 ตัว พร้อม metadata ที่บ่งบอกว่ามันเป็น post ใหม่,
repost หรือ reply กับ post อื่น
เราอยากทำ library crate aggregator media ชื่อ aggregator ที่แสดงสรุป
ข้อมูลที่อาจเก็บใน instance NewsArticle หรือ SocialPost ในการทำสิ่ง
นี้ เราต้องการสรุปจากแต่ละ type และเราจะขอสรุปนั้นโดยเรียกเมธอด
summarize บน instance Listing 10-12 แสดง definition ของ trait Summary
public ที่แสดงพฤติกรรมนี้
pub trait Summary {
fn summarize(&self) -> String;
}
Summary ที่ประกอบด้วยพฤติกรรมที่ให้โดยเมธอด summarizeที่นี่ เราประกาศ trait โดยใช้ keyword trait แล้วชื่อ trait ซึ่งคือ
Summary ในกรณีนี้ เรายังประกาศ trait เป็น pub เพื่อให้ crate ที่
พึ่ง crate นี้ใช้ trait นี้ได้ด้วย อย่างที่เราจะเห็นในตัวอย่างไม่กี่ตัว
ภายใน curly bracket เราประกาศ signature เมธอดที่บรรยายพฤติกรรมของ type
ที่ implement trait นี้ ซึ่งในกรณีนี้คือ fn summarize(&self) -> String
หลัง signature เมธอด แทนการให้ implementation ภายใน curly bracket เราใช้
semicolon แต่ละ type ที่ implement trait นี้ต้องให้พฤติกรรม custom ของ
ตัวเองสำหรับ body ของเมธอด Compiler จะบังคับใช้ว่า type ใด ๆ ที่มี trait
Summary จะมีเมธอด summarize ประกาศด้วย signature นี้เป๊ะ ๆ
Trait มีเมธอดหลายตัวใน body ได้ — signature เมธอด list หนึ่งตัวต่อบรรทัด และแต่ละบรรทัดจบด้วย semicolon
Implement Trait บน Type
ตอนนี้เราประกาศ signature ที่ต้องการของเมธอดของ trait Summary แล้ว เรา
implement บน type ใน aggregator media ของเราได้ Listing 10-13 แสดง
implementation ของ trait Summary บน struct NewsArticle ที่ใช้ headline,
author และ location สร้าง return value ของ summarize สำหรับ struct
SocialPost เราประกาศ summarize เป็น username ตามด้วย text ทั้งหมดของ
post สมมติว่าเนื้อหา post จำกัดแล้วที่ 280 อักขระ
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Summary บน type NewsArticle และ SocialPostการ implement trait บน type คล้ายกับการ implement เมธอดปกติ ความแตกต่าง
คือหลัง impl เราใส่ชื่อ trait ที่เราอยาก implement แล้วใช้ keyword
for แล้วระบุชื่อ type ที่เราอยาก implement trait ให้ ภายใน block
impl เราใส่ signature เมธอดที่ definition trait ประกาศ แทนการเพิ่ม
semicolon หลังแต่ละ signature เราใช้ curly bracket และเติม body เมธอด
ด้วยพฤติกรรมเฉพาะที่เราอยากให้เมธอดของ trait มีสำหรับ type เฉพาะ
ตอนนี้ library implement trait Summary บน NewsArticle และ
SocialPost แล้ว user ของ crate เรียกเมธอด trait บน instance ของ
NewsArticle และ SocialPost ในแบบเดียวกับที่เราเรียกเมธอดปกติ ความ
แตกต่างเดียวคือ user ต้องนำ trait เข้า scope เช่นเดียวกับ type นี่คือ
ตัวอย่างที่ binary crate ใช้ library crate aggregator ของเรา:
use aggregator::{SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
โค้ดนี้พิมพ์ 1 new post: horse_ebooks: of course, as you probably already know, people
Crate อื่นที่พึ่ง crate aggregator ยังนำ trait Summary เข้า scope
เพื่อ implement Summary บน type ของพวกเขาเองได้ ข้อจำกัดหนึ่งที่ต้อง
หมายเหตุคือ เรา implement trait บน type ได้แค่ถ้า trait หรือ type หรือ
ทั้งคู่ เป็น local ของ crate เรา เช่น เรา implement standard library
trait อย่าง Display บน type custom อย่าง SocialPost ได้ เป็นส่วนของ
functionality crate aggregator ของเรา เพราะ type SocialPost เป็น
local ของ crate aggregator เรา เรายัง implement Summary บน Vec<T>
ใน crate aggregator เราได้ เพราะ trait Summary เป็น local ของ crate
aggregator เรา
แต่เรา implement trait ภายนอกบน type ภายนอกไม่ได้ เช่น เรา implement
trait Display บน Vec<T> ภายใน crate aggregator เราไม่ได้ เพราะ
Display และ Vec<T> ทั้งคู่ประกาศใน standard library และไม่ใช่ local
ของ crate aggregator เรา ข้อจำกัดนี้เป็นส่วนของคุณสมบัติที่เรียกว่า
coherence และเฉพาะกว่า orphan rule ที่ตั้งชื่อแบบนั้นเพราะ type พ่อ
ไม่มี กฎนี้รับประกันว่าโค้ดของคนอื่นทำให้โค้ดของคุณเสียและตรงข้ามไม่ได้
ไม่มีกฎ สอง crate implement trait เดียวกันสำหรับ type เดียวกันได้ และ
Rust ไม่รู้ว่าจะใช้ implementation ไหน
ใช้ Default Implementation
บางครั้งมีประโยชน์ที่จะมีพฤติกรรม default สำหรับเมธอดบางตัวหรือทั้งหมดใน trait แทนการกำหนด implementation สำหรับเมธอดทั้งหมดบนทุก type จากนั้น เมื่อเรา implement trait บน type เฉพาะ เราเก็บหรือ override พฤติกรรม default ของแต่ละเมธอดได้
ใน Listing 10-14 เราระบุ string default สำหรับเมธอด summarize ของ
trait Summary แทนการประกาศแค่ signature เมธอด อย่างที่เราทำใน
Listing 10-12
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Summary ด้วย default implementation ของเมธอด summarizeในการใช้ default implementation สรุป instance ของ NewsArticle เราระบุ
block impl ว่างด้วย impl Summary for NewsArticle {}
แม้เราไม่ประกาศเมธอด summarize บน NewsArticle ตรง ๆ แล้ว เราให้
default implementation และระบุว่า NewsArticle implement trait
Summary ผลคือ เรายังเรียกเมธอด summarize บน instance ของ
NewsArticle ได้ ดังนี้:
use aggregator::{self, NewsArticle, Summary};
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
println!("New article available! {}", article.summarize());
}
โค้ดนี้พิมพ์ New article available! (Read more...)
การสร้าง default implementation ไม่บังคับให้เราเปลี่ยนอะไรเรื่อง
implementation ของ Summary บน SocialPost ใน Listing 10-13 เหตุผลคือ
syntax สำหรับ override default implementation เหมือนกับ syntax สำหรับ
implement เมธอด trait ที่ไม่มี default implementation
Default implementation เรียกเมธอดอื่นใน trait เดียวกันได้ แม้เมธอดอื่น
เหล่านั้นไม่มี default implementation ในวิธีนี้ trait ให้ functionality
ที่มีประโยชน์เยอะและบังคับให้ผู้ implement ระบุแค่ส่วนเล็กของมัน เช่น
เราประกาศ trait Summary ให้มีเมธอด summarize_author ที่บังคับ
implement และจากนั้นประกาศเมธอด summarize ที่มี default implementation
ที่เรียกเมธอด summarize_author:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
ในการใช้ version Summary นี้ เราแค่ต้องประกาศ summarize_author เมื่อ
เรา implement trait บน type:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
หลังเราประกาศ summarize_author เราเรียก summarize บน instance ของ
struct SocialPost ได้ และ default implementation ของ summarize จะ
เรียก definition ของ summarize_author ที่เราให้ เพราะเรา implement
summarize_author trait Summary ให้เราพฤติกรรมของเมธอด summarize
โดยไม่บังคับให้เราเขียนโค้ดเพิ่ม นี่คือสิ่งที่ดูเหมือน:
use aggregator::{self, SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
โค้ดนี้พิมพ์ 1 new post: (Read more from @horse_ebooks...)
หมายเหตุว่าเป็นไปไม่ได้ที่จะเรียก default implementation จาก implementation ที่ override เมธอดเดียวกัน
ใช้ Trait เป็น Parameter
ตอนนี้คุณรู้วิธีประกาศและ implement trait แล้ว เราสำรวจวิธีใช้ trait
ประกาศฟังก์ชันที่รับ type ต่างกันหลายตัวได้ เราจะใช้ trait Summary ที่
เรา implement บน type NewsArticle และ SocialPost ใน Listing 10-13
ประกาศฟังก์ชัน notify ที่เรียกเมธอด summarize บน parameter item
ซึ่งเป็น type บางตัวที่ implement trait Summary ในการทำสิ่งนี้ เราใช้
syntax impl Trait ดังนี้:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
แทน type คอนกรีตสำหรับ parameter item เราระบุ keyword impl และชื่อ
trait Parameter นี้รับ type ใด ๆ ที่ implement trait ที่ระบุ ใน body ของ
notify เราเรียกเมธอดใด ๆ บน item ที่มาจาก trait Summary ได้ เช่น
summarize เราเรียก notify และส่ง instance ใด ๆ ของ NewsArticle
หรือ SocialPost ได้ โค้ดที่เรียกฟังก์ชันด้วย type อื่นใด เช่น String
หรือ i32 จะ compile ไม่ผ่าน เพราะ type เหล่านั้นไม่ implement
Summary
Syntax ของ Trait Bound
syntax impl Trait ทำงานสำหรับกรณีตรงไปตรงมา แต่จริง ๆ เป็น syntax sugar
ของรูปยาวที่รู้จักในชื่อ trait bound ดูแบบนี้:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
รูปยาวกว่านี้เทียบเท่ากับตัวอย่างในส่วนก่อนหน้า แต่ยาวกว่า เราวาง trait bound พร้อมการประกาศ generic type parameter หลัง colon และภายใน angle bracket
syntax impl Trait สะดวกและทำให้โค้ดกระชับขึ้นในกรณีง่าย ขณะที่ syntax
trait bound เต็มแสดงความซับซ้อนได้มากขึ้นในกรณีอื่น เช่น เรามีสอง
parameter ที่ implement Summary ได้ การทำเช่นนั้นด้วย syntax
impl Trait ดูแบบนี้:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
การใช้ impl Trait เหมาะถ้าเราอยากให้ฟังก์ชันนี้อนุญาตให้ item1 และ
item2 มี type ต่างกัน (ตราบที่ทั้งสอง type implement Summary) ถ้าเรา
อยากบังคับให้ทั้งสอง parameter มี type เดียวกัน เราต้องใช้ trait bound
แบบนี้:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
Generic type T ที่ระบุเป็น type ของ parameter item1 และ item2
จำกัดฟังก์ชัน ดังนั้น type คอนกรีตของค่าที่ส่งเป็น argument สำหรับ item1
และ item2 ต้องเหมือนกัน
Trait Bound หลายตัวด้วย Syntax +
เรายังระบุ trait bound มากกว่าหนึ่งตัวได้ สมมติเราอยากให้ notify ใช้
display formatting และ summarize บน item ด้วย เราระบุใน definition
notify ว่า item ต้อง implement ทั้ง Display และ Summary เราทำ
ได้โดยใช้ syntax +:
pub fn notify(item: &(impl Summary + Display)) {
syntax + ยัง valid กับ trait bound บน generic type:
pub fn notify<T: Summary + Display>(item: &T) {
ด้วยสอง trait bound ที่ระบุ body ของ notify เรียก summarize และใช้
{} format item ได้
Trait Bound ที่ชัดเจนขึ้นด้วย Clause where
การใช้ trait bound มากเกินไปมีข้อเสีย แต่ละ generic มี trait bound ของ
ตัวเอง ดังนั้นฟังก์ชันที่มี generic type parameter หลายตัวมีข้อมูล trait
bound มากระหว่างชื่อฟังก์ชันและ list parameter ทำให้ signature ฟังก์ชัน
อ่านยาก ด้วยเหตุนี้ Rust มี syntax ทางเลือกสำหรับระบุ trait bound ภายใน
clause where หลัง signature ฟังก์ชัน ดังนั้น แทนการเขียนแบบนี้:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
เราใช้ clause where ดังนี้:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
unimplemented!()
}
Signature ฟังก์ชันนี้เลอะเทอะน้อยลง — ชื่อฟังก์ชัน, list parameter และ return type ใกล้กัน คล้ายกับฟังก์ชันที่ไม่มี trait bound เยอะ
Return Type ที่ Implement Trait
เรายังใช้ syntax impl Trait ในตำแหน่ง return เพื่อ return ค่าของ type
บางตัวที่ implement trait ดังที่แสดงที่นี่:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
โดยใช้ impl Summary สำหรับ return type เราระบุว่าฟังก์ชัน
returns_summarizable return type บางตัวที่ implement trait Summary
โดยไม่ตั้งชื่อ type คอนกรีต ในกรณีนี้ returns_summarizable return
SocialPost แต่โค้ดที่เรียกฟังก์ชันไม่ต้องรู้
ความสามารถในการระบุ return type แค่ด้วย trait ที่มัน implement มีประโยชน์
เป็นพิเศษในบริบทของ closure และ iterator ซึ่งเราครอบคลุมในบทที่ 13
Closure และ iterator สร้าง type ที่แค่ compiler รู้ หรือ type ที่ยาวมาก
ในการระบุ syntax impl Trait ให้คุณระบุแบบกระชับว่าฟังก์ชัน return type
บางตัวที่ implement trait Iterator โดยไม่ต้องเขียน type ยาวมาก
อย่างไรก็ตาม คุณใช้ impl Trait ได้แค่ถ้าคุณ return type เดียว เช่น
โค้ดนี้ที่ return ทั้ง NewsArticle หรือ SocialPost ที่ระบุ return
type เป็น impl Summary จะไม่ทำงาน:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"Penguins win the Stanley Cup Championship!",
),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
}
} else {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
repost: false,
}
}
}
การ return ทั้ง NewsArticle หรือ SocialPost ไม่อนุญาตเพราะข้อจำกัด
รอบวิธีที่ syntax impl Trait ถูก implement ใน compiler เราจะครอบคลุม
วิธีเขียนฟังก์ชันที่มีพฤติกรรมนี้ในส่วน
“ใช้ Trait Object เพื่อนามธรรมพฤติกรรมร่วม”
ของบทที่ 18
ใช้ Trait Bound Implement เมธอดแบบมีเงื่อนไข
ด้วยการใช้ trait bound กับ block impl ที่ใช้ generic type parameter
เรา implement เมธอดแบบมีเงื่อนไขสำหรับ type ที่ implement trait ที่ระบุ
ได้ เช่น type Pair<T> ใน Listing 10-15 มี implement ฟังก์ชัน new
เสมอ เพื่อ return instance ใหม่ของ Pair<T> (จำจากส่วน
“Method Syntax” ของบทที่ 5 ว่า Self เป็น
type alias สำหรับ type ของ block impl ซึ่งในกรณีนี้คือ Pair<T>) แต่
ใน block impl ถัดไป Pair<T> implement เมธอด cmp_display แค่ถ้า
type ภายใน T implement trait PartialOrd ที่เปิดทางการเปรียบเทียบ
และ trait Display ที่เปิดทางการพิมพ์
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
เรายัง implement trait แบบมีเงื่อนไขสำหรับ type ใด ๆ ที่ implement trait
อื่นได้ Implementation ของ trait บน type ใด ๆ ที่ตรงตาม trait bound
เรียกว่า blanket implementation และใช้กันอย่างกว้างขวางใน Rust
standard library เช่น standard library implement trait ToString บน
type ใด ๆ ที่ implement trait Display Block impl ใน standard library
ดูคล้ายโค้ดนี้:
impl<T: Display> ToString for T {
// --snip--
}
เพราะ standard library มี blanket implementation นี้ เราเรียกเมธอด
to_string ที่ประกาศโดย trait ToString บน type ใด ๆ ที่ implement
trait Display ได้ เช่น เราเปลี่ยน integer เป็นค่า String ที่สอดคล้อง
ของพวกมันแบบนี้ได้ เพราะ integer implement Display:
#![allow(unused)]
fn main() {
let s = 3.to_string();
}
Blanket implementation ปรากฏใน documentation สำหรับ trait ในส่วน “Implementors”
Trait และ trait bound ให้เราเขียนโค้ดที่ใช้ generic type parameter ลด การซ้ำ แต่ยังระบุให้ compiler ว่าเราอยากให้ generic type มีพฤติกรรมเฉพาะ ด้วย Compiler ใช้ข้อมูล trait bound เช็คว่า type คอนกรีตทั้งหมดที่ใช้ กับโค้ดของเราให้พฤติกรรมที่ถูก ในภาษา dynamically typed เราจะได้ error ตอน runtime ถ้าเราเรียกเมธอดบน type ที่ไม่ประกาศเมธอด แต่ Rust ย้าย error เหล่านี้ไป compile time เพื่อให้เราถูกบังคับให้แก้ปัญหาก่อนโค้ด ของเรารันได้ด้วยซ้ำ นอกจากนี้ เราไม่ต้องเขียนโค้ดที่เช็คพฤติกรรมตอน runtime เพราะเราเช็คตอน compile time แล้ว การทำเช่นนั้นปรับปรุง performance โดยไม่ต้องเสียความยืดหยุ่นของ generic
ตรวจสอบ reference ด้วย lifetime
ตรวจสอบ Reference ด้วย Lifetime
Lifetime เป็น generic อีกชนิดที่เราใช้มาแล้ว แทนการรับประกันว่า type มี พฤติกรรมที่เราอยาก lifetime รับประกันว่า reference valid ตราบที่เราต้อง การ
รายละเอียดหนึ่งที่เราไม่พูดถึงในส่วน “Reference และ Borrowing” ใน บทที่ 4 คือทุก reference ใน Rust มี lifetime ซึ่งคือ scope ที่ reference นั้น valid โดยปกติ lifetime เป็น implicit และ infer เช่นเดียวกับโดยปกติ type ถูก infer เราถูกบังคับให้ annotate type แค่เมื่อ type หลายตัวเป็นไป ได้ ในแบบคล้ายกัน เราต้อง annotate lifetime เมื่อ lifetime ของ reference สัมพันธ์กันได้ในไม่กี่แบบ Rust บังคับให้เรา annotate ความสัมพันธ์โดยใช้ generic lifetime parameter เพื่อรับประกันว่า reference จริงที่ใช้ตอน runtime จะ valid แน่นอน
การ annotate lifetime ไม่ใช่แม้แต่แนวคิดที่ภาษาโปรแกรมอื่นส่วนใหญ่มี นี่จึงจะรู้สึกไม่คุ้นเคย แม้เราจะไม่ครอบคลุม lifetime ทั้งหมดในบทนี้ เรา จะพูดถึงวิธีที่ใช้บ่อยที่คุณอาจเจอ syntax lifetime เพื่อให้คุณคุ้นเคยกับ แนวคิด
Dangling Reference
จุดประสงค์หลักของ lifetime คือป้องกัน dangling reference ซึ่ง ถ้าอนุญาต ให้มีอยู่ จะทำให้โปรแกรมอ้างถึงข้อมูลอื่นนอกจากข้อมูลที่ตั้งใจอ้าง พิจารณาโปรแกรมใน Listing 10-16 ซึ่งมี outer scope และ inner scope
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
หมายเหตุ: ตัวอย่างใน Listing 10-16, 10-17 และ 10-23 ประกาศตัวแปรโดยไม่ ให้ค่าเริ่มต้น ดังนั้นชื่อตัวแปรมีอยู่ใน outer scope แวบแรก นี่อาจดู ขัดกับการที่ Rust ไม่มีค่า null อย่างไรก็ตาม ถ้าเราลองใช้ตัวแปรก่อนให้ ค่ามัน เราจะได้ compile-time error ซึ่งแสดงว่า Rust ไม่อนุญาตค่า null จริง
Outer scope ประกาศตัวแปรชื่อ r โดยไม่มีค่าเริ่มต้น และ inner scope
ประกาศตัวแปรชื่อ x ด้วยค่าเริ่มต้น 5 ภายใน inner scope เราพยายาม set
ค่าของ r เป็น reference ของ x จากนั้น inner scope จบ และเราพยายาม
พิมพ์ค่าใน r โค้ดนี้จะ compile ไม่ผ่าน เพราะค่าที่ r อ้างถึงออกจาก
scope ก่อนเราลองใช้ นี่คือ error message:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
5 | let x = 5;
| - binding `x` declared here
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {r}");
| - borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Error message บอกว่าตัวแปร x “ไม่อยู่นานพอ” เหตุผลคือ x จะออกจาก
scope เมื่อ inner scope จบในบรรทัด 7 แต่ r ยัง valid สำหรับ outer
scope เพราะ scope ของมันใหญ่กว่า เราบอกว่ามัน “อยู่นานกว่า” ถ้า Rust
อนุญาตให้โค้ดนี้ทำงาน r จะอ้างถึงหน่วยความจำที่ถูก deallocate เมื่อ
x ออกจาก scope และอะไรที่เราพยายามทำกับ r จะไม่ทำงานถูก แล้ว Rust
กำหนดยังไงว่าโค้ดนี้ invalid? มันใช้ borrow checker
Borrow Checker
Rust compiler มี borrow checker ที่เปรียบเทียบ scope กำหนดว่าการ borrow ทั้งหมด valid ไหม Listing 10-17 แสดงโค้ดเดียวกับ Listing 10-16 แต่มี annotation แสดง lifetime ของตัวแปร
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+
r และ x ชื่อ 'a และ 'b ตามลำดับที่นี่ เรา annotate lifetime ของ r ด้วย 'a และ lifetime ของ x ด้วย
'b อย่างที่คุณเห็น block 'b ภายในเล็กกว่า block lifetime 'a ภายนอก
มาก ตอน compile time Rust เปรียบเทียบขนาดของสอง lifetime และเห็นว่า r
มี lifetime 'a แต่อ้างถึงหน่วยความจำที่มี lifetime 'b โปรแกรมถูก
ปฏิเสธเพราะ 'b สั้นกว่า 'a — เรื่องของ reference ไม่อยู่นานเท่า
reference
Listing 10-18 แก้โค้ดเพื่อให้ไม่มี dangling reference และมัน compile ผ่านโดยไม่มี error
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {r}"); // | |
// --+ |
} // ----------+
ที่นี่ x มี lifetime 'b ซึ่งในกรณีนี้ใหญ่กว่า 'a นี่หมายความว่า
r อ้างถึง x ได้ เพราะ Rust รู้ว่า reference ใน r จะ valid เสมอ
ขณะที่ x valid
ตอนนี้คุณรู้ว่า lifetime ของ reference อยู่ที่ไหน และวิธีที่ Rust วิเคราะห์ lifetime เพื่อรับประกันว่า reference จะ valid เสมอ มาสำรวจ generic lifetime ใน parameter ฟังก์ชันและ return value
Generic Lifetime ในฟังก์ชัน
เราจะเขียนฟังก์ชันที่ return string slice ที่ยาวกว่าของสองตัว ฟังก์ชันนี้
จะรับ string slice สองตัวและ return string slice หนึ่งตัว หลังเรา
implement ฟังก์ชัน longest แล้ว โค้ดใน Listing 10-19 ควรพิมพ์
The longest string is abcd
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
main ที่เรียกฟังก์ชัน longest หา string slice ที่ยาวกว่าของสองตัวหมายเหตุว่าเราอยากให้ฟังก์ชันรับ string slice ซึ่งเป็น reference แทน
string เพราะเราไม่อยากให้ฟังก์ชัน longest รับ ownership ของ parameter
ดู “String Slice เป็น Parameter”
ในบทที่ 4 สำหรับการพูดถึงเพิ่มเติมว่าทำไม parameter ที่เราใช้ใน Listing
10-19 คือตัวที่เราอยากได้
ถ้าเราลอง implement ฟังก์ชัน longest ดังที่แสดงใน Listing 10-20 มันจะ
compile ไม่ผ่าน
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
longest ที่ return string slice ที่ยาวกว่าของสองตัว แต่ยัง compile ไม่ผ่านแทน เราได้ error ต่อไปนี้ที่พูดถึง lifetime:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
ข้อความ help เผยว่า return type ต้อง generic lifetime parameter บนมัน
เพราะ Rust บอกไม่ได้ว่า reference ที่ถูก return อ้างถึง x หรือ y
จริง ๆ เราก็ไม่รู้เหมือนกัน เพราะ block if ใน body ของฟังก์ชันนี้
return reference ของ x และ block else return reference ของ y!
เมื่อเราประกาศฟังก์ชันนี้ เราไม่รู้ค่าคอนกรีตที่จะส่งเข้าฟังก์ชันนี้ เรา
จึงไม่รู้ว่ากรณี if หรือกรณี else จะ execute เรายังไม่รู้ lifetime
คอนกรีตของ reference ที่จะส่งเข้า เราจึงดู scope เหมือนที่เราทำใน
Listing 10-17 และ 10-18 กำหนดว่า reference ที่เรา return จะ valid เสมอ
ไม่ได้ Borrow checker ก็กำหนดไม่ได้ เพราะมันไม่รู้ว่า lifetime ของ x
และ y สัมพันธ์กับ lifetime ของ return value อย่างไร ในการแก้ error นี้
เราจะเพิ่ม generic lifetime parameter ที่ประกาศความสัมพันธ์ระหว่าง
reference เพื่อให้ borrow checker ทำการวิเคราะห์ได้
Syntax ของ Lifetime Annotation
Lifetime annotation ไม่เปลี่ยนนานแค่ไหนที่ reference ใด ๆ อยู่ ตรงข้าม พวกมันบรรยายความสัมพันธ์ของ lifetime ของ reference หลายตัวกับกันโดยไม่ กระทบ lifetime เช่นเดียวกับฟังก์ชันรับ type ใด ๆ ได้เมื่อ signature ระบุ generic type parameter ฟังก์ชันรับ reference ที่มี lifetime ใด ๆ ได้ โดยระบุ generic lifetime parameter
Lifetime annotation มี syntax ที่ผิดปกตินิดหน่อย — ชื่อ lifetime
parameter ต้องขึ้นต้นด้วย apostrophe (') และโดยปกติเป็นตัวพิมพ์เล็ก
ทั้งหมดและสั้นมาก เหมือน generic type คนส่วนใหญ่ใช้ชื่อ 'a สำหรับ
lifetime annotation แรก เราวาง lifetime parameter annotation หลัง &
ของ reference โดยใช้ space คั่น annotation จาก type ของ reference
นี่คือตัวอย่างบางอัน — reference ของ i32 โดยไม่มี lifetime parameter,
reference ของ i32 ที่มี lifetime parameter ชื่อ 'a และ mutable
reference ของ i32 ที่ก็มี lifetime 'a:
&i32 // reference
&'a i32 // reference ที่มี lifetime explicit
&'a mut i32 // mutable reference ที่มี lifetime explicit
Lifetime annotation หนึ่งตัวเองไม่มีความหมายมาก เพราะ annotation ตั้งใจ
บอก Rust ว่า generic lifetime parameter ของ reference หลายตัวสัมพันธ์
กันยังไง มาตรวจสอบว่า lifetime annotation สัมพันธ์กันอย่างไรในบริบทของ
ฟังก์ชัน longest
ใน Signature ฟังก์ชัน
ในการใช้ lifetime annotation ใน signature ฟังก์ชัน เราต้องประกาศ generic lifetime parameter ภายใน angle bracket ระหว่างชื่อฟังก์ชันและ list parameter เช่นเดียวกับที่เราทำกับ generic type parameter
เราอยากให้ signature แสดงข้อจำกัดต่อไปนี้ — reference ที่ return จะ valid
ตราบที่ parameter ทั้งสองตัว valid นี่คือความสัมพันธ์ระหว่าง lifetime
ของ parameter และ return value เราจะตั้งชื่อ lifetime ว่า 'a แล้วเพิ่ม
เข้าแต่ละ reference ดังที่แสดงใน Listing 10-21
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
longest ระบุว่า reference ทั้งหมดใน signature ต้องมี lifetime 'a เดียวกันโค้ดนี้ควร compile และให้ผลที่เราอยากเมื่อเราใช้กับฟังก์ชัน main ใน
Listing 10-19
Signature ฟังก์ชันตอนนี้บอก Rust ว่าสำหรับ lifetime 'a บางตัว ฟังก์ชัน
รับสอง parameter ที่ทั้งคู่เป็น string slice ที่อยู่อย่างน้อยนานเท่า
lifetime 'a Signature ฟังก์ชันยังบอก Rust ว่า string slice ที่ return
จากฟังก์ชันจะอยู่อย่างน้อยนานเท่า lifetime 'a ในทางปฏิบัติ มันหมายความ
ว่า lifetime ของ reference ที่ return โดยฟังก์ชัน longest เหมือนกับ
lifetime ที่เล็กกว่าของ lifetime ของค่าที่อ้างถึงโดย argument ของ
ฟังก์ชัน ความสัมพันธ์เหล่านี้คือสิ่งที่เราอยากให้ Rust ใช้เมื่อวิเคราะห์
โค้ดนี้
จำไว้ เมื่อเราระบุ lifetime parameter ใน signature ฟังก์ชันนี้ เราไม่
เปลี่ยน lifetime ของค่าใด ๆ ที่ส่งเข้าหรือ return ตรงข้าม เรากำลังระบุ
ว่า borrow checker ควรปฏิเสธค่าใด ๆ ที่ไม่ตามข้อจำกัดเหล่านี้ หมายเหตุว่า
ฟังก์ชัน longest ไม่ต้องรู้เป๊ะ ๆ ว่า x และ y จะอยู่นานเท่าไร แค่
ว่า scope บางตัวสามารถแทน 'a ที่จะ satisfy signature นี้
เมื่อ annotate lifetime ในฟังก์ชัน annotation ไปใน signature ฟังก์ชัน ไม่ใช่ใน body ฟังก์ชัน Lifetime annotation กลายเป็นส่วนของสัญญาของ ฟังก์ชัน เหมือน type ใน signature การให้ signature ฟังก์ชันมีสัญญา lifetime หมายความว่าการวิเคราะห์ที่ Rust compiler ทำง่ายกว่า ถ้ามีปัญหา กับวิธีที่ฟังก์ชันถูก annotate หรือวิธีที่มันถูกเรียก compiler error ชี้ ไปยังส่วนของโค้ดเราและข้อจำกัดอย่างแม่นยำกว่า ถ้าตรงข้าม Rust compiler ทำการ infer มากกว่าเรื่องสิ่งที่เราตั้งใจให้ความสัมพันธ์ของ lifetime เป็น compiler อาจชี้ไปยังการใช้โค้ดของเราที่ห่างหลายขั้นจากต้นเหตุของ ปัญหาเท่านั้น
เมื่อเราส่ง reference คอนกรีตให้ longest lifetime คอนกรีตที่แทน 'a
คือส่วนของ scope ของ x ที่ทับซ้อนกับ scope ของ y พูดอีกอย่าง
generic lifetime 'a จะได้ lifetime คอนกรีตที่เท่ากับเล็กกว่าของ
lifetime ของ x และ y เพราะเรา annotate reference ที่ return ด้วย
lifetime parameter 'a เดียวกัน reference ที่ return จะ valid สำหรับ
ความยาวของเล็กกว่าของ lifetime ของ x และ y ด้วย
มาดูว่า lifetime annotation จำกัดฟังก์ชัน longest ยังไง โดยส่ง
reference ที่มี lifetime คอนกรีตต่างกัน Listing 10-22 เป็นตัวอย่างตรงไป
ตรงมา
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {result}");
}
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
longest กับ reference ของค่า String ที่มี lifetime คอนกรีตต่างกันในตัวอย่างนี้ string1 valid จนถึงท้าย outer scope, string2 valid
จนถึงท้าย inner scope และ result อ้างถึงบางอย่างที่ valid จนถึงท้าย
inner scope รันโค้ดนี้และคุณจะเห็นว่า borrow checker อนุมัติ มันจะ
compile และพิมพ์ The longest string is long string is long
ถัดไป ลองตัวอย่างที่แสดงว่า lifetime ของ reference ใน result ต้องเป็น
lifetime เล็กกว่าของสอง argument เราจะย้ายการประกาศตัวแปร result ออก
นอก inner scope แต่ทิ้งการ assign ค่าให้ตัวแปร result ภายใน scope กับ
string2 จากนั้นเราจะย้าย println! ที่ใช้ result ออกนอก inner scope
หลัง inner scope จบ โค้ดใน Listing 10-23 จะ compile ไม่ผ่าน
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
result หลัง string2 ออกจาก scopeเมื่อเราลอง compile โค้ดนี้ เราได้ error นี้:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
5 | let string2 = String::from("xyz");
| ------- binding `string2` declared here
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {result}");
| ------ borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
Error แสดงว่าเพื่อให้ result valid สำหรับ statement println!
string2 จะต้อง valid จนถึงท้าย outer scope Rust รู้เพราะเรา annotate
lifetime ของ parameter ฟังก์ชันและ return value โดยใช้ lifetime
parameter 'a เดียวกัน
ในฐานะมนุษย์ เราดูโค้ดนี้และเห็นว่า string1 ยาวกว่า string2 ดังนั้น
result จะมี reference ของ string1 เพราะ string1 ยังไม่ออกจาก scope
reference ของ string1 จะยัง valid สำหรับ statement println! อย่างไร
ก็ตาม compiler เห็นไม่ได้ว่า reference valid ในกรณีนี้ เราบอก Rust ว่า
lifetime ของ reference ที่ return โดยฟังก์ชัน longest เหมือนกับ
lifetime เล็กกว่าของ reference ที่ส่งเข้า ดังนั้น borrow checker ไม่
อนุญาตโค้ดใน Listing 10-23 ว่าอาจมี reference invalid
ลองออกแบบการทดลองเพิ่มที่แตกต่างค่าและ lifetime ของ reference ที่ส่งเข้า
ฟังก์ชัน longest และวิธีที่ใช้ reference ที่ return ทำสมมติฐานว่าการ
ทดลองของคุณจะผ่าน borrow checker หรือไม่ก่อน compile แล้วเช็คว่าคุณถูก!
ความสัมพันธ์
วิธีที่คุณต้องระบุ lifetime parameter ขึ้นกับสิ่งที่ฟังก์ชันของคุณทำ
เช่น ถ้าเราเปลี่ยน implementation ของฟังก์ชัน longest ให้ return
parameter แรกเสมอ แทน string slice ที่ยาวที่สุด เราไม่ต้องระบุ lifetime
บน parameter y โค้ดต่อไปนี้จะ compile ผ่าน:
fn main() {
let string1 = String::from("abcd");
let string2 = "efghijklmnopqrstuvwxyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
เราระบุ lifetime parameter 'a สำหรับ parameter x และ return type
แต่ไม่สำหรับ parameter y เพราะ lifetime ของ y ไม่มีความสัมพันธ์ใด
กับ lifetime ของ x หรือ return value
เมื่อ return reference จากฟังก์ชัน lifetime parameter สำหรับ return type
ต้อง match lifetime parameter สำหรับ parameter หนึ่งตัว ถ้า reference ที่
return ไม่ อ้างถึง parameter ตัวหนึ่ง มันต้องอ้างถึงค่าที่สร้างภายใน
ฟังก์ชันนี้ อย่างไรก็ตาม นี่จะเป็น dangling reference เพราะค่าจะออกจาก
scope ที่ท้ายฟังก์ชัน พิจารณา implementation ที่ลองของฟังก์ชัน longest
ที่จะ compile ไม่ผ่าน:
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {result}");
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
ที่นี่ แม้เราระบุ lifetime parameter 'a สำหรับ return type implementation
นี้จะ compile ไม่ผ่าน เพราะ lifetime ของ return value ไม่สัมพันธ์กับ
lifetime ของ parameter เลย นี่คือ error message ที่เราได้:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `result` is borrowed here
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error
ปัญหาคือ result ออกจาก scope และถูก cleanup ที่ท้ายฟังก์ชัน longest
เรายังพยายาม return reference ของ result จากฟังก์ชัน ไม่มีวิธีที่เรา
ระบุ lifetime parameter ที่จะเปลี่ยน dangling reference และ Rust จะไม่
ให้เราสร้าง dangling reference ในกรณีนี้ การแก้ดีที่สุดคือ return type
ข้อมูลที่ owned แทน reference เพื่อให้ฟังก์ชันที่เรียกรับผิดชอบในการ
cleanup ค่า
สุดท้าย syntax lifetime เกี่ยวกับการเชื่อม lifetime ของ parameter และ return value ต่าง ๆ ของฟังก์ชัน เมื่อเชื่อมกัน Rust มีข้อมูลพอที่จะ อนุญาต operation memory-safe และไม่อนุญาต operation ที่จะสร้าง dangling pointer หรือฝ่าฝืน memory safety
ใน Definition Struct
ที่ผ่านมา struct ที่เราประกาศทั้งหมดเก็บ type ที่ owned เราประกาศ struct
ให้เก็บ reference ได้ แต่ในกรณีนั้น เราต้องเพิ่ม lifetime annotation
บนทุก reference ใน definition struct Listing 10-24 มี struct ชื่อ
ImportantExcerpt ที่เก็บ string slice
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
struct นี้มี field เดียว part ที่เก็บ string slice ซึ่งเป็น reference
เช่นเดียวกับ generic data type เราประกาศชื่อ generic lifetime parameter
ภายใน angle bracket หลังชื่อ struct เพื่อให้เราใช้ lifetime parameter
ใน body ของ definition struct annotation นี้หมายความว่า instance ของ
ImportantExcerpt outlive reference ที่มันเก็บใน field part ไม่ได้
ฟังก์ชัน main ที่นี่สร้าง instance ของ struct ImportantExcerpt ที่
เก็บ reference ของประโยคแรกของ String ที่ owned โดยตัวแปร novel
ข้อมูลใน novel มีอยู่ก่อน instance ImportantExcerpt ถูกสร้าง นอกจาก
นี้ novel ไม่ออกจาก scope จนกว่าหลัง ImportantExcerpt ออกจาก scope
ดังนั้น reference ใน instance ImportantExcerpt valid
Lifetime Elision
คุณได้เรียนว่าทุก reference มี lifetime และคุณต้องระบุ lifetime parameter สำหรับฟังก์ชันหรือ struct ที่ใช้ reference อย่างไรก็ตาม เรามีฟังก์ชันใน Listing 4-9 แสดงอีกครั้งใน Listing 10-25 ที่ compile ผ่านโดยไม่มี lifetime annotation
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
fn main() {
let my_string = String::from("hello world");
// first_word works on slices of `String`s
let word = first_word(&my_string[..]);
let my_string_literal = "hello world";
// first_word works on slices of string literals
let word = first_word(&my_string_literal[..]);
// Because string literals *are* string slices already,
// this works too, without the slice syntax!
let word = first_word(my_string_literal);
}
เหตุผลที่ฟังก์ชันนี้ compile ผ่านโดยไม่มี lifetime annotation เป็น ประวัติศาสตร์ — ใน version ก่อน 1.0 ของ Rust โค้ดนี้จะ compile ไม่ผ่าน เพราะทุก reference ต้อง lifetime explicit ในเวลานั้น signature ฟังก์ชัน จะถูกเขียนแบบนี้:
fn first_word<'a>(s: &'a str) -> &'a str {
หลังเขียนโค้ด Rust เยอะ ทีม Rust พบว่าโปรแกรมเมอร์ Rust กำลังป้อน lifetime annotation เดียวกันซ้ำ ๆ ในสถานการณ์เฉพาะ สถานการณ์เหล่านี้คาด เดาได้และตาม pattern กำหนดได้ไม่กี่ตัว นักพัฒนา program pattern เหล่านี้ เข้าโค้ดของ compiler เพื่อให้ borrow checker infer lifetime ในสถานการณ์ เหล่านี้ และไม่ต้องการ annotation explicit
ประวัติ Rust ชิ้นนี้เกี่ยวข้องเพราะเป็นไปได้ที่ pattern กำหนดได้มากขึ้น จะเกิดและถูกเพิ่มเข้า compiler ในอนาคต อาจต้องการ lifetime annotation น้อยลงอีก
Pattern ที่ program เข้าการวิเคราะห์ reference ของ Rust เรียกว่า lifetime elision rules เหล่านี้ไม่ใช่กฎสำหรับโปรแกรมเมอร์ตาม — พวกมัน เป็นชุดของกรณีเฉพาะที่ compiler จะพิจารณา และถ้าโค้ดของคุณตรงกับกรณีเหล่า นี้ คุณไม่ต้องเขียน lifetime explicit
Elision rules ไม่ให้การ infer เต็ม ถ้ายังมีความกำกวมเรื่อง lifetime ที่ reference มี หลัง Rust ใช้กฎ compiler จะไม่เดาว่า lifetime ของ reference ที่เหลือควรเป็น แทนการเดา compiler จะให้ error ที่คุณแก้ได้โดยเพิ่ม lifetime annotation
Lifetime บน parameter ฟังก์ชันหรือเมธอดเรียกว่า input lifetime และ lifetime บน return value เรียกว่า output lifetime
Compiler ใช้สามกฎหา lifetime ของ reference เมื่อไม่มี annotation
explicit กฎแรกใช้กับ input lifetime และกฎที่สองและสามใช้กับ output
lifetime ถ้า compiler ถึงท้ายของสามกฎและยังมี reference ที่หา lifetime
ไม่ได้ compiler จะหยุดด้วย error กฎเหล่านี้ใช้กับ definition fn และ
block impl
กฎแรกคือ compiler assign lifetime parameter ให้แต่ละ parameter ที่เป็น
reference พูดอีกอย่าง ฟังก์ชันที่มี parameter หนึ่งตัวได้ lifetime
parameter หนึ่งตัว: fn foo<'a>(x: &'a i32); ฟังก์ชันที่มี parameter
สองตัวได้ lifetime parameter แยกสองตัว: fn foo<'a, 'b>(x: &'a i32, y: &'b i32); และต่อไป
กฎที่สองคือ ถ้ามี input lifetime parameter เดียวเป๊ะ ๆ lifetime นั้นถูก
assign ให้ output lifetime parameter ทั้งหมด: fn foo<'a>(x: &'a i32) -> &'a i32
กฎที่สามคือ ถ้ามี input lifetime parameter หลายตัว แต่หนึ่งในนั้นเป็น
&self หรือ &mut self เพราะนี่คือเมธอด lifetime ของ self ถูก
assign ให้ output lifetime parameter ทั้งหมด กฎที่สามนี้ทำให้เมธอดอ่าน
และเขียนได้ดีกว่ามาก เพราะต้องการสัญลักษณ์น้อยกว่า
ลองสมมติเราเป็น compiler เราจะใช้กฎเหล่านี้หา lifetime ของ reference ใน
signature ของฟังก์ชัน first_word ใน Listing 10-25 Signature เริ่มโดย
ไม่มี lifetime ผูกกับ reference:
fn first_word(s: &str) -> &str {
จากนั้น compiler ใช้กฎแรก ซึ่งระบุว่าแต่ละ parameter ได้ lifetime ของ
ตัวเอง เราจะเรียก 'a ตามปกติ ดังนั้นตอนนี้ signature เป็นนี้:
fn first_word<'a>(s: &'a str) -> &str {
กฎที่สองใช้เพราะมี input lifetime เดียวเป๊ะ ๆ กฎที่สองระบุว่า lifetime ของ input parameter หนึ่งตัวถูก assign ให้ output lifetime ดังนั้น signature เป็นนี้ตอนนี้:
fn first_word<'a>(s: &'a str) -> &'a str {
ตอนนี้ reference ทั้งหมดใน signature ฟังก์ชันนี้มี lifetime และ compiler ดำเนินการวิเคราะห์ต่อโดยไม่ต้องการให้โปรแกรมเมอร์ annotate lifetime ใน signature ฟังก์ชันนี้
มาดูตัวอย่างอีก ครั้งนี้ใช้ฟังก์ชัน longest ที่ไม่มี lifetime parameter
เมื่อเราเริ่มทำงานกับมันใน Listing 10-20:
fn longest(x: &str, y: &str) -> &str {
มาใช้กฎแรก — แต่ละ parameter ได้ lifetime ของตัวเอง ครั้งนี้เรามีสอง parameter แทนหนึ่ง เราจึงมีสอง lifetime:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
คุณเห็นว่ากฎที่สองไม่ใช้ เพราะมี input lifetime มากกว่าหนึ่งตัว กฎที่สาม
ก็ไม่ใช้ เพราะ longest เป็นฟังก์ชันแทนเมธอด parameter ไม่มีตัวไหนเป็น
self หลังทำงานผ่านสามกฎทั้งหมด เรายังไม่หา lifetime ของ return type
ได้ นี่คือเหตุผลที่เราได้ error พยายาม compile โค้ดใน Listing 10-20 —
compiler ทำงานผ่าน lifetime elision rules แต่ยังหา lifetime ของ
reference ใน signature ทั้งหมดไม่ได้
เพราะกฎที่สามจริง ๆ ใช้แค่ใน signature เมธอด เราจะดู lifetime ในบริบทนั้น ถัดไป เพื่อดูว่าทำไมกฎที่สามหมายความว่าเราไม่ต้อง annotate lifetime ใน signature เมธอดบ่อย
ใน Definition เมธอด
เมื่อเรา implement เมธอดบน struct ที่มี lifetime เราใช้ syntax เดียวกัน กับของ generic type parameter ดังที่แสดงใน Listing 10-11 ตำแหน่งที่เรา ประกาศและใช้ lifetime parameter ขึ้นกับว่าพวกมันสัมพันธ์กับ field struct หรือ parameter เมธอดและ return value
ชื่อ lifetime สำหรับ field struct ต้องประกาศหลัง keyword impl เสมอ และ
ใช้หลังชื่อ struct เพราะ lifetime เหล่านั้นเป็นส่วนของ type ของ struct
ใน signature เมธอดภายใน block impl reference อาจผูกกับ lifetime ของ
reference ใน field struct หรืออาจอิสระ นอกจากนี้ lifetime elision rules
มักทำให้ lifetime annotation ไม่จำเป็นใน signature เมธอด มาดูตัวอย่าง
บางตัวที่ใช้ struct ชื่อ ImportantExcerpt ที่เราประกาศใน Listing 10-24
ก่อนอื่น เราจะใช้เมธอดชื่อ level ที่ parameter เดียวคือ reference ของ
self และ return value คือ i32 ซึ่งไม่ใช่ reference ของอะไร:
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
การประกาศ lifetime parameter หลัง impl และการใช้หลังชื่อ type บังคับ
แต่เพราะกฎ elision แรก เราไม่ต้อง annotate lifetime ของ reference ของ
self
นี่คือตัวอย่างที่ lifetime elision rule ที่สามใช้:
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let i = ImportantExcerpt {
part: first_sentence,
};
}
มีสอง input lifetime Rust จึงใช้ lifetime elision rule แรก และให้ทั้ง
&self และ announcement lifetime ของตัวเอง จากนั้น เพราะ parameter
ตัวหนึ่งเป็น &self return type ได้ lifetime ของ &self และ lifetime
ทั้งหมดถูกบัญชี
Static Lifetime
Lifetime พิเศษหนึ่งที่เราต้องพูดถึงคือ 'static ซึ่งบ่งบอกว่า reference
ที่ได้รับผลกระทบ สามารถ อยู่ตลอดระยะเวลาของโปรแกรม String literal
ทั้งหมดมี lifetime 'static ซึ่งเรา annotate ดังนี้:
#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}
ข้อความของ string นี้ถูกเก็บโดยตรงใน binary ของโปรแกรม ซึ่งมีให้เสมอ
ดังนั้น lifetime ของ string literal ทั้งหมดเป็น 'static
คุณอาจเห็นคำแนะนำใน error message ให้ใช้ lifetime 'static แต่ก่อนระบุ
'static เป็น lifetime สำหรับ reference คิดว่า reference ที่คุณมีจริง ๆ
อยู่ตลอด lifetime ของโปรแกรมของคุณไหม และคุณอยากให้มันเป็นแบบนั้นไหม
โดยส่วนใหญ่ error message ที่แนะนำ lifetime 'static เป็นผลของการ
พยายามสร้าง dangling reference หรือ mismatch ของ lifetime ที่มี ในกรณี
เช่นนั้น คำตอบคือแก้ปัญหาเหล่านั้น ไม่ใช่ระบุ lifetime 'static
Generic Type Parameter, Trait Bound และ Lifetime
มาดูสั้น ๆ ที่ syntax ของการระบุ generic type parameter, trait bound และ lifetime ทั้งหมดในฟังก์ชันเดียว!
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest_with_an_announcement(
string1.as_str(),
string2,
"Today is someone's birthday!",
);
println!("The longest string is {result}");
}
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {ann}");
if x.len() > y.len() { x } else { y }
}
นี่คือฟังก์ชัน longest จาก Listing 10-21 ที่ return string slice ที่
ยาวกว่าของสองตัว แต่ตอนนี้มี parameter เพิ่มชื่อ ann ของ generic type
T ซึ่งเติมได้ด้วย type ใด ๆ ที่ implement trait Display ตามที่ระบุ
โดย clause where parameter เพิ่มนี้จะถูกพิมพ์โดยใช้ {} นี่คือเหตุผล
ที่ trait bound Display จำเป็น เพราะ lifetime เป็น type ของ generic
การประกาศ lifetime parameter 'a และ generic type parameter T ไปใน
list เดียวกันภายใน angle bracket หลังชื่อฟังก์ชัน
สรุป
เราครอบคลุมเยอะในบทนี้! ตอนนี้คุณรู้เรื่อง generic type parameter, trait และ trait bound และ generic lifetime parameter คุณพร้อมเขียนโค้ดโดยไม่ มีการซ้ำที่ทำงานในสถานการณ์ต่างกันหลายตัว Generic type parameter ให้คุณ ใช้โค้ดกับ type ต่างกัน Trait และ trait bound รับประกันว่าแม้ type เป็น generic พวกมันจะมีพฤติกรรมที่โค้ดต้องการ คุณได้เรียนวิธีใช้ lifetime annotation รับประกันว่าโค้ดยืดหยุ่นนี้จะไม่มี dangling reference และการ วิเคราะห์ทั้งหมดนี้เกิดตอน compile time ซึ่งไม่กระทบ performance ตอน runtime!
เชื่อหรือไม่ มีอีกมากให้เรียนเรื่องหัวข้อที่เราพูดถึงในบทนี้ — บทที่ 18 พูดถึง trait object ซึ่งเป็นอีกวิธีใช้ trait ยังมี scenario ซับซ้อนกว่า ที่เกี่ยวกับ lifetime annotation ที่คุณจะต้องการแค่ใน scenario ขั้นสูง มาก สำหรับเหล่านั้น คุณควรอ่าน Rust Reference แต่ถัดไป คุณ จะเรียนวิธีเขียนเทสใน Rust เพื่อให้แน่ใจว่าโค้ดของคุณทำงานในแบบที่ควร
Writing Automated Tests
In his 1972 essay “The Humble Programmer,” Edsger W. Dijkstra said that “program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.” That doesn’t mean we shouldn’t try to test as much as we can!
Correctness in our programs is the extent to which our code does what we intend it to do. Rust is designed with a high degree of concern about the correctness of programs, but correctness is complex and not easy to prove. Rust’s type system shoulders a huge part of this burden, but the type system cannot catch everything. As such, Rust includes support for writing automated software tests.
Say we write a function add_two that adds 2 to whatever number is passed to
it. This function’s signature accepts an integer as a parameter and returns an
integer as a result. When we implement and compile that function, Rust does all
the type checking and borrow checking that you’ve learned so far to ensure
that, for instance, we aren’t passing a String value or an invalid reference
to this function. But Rust can’t check that this function will do precisely
what we intend, which is return the parameter plus 2 rather than, say, the
parameter plus 10 or the parameter minus 50! That’s where tests come in.
We can write tests that assert, for example, that when we pass 3 to the
add_two function, the returned value is 5. We can run these tests whenever
we make changes to our code to make sure any existing correct behavior has not
changed.
Testing is a complex skill: Although we can’t cover in one chapter every detail about how to write good tests, in this chapter we will discuss the mechanics of Rust’s testing facilities. We’ll talk about the annotations and macros available to you when writing your tests, the default behavior and options provided for running your tests, and how to organize tests into unit tests and integration tests.
วิธีเขียนเทส
How to Write Tests
Tests are Rust functions that verify that the non-test code is functioning in the expected manner. The bodies of test functions typically perform these three actions:
- Set up any needed data or state.
- Run the code you want to test.
- Assert that the results are what you expect.
Let’s look at the features Rust provides specifically for writing tests that
take these actions, which include the test attribute, a few macros, and the
should_panic attribute.
Structuring Test Functions
At its simplest, a test in Rust is a function that’s annotated with the test
attribute. Attributes are metadata about pieces of Rust code; one example is
the derive attribute we used with structs in Chapter 5. To change a function
into a test function, add #[test] on the line before fn. When you run your
tests with the cargo test command, Rust builds a test runner binary that runs
the annotated functions and reports on whether each test function passes or
fails.
Whenever we make a new library project with Cargo, a test module with a test function in it is automatically generated for us. This module gives you a template for writing your tests so that you don’t have to look up the exact structure and syntax every time you start a new project. You can add as many additional test functions and as many test modules as you want!
We’ll explore some aspects of how tests work by experimenting with the template test before we actually test any code. Then, we’ll write some real-world tests that call some code that we’ve written and assert that its behavior is correct.
Let’s create a new library project called adder that will add two numbers:
$ cargo new adder --lib
Created library `adder` project
$ cd adder
The contents of the src/lib.rs file in your adder library should look like
Listing 11-1.
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
cargo newThe file starts with an example add function so that we have something to
test.
For now, let’s focus solely on the it_works function. Note the #[test]
annotation: This attribute indicates this is a test function, so the test
runner knows to treat this function as a test. We might also have non-test
functions in the tests module to help set up common scenarios or perform
common operations, so we always need to indicate which functions are tests.
The example function body uses the assert_eq! macro to assert that result,
which contains the result of calling add with 2 and 2, equals 4. This
assertion serves as an example of the format for a typical test. Let’s run it
to see that this test passes.
The cargo test command runs all tests in our project, as shown in Listing
11-2.
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.57s
Running unittests src/lib.rs (target/debug/deps/adder-01ad14159ff659ab)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Cargo compiled and ran the test. We see the line running 1 test. The next
line shows the name of the generated test function, called tests::it_works,
and that the result of running that test is ok. The overall summary test result: ok. means that all the tests passed, and the portion that reads 1 passed; 0 failed totals the number of tests that passed or failed.
It’s possible to mark a test as ignored so that it doesn’t run in a particular
instance; we’ll cover that in the “Ignoring Tests Unless Specifically
Requested” section later in this chapter. Because we
haven’t done that here, the summary shows 0 ignored. We can also pass an
argument to the cargo test command to run only tests whose name matches a
string; this is called filtering, and we’ll cover it in the “Running a
Subset of Tests by Name” section. Here, we haven’t
filtered the tests being run, so the end of the summary shows 0 filtered out.
The 0 measured statistic is for benchmark tests that measure performance.
Benchmark tests are, as of this writing, only available in nightly Rust. See
the documentation about benchmark tests to learn more.
The next part of the test output starting at Doc-tests adder is for the
results of any documentation tests. We don’t have any documentation tests yet,
but Rust can compile any code examples that appear in our API documentation.
This feature helps keep your docs and your code in sync! We’ll discuss how to
write documentation tests in the “Documentation Comments as
Tests” section of Chapter 14. For now, we’ll
ignore the Doc-tests output.
Let’s start to customize the test to our own needs. First, change the name of
the it_works function to a different name, such as exploration, like so:
Filename: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
Then, run cargo test again. The output now shows exploration instead of
it_works:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.59s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::exploration ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Now we’ll add another test, but this time we’ll make a test that fails! Tests
fail when something in the test function panics. Each test is run in a new
thread, and when the main thread sees that a test thread has died, the test is
marked as failed. In Chapter 9, we talked about how the simplest way to panic
is to call the panic! macro. Enter the new test as a function named
another, so your src/lib.rs file looks like Listing 11-3.
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exploration() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
fn another() {
panic!("Make this test fail");
}
}
panic! macroRun the tests again using cargo test. The output should look like Listing
11-4, which shows that our exploration test passed and another failed.
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.72s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::another ... FAILED
test tests::exploration ... ok
failures:
---- tests::another stdout ----
thread 'tests::another' panicked at src/lib.rs:17:9:
Make this test fail
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::another
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Instead of ok, the line test tests::another shows FAILED. Two new
sections appear between the individual results and the summary: The first
displays the detailed reason for each test failure. In this case, we get the
details that tests::another failed because it panicked with the message Make this test fail on line 17 in the src/lib.rs file. The next section lists
just the names of all the failing tests, which is useful when there are lots of
tests and lots of detailed failing test output. We can use the name of a
failing test to run just that test to debug it more easily; we’ll talk more
about ways to run tests in the “Controlling How Tests Are
Run” section.
The summary line displays at the end: Overall, our test result is FAILED. We
had one test pass and one test fail.
Now that you’ve seen what the test results look like in different scenarios,
let’s look at some macros other than panic! that are useful in tests.
Checking Results with assert!
The assert! macro, provided by the standard library, is useful when you want
to ensure that some condition in a test evaluates to true. We give the
assert! macro an argument that evaluates to a Boolean. If the value is
true, nothing happens and the test passes. If the value is false, the
assert! macro calls panic! to cause the test to fail. Using the assert!
macro helps us check that our code is functioning in the way we intend.
In Chapter 5, Listing 5-15, we used a Rectangle struct and a can_hold
method, which are repeated here in Listing 11-5. Let’s put this code in the
src/lib.rs file, then write some tests for it using the assert! macro.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
Rectangle struct and its can_hold method from Chapter 5The can_hold method returns a Boolean, which means it’s a perfect use case
for the assert! macro. In Listing 11-6, we write a test that exercises the
can_hold method by creating a Rectangle instance that has a width of 8 and
a height of 7 and asserting that it can hold another Rectangle instance that
has a width of 5 and a height of 1.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
}
can_hold that checks whether a larger rectangle can indeed hold a smaller rectangleNote the use super::*; line inside the tests module. The tests module is
a regular module that follows the usual visibility rules we covered in Chapter
7 in the “Paths for Referring to an Item in the Module
Tree”
section. Because the tests module is an inner module, we need to bring the
code under test in the outer module into the scope of the inner module. We use
a glob here, so anything we define in the outer module is available to this
tests module.
We’ve named our test larger_can_hold_smaller, and we’ve created the two
Rectangle instances that we need. Then, we called the assert! macro and
passed it the result of calling larger.can_hold(&smaller). This expression is
supposed to return true, so our test should pass. Let’s find out!
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 1 test
test tests::larger_can_hold_smaller ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
It does pass! Let’s add another test, this time asserting that a smaller rectangle cannot hold a larger rectangle:
Filename: src/lib.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
// --snip--
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
Because the correct result of the can_hold function in this case is false,
we need to negate that result before we pass it to the assert! macro. As a
result, our test will pass if can_hold returns false:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... ok
test tests::smaller_cannot_hold_larger ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests rectangle
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Two tests that pass! Now let’s see what happens to our test results when we
introduce a bug in our code. We’ll change the implementation of the can_hold
method by replacing the greater-than sign (>) with a less-than sign (<)
when it compares the widths:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
// --snip--
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width < other.width && self.height > other.height
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&smaller));
}
#[test]
fn smaller_cannot_hold_larger() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(!smaller.can_hold(&larger));
}
}
Running the tests now produces the following:
$ cargo test
Compiling rectangle v0.1.0 (file:///projects/rectangle)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e)
running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... ok
failures:
---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' panicked at src/lib.rs:28:9:
assertion failed: larger.can_hold(&smaller)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::larger_can_hold_smaller
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Our tests caught the bug! Because larger.width is 8 and smaller.width is
5, the comparison of the widths in can_hold now returns false: 8 is not
less than 5.
Testing Equality with assert_eq! and assert_ne!
A common way to verify functionality is to test for equality between the result
of the code under test and the value you expect the code to return. You could
do this by using the assert! macro and passing it an expression using the
== operator. However, this is such a common test that the standard library
provides a pair of macros—assert_eq! and assert_ne!—to perform this test
more conveniently. These macros compare two arguments for equality or
inequality, respectively. They’ll also print the two values if the assertion
fails, which makes it easier to see why the test failed; conversely, the
assert! macro only indicates that it got a false value for the ==
expression, without printing the values that led to the false value.
In Listing 11-7, we write a function named add_two that adds 2 to its
parameter, and then we test this function using the assert_eq! macro.
pub fn add_two(a: u64) -> u64 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
add_two using the assert_eq! macroLet’s check that it passes!
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
We create a variable named result that holds the result of calling
add_two(2). Then, we pass result and 4 as the arguments to the
assert_eq! macro. The output line for this test is test tests::it_adds_two ... ok, and the ok text indicates that our test passed!
Let’s introduce a bug into our code to see what assert_eq! looks like when it
fails. Change the implementation of the add_two function to instead add 3:
pub fn add_two(a: u64) -> u64 {
a + 3
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
}
Run the tests again:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::it_adds_two ... FAILED
failures:
---- tests::it_adds_two stdout ----
thread 'tests::it_adds_two' panicked at src/lib.rs:12:9:
assertion `left == right` failed
left: 5
right: 4
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_adds_two
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Our test caught the bug! The tests::it_adds_two test failed, and the message
tells us that the assertion that failed was left == right and what the left
and right values are. This message helps us start debugging: The left
argument, where we had the result of calling add_two(2), was 5, but the
right argument was 4. You can imagine that this would be especially helpful
when we have a lot of tests going on.
Note that in some languages and test frameworks, the parameters to equality
assertion functions are called expected and actual, and the order in which
we specify the arguments matters. However, in Rust, they’re called left and
right, and the order in which we specify the value we expect and the value
the code produces doesn’t matter. We could write the assertion in this test as
assert_eq!(4, result), which would result in the same failure message that
displays assertion `left == right` failed.
The assert_ne! macro will pass if the two values we give it are not equal and
will fail if they are equal. This macro is most useful for cases when we’re not
sure what a value will be, but we know what the value definitely shouldn’t
be. For example, if we’re testing a function that is guaranteed to change its
input in some way, but the way in which the input is changed depends on the day
of the week that we run our tests, the best thing to assert might be that the
output of the function is not equal to the input.
Under the surface, the assert_eq! and assert_ne! macros use the operators
== and !=, respectively. When the assertions fail, these macros print their
arguments using debug formatting, which means the values being compared must
implement the PartialEq and Debug traits. All primitive types and most of
the standard library types implement these traits. For structs and enums that
you define yourself, you’ll need to implement PartialEq to assert equality of
those types. You’ll also need to implement Debug to print the values when the
assertion fails. Because both traits are derivable traits, as mentioned in
Listing 5-12 in Chapter 5, this is usually as straightforward as adding the
#[derive(PartialEq, Debug)] annotation to your struct or enum definition. See
Appendix C, “Derivable Traits,” for more
details about these and other derivable traits.
Adding Custom Failure Messages
You can also add a custom message to be printed with the failure message as
optional arguments to the assert!, assert_eq!, and assert_ne! macros. Any
arguments specified after the required arguments are passed along to the
format! macro (discussed in “Concatenating with + or
format!” in Chapter 8), so you can pass a format string that contains {}
placeholders and values to go in those placeholders. Custom messages are useful
for documenting what an assertion means; when a test fails, you’ll have a better
idea of what the problem is with the code.
For example, let’s say we have a function that greets people by name and we want to test that the name we pass into the function appears in the output:
Filename: src/lib.rs
pub fn greeting(name: &str) -> String {
format!("Hello {name}!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
The requirements for this program haven’t been agreed upon yet, and we’re
pretty sure the Hello text at the beginning of the greeting will change. We
decided we don’t want to have to update the test when the requirements change,
so instead of checking for exact equality to the value returned from the
greeting function, we’ll just assert that the output contains the text of the
input parameter.
Now let’s introduce a bug into this code by changing greeting to exclude
name to see what the default test failure looks like:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(result.contains("Carol"));
}
}
Running this test produces the following:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
assertion failed: result.contains("Carol")
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
This result just indicates that the assertion failed and which line the
assertion is on. A more useful failure message would print the value from the
greeting function. Let’s add a custom failure message composed of a format
string with a placeholder filled in with the actual value we got from the
greeting function:
pub fn greeting(name: &str) -> String {
String::from("Hello!")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{result}`"
);
}
}
Now when we run the test, we’ll get a more informative error message:
$ cargo test
Compiling greeter v0.1.0 (file:///projects/greeter)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.93s
Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a)
running 1 test
test tests::greeting_contains_name ... FAILED
failures:
---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at src/lib.rs:12:9:
Greeting did not contain name, value was `Hello!`
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::greeting_contains_name
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
We can see the value we actually got in the test output, which would help us debug what happened instead of what we were expecting to happen.
Checking for Panics with should_panic
In addition to checking return values, it’s important to check that our code
handles error conditions as we expect. For example, consider the Guess type
that we created in Chapter 9, Listing 9-13. Other code that uses Guess
depends on the guarantee that Guess instances will contain only values
between 1 and 100. We can write a test that ensures that attempting to create a
Guess instance with a value outside that range panics.
We do this by adding the attribute should_panic to our test function. The
test passes if the code inside the function panics; the test fails if the code
inside the function doesn’t panic.
Listing 11-8 shows a test that checks that the error conditions of Guess::new
happen when we expect them to.
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
panic!We place the #[should_panic] attribute after the #[test] attribute and
before the test function it applies to. Let’s look at the result when this test
passes:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests guessing_game
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Looks good! Now let’s introduce a bug in our code by removing the condition
that the new function will panic if the value is greater than 100:
pub struct Guess {
value: i32,
}
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!("Guess value must be between 1 and 100, got {value}.");
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic]
fn greater_than_100() {
Guess::new(200);
}
}
When we run the test in Listing 11-8, it will fail:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected at src/lib.rs:21:8
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
We don’t get a very helpful message in this case, but when we look at the test
function, we see that it’s annotated with #[should_panic]. The failure we got
means that the code in the test function did not cause a panic.
Tests that use should_panic can be imprecise. A should_panic test would
pass even if the test panics for a different reason from the one we were
expecting. To make should_panic tests more precise, we can add an optional
expected parameter to the should_panic attribute. The test harness will
make sure that the failure message contains the provided text. For example,
consider the modified code for Guess in Listing 11-9 where the new function
panics with different messages depending on whether the value is too small or
too large.
pub struct Guess {
value: i32,
}
// --snip--
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
panic! with a panic message containing a specified substringThis test will pass because the value we put in the should_panic attribute’s
expected parameter is a substring of the message that the Guess::new
function panics with. We could have specified the entire panic message that we
expect, which in this case would be Guess value must be less than or equal to 100, got 200. What you choose to specify depends on how much of the panic
message is unique or dynamic and how precise you want your test to be. In this
case, a substring of the panic message is enough to ensure that the code in the
test function executes the else if value > 100 case.
To see what happens when a should_panic test with an expected message
fails, let’s again introduce a bug into our code by swapping the bodies of the
if value < 1 and the else if value > 100 blocks:
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 {
panic!(
"Guess value must be less than or equal to 100, got {value}."
);
} else if value > 100 {
panic!(
"Guess value must be greater than or equal to 1, got {value}."
);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "less than or equal to 100")]
fn greater_than_100() {
Guess::new(200);
}
}
This time when we run the should_panic test, it will fail:
$ cargo test
Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.66s
Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d)
running 1 test
test tests::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
thread 'tests::greater_than_100' panicked at src/lib.rs:12:13:
Guess value must be greater than or equal to 1, got 200.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
note: panic did not contain expected string
panic message: "Guess value must be greater than or equal to 1, got 200."
expected substring: "less than or equal to 100"
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
The failure message indicates that this test did indeed panic as we expected,
but the panic message did not include the expected string less than or equal to 100. The panic message that we did get in this case was Guess value must be greater than or equal to 1, got 200. Now we can start figuring out where
our bug is!
Using Result<T, E> in Tests
All of our tests so far panic when they fail. We can also write tests that use
Result<T, E>! Here’s the test from Listing 11-1, rewritten to use Result<T, E> and return an Err instead of panicking:
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() -> Result<(), String> {
let result = add(2, 2);
if result == 4 {
Ok(())
} else {
Err(String::from("two plus two does not equal four"))
}
}
}
The it_works function now has the Result<(), String> return type. In the
body of the function, rather than calling the assert_eq! macro, we return
Ok(()) when the test passes and an Err with a String inside when the test
fails.
Writing tests so that they return a Result<T, E> enables you to use the
question mark operator in the body of tests, which can be a convenient way to
write tests that should fail if any operation within them returns an Err
variant.
You can’t use the #[should_panic] annotation on tests that use Result<T, E>. To assert that an operation returns an Err variant, don’t use the
question mark operator on the Result<T, E> value. Instead, use
assert!(value.is_err()).
Now that you know several ways to write tests, let’s look at what is happening
when we run our tests and explore the different options we can use with cargo test.
ควบคุมการรันเทส
Controlling How Tests Are Run
Just as cargo run compiles your code and then runs the resultant binary,
cargo test compiles your code in test mode and runs the resultant test
binary. The default behavior of the binary produced by cargo test is to run
all the tests in parallel and capture output generated during test runs,
preventing the output from being displayed and making it easier to read the
output related to the test results. You can, however, specify command line
options to change this default behavior.
Some command line options go to cargo test, and some go to the resultant test
binary. To separate these two types of arguments, you list the arguments that
go to cargo test followed by the separator -- and then the ones that go to
the test binary. Running cargo test --help displays the options you can use
with cargo test, and running cargo test -- --help displays the options you
can use after the separator. These options are also documented in the “Tests”
section of The rustc Book.
Running Tests in Parallel or Consecutively
When you run multiple tests, by default they run in parallel using threads, meaning they finish running more quickly and you get feedback sooner. Because the tests are running at the same time, you must make sure your tests don’t depend on each other or on any shared state, including a shared environment, such as the current working directory or environment variables.
For example, say each of your tests runs some code that creates a file on disk named test-output.txt and writes some data to that file. Then, each test reads the data in that file and asserts that the file contains a particular value, which is different in each test. Because the tests run at the same time, one test might overwrite the file in the time between when another test is writing and reading the file. The second test will then fail, not because the code is incorrect but because the tests have interfered with each other while running in parallel. One solution is to make sure each test writes to a different file; another solution is to run the tests one at a time.
If you don’t want to run the tests in parallel or if you want more fine-grained
control over the number of threads used, you can send the --test-threads flag
and the number of threads you want to use to the test binary. Take a look at
the following example:
$ cargo test -- --test-threads=1
We set the number of test threads to 1, telling the program not to use any
parallelism. Running the tests using one thread will take longer than running
them in parallel, but the tests won’t interfere with each other if they share
state.
Showing Function Output
By default, if a test passes, Rust’s test library captures anything printed to
standard output. For example, if we call println! in a test and the test
passes, we won’t see the println! output in the terminal; we’ll see only the
line that indicates the test passed. If a test fails, we’ll see whatever was
printed to standard output with the rest of the failure message.
As an example, Listing 11-10 has a silly function that prints the value of its parameter and returns 10, as well as a test that passes and a test that fails.
fn prints_and_returns_10(a: i32) -> i32 {
println!("I got the value {a}");
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(value, 10);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(value, 5);
}
}
println!When we run these tests with cargo test, we’ll see the following output:
$ cargo test
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Note that nowhere in this output do we see I got the value 4, which is
printed when the test that passes runs. That output has been captured. The
output from the test that failed, I got the value 8, appears in the section
of the test summary output, which also shows the cause of the test failure.
If we want to see printed values for passing tests as well, we can tell Rust to
also show the output of successful tests with --show-output:
$ cargo test -- --show-output
When we run the tests in Listing 11-10 again with the --show-output flag, we
see the following output:
$ cargo test -- --show-output
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
successes:
---- tests::this_test_will_pass stdout ----
I got the value 4
successes:
tests::this_test_will_pass
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Running a Subset of Tests by Name
Running a full test suite can sometimes take a long time. If you’re working on
code in a particular area, you might want to run only the tests pertaining to
that code. You can choose which tests to run by passing cargo test the name
or names of the test(s) you want to run as an argument.
To demonstrate how to run a subset of tests, we’ll first create three tests for
our add_two function, as shown in Listing 11-11, and choose which ones to run.
pub fn add_two(a: u64) -> u64 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
#[test]
fn add_three_and_two() {
let result = add_two(3);
assert_eq!(result, 5);
}
#[test]
fn one_hundred() {
let result = add_two(100);
assert_eq!(result, 102);
}
}
If we run the tests without passing any arguments, as we saw earlier, all the tests will run in parallel:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running Single Tests
We can pass the name of any test function to cargo test to run only that test:
$ cargo test one_hundred
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::one_hundred ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
Only the test with the name one_hundred ran; the other two tests didn’t match
that name. The test output lets us know we had more tests that didn’t run by
displaying 2 filtered out at the end.
We can’t specify the names of multiple tests in this way; only the first value
given to cargo test will be used. But there is a way to run multiple tests.
Filtering to Run Multiple Tests
We can specify part of a test name, and any test whose name matches that value
will be run. For example, because two of our tests’ names contain add, we can
run those two by running cargo test add:
$ cargo test add
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
This command ran all tests with add in the name and filtered out the test
named one_hundred. Also note that the module in which a test appears becomes
part of the test’s name, so we can run all the tests in a module by filtering
on the module’s name.
Ignoring Tests Unless Specifically Requested
Sometimes a few specific tests can be very time-consuming to execute, so you
might want to exclude them during most runs of cargo test. Rather than
listing as arguments all tests you do want to run, you can instead annotate the
time-consuming tests using the ignore attribute to exclude them, as shown
here:
Filename: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
}
After #[test], we add the #[ignore] line to the test we want to exclude.
Now when we run our tests, it_works runs, but expensive_test doesn’t:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::expensive_test ... ignored
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
The expensive_test function is listed as ignored. If we want to run only
the ignored tests, we can use cargo test -- --ignored:
$ cargo test -- --ignored
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
By controlling which tests run, you can make sure your cargo test results
will be returned quickly. When you’re at a point where it makes sense to check
the results of the ignored tests and you have time to wait for the results,
you can run cargo test -- --ignored instead. If you want to run all tests
whether they’re ignored or not, you can run cargo test -- --include-ignored.
การจัดระเบียบเทส
Test Organization
As mentioned at the start of the chapter, testing is a complex discipline, and different people use different terminology and organization. The Rust community thinks about tests in terms of two main categories: unit tests and integration tests. Unit tests are small and more focused, testing one module in isolation at a time, and can test private interfaces. Integration tests are entirely external to your library and use your code in the same way any other external code would, using only the public interface and potentially exercising multiple modules per test.
Writing both kinds of tests is important to ensure that the pieces of your library are doing what you expect them to, separately and together.
Unit Tests
The purpose of unit tests is to test each unit of code in isolation from the
rest of the code to quickly pinpoint where code is and isn’t working as
expected. You’ll put unit tests in the src directory in each file with the
code that they’re testing. The convention is to create a module named tests
in each file to contain the test functions and to annotate the module with
cfg(test).
The tests Module and #[cfg(test)]
The #[cfg(test)] annotation on the tests module tells Rust to compile and
run the test code only when you run cargo test, not when you run cargo build. This saves compile time when you only want to build the library and
saves space in the resultant compiled artifact because the tests are not
included. You’ll see that because integration tests go in a different
directory, they don’t need the #[cfg(test)] annotation. However, because unit
tests go in the same files as the code, you’ll use #[cfg(test)] to specify
that they shouldn’t be included in the compiled result.
Recall that when we generated the new adder project in the first section of
this chapter, Cargo generated this code for us:
Filename: src/lib.rs
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
On the automatically generated tests module, the attribute cfg stands for
configuration and tells Rust that the following item should only be included
given a certain configuration option. In this case, the configuration option is
test, which is provided by Rust for compiling and running tests. By using the
cfg attribute, Cargo compiles our test code only if we actively run the tests
with cargo test. This includes any helper functions that might be within this
module, in addition to the functions annotated with #[test].
Private Function Tests
There’s debate within the testing community about whether or not private
functions should be tested directly, and other languages make it difficult or
impossible to test private functions. Regardless of which testing ideology you
adhere to, Rust’s privacy rules do allow you to test private functions.
Consider the code in Listing 11-12 with the private function internal_adder.
pub fn add_two(a: u64) -> u64 {
internal_adder(a, 2)
}
fn internal_adder(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn internal() {
let result = internal_adder(2, 2);
assert_eq!(result, 4);
}
}
Note that the internal_adder function is not marked as pub. Tests are just
Rust code, and the tests module is just another module. As we discussed in
“Paths for Referring to an Item in the Module Tree”,
items in child modules can use the items in their ancestor modules. In this
test, we bring all of the items belonging to the tests module’s parent into
scope with use super::*, and then the test can call internal_adder. If you
don’t think private functions should be tested, there’s nothing in Rust that
will compel you to do so.
Integration Tests
In Rust, integration tests are entirely external to your library. They use your library in the same way any other code would, which means they can only call functions that are part of your library’s public API. Their purpose is to test whether many parts of your library work together correctly. Units of code that work correctly on their own could have problems when integrated, so test coverage of the integrated code is important as well. To create integration tests, you first need a tests directory.
The tests Directory
We create a tests directory at the top level of our project directory, next to src. Cargo knows to look for integration test files in this directory. We can then make as many test files as we want, and Cargo will compile each of the files as an individual crate.
Let’s create an integration test. With the code in Listing 11-12 still in the src/lib.rs file, make a tests directory, and create a new file named tests/integration_test.rs. Your directory structure should look like this:
adder
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
└── integration_test.rs
Enter the code in Listing 11-13 into the tests/integration_test.rs file.
use adder::add_two;
#[test]
fn it_adds_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
adder crateEach file in the tests directory is a separate crate, so we need to bring our
library into each test crate’s scope. For that reason, we add use adder::add_two; at the top of the code, which we didn’t need in the unit tests.
We don’t need to annotate any code in tests/integration_test.rs with
#[cfg(test)]. Cargo treats the tests directory specially and compiles files
in this directory only when we run cargo test. Run cargo test now:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
The three sections of output include the unit tests, the integration test, and the doc tests. Note that if any test in a section fails, the following sections will not be run. For example, if a unit test fails, there won’t be any output for integration and doc tests, because those tests will only be run if all unit tests are passing.
The first section for the unit tests is the same as we’ve been seeing: one line
for each unit test (one named internal that we added in Listing 11-12) and
then a summary line for the unit tests.
The integration tests section starts with the line Running tests/integration_test.rs. Next, there is a line for each test function in
that integration test and a summary line for the results of the integration
test just before the Doc-tests adder section starts.
Each integration test file has its own section, so if we add more files in the tests directory, there will be more integration test sections.
We can still run a particular integration test function by specifying the test
function’s name as an argument to cargo test. To run all the tests in a
particular integration test file, use the --test argument of cargo test
followed by the name of the file:
$ cargo test --test integration_test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
This command runs only the tests in the tests/integration_test.rs file.
Submodules in Integration Tests
As you add more integration tests, you might want to make more files in the tests directory to help organize them; for example, you can group the test functions by the functionality they’re testing. As mentioned earlier, each file in the tests directory is compiled as its own separate crate, which is useful for creating separate scopes to more closely imitate the way end users will be using your crate. However, this means files in the tests directory don’t share the same behavior as files in src do, as you learned in Chapter 7 regarding how to separate code into modules and files.
The different behavior of tests directory files is most noticeable when you
have a set of helper functions to use in multiple integration test files, and
you try to follow the steps in the “Separating Modules into Different
Files” section of Chapter 7 to
extract them into a common module. For example, if we create tests/common.rs
and place a function named setup in it, we can add some code to setup that
we want to call from multiple test functions in multiple test files:
Filename: tests/common.rs
pub fn setup() {
// setup code specific to your library's tests would go here
}
When we run the tests again, we’ll see a new section in the test output for the
common.rs file, even though this file doesn’t contain any test functions nor
did we call the setup function from anywhere:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::internal ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)
running 1 test
test it_adds_two ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests adder
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Having common appear in the test results with running 0 tests displayed for
it is not what we wanted. We just wanted to share some code with the other
integration test files. To avoid having common appear in the test output,
instead of creating tests/common.rs, we’ll create tests/common/mod.rs. The
project directory now looks like this:
├── Cargo.lock
├── Cargo.toml
├── src
│ └── lib.rs
└── tests
├── common
│ └── mod.rs
└── integration_test.rs
This is the older naming convention that Rust also understands that we mentioned
in “Alternate File Paths” in Chapter 7. Naming the
file this way tells Rust not to treat the common module as an integration test
file. When we move the setup function code into tests/common/mod.rs and
delete the tests/common.rs file, the section in the test output will no longer
appear. Files in subdirectories of the tests directory don’t get compiled as
separate crates or have sections in the test output.
After we’ve created tests/common/mod.rs, we can use it from any of the
integration test files as a module. Here’s an example of calling the setup
function from the it_adds_two test in tests/integration_test.rs:
Filename: tests/integration_test.rs
use adder::add_two;
mod common;
#[test]
fn it_adds_two() {
common::setup();
let result = add_two(2);
assert_eq!(result, 4);
}
Note that the mod common; declaration is the same as the module declaration
we demonstrated in Listing 7-21. Then, in the test function, we can call the
common::setup() function.
Integration Tests for Binary Crates
If our project is a binary crate that only contains a src/main.rs file and
doesn’t have a src/lib.rs file, we can’t create integration tests in the
tests directory and bring functions defined in the src/main.rs file into
scope with a use statement. Only library crates expose functions that other
crates can use; binary crates are meant to be run on their own.
This is one of the reasons Rust projects that provide a binary have a
straightforward src/main.rs file that calls logic that lives in the
src/lib.rs file. Using that structure, integration tests can test the
library crate with use to make the important functionality available. If the
important functionality works, the small amount of code in the src/main.rs
file will work as well, and that small amount of code doesn’t need to be tested.
Summary
Rust’s testing features provide a way to specify how code should function to ensure that it continues to work as you expect, even as you make changes. Unit tests exercise different parts of a library separately and can test private implementation details. Integration tests check that many parts of the library work together correctly, and they use the library’s public API to test the code in the same way external code will use it. Even though Rust’s type system and ownership rules help prevent some kinds of bugs, tests are still important to reduce logic bugs having to do with how your code is expected to behave.
Let’s combine the knowledge you learned in this chapter and in previous chapters to work on a project!
An I/O Project: Building a Command Line Program
This chapter is a recap of the many skills you’ve learned so far and an exploration of a few more standard library features. We’ll build a command line tool that interacts with file and command line input/output to practice some of the Rust concepts you now have under your belt.
Rust’s speed, safety, single binary output, and cross-platform support make it
an ideal language for creating command line tools, so for our project, we’ll
make our own version of the classic command line search tool grep
(globally search a regular expression and print). In the
simplest use case, grep searches a specified file for a specified string. To
do so, grep takes as its arguments a file path and a string. Then, it reads
the file, finds lines in that file that contain the string argument, and prints
those lines.
Along the way, we’ll show how to make our command line tool use the terminal
features that many other command line tools use. We’ll read the value of an
environment variable to allow the user to configure the behavior of our tool.
We’ll also print error messages to the standard error console stream (stderr)
instead of standard output (stdout) so that, for example, the user can
redirect successful output to a file while still seeing error messages onscreen.
One Rust community member, Andrew Gallant, has already created a fully
featured, very fast version of grep, called ripgrep. By comparison, our
version will be fairly simple, but this chapter will give you some of the
background knowledge you need to understand a real-world project such as
ripgrep.
Our grep project will combine a number of concepts you’ve learned so far:
- Organizing code (Chapter 7)
- Using vectors and strings (Chapter 8)
- Handling errors (Chapter 9)
- Using traits and lifetimes where appropriate (Chapter 10)
- Writing tests (Chapter 11)
We’ll also briefly introduce closures, iterators, and trait objects, which Chapter 13 and Chapter 18 will cover in detail.
รับ command line argument
Accepting Command Line Arguments
Let’s create a new project with, as always, cargo new. We’ll call our project
minigrep to distinguish it from the grep tool that you might already have
on your system:
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrep
The first task is to make minigrep accept its two command line arguments: the
file path and a string to search for. That is, we want to be able to run our
program with cargo run, two hyphens to indicate the following arguments are
for our program rather than for cargo, a string to search for, and a path to
a file to search in, like so:
$ cargo run -- searchstring example-filename.txt
Right now, the program generated by cargo new cannot process arguments we
give it. Some existing libraries on crates.io can help
with writing a program that accepts command line arguments, but because you’re
just learning this concept, let’s implement this capability ourselves.
Reading the Argument Values
To enable minigrep to read the values of command line arguments we pass to
it, we’ll need the std::env::args function provided in Rust’s standard
library. This function returns an iterator of the command line arguments passed
to minigrep. We’ll cover iterators fully in Chapter 13. For now, you only need to know two details about iterators: Iterators
produce a series of values, and we can call the collect method on an iterator
to turn it into a collection, such as a vector, which contains all the elements
the iterator produces.
The code in Listing 12-1 allows your minigrep program to read any command
line arguments passed to it and then collect the values into a vector.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
First, we bring the std::env module into scope with a use statement so that
we can use its args function. Notice that the std::env::args function is
nested in two levels of modules. As we discussed in Chapter
7, in cases where the desired function is
nested in more than one module, we’ve chosen to bring the parent module into
scope rather than the function. By doing so, we can easily use other functions
from std::env. It’s also less ambiguous than adding use std::env::args and
then calling the function with just args, because args might easily be
mistaken for a function that’s defined in the current module.
The args Function and Invalid Unicode
Note that std::env::args will panic if any argument contains invalid
Unicode. If your program needs to accept arguments containing invalid
Unicode, use std::env::args_os instead. That function returns an iterator
that produces OsString values instead of String values. We’ve chosen to
use std::env::args here for simplicity because OsString values differ per
platform and are more complex to work with than String values.
On the first line of main, we call env::args, and we immediately use
collect to turn the iterator into a vector containing all the values produced
by the iterator. We can use the collect function to create many kinds of
collections, so we explicitly annotate the type of args to specify that we
want a vector of strings. Although you very rarely need to annotate types in
Rust, collect is one function you do often need to annotate because Rust
isn’t able to infer the kind of collection you want.
Finally, we print the vector using the debug macro. Let’s try running the code first with no arguments and then with two arguments:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/minigrep`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
]
$ cargo run -- needle haystack
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
Running `target/debug/minigrep needle haystack`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
"needle",
"haystack",
]
Notice that the first value in the vector is "target/debug/minigrep", which
is the name of our binary. This matches the behavior of the arguments list in
C, letting programs use the name by which they were invoked in their execution.
It’s often convenient to have access to the program name in case you want to
print it in messages or change the behavior of the program based on what
command line alias was used to invoke the program. But for the purposes of this
chapter, we’ll ignore it and save only the two arguments we need.
Saving the Argument Values in Variables
The program is currently able to access the values specified as command line arguments. Now we need to save the values of the two arguments in variables so that we can use the values throughout the rest of the program. We do that in Listing 12-2.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {query}");
println!("In file {file_path}");
}
As we saw when we printed the vector, the program’s name takes up the first
value in the vector at args[0], so we’re starting arguments at index 1. The
first argument minigrep takes is the string we’re searching for, so we put a
reference to the first argument in the variable query. The second argument
will be the file path, so we put a reference to the second argument in the
variable file_path.
We temporarily print the values of these variables to prove that the code is
working as we intend. Let’s run this program again with the arguments test
and sample.txt:
$ cargo run -- test sample.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt
Great, the program is working! The values of the arguments we need are being saved into the right variables. Later we’ll add some error handling to deal with certain potential erroneous situations, such as when the user provides no arguments; for now, we’ll ignore that situation and work on adding file-reading capabilities instead.
อ่านไฟล์
Reading a File
Now we’ll add functionality to read the file specified in the file_path
argument. First, we need a sample file to test it with: We’ll use a file with a
small amount of text over multiple lines with some repeated words. Listing 12-3
has an Emily Dickinson poem that will work well! Create a file called
poem.txt at the root level of your project, and enter the poem “I’m Nobody!
Who are you?”
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
With the text in place, edit src/main.rs and add code to read the file, as shown in Listing 12-4.
use std::env;
use std::fs;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {query}");
println!("In file {file_path}");
let contents = fs::read_to_string(file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
First, we bring in a relevant part of the standard library with a use
statement: We need std::fs to handle files.
In main, the new statement fs::read_to_string takes the file_path, opens
that file, and returns a value of type std::io::Result<String> that contains
the file’s contents.
After that, we again add a temporary println! statement that prints the value
of contents after the file is read so that we can check that the program is
working so far.
Let’s run this code with any string as the first command line argument (because we haven’t implemented the searching part yet) and the poem.txt file as the second argument:
$ cargo run -- the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
Great! The code read and then printed the contents of the file. But the code
has a few flaws. At the moment, the main function has multiple
responsibilities: Generally, functions are clearer and easier to maintain if
each function is responsible for only one idea. The other problem is that we’re
not handling errors as well as we could. The program is still small, so these
flaws aren’t a big problem, but as the program grows, it will be harder to fix
them cleanly. It’s a good practice to begin refactoring early on when
developing a program because it’s much easier to refactor smaller amounts of
code. We’ll do that next.
Refactor เพื่อ modularity และจัดการ error
Refactoring to Improve Modularity and Error Handling
To improve our program, we’ll fix four problems that have to do with the
program’s structure and how it’s handling potential errors. First, our main
function now performs two tasks: It parses arguments and reads files. As our
program grows, the number of separate tasks the main function handles will
increase. As a function gains responsibilities, it becomes more difficult to
reason about, harder to test, and harder to change without breaking one of its
parts. It’s best to separate functionality so that each function is responsible
for one task.
This issue also ties into the second problem: Although query and file_path
are configuration variables to our program, variables like contents are used
to perform the program’s logic. The longer main becomes, the more variables
we’ll need to bring into scope; the more variables we have in scope, the harder
it will be to keep track of the purpose of each. It’s best to group the
configuration variables into one structure to make their purpose clear.
The third problem is that we’ve used expect to print an error message when
reading the file fails, but the error message just prints Should have been able to read the file. Reading a file can fail in a number of ways: For
example, the file could be missing, or we might not have permission to open it.
Right now, regardless of the situation, we’d print the same error message for
everything, which wouldn’t give the user any information!
Fourth, we use expect to handle an error, and if the user runs our program
without specifying enough arguments, they’ll get an index out of bounds error
from Rust that doesn’t clearly explain the problem. It would be best if all the
error-handling code were in one place so that future maintainers had only one
place to consult the code if the error-handling logic needed to change. Having
all the error-handling code in one place will also ensure that we’re printing
messages that will be meaningful to our end users.
Let’s address these four problems by refactoring our project.
Separating Concerns in Binary Projects
The organizational problem of allocating responsibility for multiple tasks to
the main function is common to many binary projects. As a result, many Rust
programmers find it useful to split up the separate concerns of a binary
program when the main function starts getting large. This process has the
following steps:
- Split your program into a main.rs file and a lib.rs file and move your program’s logic to lib.rs.
- As long as your command line parsing logic is small, it can remain in
the
mainfunction. - When the command line parsing logic starts getting complicated, extract it
from the
mainfunction into other functions or types.
The responsibilities that remain in the main function after this process
should be limited to the following:
- Calling the command line parsing logic with the argument values
- Setting up any other configuration
- Calling a
runfunction in lib.rs - Handling the error if
runreturns an error
This pattern is about separating concerns: main.rs handles running the
program and lib.rs handles all the logic of the task at hand. Because you
can’t test the main function directly, this structure lets you test all of
your program’s logic by moving it out of the main function. The code that
remains in the main function will be small enough to verify its correctness
by reading it. Let’s rework our program by following this process.
Extracting the Argument Parser
We’ll extract the functionality for parsing arguments into a function that
main will call. Listing 12-5 shows the new start of the main function that
calls a new function parse_config, which we’ll define in src/main.rs.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let (query, file_path) = parse_config(&args);
// --snip--
println!("Searching for {query}");
println!("In file {file_path}");
let contents = fs::read_to_string(file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let file_path = &args[2];
(query, file_path)
}
parse_config function from mainWe’re still collecting the command line arguments into a vector, but instead of
assigning the argument value at index 1 to the variable query and the
argument value at index 2 to the variable file_path within the main
function, we pass the whole vector to the parse_config function. The
parse_config function then holds the logic that determines which argument
goes in which variable and passes the values back to main. We still create
the query and file_path variables in main, but main no longer has the
responsibility of determining how the command line arguments and variables
correspond.
This rework may seem like overkill for our small program, but we’re refactoring in small, incremental steps. After making this change, run the program again to verify that the argument parsing still works. It’s good to check your progress often, to help identify the cause of problems when they occur.
Grouping Configuration Values
We can take another small step to improve the parse_config function further.
At the moment, we’re returning a tuple, but then we immediately break that
tuple into individual parts again. This is a sign that perhaps we don’t have
the right abstraction yet.
Another indicator that shows there’s room for improvement is the config part
of parse_config, which implies that the two values we return are related and
are both part of one configuration value. We’re not currently conveying this
meaning in the structure of the data other than by grouping the two values into
a tuple; we’ll instead put the two values into one struct and give each of the
struct fields a meaningful name. Doing so will make it easier for future
maintainers of this code to understand how the different values relate to each
other and what their purpose is.
Listing 12-6 shows the improvements to the parse_config function.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
// --snip--
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
parse_config to return an instance of a Config structWe’ve added a struct named Config defined to have fields named query and
file_path. The signature of parse_config now indicates that it returns a
Config value. In the body of parse_config, where we used to return
string slices that reference String values in args, we now define Config
to contain owned String values. The args variable in main is the owner of
the argument values and is only letting the parse_config function borrow
them, which means we’d violate Rust’s borrowing rules if Config tried to take
ownership of the values in args.
There are a number of ways we could manage the String data; the easiest,
though somewhat inefficient, route is to call the clone method on the values.
This will make a full copy of the data for the Config instance to own, which
takes more time and memory than storing a reference to the string data.
However, cloning the data also makes our code very straightforward because we
don’t have to manage the lifetimes of the references; in this circumstance,
giving up a little performance to gain simplicity is a worthwhile trade-off.
The Trade-Offs of Using clone
There’s a tendency among many Rustaceans to avoid using clone to fix
ownership problems because of its runtime cost. In
Chapter 13, you’ll learn how to use more efficient
methods in this type of situation. But for now, it’s okay to copy a few
strings to continue making progress because you’ll make these copies only
once and your file path and query string are very small. It’s better to have
a working program that’s a bit inefficient than to try to hyperoptimize code
on your first pass. As you become more experienced with Rust, it’ll be
easier to start with the most efficient solution, but for now, it’s
perfectly acceptable to call clone.
We’ve updated main so that it places the instance of Config returned by
parse_config into a variable named config, and we updated the code that
previously used the separate query and file_path variables so that it now
uses the fields on the Config struct instead.
Now our code more clearly conveys that query and file_path are related and
that their purpose is to configure how the program will work. Any code that
uses these values knows to find them in the config instance in the fields
named for their purpose.
Creating a Constructor for Config
So far, we’ve extracted the logic responsible for parsing the command line
arguments from main and placed it in the parse_config function. Doing so
helped us see that the query and file_path values were related, and that
relationship should be conveyed in our code. We then added a Config struct to
name the related purpose of query and file_path and to be able to return the
values’ names as struct field names from the parse_config function.
So, now that the purpose of the parse_config function is to create a Config
instance, we can change parse_config from a plain function to a function
named new that is associated with the Config struct. Making this change
will make the code more idiomatic. We can create instances of types in the
standard library, such as String, by calling String::new. Similarly, by
changing parse_config into a new function associated with Config, we’ll
be able to create instances of Config by calling Config::new. Listing 12-7
shows the changes we need to make.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
// --snip--
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
parse_config into Config::newWe’ve updated main where we were calling parse_config to instead call
Config::new. We’ve changed the name of parse_config to new and moved it
within an impl block, which associates the new function with Config. Try
compiling this code again to make sure it works.
Fixing the Error Handling
Now we’ll work on fixing our error handling. Recall that attempting to access
the values in the args vector at index 1 or index 2 will cause the program to
panic if the vector contains fewer than three items. Try running the program
without any arguments; it will look like this:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The line index out of bounds: the len is 1 but the index is 1 is an error
message intended for programmers. It won’t help our end users understand what
they should do instead. Let’s fix that now.
Improving the Error Message
In Listing 12-8, we add a check in the new function that will verify that the
slice is long enough before accessing index 1 and index 2. If the slice isn’t
long enough, the program panics and displays a better error message.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("not enough arguments");
}
// --snip--
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
This code is similar to the Guess::new function we wrote in Listing
9-13, where we called panic! when the
value argument was out of the range of valid values. Instead of checking for
a range of values here, we’re checking that the length of args is at least
3 and the rest of the function can operate under the assumption that this
condition has been met. If args has fewer than three items, this condition
will be true, and we call the panic! macro to end the program immediately.
With these extra few lines of code in new, let’s run the program without any
arguments again to see what the error looks like now:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
This output is better: We now have a reasonable error message. However, we also
have extraneous information we don’t want to give to our users. Perhaps the
technique we used in Listing 9-13 isn’t the best one to use here: A call to
panic! is more appropriate for a programming problem than a usage problem,
as discussed in Chapter 9. Instead,
we’ll use the other technique you learned about in Chapter 9—returning a
Result that indicates either success or an error.
Returning a Result Instead of Calling panic!
We can instead return a Result value that will contain a Config instance in
the successful case and will describe the problem in the error case. We’re also
going to change the function name from new to build because many
programmers expect new functions to never fail. When Config::build is
communicating to main, we can use the Result type to signal there was a
problem. Then, we can change main to convert an Err variant into a more
practical error for our users without the surrounding text about thread 'main' and RUST_BACKTRACE that a call to panic! causes.
Listing 12-9 shows the changes we need to make to the return value of the
function we’re now calling Config::build and the body of the function needed
to return a Result. Note that this won’t compile until we update main as
well, which we’ll do in the next listing.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
Result from Config::buildOur build function returns a Result with a Config instance in the success
case and a string literal in the error case. Our error values will always be
string literals that have the 'static lifetime.
We’ve made two changes in the body of the function: Instead of calling panic!
when the user doesn’t pass enough arguments, we now return an Err value, and
we’ve wrapped the Config return value in an Ok. These changes make the
function conform to its new type signature.
Returning an Err value from Config::build allows the main function to
handle the Result value returned from the build function and exit the
process more cleanly in the error case.
Calling Config::build and Handling Errors
To handle the error case and print a user-friendly message, we need to update
main to handle the Result being returned by Config::build, as shown in
Listing 12-10. We’ll also take the responsibility of exiting the command line
tool with a nonzero error code away from panic! and instead implement it by
hand. A nonzero exit status is a convention to signal to the process that
called our program that the program exited with an error state.
use std::env;
use std::fs;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
Config failsIn this listing, we’ve used a method we haven’t covered in detail yet:
unwrap_or_else, which is defined on Result<T, E> by the standard library.
Using unwrap_or_else allows us to define some custom, non-panic! error
handling. If the Result is an Ok value, this method’s behavior is similar
to unwrap: It returns the inner value that Ok is wrapping. However, if the
value is an Err value, this method calls the code in the closure, which is
an anonymous function we define and pass as an argument to unwrap_or_else.
We’ll cover closures in more detail in Chapter 13. For
now, you just need to know that unwrap_or_else will pass the inner value of
the Err, which in this case is the static string "not enough arguments"
that we added in Listing 12-9, to our closure in the argument err that
appears between the vertical pipes. The code in the closure can then use the
err value when it runs.
We’ve added a new use line to bring process from the standard library into
scope. The code in the closure that will be run in the error case is only two
lines: We print the err value and then call process::exit. The
process::exit function will stop the program immediately and return the
number that was passed as the exit status code. This is similar to the
panic!-based handling we used in Listing 12-8, but we no longer get all the
extra output. Let’s try it:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
Great! This output is much friendlier for our users.
Extracting Logic from main
Now that we’ve finished refactoring the configuration parsing, let’s turn to
the program’s logic. As we stated in “Separating Concerns in Binary
Projects”, we’ll
extract a function named run that will hold all the logic currently in the
main function that isn’t involved with setting up configuration or handling
errors. When we’re done, the main function will be concise and easy to verify
by inspection, and we’ll be able to write tests for all the other logic.
Listing 12-11 shows the small, incremental improvement of extracting a run
function.
use std::env;
use std::fs;
use std::process;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) {
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:\n{contents}");
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
run function containing the rest of the program logicThe run function now contains all the remaining logic from main, starting
from reading the file. The run function takes the Config instance as an
argument.
Returning Errors from run
With the remaining program logic separated into the run function, we can
improve the error handling, as we did with Config::build in Listing 12-9.
Instead of allowing the program to panic by calling expect, the run
function will return a Result<T, E> when something goes wrong. This will let
us further consolidate the logic around handling errors into main in a
user-friendly way. Listing 12-12 shows the changes we need to make to the
signature and body of run.
use std::env;
use std::fs;
use std::process;
use std::error::Error;
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
run function to return ResultWe’ve made three significant changes here. First, we changed the return type of
the run function to Result<(), Box<dyn Error>>. This function previously
returned the unit type, (), and we keep that as the value returned in the
Ok case.
For the error type, we used the trait object Box<dyn Error> (and we brought
std::error::Error into scope with a use statement at the top). We’ll cover
trait objects in Chapter 18. For now, just know that
Box<dyn Error> means the function will return a type that implements the
Error trait, but we don’t have to specify what particular type the return
value will be. This gives us flexibility to return error values that may be of
different types in different error cases. The dyn keyword is short for
dynamic.
Second, we’ve removed the call to expect in favor of the ? operator, as we
talked about in Chapter 9. Rather than
panic! on an error, ? will return the error value from the current function
for the caller to handle.
Third, the run function now returns an Ok value in the success case.
We’ve declared the run function’s success type as () in the signature,
which means we need to wrap the unit type value in the Ok value. This
Ok(()) syntax might look a bit strange at first. But using () like this is
the idiomatic way to indicate that we’re calling run for its side effects
only; it doesn’t return a value we need.
When you run this code, it will compile but will display a warning:
$ cargo run -- the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
--> src/main.rs:19:5
|
19 | run(config);
| ^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
19 | let _ = run(config);
| +++++++
warning: `minigrep` (bin "minigrep") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
Rust tells us that our code ignored the Result value and the Result value
might indicate that an error occurred. But we’re not checking to see whether or
not there was an error, and the compiler reminds us that we probably meant to
have some error-handling code here! Let’s rectify that problem now.
Handling Errors Returned from run in main
We’ll check for errors and handle them using a technique similar to one we used
with Config::build in Listing 12-10, but with a slight difference:
Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:\n{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
We use if let rather than unwrap_or_else to check whether run returns an
Err value and to call process::exit(1) if it does. The run function
doesn’t return a value that we want to unwrap in the same way that
Config::build returns the Config instance. Because run returns () in
the success case, we only care about detecting an error, so we don’t need
unwrap_or_else to return the unwrapped value, which would only be ().
The bodies of the if let and the unwrap_or_else functions are the same in
both cases: We print the error and exit.
Splitting Code into a Library Crate
Our minigrep project is looking good so far! Now we’ll split the
src/main.rs file and put some code into the src/lib.rs file. That way, we
can test the code and have a src/main.rs file with fewer responsibilities.
Let’s define the code responsible for searching text in src/lib.rs rather
than in src/main.rs, which will let us (or anyone else using our
minigrep library) call the searching function from more contexts than our
minigrep binary.
First, let’s define the search function signature in src/lib.rs as shown in
Listing 12-13, with a body that calls the unimplemented! macro. We’ll explain
the signature in more detail when we fill in the implementation.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
unimplemented!();
}
search function in src/lib.rsWe’ve used the pub keyword on the function definition to designate search
as part of our library crate’s public API. We now have a library crate that we
can use from our binary crate and that we can test!
Now we need to bring the code defined in src/lib.rs into the scope of the binary crate in src/main.rs and call it, as shown in Listing 12-14.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
// --snip--
use minigrep::search;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
for line in search(&config.query, &contents) {
println!("{line}");
}
Ok(())
}
minigrep library crate’s search function in src/main.rsWe add a use minigrep::search line to bring the search function from
the library crate into the binary crate’s scope. Then, in the run function,
rather than printing out the contents of the file, we call the search
function and pass the config.query value and contents as arguments. Then,
run will use a for loop to print each line returned from search that
matched the query. This is also a good time to remove the println! calls in
the main function that displayed the query and the file path so that our
program only prints the search results (if no errors occur).
Note that the search function will be collecting all the results into a vector it returns before any printing happens. This implementation could be slow to display results when searching large files, because results aren’t printed as they’re found; we’ll discuss a possible way to fix this using iterators in Chapter 13.
Whew! That was a lot of work, but we’ve set ourselves up for success in the future. Now it’s much easier to handle errors, and we’ve made the code more modular. Almost all of our work will be done in src/lib.rs from here on out.
Let’s take advantage of this newfound modularity by doing something that would have been difficult with the old code but is easy with the new code: We’ll write some tests!
เพิ่มฟีเจอร์ด้วย TDD
Adding Functionality with Test-Driven Development
Now that we have the search logic in src/lib.rs separate from the main
function, it’s much easier to write tests for the core functionality of our
code. We can call functions directly with various arguments and check return
values without having to call our binary from the command line.
In this section, we’ll add the searching logic to the minigrep program using
the test-driven development (TDD) process with the following steps:
- Write a test that fails and run it to make sure it fails for the reason you expect.
- Write or modify just enough code to make the new test pass.
- Refactor the code you just added or changed and make sure the tests continue to pass.
- Repeat from step 1!
Though it’s just one of many ways to write software, TDD can help drive code design. Writing the test before you write the code that makes the test pass helps maintain high test coverage throughout the process.
We’ll test-drive the implementation of the functionality that will actually do
the searching for the query string in the file contents and produce a list of
lines that match the query. We’ll add this functionality in a function called
search.
Writing a Failing Test
In src/lib.rs, we’ll add a tests module with a test function, as we did in
Chapter 11. The test function specifies the
behavior we want the search function to have: It will take a query and the
text to search, and it will return only the lines from the text that contain
the query. Listing 12-15 shows this test.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
unimplemented!();
}
// --snip--
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
search function for the functionality we wish we hadThis test searches for the string "duct". The text we’re searching is three
lines, only one of which contains "duct" (note that the backslash after the
opening double quote tells Rust not to put a newline character at the beginning
of the contents of this string literal). We assert that the value returned from
the search function contains only the line we expect.
If we run this test, it will currently fail because the unimplemented! macro
panics with the message “not implemented”. In accordance with TDD principles,
we’ll take a small step of adding just enough code to get the test to not panic
when calling the function by defining the search function to always return an
empty vector, as shown in Listing 12-16. Then, the test should compile and fail
because an empty vector doesn’t match a vector containing the line "safe, fast, productive.".
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
vec![]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
search function so that calling it won’t panicNow let’s discuss why we need to define an explicit lifetime 'a in the
signature of search and use that lifetime with the contents argument and
the return value. Recall in Chapter 10 that
the lifetime parameters specify which argument lifetime is connected to the
lifetime of the return value. In this case, we indicate that the returned
vector should contain string slices that reference slices of the argument
contents (rather than the argument query).
In other words, we tell Rust that the data returned by the search function
will live as long as the data passed into the search function in the
contents argument. This is important! The data referenced by a slice needs
to be valid for the reference to be valid; if the compiler assumes we’re making
string slices of query rather than contents, it will do its safety checking
incorrectly.
If we forget the lifetime annotations and try to compile this function, we’ll get this error:
$ cargo build
Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
--> src/lib.rs:1:51
|
1 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
|
1 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to 1 previous error
Rust can’t know which of the two parameters we need for the output, so we need
to tell it explicitly. Note that the help text suggests specifying the same
lifetime parameter for all the parameters and the output type, which is
incorrect! Because contents is the parameter that contains all of our text
and we want to return the parts of that text that match, we know contents is
the only parameter that should be connected to the return value using the
lifetime syntax.
Other programming languages don’t require you to connect arguments to return values in the signature, but this practice will get easier over time. You might want to compare this example with the examples in the “Validating References with Lifetimes” section in Chapter 10.
Writing Code to Pass the Test
Currently, our test is failing because we always return an empty vector. To fix
that and implement search, our program needs to follow these steps:
- Iterate through each line of the contents.
- Check whether the line contains our query string.
- If it does, add it to the list of values we’re returning.
- If it doesn’t, do nothing.
- Return the list of results that match.
Let’s work through each step, starting with iterating through lines.
Iterating Through Lines with the lines Method
Rust has a helpful method to handle line-by-line iteration of strings,
conveniently named lines, that works as shown in Listing 12-17. Note that
this won’t compile yet.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
// do something with line
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
contentsThe lines method returns an iterator. We’ll talk about iterators in depth in
Chapter 13. But recall that you saw this way
of using an iterator in Listing 3-5, where we used a
for loop with an iterator to run some code on each item in a collection.
Searching Each Line for the Query
Next, we’ll check whether the current line contains our query string.
Fortunately, strings have a helpful method named contains that does this for
us! Add a call to the contains method in the search function, as shown in
Listing 12-18. Note that this still won’t compile yet.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
for line in contents.lines() {
if line.contains(query) {
// do something with line
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
queryAt the moment, we’re building up functionality. To get the code to compile, we need to return a value from the body as we indicated we would in the function signature.
Storing Matching Lines
To finish this function, we need a way to store the matching lines that we want
to return. For that, we can make a mutable vector before the for loop and
call the push method to store a line in the vector. After the for loop,
we return the vector, as shown in Listing 12-19.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
Now the search function should return only the lines that contain query,
and our test should pass. Let’s run the test:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 1 test
test tests::one_result ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Our test passed, so we know it works!
At this point, we could consider opportunities for refactoring the implementation of the search function while keeping the tests passing to maintain the same functionality. The code in the search function isn’t too bad, but it doesn’t take advantage of some useful features of iterators. We’ll return to this example in Chapter 13, where we’ll explore iterators in detail, and look at how to improve it.
Now the entire program should work! Let’s try it out, first with a word that should return exactly one line from the Emily Dickinson poem: frog.
$ cargo run -- frog poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/minigrep frog poem.txt`
How public, like a frog
Cool! Now let’s try a word that will match multiple lines, like body:
$ cargo run -- body poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!
And finally, let’s make sure that we don’t get any lines when we search for a word that isn’t anywhere in the poem, such as monomorphization:
$ cargo run -- monomorphization poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep monomorphization poem.txt`
Excellent! We’ve built our own mini version of a classic tool and learned a lot about how to structure applications. We’ve also learned a bit about file input and output, lifetimes, testing, and command line parsing.
To round out this project, we’ll briefly demonstrate how to work with environment variables and how to print to standard error, both of which are useful when you’re writing command line programs.
ใช้งาน environment variable
Working with Environment Variables
We’ll improve the minigrep binary by adding an extra feature: an option for
case-insensitive searching that the user can turn on via an environment
variable. We could make this feature a command line option and require that
users enter it each time they want it to apply, but by instead making it an
environment variable, we allow our users to set the environment variable once
and have all their searches be case insensitive in that terminal session.
Writing a Failing Test for Case-Insensitive Search
We first add a new search_case_insensitive function to the minigrep library
that will be called when the environment variable has a value. We’ll continue
to follow the TDD process, so the first step is again to write a failing test.
We’ll add a new test for the new search_case_insensitive function and rename
our old test from one_result to case_sensitive to clarify the differences
between the two tests, as shown in Listing 12-20.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
Note that we’ve edited the old test’s contents too. We’ve added a new line
with the text "Duct tape." using a capital D that shouldn’t match the query
"duct" when we’re searching in a case-sensitive manner. Changing the old test
in this way helps ensure that we don’t accidentally break the case-sensitive
search functionality that we’ve already implemented. This test should pass now
and should continue to pass as we work on the case-insensitive search.
The new test for the case-insensitive search uses "rUsT" as its query. In
the search_case_insensitive function we’re about to add, the query "rUsT"
should match the line containing "Rust:" with a capital R and match the
line "Trust me." even though both have different casing from the query. This
is our failing test, and it will fail to compile because we haven’t yet defined
the search_case_insensitive function. Feel free to add a skeleton
implementation that always returns an empty vector, similar to the way we did
for the search function in Listing 12-16 to see the test compile and fail.
Implementing the search_case_insensitive Function
The search_case_insensitive function, shown in Listing 12-21, will be almost
the same as the search function. The only difference is that we’ll lowercase
the query and each line so that whatever the case of the input arguments,
they’ll be the same case when we check whether the line contains the query.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
search_case_insensitive function to lowercase the query and the line before comparing themFirst, we lowercase the query string and store it in a new variable with the
same name, shadowing the original query. Calling to_lowercase on the query
is necessary so that no matter whether the user’s query is "rust", "RUST",
"Rust", or "rUsT", we’ll treat the query as if it were "rust" and be
insensitive to the case. While to_lowercase will handle basic Unicode, it
won’t be 100 percent accurate. If we were writing a real application, we’d want
to do a bit more work here, but this section is about environment variables,
not Unicode, so we’ll leave it at that here.
Note that query is now a String rather than a string slice because calling
to_lowercase creates new data rather than referencing existing data. Say the
query is "rUsT", as an example: That string slice doesn’t contain a lowercase
u or t for us to use, so we have to allocate a new String containing
"rust". When we pass query as an argument to the contains method now, we
need to add an ampersand because the signature of contains is defined to take
a string slice.
Next, we add a call to to_lowercase on each line to lowercase all
characters. Now that we’ve converted line and query to lowercase, we’ll
find matches no matter what the case of the query is.
Let’s see if this implementation passes the tests:
$ cargo test
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.33s
Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 2 tests
test tests::case_insensitive ... ok
test tests::case_sensitive ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests minigrep
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Great! They passed. Now let’s call the new search_case_insensitive function
from the run function. First, we’ll add a configuration option to the Config
struct to switch between case-sensitive and case-insensitive search. Adding
this field will cause compiler errors because we aren’t initializing this field
anywhere yet:
Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
We added the ignore_case field that holds a Boolean. Next, we need the run
function to check the ignore_case field’s value and use that to decide
whether to call the search function or the search_case_insensitive
function, as shown in Listing 12-22. This still won’t compile yet.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
// --snip--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
search or search_case_insensitive based on the value in config.ignore_caseFinally, we need to check for the environment variable. The functions for
working with environment variables are in the env module in the standard
library, which is already in scope at the top of src/main.rs. We’ll use the
var function from the env module to check to see if any value has been set
for an environment variable named IGNORE_CASE, as shown in Listing 12-23.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
IGNORE_CASEHere, we create a new variable, ignore_case. To set its value, we call the
env::var function and pass it the name of the IGNORE_CASE environment
variable. The env::var function returns a Result that will be the
successful Ok variant that contains the value of the environment variable if
the environment variable is set to any value. It will return the Err variant
if the environment variable is not set.
We’re using the is_ok method on the Result to check whether the environment
variable is set, which means the program should do a case-insensitive search.
If the IGNORE_CASE environment variable isn’t set to anything, is_ok will
return false and the program will perform a case-sensitive search. We don’t
care about the value of the environment variable, just whether it’s set or
unset, so we’re checking is_ok rather than using unwrap, expect, or any
of the other methods we’ve seen on Result.
We pass the value in the ignore_case variable to the Config instance so
that the run function can read that value and decide whether to call
search_case_insensitive or search, as we implemented in Listing 12-22.
Let’s give it a try! First, we’ll run our program without the environment
variable set and with the query to, which should match any line that contains
the word to in all lowercase:
$ cargo run -- to poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep to poem.txt`
Are you nobody, too?
How dreary to be somebody!
Looks like that still works! Now let’s run the program with IGNORE_CASE set
to 1 but with the same query to:
$ IGNORE_CASE=1 cargo run -- to poem.txt
If you’re using PowerShell, you will need to set the environment variable and run the program as separate commands:
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
This will make IGNORE_CASE persist for the remainder of your shell session.
It can be unset with the Remove-Item cmdlet:
PS> Remove-Item Env:IGNORE_CASE
We should get lines that contain to that might have uppercase letters:
Are you nobody, too?
How dreary to be somebody!
To tell your name the livelong day
To an admiring bog!
Excellent, we also got lines containing To! Our minigrep program can now do
case-insensitive searching controlled by an environment variable. Now you know
how to manage options set using either command line arguments or environment
variables.
Some programs allow arguments and environment variables for the same configuration. In those cases, the programs decide that one or the other takes precedence. For another exercise on your own, try controlling case sensitivity through either a command line argument or an environment variable. Decide whether the command line argument or the environment variable should take precedence if the program is run with one set to case sensitive and one set to ignore case.
The std::env module contains many more useful features for dealing with
environment variables: Check out its documentation to see what is available.
Redirect error ไปยัง standard error
Redirecting Errors to Standard Error
At the moment, we’re writing all of our output to the terminal using the
println! macro. In most terminals, there are two kinds of output: standard
output (stdout) for general information and standard error (stderr) for
error messages. This distinction enables users to choose to direct the
successful output of a program to a file but still print error messages to the
screen.
The println! macro is only capable of printing to standard output, so we have
to use something else to print to standard error.
Checking Where Errors Are Written
First, let’s observe how the content printed by minigrep is currently being
written to standard output, including any error messages we want to write to
standard error instead. We’ll do that by redirecting the standard output stream
to a file while intentionally causing an error. We won’t redirect the standard
error stream, so any content sent to standard error will continue to display on
the screen.
Command line programs are expected to send error messages to the standard error stream so that we can still see error messages on the screen even if we redirect the standard output stream to a file. Our program is not currently well behaved: We’re about to see that it saves the error message output to a file instead!
To demonstrate this behavior, we’ll run the program with > and the file path,
output.txt, that we want to redirect the standard output stream to. We won’t
pass any arguments, which should cause an error:
$ cargo run > output.txt
The > syntax tells the shell to write the contents of standard output to
output.txt instead of the screen. We didn’t see the error message we were
expecting printed to the screen, so that means it must have ended up in the
file. This is what output.txt contains:
Problem parsing arguments: not enough arguments
Yup, our error message is being printed to standard output. It’s much more useful for error messages like this to be printed to standard error so that only data from a successful run ends up in the file. We’ll change that.
Printing Errors to Standard Error
We’ll use the code in Listing 12-24 to change how error messages are printed.
Because of the refactoring we did earlier in this chapter, all the code that
prints error messages is in one function, main. The standard library provides
the eprintln! macro that prints to the standard error stream, so let’s change
the two places we were calling println! to print errors to use eprintln!
instead.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
eprintln!Let’s now run the program again in the same way, without any arguments and
redirecting standard output with >:
$ cargo run > output.txt
Problem parsing arguments: not enough arguments
Now we see the error onscreen and output.txt contains nothing, which is the behavior we expect of command line programs.
Let’s run the program again with arguments that don’t cause an error but still redirect standard output to a file, like so:
$ cargo run -- to poem.txt > output.txt
We won’t see any output to the terminal, and output.txt will contain our results:
Filename: output.txt
Are you nobody, too?
How dreary to be somebody!
This demonstrates that we’re now using standard output for successful output and standard error for error output as appropriate.
Summary
This chapter recapped some of the major concepts you’ve learned so far and
covered how to perform common I/O operations in Rust. By using command line
arguments, files, environment variables, and the eprintln! macro for printing
errors, you’re now prepared to write command line applications. Combined with
the concepts in previous chapters, your code will be well organized, store data
effectively in the appropriate data structures, handle errors nicely, and be
well tested.
Next, we’ll explore some Rust features that were influenced by functional languages: closures and iterators.
Functional Language Features: Iterators and Closures
Rust’s design has taken inspiration from many existing languages and techniques, and one significant influence is functional programming. Programming in a functional style often includes using functions as values by passing them in arguments, returning them from other functions, assigning them to variables for later execution, and so forth.
In this chapter, we won’t debate the issue of what functional programming is or isn’t but will instead discuss some features of Rust that are similar to features in many languages often referred to as functional.
More specifically, we’ll cover:
- Closures, a function-like construct you can store in a variable
- Iterators, a way of processing a series of elements
- How to use closures and iterators to improve the I/O project in Chapter 12
- The performance of closures and iterators (spoiler alert: They’re faster than you might think!)
We’ve already covered some other Rust features, such as pattern matching and enums, that are also influenced by the functional style. Because mastering closures and iterators is an important part of writing fast, idiomatic, Rust code, we’ll devote this entire chapter to them.
Closure
Closures
Rust’s closures are anonymous functions you can save in a variable or pass as arguments to other functions. You can create the closure in one place and then call the closure elsewhere to evaluate it in a different context. Unlike functions, closures can capture values from the scope in which they’re defined. We’ll demonstrate how these closure features allow for code reuse and behavior customization.
Capturing the Environment
We’ll first examine how we can use closures to capture values from the environment they’re defined in for later use. Here’s the scenario: Every so often, our T-shirt company gives away an exclusive, limited-edition shirt to someone on our mailing list as a promotion. People on the mailing list can optionally add their favorite color to their profile. If the person chosen for a free shirt has their favorite color set, they get that color shirt. If the person hasn’t specified a favorite color, they get whatever color the company currently has the most of.
There are many ways to implement this. For this example, we’re going to use an
enum called ShirtColor that has the variants Red and Blue (limiting the
number of colors available for simplicity). We represent the company’s
inventory with an Inventory struct that has a field named shirts that
contains a Vec<ShirtColor> representing the shirt colors currently in stock.
The method giveaway defined on Inventory gets the optional shirt color
preference of the free-shirt winner, and it returns the shirt color the
person will get. This setup is shown in Listing 13-1.
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
Red,
Blue,
}
struct Inventory {
shirts: Vec<ShirtColor>,
}
impl Inventory {
fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
user_preference.unwrap_or_else(|| self.most_stocked())
}
fn most_stocked(&self) -> ShirtColor {
let mut num_red = 0;
let mut num_blue = 0;
for color in &self.shirts {
match color {
ShirtColor::Red => num_red += 1,
ShirtColor::Blue => num_blue += 1,
}
}
if num_red > num_blue {
ShirtColor::Red
} else {
ShirtColor::Blue
}
}
}
fn main() {
let store = Inventory {
shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
};
let user_pref1 = Some(ShirtColor::Red);
let giveaway1 = store.giveaway(user_pref1);
println!(
"The user with preference {:?} gets {:?}",
user_pref1, giveaway1
);
let user_pref2 = None;
let giveaway2 = store.giveaway(user_pref2);
println!(
"The user with preference {:?} gets {:?}",
user_pref2, giveaway2
);
}
The store defined in main has two blue shirts and one red shirt remaining
to distribute for this limited-edition promotion. We call the giveaway method
for a user with a preference for a red shirt and a user without any preference.
Again, this code could be implemented in many ways, and here, to focus on
closures, we’ve stuck to concepts you’ve already learned, except for the body of
the giveaway method that uses a closure. In the giveaway method, we get the
user preference as a parameter of type Option<ShirtColor> and call the
unwrap_or_else method on user_preference. The unwrap_or_else method on
Option<T> is defined by the standard library.
It takes one argument: a closure without any arguments that returns a value T
(the same type stored in the Some variant of the Option<T>, in this case
ShirtColor). If the Option<T> is the Some variant, unwrap_or_else
returns the value from within the Some. If the Option<T> is the None
variant, unwrap_or_else calls the closure and returns the value returned by
the closure.
We specify the closure expression || self.most_stocked() as the argument to
unwrap_or_else. This is a closure that takes no parameters itself (if the
closure had parameters, they would appear between the two vertical pipes). The
body of the closure calls self.most_stocked(). We’re defining the closure
here, and the implementation of unwrap_or_else will evaluate the closure
later if the result is needed.
Running this code prints the following:
$ cargo run
Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue
One interesting aspect here is that we’ve passed a closure that calls
self.most_stocked() on the current Inventory instance. The standard library
didn’t need to know anything about the Inventory or ShirtColor types we
defined, or the logic we want to use in this scenario. The closure captures an
immutable reference to the self Inventory instance and passes it with the
code we specify to the unwrap_or_else method. Functions, on the other hand,
are not able to capture their environment in this way.
Inferring and Annotating Closure Types
There are more differences between functions and closures. Closures don’t
usually require you to annotate the types of the parameters or the return value
like fn functions do. Type annotations are required on functions because the
types are part of an explicit interface exposed to your users. Defining this
interface rigidly is important for ensuring that everyone agrees on what types
of values a function uses and returns. Closures, on the other hand, aren’t used
in an exposed interface like this: They’re stored in variables, and they’re
used without naming them and exposing them to users of our library.
Closures are typically short and relevant only within a narrow context rather than in any arbitrary scenario. Within these limited contexts, the compiler can infer the types of the parameters and the return type, similar to how it’s able to infer the types of most variables (there are rare cases where the compiler needs closure type annotations too).
As with variables, we can add type annotations if we want to increase explicitness and clarity at the cost of being more verbose than is strictly necessary. Annotating the types for a closure would look like the definition shown in Listing 13-2. In this example, we’re defining a closure and storing it in a variable rather than defining the closure in the spot we pass it as an argument, as we did in Listing 13-1.
use std::thread;
use std::time::Duration;
fn generate_workout(intensity: u32, random_number: u32) {
let expensive_closure = |num: u32| -> u32 {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
println!("Today, do {} pushups!", expensive_closure(intensity));
println!("Next, do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!(
"Today, run for {} minutes!",
expensive_closure(intensity)
);
}
}
}
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
With type annotations added, the syntax of closures looks more similar to the syntax of functions. Here, we define a function that adds 1 to its parameter and a closure that has the same behavior, for comparison. We’ve added some spaces to line up the relevant parts. This illustrates how closure syntax is similar to function syntax except for the use of pipes and the amount of syntax that is optional:
fn add_one_v1 (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
The first line shows a function definition and the second line shows a fully
annotated closure definition. In the third line, we remove the type annotations
from the closure definition. In the fourth line, we remove the brackets, which
are optional because the closure body has only one expression. These are all
valid definitions that will produce the same behavior when they’re called. The
add_one_v3 and add_one_v4 lines require the closures to be evaluated to be
able to compile because the types will be inferred from their usage. This is
similar to let v = Vec::new(); needing either type annotations or values of
some type to be inserted into the Vec for Rust to be able to infer the type.
For closure definitions, the compiler will infer one concrete type for each of
their parameters and for their return value. For instance, Listing 13-3 shows
the definition of a short closure that just returns the value it receives as a
parameter. This closure isn’t very useful except for the purposes of this
example. Note that we haven’t added any type annotations to the definition.
Because there are no type annotations, we can call the closure with any type,
which we’ve done here with String the first time. If we then try to call
example_closure with an integer, we’ll get an error.
fn main() {
let example_closure = |x| x;
let s = example_closure(String::from("hello"));
let n = example_closure(5);
}
The compiler gives us this error:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
--> src/main.rs:5:29
|
5 | let n = example_closure(5);
| --------------- ^ expected `String`, found integer
| |
| arguments to this function are incorrect
|
note: expected because the closure was earlier called with an argument of type `String`
--> src/main.rs:4:29
|
4 | let s = example_closure(String::from("hello"));
| --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
| |
| in this closure call
note: closure parameter defined here
--> src/main.rs:2:28
|
2 | let example_closure = |x| x;
| ^
help: try using a conversion method
|
5 | let n = example_closure(5.to_string());
| ++++++++++++
For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error
The first time we call example_closure with the String value, the compiler
infers the type of x and the return type of the closure to be String. Those
types are then locked into the closure in example_closure, and we get a type
error when we next try to use a different type with the same closure.
Capturing References or Moving Ownership
Closures can capture values from their environment in three ways, which directly map to the three ways a function can take a parameter: borrowing immutably, borrowing mutably, and taking ownership. The closure will decide which of these to use based on what the body of the function does with the captured values.
In Listing 13-4, we define a closure that captures an immutable reference to
the vector named list because it only needs an immutable reference to print
the value.
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let only_borrows = || println!("From closure: {list:?}");
println!("Before calling closure: {list:?}");
only_borrows();
println!("After calling closure: {list:?}");
}
This example also illustrates that a variable can bind to a closure definition, and we can later call the closure by using the variable name and parentheses as if the variable name were a function name.
Because we can have multiple immutable references to list at the same time,
list is still accessible from the code before the closure definition, after
the closure definition but before the closure is called, and after the closure
is called. This code compiles, runs, and prints:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]
Next, in Listing 13-5, we change the closure body so that it adds an element to
the list vector. The closure now captures a mutable reference.
fn main() {
let mut list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
let mut borrows_mutably = || list.push(7);
borrows_mutably();
println!("After calling closure: {list:?}");
}
This code compiles, runs, and prints:
$ cargo run
Compiling closure-example v0.1.0 (file:///projects/closure-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]
Note that there’s no longer a println! between the definition and the call of
the borrows_mutably closure: When borrows_mutably is defined, it captures a
mutable reference to list. We don’t use the closure again after the closure
is called, so the mutable borrow ends. Between the closure definition and the
closure call, an immutable borrow to print isn’t allowed, because no other
borrows are allowed when there’s a mutable borrow. Try adding a println!
there to see what error message you get!
If you want to force the closure to take ownership of the values it uses in the
environment even though the body of the closure doesn’t strictly need
ownership, you can use the move keyword before the parameter list.
This technique is mostly useful when passing a closure to a new thread to move
the data so that it’s owned by the new thread. We’ll discuss threads and why
you would want to use them in detail in Chapter 16 when we talk about
concurrency, but for now, let’s briefly explore spawning a new thread using a
closure that needs the move keyword. Listing 13-6 shows Listing 13-4 modified
to print the vector in a new thread rather than in the main thread.
use std::thread;
fn main() {
let list = vec![1, 2, 3];
println!("Before defining closure: {list:?}");
thread::spawn(move || println!("From thread: {list:?}"))
.join()
.unwrap();
}
move to force the closure for the thread to take ownership of listWe spawn a new thread, giving the thread a closure to run as an argument. The
closure body prints out the list. In Listing 13-4, the closure only captured
list using an immutable reference because that’s the least amount of access
to list needed to print it. In this example, even though the closure body
still only needs an immutable reference, we need to specify that list should
be moved into the closure by putting the move keyword at the beginning of the
closure definition. If the main thread performed more operations before calling
join on the new thread, the new thread might finish before the rest of the
main thread finishes, or the main thread might finish first. If the main thread
maintained ownership of list but ended before the new thread and drops
list, the immutable reference in the thread would be invalid. Therefore, the
compiler requires that list be moved into the closure given to the new thread
so that the reference will be valid. Try removing the move keyword or using
list in the main thread after the closure is defined to see what compiler
errors you get!
Moving Captured Values Out of Closures
Once a closure has captured a reference or captured ownership of a value from the environment where the closure is defined (thus affecting what, if anything, is moved into the closure), the code in the body of the closure defines what happens to the references or values when the closure is evaluated later (thus affecting what, if anything, is moved out of the closure).
A closure body can do any of the following: Move a captured value out of the closure, mutate the captured value, neither move nor mutate the value, or capture nothing from the environment to begin with.
The way a closure captures and handles values from the environment affects
which traits the closure implements, and traits are how functions and structs
can specify what kinds of closures they can use. Closures will automatically
implement one, two, or all three of these Fn traits, in an additive fashion,
depending on how the closure’s body handles the values:
FnOnceapplies to closures that can be called once. All closures implement at least this trait because all closures can be called. A closure that moves captured values out of its body will only implementFnOnceand none of the otherFntraits because it can only be called once.FnMutapplies to closures that don’t move captured values out of their body but might mutate the captured values. These closures can be called more than once.Fnapplies to closures that don’t move captured values out of their body and don’t mutate captured values, as well as closures that capture nothing from their environment. These closures can be called more than once without mutating their environment, which is important in cases such as calling a closure multiple times concurrently.
Let’s look at the definition of the unwrap_or_else method on Option<T> that
we used in Listing 13-1:
impl<T> Option<T> {
pub fn unwrap_or_else<F>(self, f: F) -> T
where
F: FnOnce() -> T
{
match self {
Some(x) => x,
None => f(),
}
}
}
Recall that T is the generic type representing the type of the value in the
Some variant of an Option. That type T is also the return type of the
unwrap_or_else function: Code that calls unwrap_or_else on an
Option<String>, for example, will get a String.
Next, notice that the unwrap_or_else function has the additional generic type
parameter F. The F type is the type of the parameter named f, which is
the closure we provide when calling unwrap_or_else.
The trait bound specified on the generic type F is FnOnce() -> T, which
means F must be able to be called once, take no arguments, and return a T.
Using FnOnce in the trait bound expresses the constraint that
unwrap_or_else will not call f more than once. In the body of
unwrap_or_else, we can see that if the Option is Some, f won’t be
called. If the Option is None, f will be called once. Because all
closures implement FnOnce, unwrap_or_else accepts all three kinds of
closures and is as flexible as it can be.
Note: If what we want to do doesn’t require capturing a value from the
environment, we can use the name of a function rather than a closure where we
need something that implements one of the Fn traits. For example, on an
Option<Vec<T>> value, we could call unwrap_or_else(Vec::new) to get a
new, empty vector if the value is None. The compiler automatically
implements whichever of the Fn traits is applicable for a function
definition.
Now let’s look at the standard library method sort_by_key, defined on slices,
to see how that differs from unwrap_or_else and why sort_by_key uses
FnMut instead of FnOnce for the trait bound. The closure gets one argument
in the form of a reference to the current item in the slice being considered,
and it returns a value of type K that can be ordered. This function is useful
when you want to sort a slice by a particular attribute of each item. In
Listing 13-7, we have a list of Rectangle instances, and we use sort_by_key
to order them by their width attribute from low to high.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
list.sort_by_key(|r| r.width);
println!("{list:#?}");
}
sort_by_key to order rectangles by widthThis code prints:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/rectangles`
[
Rectangle {
width: 3,
height: 5,
},
Rectangle {
width: 7,
height: 12,
},
Rectangle {
width: 10,
height: 1,
},
]
The reason sort_by_key is defined to take an FnMut closure is that it calls
the closure multiple times: once for each item in the slice. The closure |r| r.width doesn’t capture, mutate, or move anything out from its environment, so
it meets the trait bound requirements.
In contrast, Listing 13-8 shows an example of a closure that implements just
the FnOnce trait, because it moves a value out of the environment. The
compiler won’t let us use this closure with sort_by_key.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut sort_operations = vec![];
let value = String::from("closure called");
list.sort_by_key(|r| {
sort_operations.push(value);
r.width
});
println!("{list:#?}");
}
FnOnce closure with sort_by_keyThis is a contrived, convoluted way (that doesn’t work) to try to count the
number of times sort_by_key calls the closure when sorting list. This code
attempts to do this counting by pushing value—a String from the closure’s
environment—into the sort_operations vector. The closure captures value and
then moves value out of the closure by transferring ownership of value to
the sort_operations vector. This closure can be called once; trying to call
it a second time wouldn’t work, because value would no longer be in the
environment to be pushed into sort_operations again! Therefore, this closure
only implements FnOnce. When we try to compile this code, we get this error
that value can’t be moved out of the closure because the closure must
implement FnMut:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
--> src/main.rs:18:30
|
15 | let value = String::from("closure called");
| ----- ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
| |
| captured outer variable
16 |
17 | list.sort_by_key(|r| {
| --- captured by this `FnMut` closure
18 | sort_operations.push(value);
| ^^^^^ `value` is moved here
|
help: consider cloning the value if the performance cost is acceptable
|
18 | sort_operations.push(value.clone());
| ++++++++
For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error
The error points to the line in the closure body that moves value out of the
environment. To fix this, we need to change the closure body so that it doesn’t
move values out of the environment. Keeping a counter in the environment and
incrementing its value in the closure body is a more straightforward way to
count the number of times the closure is called. The closure in Listing 13-9
works with sort_by_key because it is only capturing a mutable reference to the
num_sort_operations counter and can therefore be called more than once.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let mut list = [
Rectangle { width: 10, height: 1 },
Rectangle { width: 3, height: 5 },
Rectangle { width: 7, height: 12 },
];
let mut num_sort_operations = 0;
list.sort_by_key(|r| {
num_sort_operations += 1;
r.width
});
println!("{list:#?}, sorted in {num_sort_operations} operations");
}
FnMut closure with sort_by_key is allowed.The Fn traits are important when defining or using functions or types that
make use of closures. In the next section, we’ll discuss iterators. Many
iterator methods take closure arguments, so keep these closure details in mind
as we continue!
ประมวลผลชุดข้อมูลด้วย iterator
Processing a Series of Items with Iterators
The iterator pattern allows you to perform some task on a sequence of items in turn. An iterator is responsible for the logic of iterating over each item and determining when the sequence has finished. When you use iterators, you don’t have to reimplement that logic yourself.
In Rust, iterators are lazy, meaning they have no effect until you call
methods that consume the iterator to use it up. For example, the code in
Listing 13-10 creates an iterator over the items in the vector v1 by calling
the iter method defined on Vec<T>. This code by itself doesn’t do anything
useful.
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
}
The iterator is stored in the v1_iter variable. Once we’ve created an
iterator, we can use it in a variety of ways. In Listing 3-5, we iterated over
an array using a for loop to execute some code on each of its items. Under
the hood, this implicitly created and then consumed an iterator, but we glossed
over how exactly that works until now.
In the example in Listing 13-11, we separate the creation of the iterator from
the use of the iterator in the for loop. When the for loop is called using
the iterator in v1_iter, each element in the iterator is used in one
iteration of the loop, which prints out each value.
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter {
println!("Got: {val}");
}
}
for loopIn languages that don’t have iterators provided by their standard libraries, you would likely write this same functionality by starting a variable at index 0, using that variable to index into the vector to get a value, and incrementing the variable value in a loop until it reached the total number of items in the vector.
Iterators handle all of that logic for you, cutting down on repetitive code you could potentially mess up. Iterators give you more flexibility to use the same logic with many different kinds of sequences, not just data structures you can index into, like vectors. Let’s examine how iterators do that.
The Iterator Trait and the next Method
All iterators implement a trait named Iterator that is defined in the
standard library. The definition of the trait looks like this:
#![allow(unused)]
fn main() {
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// methods with default implementations elided
}
}
Notice that this definition uses some new syntax: type Item and Self::Item,
which are defining an associated type with this trait. We’ll talk about
associated types in depth in Chapter 20. For now, all you need to know is that
this code says implementing the Iterator trait requires that you also define
an Item type, and this Item type is used in the return type of the next
method. In other words, the Item type will be the type returned from the
iterator.
The Iterator trait only requires implementors to define one method: the
next method, which returns one item of the iterator at a time, wrapped in
Some, and, when iteration is over, returns None.
We can call the next method on iterators directly; Listing 13-12 demonstrates
what values are returned from repeated calls to next on the iterator created
from the vector.
#[cfg(test)]
mod tests {
#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
}
}
next method on an iteratorNote that we needed to make v1_iter mutable: Calling the next method on an
iterator changes internal state that the iterator uses to keep track of where
it is in the sequence. In other words, this code consumes, or uses up, the
iterator. Each call to next eats up an item from the iterator. We didn’t need
to make v1_iter mutable when we used a for loop, because the loop took
ownership of v1_iter and made it mutable behind the scenes.
Also note that the values we get from the calls to next are immutable
references to the values in the vector. The iter method produces an iterator
over immutable references. If we want to create an iterator that takes
ownership of v1 and returns owned values, we can call into_iter instead of
iter. Similarly, if we want to iterate over mutable references, we can call
iter_mut instead of iter.
Methods That Consume the Iterator
The Iterator trait has a number of different methods with default
implementations provided by the standard library; you can find out about these
methods by looking in the standard library API documentation for the Iterator
trait. Some of these methods call the next method in their definition, which
is why you’re required to implement the next method when implementing the
Iterator trait.
Methods that call next are called consuming adapters because calling them
uses up the iterator. One example is the sum method, which takes ownership of
the iterator and iterates through the items by repeatedly calling next, thus
consuming the iterator. As it iterates through, it adds each item to a running
total and returns the total when iteration is complete. Listing 13-13 has a
test illustrating a use of the sum method.
#[cfg(test)]
mod tests {
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
}
sum method to get the total of all items in the iteratorWe aren’t allowed to use v1_iter after the call to sum, because sum takes
ownership of the iterator we call it on.
Methods That Produce Other Iterators
Iterator adapters are methods defined on the Iterator trait that don’t
consume the iterator. Instead, they produce different iterators by changing
some aspect of the original iterator.
Listing 13-14 shows an example of calling the iterator adapter method map,
which takes a closure to call on each item as the items are iterated through.
The map method returns a new iterator that produces the modified items. The
closure here creates a new iterator in which each item from the vector will be
incremented by 1.
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
}
map to create a new iteratorHowever, this code produces a warning:
$ cargo run
Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
--> src/main.rs:4:5
|
4 | v1.iter().map(|x| x + 1);
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: iterators are lazy and do nothing unless consumed
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
4 | let _ = v1.iter().map(|x| x + 1);
| +++++++
warning: `iterators` (bin "iterators") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.47s
Running `target/debug/iterators`
The code in Listing 13-14 doesn’t do anything; the closure we’ve specified never gets called. The warning reminds us why: Iterator adapters are lazy, and we need to consume the iterator here.
To fix this warning and consume the iterator, we’ll use the collect method,
which we used with env::args in Listing 12-1. This method consumes the
iterator and collects the resultant values into a collection data type.
In Listing 13-15, we collect the results of iterating over the iterator that’s
returned from the call to map into a vector. This vector will end up
containing each item from the original vector, incremented by 1.
fn main() {
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
assert_eq!(v2, vec![2, 3, 4]);
}
map method to create a new iterator, and then calling the collect method to consume the new iterator and create a vectorBecause map takes a closure, we can specify any operation we want to perform
on each item. This is a great example of how closures let you customize some
behavior while reusing the iteration behavior that the Iterator trait
provides.
You can chain multiple calls to iterator adapters to perform complex actions in a readable way. But because all iterators are lazy, you have to call one of the consuming adapter methods to get results from calls to iterator adapters.
Closures That Capture Their Environment
Many iterator adapters take closures as arguments, and commonly the closures we’ll specify as arguments to iterator adapters will be closures that capture their environment.
For this example, we’ll use the filter method that takes a closure. The
closure gets an item from the iterator and returns a bool. If the closure
returns true, the value will be included in the iteration produced by
filter. If the closure returns false, the value won’t be included.
In Listing 13-16, we use filter with a closure that captures the shoe_size
variable from its environment to iterate over a collection of Shoe struct
instances. It will return only shoes that are the specified size.
#[derive(PartialEq, Debug)]
struct Shoe {
size: u32,
style: String,
}
fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filters_by_size() {
let shoes = vec![
Shoe {
size: 10,
style: String::from("sneaker"),
},
Shoe {
size: 13,
style: String::from("sandal"),
},
Shoe {
size: 10,
style: String::from("boot"),
},
];
let in_my_size = shoes_in_size(shoes, 10);
assert_eq!(
in_my_size,
vec![
Shoe {
size: 10,
style: String::from("sneaker")
},
Shoe {
size: 10,
style: String::from("boot")
},
]
);
}
}
filter method with a closure that captures shoe_sizeThe shoes_in_size function takes ownership of a vector of shoes and a shoe
size as parameters. It returns a vector containing only shoes of the specified
size.
In the body of shoes_in_size, we call into_iter to create an iterator that
takes ownership of the vector. Then, we call filter to adapt that iterator
into a new iterator that only contains elements for which the closure returns
true.
The closure captures the shoe_size parameter from the environment and
compares the value with each shoe’s size, keeping only shoes of the size
specified. Finally, calling collect gathers the values returned by the
adapted iterator into a vector that’s returned by the function.
The test shows that when we call shoes_in_size, we get back only shoes that
have the same size as the value we specified.
ปรับปรุงโปรเจกต์ I/O
Improving Our I/O Project
With this new knowledge about iterators, we can improve the I/O project in
Chapter 12 by using iterators to make places in the code clearer and more
concise. Let’s look at how iterators can improve our implementation of the
Config::build function and the search function.
Removing a clone Using an Iterator
In Listing 12-6, we added code that took a slice of String values and created
an instance of the Config struct by indexing into the slice and cloning the
values, allowing the Config struct to own those values. In Listing 13-17,
we’ve reproduced the implementation of the Config::build function as it was
in Listing 12-23.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Config::build function from Listing 12-23At the time, we said not to worry about the inefficient clone calls because
we would remove them in the future. Well, that time is now!
We needed clone here because we have a slice with String elements in the
parameter args, but the build function doesn’t own args. To return
ownership of a Config instance, we had to clone the values from the query
and file_path fields of Config so that the Config instance can own its
values.
With our new knowledge about iterators, we can change the build function to
take ownership of an iterator as its argument instead of borrowing a slice.
We’ll use the iterator functionality instead of the code that checks the length
of the slice and indexes into specific locations. This will clarify what the
Config::build function is doing because the iterator will access the values.
Once Config::build takes ownership of the iterator and stops using indexing
operations that borrow, we can move the String values from the iterator into
Config rather than calling clone and making a new allocation.
Using the Returned Iterator Directly
Open your I/O project’s src/main.rs file, which should look like this:
Filename: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
We’ll first change the start of the main function that we had in Listing
12-24 to the code in Listing 13-18, which this time uses an iterator. This
won’t compile until we update Config::build as well.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
env::args to Config::buildThe env::args function returns an iterator! Rather than collecting the
iterator values into a vector and then passing a slice to Config::build, now
we’re passing ownership of the iterator returned from env::args to
Config::build directly.
Next, we need to update the definition of Config::build. Let’s change the
signature of Config::build to look like Listing 13-19. This still won’t
compile, because we need to update the function body.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
if args.len() < 3 {
return Err("not enough arguments");
}
let query = args[1].clone();
let file_path = args[2].clone();
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Config::build to expect an iteratorThe standard library documentation for the env::args function shows that the
type of the iterator it returns is std::env::Args, and that type implements
the Iterator trait and returns String values.
We’ve updated the signature of the Config::build function so that the
parameter args has a generic type with the trait bounds impl Iterator<Item = String> instead of &[String]. This usage of the impl Trait syntax we
discussed in the “Using Traits as Parameters”
section of Chapter 10 means that args can be any type that implements the
Iterator trait and returns String items.
Because we’re taking ownership of args and we’ll be mutating args by
iterating over it, we can add the mut keyword into the specification of the
args parameter to make it mutable.
Using Iterator Trait Methods
Next, we’ll fix the body of Config::build. Because args implements the
Iterator trait, we know we can call the next method on it! Listing 13-20
updates the code from Listing 12-23 to use the next method.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let query = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Config::build to use iterator methodsRemember that the first value in the return value of env::args is the name of
the program. We want to ignore that and get to the next value, so first we call
next and do nothing with the return value. Then, we call next to get the
value we want to put in the query field of Config. If next returns
Some, we use a match to extract the value. If it returns None, it means
not enough arguments were given, and we return early with an Err value. We do
the same thing for the file_path value.
Clarifying Code with Iterator Adapters
We can also take advantage of iterators in the search function in our I/O
project, which is reproduced here in Listing 13-21 as it was in Listing 12-19.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
search function from Listing 12-19We can write this code in a more concise way using iterator adapter methods.
Doing so also lets us avoid having a mutable intermediate results vector. The
functional programming style prefers to minimize the amount of mutable state to
make code clearer. Removing the mutable state might enable a future enhancement
to make searching happen in parallel because we wouldn’t have to manage
concurrent access to the results vector. Listing 13-22 shows this change.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
search functionRecall that the purpose of the search function is to return all lines in
contents that contain the query. Similar to the filter example in Listing
13-16, this code uses the filter adapter to keep only the lines for which
line.contains(query) returns true. We then collect the matching lines into
another vector with collect. Much simpler! Feel free to make the same change
to use iterator methods in the search_case_insensitive function as well.
For a further improvement, return an iterator from the search function by
removing the call to collect and changing the return type to impl Iterator<Item = &'a str> so that the function becomes an iterator adapter.
Note that you’ll also need to update the tests! Search through a large file
using your minigrep tool before and after making this change to observe the
difference in behavior. Before this change, the program won’t print any results
until it has collected all of the results, but after the change, the results
will be printed as each matching line is found because the for loop in the
run function is able to take advantage of the laziness of the iterator.
Choosing Between Loops and Iterators
The next logical question is which style you should choose in your own code and why: the original implementation in Listing 13-21 or the version using iterators in Listing 13-22 (assuming we’re collecting all the results before returning them rather than returning the iterator). Most Rust programmers prefer to use the iterator style. It’s a bit tougher to get the hang of at first, but once you get a feel for the various iterator adapters and what they do, iterators can be easier to understand. Instead of fiddling with the various bits of looping and building new vectors, the code focuses on the high-level objective of the loop. This abstracts away some of the commonplace code so that it’s easier to see the concepts that are unique to this code, such as the filtering condition each element in the iterator must pass.
But are the two implementations truly equivalent? The intuitive assumption might be that the lower-level loop will be faster. Let’s talk about performance.
Performance: loop vs. iterator
Performance in Loops vs. Iterators
To determine whether to use loops or iterators, you need to know which
implementation is faster: the version of the search function with an explicit
for loop or the version with iterators.
We ran a benchmark by loading the entire contents of The Adventures of
Sherlock Holmes by Sir Arthur Conan Doyle into a String and looking for the
word the in the contents. Here are the results of the benchmark on the
version of search using the for loop and the version using iterators:
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
The two implementations have similar performance! We won’t explain the benchmark code here because the point is not to prove that the two versions are equivalent but to get a general sense of how these two implementations compare performance-wise.
For a more comprehensive benchmark, you should check using various texts of
various sizes as the contents, different words and words of different lengths
as the query, and all kinds of other variations. The point is this:
Iterators, although a high-level abstraction, get compiled down to roughly the
same code as if you’d written the lower-level code yourself. Iterators are one
of Rust’s zero-cost abstractions, by which we mean that using the abstraction
imposes no additional runtime overhead. This is analogous to how Bjarne
Stroustrup, the original designer and implementor of C++, defines
zero-overhead in his 2012 ETAPS keynote presentation “Foundations of C++”:
In general, C++ implementations obey the zero-overhead principle: What you don’t use, you don’t pay for. And further: What you do use, you couldn’t hand code any better.
In many cases, Rust code using iterators compiles to the same assembly you’d write by hand. Optimizations such as loop unrolling and eliminating bounds checking on array access apply and make the resultant code extremely efficient. Now that you know this, you can use iterators and closures without fear! They make code seem like it’s higher level but don’t impose a runtime performance penalty for doing so.
Summary
Closures and iterators are Rust features inspired by functional programming language ideas. They contribute to Rust’s capability to clearly express high-level ideas at low-level performance. The implementations of closures and iterators are such that runtime performance is not affected. This is part of Rust’s goal to strive to provide zero-cost abstractions.
Now that we’ve improved the expressiveness of our I/O project, let’s look at
some more features of cargo that will help us share the project with the
world.
More About Cargo and Crates.io
So far, we’ve used only the most basic features of Cargo to build, run, and test our code, but it can do a lot more. In this chapter, we’ll discuss some of its other, more advanced features to show you how to do the following:
- Customize your build through release profiles.
- Publish libraries on crates.io.
- Organize large projects with workspaces.
- Install binaries from crates.io.
- Extend Cargo using custom commands.
Cargo can do even more than the functionality we cover in this chapter, so for a full explanation of all its features, see its documentation.
ปรับ build ด้วย release profile
Customizing Builds with Release Profiles
In Rust, release profiles are predefined, customizable profiles with different configurations that allow a programmer to have more control over various options for compiling code. Each profile is configured independently of the others.
Cargo has two main profiles: the dev profile Cargo uses when you run cargo build, and the release profile Cargo uses when you run cargo build --release. The dev profile is defined with good defaults for development,
and the release profile has good defaults for release builds.
These profile names might be familiar from the output of your builds:
$ cargo build
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
$ cargo build --release
Finished `release` profile [optimized] target(s) in 0.32s
The dev and release are these different profiles used by the compiler.
Cargo has default settings for each of the profiles that apply when you haven’t
explicitly added any [profile.*] sections in the project’s Cargo.toml file.
By adding [profile.*] sections for any profile you want to customize, you
override any subset of the default settings. For example, here are the default
values for the opt-level setting for the dev and release profiles:
Filename: Cargo.toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
The opt-level setting controls the number of optimizations Rust will apply to
your code, with a range of 0 to 3. Applying more optimizations extends
compiling time, so if you’re in development and compiling your code often,
you’ll want fewer optimizations to compile faster even if the resultant code
runs slower. The default opt-level for dev is therefore 0. When you’re
ready to release your code, it’s best to spend more time compiling. You’ll only
compile in release mode once, but you’ll run the compiled program many times,
so release mode trades longer compile time for code that runs faster. That is
why the default opt-level for the release profile is 3.
You can override a default setting by adding a different value for it in Cargo.toml. For example, if we want to use optimization level 1 in the development profile, we can add these two lines to our project’s Cargo.toml file:
Filename: Cargo.toml
[profile.dev]
opt-level = 1
This code overrides the default setting of 0. Now when we run cargo build,
Cargo will use the defaults for the dev profile plus our customization to
opt-level. Because we set opt-level to 1, Cargo will apply more
optimizations than the default, but not as many as in a release build.
For the full list of configuration options and defaults for each profile, see Cargo’s documentation.
Publish crate ไปยัง Crates.io
Publishing a Crate to Crates.io
We’ve used packages from crates.io as dependencies of our project, but you can also share your code with other people by publishing your own packages. The crate registry at crates.io distributes the source code of your packages, so it primarily hosts code that is open source.
Rust and Cargo have features that make your published package easier for people to find and use. We’ll talk about some of these features next and then explain how to publish a package.
Making Useful Documentation Comments
Accurately documenting your packages will help other users know how and when to
use them, so it’s worth investing the time to write documentation. In Chapter
3, we discussed how to comment Rust code using two slashes, //. Rust also has
a particular kind of comment for documentation, known conveniently as a
documentation comment, that will generate HTML documentation. The HTML
displays the contents of documentation comments for public API items intended
for programmers interested in knowing how to use your crate as opposed to how
your crate is implemented.
Documentation comments use three slashes, ///, instead of two and support
Markdown notation for formatting the text. Place documentation comments just
before the item they’re documenting. Listing 14-1 shows documentation comments
for an add_one function in a crate named my_crate.
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
Here, we give a description of what the add_one function does, start a
section with the heading Examples, and then provide code that demonstrates
how to use the add_one function. We can generate the HTML documentation from
this documentation comment by running cargo doc. This command runs the
rustdoc tool distributed with Rust and puts the generated HTML documentation
in the target/doc directory.
For convenience, running cargo doc --open will build the HTML for your
current crate’s documentation (as well as the documentation for all of your
crate’s dependencies) and open the result in a web browser. Navigate to the
add_one function and you’ll see how the text in the documentation comments is
rendered, as shown in Figure 14-1.
Figure 14-1: The HTML documentation for the add_one
function
Commonly Used Sections
We used the # Examples Markdown heading in Listing 14-1 to create a section
in the HTML with the title “Examples.” Here are some other sections that crate
authors commonly use in their documentation:
- Panics: These are the scenarios in which the function being documented could panic. Callers of the function who don’t want their programs to panic should make sure they don’t call the function in these situations.
- Errors: If the function returns a
Result, describing the kinds of errors that might occur and what conditions might cause those errors to be returned can be helpful to callers so that they can write code to handle the different kinds of errors in different ways. - Safety: If the function is
unsafeto call (we discuss unsafety in Chapter 20), there should be a section explaining why the function is unsafe and covering the invariants that the function expects callers to uphold.
Most documentation comments don’t need all of these sections, but this is a good checklist to remind you of the aspects of your code users will be interested in knowing about.
Documentation Comments as Tests
Adding example code blocks in your documentation comments can help demonstrate
how to use your library and has an additional bonus: Running cargo test will
run the code examples in your documentation as tests! Nothing is better than
documentation with examples. But nothing is worse than examples that don’t work
because the code has changed since the documentation was written. If we run
cargo test with the documentation for the add_one function from Listing
14-1, we will see a section in the test results that looks like this:
Doc-tests my_crate
running 1 test
test src/lib.rs - add_one (line 5) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
Now, if we change either the function or the example so that the assert_eq!
in the example panics, and run cargo test again, we’ll see that the doc tests
catch that the example and the code are out of sync with each other!
Contained Item Comments
The style of doc comment //! adds documentation to the item that contains
the comments rather than to the items following the comments. We typically
use these doc comments inside the crate root file (src/lib.rs by convention)
or inside a module to document the crate or the module as a whole.
For example, to add documentation that describes the purpose of the my_crate
crate that contains the add_one function, we add documentation comments that
start with //! to the beginning of the src/lib.rs file, as shown in Listing
14-2.
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
/// Adds one to the number given.
// --snip--
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
my_crate crate as a wholeNotice there isn’t any code after the last line that begins with //!. Because
we started the comments with //! instead of ///, we’re documenting the item
that contains this comment rather than an item that follows this comment. In
this case, that item is the src/lib.rs file, which is the crate root. These
comments describe the entire crate.
When we run cargo doc --open, these comments will display on the front page
of the documentation for my_crate above the list of public items in the
crate, as shown in Figure 14-2.
Documentation comments within items are useful for describing crates and modules especially. Use them to explain the overall purpose of the container to help your users understand the crate’s organization.
Figure 14-2: The rendered documentation for my_crate,
including the comment describing the crate as a whole
Exporting a Convenient Public API
The structure of your public API is a major consideration when publishing a crate. People who use your crate are less familiar with the structure than you are and might have difficulty finding the pieces they want to use if your crate has a large module hierarchy.
In Chapter 7, we covered how to make items public using the pub keyword, and
how to bring items into a scope with the use keyword. However, the structure
that makes sense to you while you’re developing a crate might not be very
convenient for your users. You might want to organize your structs in a
hierarchy containing multiple levels, but then people who want to use a type
you’ve defined deep in the hierarchy might have trouble finding out that type
exists. They might also be annoyed at having to enter use my_crate::some_module::another_module::UsefulType; rather than use my_crate::UsefulType;.
The good news is that if the structure isn’t convenient for others to use
from another library, you don’t have to rearrange your internal organization:
Instead, you can re-export items to make a public structure that’s different
from your private structure by using pub use. Re-exporting takes a public
item in one location and makes it public in another location, as if it were
defined in the other location instead.
For example, say we made a library named art for modeling artistic concepts.
Within this library are two modules: a kinds module containing two enums
named PrimaryColor and SecondaryColor and a utils module containing a
function named mix, as shown in Listing 14-3.
//! # Art
//!
//! A library for modeling artistic concepts.
pub mod kinds {
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
unimplemented!();
}
}
art library with items organized into kinds and utils modulesFigure 14-3 shows what the front page of the documentation for this crate
generated by cargo doc would look like.
Figure 14-3: The front page of the documentation for art
that lists the kinds and utils modules
Note that the PrimaryColor and SecondaryColor types aren’t listed on the
front page, nor is the mix function. We have to click kinds and utils to
see them.
Another crate that depends on this library would need use statements that
bring the items from art into scope, specifying the module structure that’s
currently defined. Listing 14-4 shows an example of a crate that uses the
PrimaryColor and mix items from the art crate.
use art::kinds::PrimaryColor;
use art::utils::mix;
fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
art crate’s items with its internal structure exportedThe author of the code in Listing 14-4, which uses the art crate, had to
figure out that PrimaryColor is in the kinds module and mix is in the
utils module. The module structure of the art crate is more relevant to
developers working on the art crate than to those using it. The internal
structure doesn’t contain any useful information for someone trying to
understand how to use the art crate, but rather causes confusion because
developers who use it have to figure out where to look, and must specify the
module names in the use statements.
To remove the internal organization from the public API, we can modify the
art crate code in Listing 14-3 to add pub use statements to re-export the
items at the top level, as shown in Listing 14-5.
//! # Art
//!
//! A library for modeling artistic concepts.
pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;
pub mod kinds {
// --snip--
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
// --snip--
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
SecondaryColor::Orange
}
}
pub use statements to re-export itemsThe API documentation that cargo doc generates for this crate will now list
and link re-exports on the front page, as shown in Figure 14-4, making the
PrimaryColor and SecondaryColor types and the mix function easier to find.
Figure 14-4: The front page of the documentation for art
that lists the re-exports
The art crate users can still see and use the internal structure from Listing
14-3 as demonstrated in Listing 14-4, or they can use the more convenient
structure in Listing 14-5, as shown in Listing 14-6.
use art::PrimaryColor;
use art::mix;
fn main() {
// --snip--
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
art crateIn cases where there are many nested modules, re-exporting the types at the top
level with pub use can make a significant difference in the experience of
people who use the crate. Another common use of pub use is to re-export
definitions of a dependency in the current crate to make that crate’s
definitions part of your crate’s public API.
Creating a useful public API structure is more an art than a science, and you
can iterate to find the API that works best for your users. Choosing pub use
gives you flexibility in how you structure your crate internally and decouples
that internal structure from what you present to your users. Look at some of
the code of crates you’ve installed to see if their internal structure differs
from their public API.
Setting Up a Crates.io Account
Before you can publish any crates, you need to create an account on
crates.io and get an API token. To do so,
visit the home page at crates.io and log
in via a GitHub account. (The GitHub account is currently a requirement, but
the site might support other ways of creating an account in the future.) Once
you’re logged in, visit your account settings at
https://crates.io/me/ and retrieve your
API key. Then, run the cargo login command and paste your API key when prompted, like this:
$ cargo login
abcdefghijklmnopqrstuvwxyz012345
This command will inform Cargo of your API token and store it locally in ~/.cargo/credentials.toml. Note that this token is a secret: Do not share it with anyone else. If you do share it with anyone for any reason, you should revoke it and generate a new token on crates.io.
Adding Metadata to a New Crate
Let’s say you have a crate you want to publish. Before publishing, you’ll need
to add some metadata in the [package] section of the crate’s Cargo.toml
file.
Your crate will need a unique name. While you’re working on a crate locally,
you can name a crate whatever you’d like. However, crate names on
crates.io are allocated on a first-come,
first-served basis. Once a crate name is taken, no one else can publish a crate
with that name. Before attempting to publish a crate, search for the name you
want to use. If the name has been used, you will need to find another name and
edit the name field in the Cargo.toml file under the [package] section to
use the new name for publishing, like so:
Filename: Cargo.toml
[package]
name = "guessing_game"
Even if you’ve chosen a unique name, when you run cargo publish to publish
the crate at this point, you’ll get a warning and then an error:
$ cargo publish
Updating crates.io index
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
--snip--
error: failed to publish to registry at https://crates.io
Caused by:
the remote server responded with an error (status 400 Bad Request): missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for more information on configuring these fields
This results in an error because you’re missing some crucial information: A
description and license are required so that people will know what your crate
does and under what terms they can use it. In Cargo.toml, add a description
that’s just a sentence or two, because it will appear with your crate in search
results. For the license field, you need to give a license identifier
value. The Linux Foundation’s Software Package Data Exchange (SPDX)
lists the identifiers you can use for this value. For example, to specify that
you’ve licensed your crate using the MIT License, add the MIT identifier:
Filename: Cargo.toml
[package]
name = "guessing_game"
license = "MIT"
If you want to use a license that doesn’t appear in the SPDX, you need to place
the text of that license in a file, include the file in your project, and then
use license-file to specify the name of that file instead of using the
license key.
Guidance on which license is appropriate for your project is beyond the scope
of this book. Many people in the Rust community license their projects in the
same way as Rust by using a dual license of MIT OR Apache-2.0. This practice
demonstrates that you can also specify multiple license identifiers separated
by OR to have multiple licenses for your project.
With a unique name, the version, your description, and a license added, the Cargo.toml file for a project that is ready to publish might look like this:
Filename: Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2024"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"
[dependencies]
Cargo’s documentation describes other metadata you can specify to ensure that others can discover and use your crate more easily.
Publishing to Crates.io
Now that you’ve created an account, saved your API token, chosen a name for your crate, and specified the required metadata, you’re ready to publish! Publishing a crate uploads a specific version to crates.io for others to use.
Be careful, because a publish is permanent. The version can never be overwritten, and the code cannot be deleted except in certain circumstances. One major goal of Crates.io is to act as a permanent archive of code so that builds of all projects that depend on crates from crates.io will continue to work. Allowing version deletions would make fulfilling that goal impossible. However, there is no limit to the number of crate versions you can publish.
Run the cargo publish command again. It should succeed now:
$ cargo publish
Updating crates.io index
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
Packaged 6 files, 1.2KiB (895.0B compressed)
Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.19s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
Uploaded guessing_game v0.1.0 to registry `crates-io`
note: waiting for `guessing_game v0.1.0` to be available at registry
`crates-io`.
You may press ctrl-c to skip waiting; the crate should be available shortly.
Published guessing_game v0.1.0 at registry `crates-io`
Congratulations! You’ve now shared your code with the Rust community, and anyone can easily add your crate as a dependency of their project.
Publishing a New Version of an Existing Crate
When you’ve made changes to your crate and are ready to release a new version,
you change the version value specified in your Cargo.toml file and
republish. Use the Semantic Versioning rules to decide what an
appropriate next version number is, based on the kinds of changes you’ve made.
Then, run cargo publish to upload the new version.
Deprecating Versions from Crates.io
Although you can’t remove previous versions of a crate, you can prevent any future projects from adding them as a new dependency. This is useful when a crate version is broken for one reason or another. In such situations, Cargo supports yanking a crate version.
Yanking a version prevents new projects from depending on that version while allowing all existing projects that depend on it to continue. Essentially, a yank means that all projects with a Cargo.lock will not break, and any future Cargo.lock files generated will not use the yanked version.
To yank a version of a crate, in the directory of the crate that you’ve
previously published, run cargo yank and specify which version you want to
yank. For example, if we’ve published a crate named guessing_game version
1.0.1 and we want to yank it, then we’d run the following in the project
directory for guessing_game:
$ cargo yank --vers 1.0.1
Updating crates.io index
Yank [email protected]
By adding --undo to the command, you can also undo a yank and allow projects
to start depending on a version again:
$ cargo yank --vers 1.0.1 --undo
Updating crates.io index
Unyank [email protected]
A yank does not delete any code. It cannot, for example, delete accidentally uploaded secrets. If that happens, you must reset those secrets immediately.
Cargo workspace
Cargo Workspaces
In Chapter 12, we built a package that included a binary crate and a library crate. As your project develops, you might find that the library crate continues to get bigger and you want to split your package further into multiple library crates. Cargo offers a feature called workspaces that can help manage multiple related packages that are developed in tandem.
Creating a Workspace
A workspace is a set of packages that share the same Cargo.lock and output
directory. Let’s make a project using a workspace—we’ll use trivial code so
that we can concentrate on the structure of the workspace. There are multiple
ways to structure a workspace, so we’ll just show one common way. We’ll have a
workspace containing a binary and two libraries. The binary, which will provide
the main functionality, will depend on the two libraries. One library will
provide an add_one function and the other library an add_two function.
These three crates will be part of the same workspace. We’ll start by creating
a new directory for the workspace:
$ mkdir add
$ cd add
Next, in the add directory, we create the Cargo.toml file that will
configure the entire workspace. This file won’t have a [package] section.
Instead, it will start with a [workspace] section that will allow us to add
members to the workspace. We also make a point to use the latest and greatest
version of Cargo’s resolver algorithm in our workspace by setting the
resolver value to "3":
Filename: Cargo.toml
[workspace]
resolver = "3"
Next, we’ll create the adder binary crate by running cargo new within the
add directory:
$ cargo new adder
Created binary (application) `adder` package
Adding `adder` as member of workspace at `file:///projects/add`
Running cargo new inside a workspace also automatically adds the newly created
package to the members key in the [workspace] definition in the workspace
Cargo.toml, like this:
[workspace]
resolver = "3"
members = ["adder"]
At this point, we can build the workspace by running cargo build. The files
in your add directory should look like this:
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
The workspace has one target directory at the top level that the compiled
artifacts will be placed into; the adder package doesn’t have its own
target directory. Even if we were to run cargo build from inside the
adder directory, the compiled artifacts would still end up in add/target
rather than add/adder/target. Cargo structures the target directory in a
workspace like this because the crates in a workspace are meant to depend on
each other. If each crate had its own target directory, each crate would have
to recompile each of the other crates in the workspace to place the artifacts
in its own target directory. By sharing one target directory, the crates
can avoid unnecessary rebuilding.
Creating the Second Package in the Workspace
Next, let’s create another member package in the workspace and call it
add_one. Generate a new library crate named add_one:
$ cargo new add_one --lib
Created library `add_one` package
Adding `add_one` as member of workspace at `file:///projects/add`
The top-level Cargo.toml will now include the add_one path in the members
list:
Filename: Cargo.toml
[workspace]
resolver = "3"
members = ["adder", "add_one"]
Your add directory should now have these directories and files:
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
In the add_one/src/lib.rs file, let’s add an add_one function:
Filename: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
Now we can have the adder package with our binary depend on the add_one
package that has our library. First, we’ll need to add a path dependency on
add_one to adder/Cargo.toml.
Filename: adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
Cargo doesn’t assume that crates in a workspace will depend on each other, so we need to be explicit about the dependency relationships.
Next, let’s use the add_one function (from the add_one crate) in the
adder crate. Open the adder/src/main.rs file and change the main
function to call the add_one function, as in Listing 14-7.
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
add_one library crate from the adder crateLet’s build the workspace by running cargo build in the top-level add
directory!
$ cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
To run the binary crate from the add directory, we can specify which package
in the workspace we want to run by using the -p argument and the package name
with cargo run:
$ cargo run -p adder
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
This runs the code in adder/src/main.rs, which depends on the add_one crate.
Depending on an External Package
Notice that the workspace has only one Cargo.lock file at the top level,
rather than having a Cargo.lock in each crate’s directory. This ensures that
all crates are using the same version of all dependencies. If we add the rand
package to the adder/Cargo.toml and add_one/Cargo.toml files, Cargo will
resolve both of those to one version of rand and record that in the one
Cargo.lock. Making all crates in the workspace use the same dependencies
means the crates will always be compatible with each other. Let’s add the
rand crate to the [dependencies] section in the add_one/Cargo.toml file
so that we can use the rand crate in the add_one crate:
Filename: add_one/Cargo.toml
[dependencies]
rand = "0.8.5"
We can now add use rand; to the add_one/src/lib.rs file, and building the
whole workspace by running cargo build in the add directory will bring in
and compile the rand crate. We will get one warning because we aren’t
referring to the rand we brought into scope:
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
--> add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s
The top-level Cargo.lock now contains information about the dependency of
add_one on rand. However, even though rand is used somewhere in the
workspace, we can’t use it in other crates in the workspace unless we add
rand to their Cargo.toml files as well. For example, if we add use rand;
to the adder/src/main.rs file for the adder package, we’ll get an error:
$ cargo build
--snip--
Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
--> adder/src/main.rs:2:5
|
2 | use rand;
| ^^^^ no external crate `rand`
To fix this, edit the Cargo.toml file for the adder package and indicate
that rand is a dependency for it as well. Building the adder package will
add rand to the list of dependencies for adder in Cargo.lock, but no
additional copies of rand will be downloaded. Cargo will ensure that every
crate in every package in the workspace using the rand package will use the
same version as long as they specify compatible versions of rand, saving us
space and ensuring that the crates in the workspace will be compatible with
each other.
If crates in the workspace specify incompatible versions of the same dependency, Cargo will resolve each of them but will still try to resolve as few versions as possible.
Adding a Test to a Workspace
For another enhancement, let’s add a test of the add_one::add_one function
within the add_one crate:
Filename: add_one/src/lib.rs
pub fn add_one(x: i32) -> i32 {
x + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
assert_eq!(3, add_one(2));
}
}
Now run cargo test in the top-level add directory. Running cargo test in
a workspace structured like this one will run the tests for all the crates in
the workspace:
$ cargo test
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/main.rs (target/debug/deps/adder-3a47283c568d2b6a)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
The first section of the output shows that the it_works test in the add_one
crate passed. The next section shows that zero tests were found in the adder
crate, and then the last section shows that zero documentation tests were found
in the add_one crate.
We can also run tests for one particular crate in a workspace from the
top-level directory by using the -p flag and specifying the name of the crate
we want to test:
$ cargo test -p add_one
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests add_one
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
This output shows cargo test only ran the tests for the add_one crate and
didn’t run the adder crate tests.
If you publish the crates in the workspace to
crates.io, each crate in the workspace
will need to be published separately. Like cargo test, we can publish a
particular crate in our workspace by using the -p flag and specifying the
name of the crate we want to publish.
For additional practice, add an add_two crate to this workspace in a similar
way as the add_one crate!
As your project grows, consider using a workspace: It enables you to work with smaller, easier-to-understand components than one big blob of code. Furthermore, keeping the crates in a workspace can make coordination between crates easier if they are often changed at the same time.
ติดตั้ง binary ด้วย cargo install
Installing Binaries with cargo install
The cargo install command allows you to install and use binary crates
locally. This isn’t intended to replace system packages; it’s meant to be a
convenient way for Rust developers to install tools that others have shared on
crates.io. Note that you can only install
packages that have binary targets. A binary target is the runnable program
that is created if the crate has a src/main.rs file or another file specified
as a binary, as opposed to a library target that isn’t runnable on its own but
is suitable for including within other programs. Usually, crates have
information in the README file about whether a crate is a library, has a
binary target, or both.
All binaries installed with cargo install are stored in the installation
root’s bin folder. If you installed Rust using rustup.rs and don’t have any
custom configurations, this directory will be $HOME/.cargo/bin. Ensure that
this directory is in your $PATH to be able to run programs you’ve installed
with cargo install.
For example, in Chapter 12 we mentioned that there’s a Rust implementation of
the grep tool called ripgrep for searching files. To install ripgrep, we
can run the following:
$ cargo install ripgrep
Updating crates.io index
Downloaded ripgrep v14.1.1
Downloaded 1 crate (213.6 KB) in 0.40s
Installing ripgrep v14.1.1
--snip--
Compiling grep v0.3.2
Finished `release` profile [optimized + debuginfo] target(s) in 6.73s
Installing ~/.cargo/bin/rg
Installed package `ripgrep v14.1.1` (executable `rg`)
The second-to-last line of the output shows the location and the name of the
installed binary, which in the case of ripgrep is rg. As long as the
installation directory is in your $PATH, as mentioned previously, you can
then run rg --help and start using a faster, Rustier tool for searching files!
ขยาย Cargo ด้วยคำสั่ง custom
Extending Cargo with Custom Commands
Cargo is designed so that you can extend it with new subcommands without having
to modify it. If a binary in your $PATH is named cargo-something, you can
run it as if it were a Cargo subcommand by running cargo something. Custom
commands like this are also listed when you run cargo --list. Being able to
use cargo install to install extensions and then run them just like the
built-in Cargo tools is a super-convenient benefit of Cargo’s design!
Summary
Sharing code with Cargo and crates.io is part of what makes the Rust ecosystem useful for many different tasks. Rust’s standard library is small and stable, but crates are easy to share, use, and improve on a timeline different from that of the language. Don’t be shy about sharing code that’s useful to you on crates.io; it’s likely that it will be useful to someone else as well!
Smart Pointers
A pointer is a general concept for a variable that contains an address in
memory. This address refers to, or “points at,” some other data. The most
common kind of pointer in Rust is a reference, which you learned about in
Chapter 4. References are indicated by the & symbol and borrow the value they
point to. They don’t have any special capabilities other than referring to
data, and they have no overhead.
Smart pointers, on the other hand, are data structures that act like a pointer but also have additional metadata and capabilities. The concept of smart pointers isn’t unique to Rust: Smart pointers originated in C++ and exist in other languages as well. Rust has a variety of smart pointers defined in the standard library that provide functionality beyond that provided by references. To explore the general concept, we’ll look at a couple of different examples of smart pointers, including a reference counting smart pointer type. This pointer enables you to allow data to have multiple owners by keeping track of the number of owners and, when no owners remain, cleaning up the data.
In Rust, with its concept of ownership and borrowing, there is an additional difference between references and smart pointers: While references only borrow data, in many cases smart pointers own the data they point to.
Smart pointers are usually implemented using structs. Unlike an ordinary
struct, smart pointers implement the Deref and Drop traits. The Deref
trait allows an instance of the smart pointer struct to behave like a reference
so that you can write your code to work with either references or smart
pointers. The Drop trait allows you to customize the code that’s run when an
instance of the smart pointer goes out of scope. In this chapter, we’ll discuss
both of these traits and demonstrate why they’re important to smart pointers.
Given that the smart pointer pattern is a general design pattern used frequently in Rust, this chapter won’t cover every existing smart pointer. Many libraries have their own smart pointers, and you can even write your own. We’ll cover the most common smart pointers in the standard library:
Box<T>, for allocating values on the heapRc<T>, a reference counting type that enables multiple ownershipRef<T>andRefMut<T>, accessed throughRefCell<T>, a type that enforces the borrowing rules at runtime instead of compile time
In addition, we’ll cover the interior mutability pattern where an immutable type exposes an API for mutating an interior value. We’ll also discuss reference cycles: how they can leak memory and how to prevent them.
Let’s dive in!
ใช้ Box<T> ชี้ไปยังข้อมูลบน heap
Using Box<T> to Point to Data on the Heap
The most straightforward smart pointer is a box, whose type is written
Box<T>. Boxes allow you to store data on the heap rather than the stack.
What remains on the stack is the pointer to the heap data. Refer to Chapter 4
to review the difference between the stack and the heap.
Boxes don’t have performance overhead, other than storing their data on the heap instead of on the stack. But they don’t have many extra capabilities either. You’ll use them most often in these situations:
- When you have a type whose size can’t be known at compile time, and you want to use a value of that type in a context that requires an exact size
- When you have a large amount of data, and you want to transfer ownership but ensure that the data won’t be copied when you do so
- When you want to own a value, and you care only that it’s a type that implements a particular trait rather than being of a specific type
We’ll demonstrate the first situation in “Enabling Recursive Types with Boxes”. In the second case, transferring ownership of a large amount of data can take a long time because the data is copied around on the stack. To improve performance in this situation, we can store the large amount of data on the heap in a box. Then, only the small amount of pointer data is copied around on the stack, while the data it references stays in one place on the heap. The third case is known as a trait object, and “Using Trait Objects to Abstract over Shared Behavior” in Chapter 18 is devoted to that topic. So, what you learn here you’ll apply again in that section!
Storing Data on the Heap
Before we discuss the heap storage use case for Box<T>, we’ll cover the
syntax and how to interact with values stored within a Box<T>.
Listing 15-1 shows how to use a box to store an i32 value on the heap.
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
i32 value on the heap using a boxWe define the variable b to have the value of a Box that points to the
value 5, which is allocated on the heap. This program will print b = 5; in
this case, we can access the data in the box similarly to how we would if this
data were on the stack. Just like any owned value, when a box goes out of
scope, as b does at the end of main, it will be deallocated. The
deallocation happens both for the box (stored on the stack) and the data it
points to (stored on the heap).
Putting a single value on the heap isn’t very useful, so you won’t use boxes by
themselves in this way very often. Having values like a single i32 on the
stack, where they’re stored by default, is more appropriate in the majority of
situations. Let’s look at a case where boxes allow us to define types that we
wouldn’t be allowed to define if we didn’t have boxes.
Enabling Recursive Types with Boxes
A value of a recursive type can have another value of the same type as part of itself. Recursive types pose an issue because Rust needs to know at compile time how much space a type takes up. However, the nesting of values of recursive types could theoretically continue infinitely, so Rust can’t know how much space the value needs. Because boxes have a known size, we can enable recursive types by inserting a box in the recursive type definition.
As an example of a recursive type, let’s explore the cons list. This is a data type commonly found in functional programming languages. The cons list type we’ll define is straightforward except for the recursion; therefore, the concepts in the example we’ll work with will be useful anytime you get into more complex situations involving recursive types.
Understanding the Cons List
A cons list is a data structure that comes from the Lisp programming language
and its dialects, is made up of nested pairs, and is the Lisp version of a
linked list. Its name comes from the cons function (short for construct
function) in Lisp that constructs a new pair from its two arguments. By
calling cons on a pair consisting of a value and another pair, we can
construct cons lists made up of recursive pairs.
For example, here’s a pseudocode representation of a cons list containing the
list 1, 2, 3 with each pair in parentheses:
(1, (2, (3, Nil)))
Each item in a cons list contains two elements: the value of the current item
and of the next item. The last item in the list contains only a value called
Nil without a next item. A cons list is produced by recursively calling the
cons function. The canonical name to denote the base case of the recursion is
Nil. Note that this is not the same as the “null” or “nil” concept discussed
in Chapter 6, which is an invalid or absent value.
The cons list isn’t a commonly used data structure in Rust. Most of the time
when you have a list of items in Rust, Vec<T> is a better choice to use.
Other, more complex recursive data types are useful in various situations,
but by starting with the cons list in this chapter, we can explore how boxes
let us define a recursive data type without much distraction.
Listing 15-2 contains an enum definition for a cons list. Note that this code
won’t compile yet, because the List type doesn’t have a known size, which
we’ll demonstrate.
enum List {
Cons(i32, List),
Nil,
}
fn main() {}
i32 valuesNote: We’re implementing a cons list that holds only i32 values for the
purposes of this example. We could have implemented it using generics, as we
discussed in Chapter 10, to define a cons list type that could store values of
any type.
Using the List type to store the list 1, 2, 3 would look like the code in
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 enum to store the list 1, 2, 3The first Cons value holds 1 and another List value. This List value is
another Cons value that holds 2 and another List value. This List value
is one more Cons value that holds 3 and a List value, which is finally
Nil, the non-recursive variant that signals the end of the list.
If we try to compile the code in Listing 15-3, we get the error shown in 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
The error shows this type “has infinite size.” The reason is that we’ve defined
List with a variant that is recursive: It holds another value of itself
directly. As a result, Rust can’t figure out how much space it needs to store a
List value. Let’s break down why we get this error. First, we’ll look at how
Rust decides how much space it needs to store a value of a non-recursive type.
Computing the Size of a Non-Recursive Type
Recall the Message enum we defined in Listing 6-2 when we discussed enum
definitions in Chapter 6:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {}
To determine how much space to allocate for a Message value, Rust goes
through each of the variants to see which variant needs the most space. Rust
sees that Message::Quit doesn’t need any space, Message::Move needs enough
space to store two i32 values, and so forth. Because only one variant will be
used, the most space a Message value will need is the space it would take to
store the largest of its variants.
Contrast this with what happens when Rust tries to determine how much space a
recursive type like the List enum in Listing 15-2 needs. The compiler starts
by looking at the Cons variant, which holds a value of type i32 and a value
of type List. Therefore, Cons needs an amount of space equal to the size of
an i32 plus the size of a List. To figure out how much memory the List
type needs, the compiler looks at the variants, starting with the Cons
variant. The Cons variant holds a value of type i32 and a value of type
List, and this process continues infinitely, as shown in Figure 15-1.
Figure 15-1: An infinite List consisting of infinite
Cons variants
Getting a Recursive Type with a Known Size
Because Rust can’t figure out how much space to allocate for recursively defined types, the compiler gives an error with this helpful suggestion:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
|
2 | Cons(i32, Box<List>),
| ++++ +
In this suggestion, indirection means that instead of storing a value directly, we should change the data structure to store the value indirectly by storing a pointer to the value instead.
Because a Box<T> is a pointer, Rust always knows how much space a Box<T>
needs: A pointer’s size doesn’t change based on the amount of data it’s
pointing to. This means we can put a Box<T> inside the Cons variant instead
of another List value directly. The Box<T> will point to the next List
value that will be on the heap rather than inside the Cons variant.
Conceptually, we still have a list, created with lists holding other lists, but
this implementation is now more like placing the items next to one another
rather than inside one another.
We can change the definition of the List enum in Listing 15-2 and the usage
of the List in Listing 15-3 to the code in Listing 15-5, which will compile.
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 that uses Box<T> in order to have a known sizeThe Cons variant needs the size of an i32 plus the space to store the box’s
pointer data. The Nil variant stores no values, so it needs less space on the
stack than the Cons variant. We now know that any List value will take up
the size of an i32 plus the size of a box’s pointer data. By using a box,
we’ve broken the infinite, recursive chain, so the compiler can figure out the
size it needs to store a List value. Figure 15-2 shows what the Cons
variant looks like now.
Figure 15-2: A List that is not infinitely sized,
because Cons holds a Box
Boxes provide only the indirection and heap allocation; they don’t have any other special capabilities, like those we’ll see with the other smart pointer types. They also don’t have the performance overhead that these special capabilities incur, so they can be useful in cases like the cons list where the indirection is the only feature we need. We’ll look at more use cases for boxes in Chapter 18.
The Box<T> type is a smart pointer because it implements the Deref trait,
which allows Box<T> values to be treated like references. When a Box<T>
value goes out of scope, the heap data that the box is pointing to is cleaned
up as well because of the Drop trait implementation. These two traits will be
even more important to the functionality provided by the other smart pointer
types we’ll discuss in the rest of this chapter. Let’s explore these two traits
in more detail.
ปฏิบัติต่อ smart pointer เหมือน reference ปกติ
Treating Smart Pointers Like Regular References
Implementing the Deref trait allows you to customize the behavior of the
dereference operator * (not to be confused with the multiplication or glob
operator). By implementing Deref in such a way that a smart pointer can be
treated like a regular reference, you can write code that operates on
references and use that code with smart pointers too.
Let’s first look at how the dereference operator works with regular references.
Then, we’ll try to define a custom type that behaves like Box<T> and see why
the dereference operator doesn’t work like a reference on our newly defined
type. We’ll explore how implementing the Deref trait makes it possible for
smart pointers to work in ways similar to references. Then, we’ll look at
Rust’s deref coercion feature and how it lets us work with either references or
smart pointers.
Following the Reference to the Value
A regular reference is a type of pointer, and one way to think of a pointer is
as an arrow to a value stored somewhere else. In Listing 15-6, we create a
reference to an i32 value and then use the dereference operator to follow the
reference to the value.
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
i32 valueThe variable x holds an i32 value 5. We set y equal to a reference to
x. We can assert that x is equal to 5. However, if we want to make an
assertion about the value in y, we have to use *y to follow the reference
to the value it’s pointing to (hence, dereference) so that the compiler can
compare the actual value. Once we dereference y, we have access to the
integer value y is pointing to that we can compare with 5.
If we tried to write assert_eq!(5, y); instead, we would get this compilation
error:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
Comparing a number and a reference to a number isn’t allowed because they’re different types. We must use the dereference operator to follow the reference to the value it’s pointing to.
Using Box<T> Like a Reference
We can rewrite the code in Listing 15-6 to use a Box<T> instead of a
reference; the dereference operator used on the Box<T> in Listing 15-7
functions in the same way as the dereference operator used on the reference in
Listing 15-6.
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Box<i32>The main difference between Listing 15-7 and Listing 15-6 is that here we set
y to be an instance of a box pointing to a copied value of x rather than a
reference pointing to the value of x. In the last assertion, we can use the
dereference operator to follow the box’s pointer in the same way that we did
when y was a reference. Next, we’ll explore what is special about Box<T>
that enables us to use the dereference operator by defining our own box type.
Defining Our Own Smart Pointer
Let’s build a wrapper type similar to the Box<T> type provided by the
standard library to experience how smart pointer types behave differently from
references by default. Then, we’ll look at how to add the ability to use the
dereference operator.
Note: There’s one big difference between the MyBox<T> type we’re about to
build and the real Box<T>: Our version will not store its data on the heap.
We are focusing this example on Deref, so where the data is actually stored
is less important than the pointer-like behavior.
The Box<T> type is ultimately defined as a tuple struct with one element, so
Listing 15-8 defines a MyBox<T> type in the same way. We’ll also define a
new function to match the new function defined on Box<T>.
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {}
MyBox<T> typeWe define a struct named MyBox and declare a generic parameter T because we
want our type to hold values of any type. The MyBox type is a tuple struct
with one element of type T. The MyBox::new function takes one parameter of
type T and returns a MyBox instance that holds the value passed in.
Let’s try adding the main function in Listing 15-7 to Listing 15-8 and
changing it to use the MyBox<T> type we’ve defined instead of Box<T>. The
code in Listing 15-9 won’t compile, because Rust doesn’t know how to
dereference MyBox.
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
MyBox<T> in the same way we used references and Box<T>Here’s the resultant compilation error:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^ can't be dereferenced
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
Our MyBox<T> type can’t be dereferenced because we haven’t implemented that
ability on our type. To enable dereferencing with the * operator, we
implement the Deref trait.
Implementing the Deref Trait
As discussed in “Implementing a Trait on a Type” in
Chapter 10, to implement a trait we need to provide implementations for the
trait’s required methods. The Deref trait, provided by the standard library,
requires us to implement one method named deref that borrows self and
returns a reference to the inner data. Listing 15-10 contains an implementation
of Deref to add to the definition of MyBox<T>.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
Deref on MyBox<T>The type Target = T; syntax defines an associated type for the Deref trait
to use. Associated types are a slightly different way of declaring a generic
parameter, but you don’t need to worry about them for now; we’ll cover them in
more detail in Chapter 20.
We fill in the body of the deref method with &self.0 so that deref
returns a reference to the value we want to access with the * operator;
recall from “Creating Different Types with Tuple Structs” in Chapter 5 that .0 accesses the first value in a tuple struct.
The main function in Listing 15-9 that calls * on the MyBox<T> value now
compiles, and the assertions pass!
Without the Deref trait, the compiler can only dereference & references.
The deref method gives the compiler the ability to take a value of any type
that implements Deref and call the deref method to get a reference that
it knows how to dereference.
When we entered *y in Listing 15-9, behind the scenes Rust actually ran this
code:
*(y.deref())
Rust substitutes the * operator with a call to the deref method and then a
plain dereference so that we don’t have to think about whether or not we need
to call the deref method. This Rust feature lets us write code that functions
identically whether we have a regular reference or a type that implements
Deref.
The reason the deref method returns a reference to a value, and that the
plain dereference outside the parentheses in *(y.deref()) is still necessary,
has to do with the ownership system. If the deref method returned the value
directly instead of a reference to the value, the value would be moved out of
self. We don’t want to take ownership of the inner value inside MyBox<T> in
this case or in most cases where we use the dereference operator.
Note that the * operator is replaced with a call to the deref method and
then a call to the * operator just once, each time we use a * in our code.
Because the substitution of the * operator does not recurse infinitely, we
end up with data of type i32, which matches the 5 in assert_eq! in
Listing 15-9.
Using Deref Coercion in Functions and Methods
Deref coercion converts a reference to a type that implements the Deref
trait into a reference to another type. For example, deref coercion can convert
&String to &str because String implements the Deref trait such that it
returns &str. Deref coercion is a convenience Rust performs on arguments to
functions and methods, and it works only on types that implement the Deref
trait. It happens automatically when we pass a reference to a particular type’s
value as an argument to a function or method that doesn’t match the parameter
type in the function or method definition. A sequence of calls to the deref
method converts the type we provided into the type the parameter needs.
Deref coercion was added to Rust so that programmers writing function and
method calls don’t need to add as many explicit references and dereferences
with & and *. The deref coercion feature also lets us write more code that
can work for either references or smart pointers.
To see deref coercion in action, let’s use the MyBox<T> type we defined in
Listing 15-8 as well as the implementation of Deref that we added in Listing
15-10. Listing 15-11 shows the definition of a function that has a string slice
parameter.
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {}
hello function that has the parameter name of type &strWe can call the hello function with a string slice as an argument, such as
hello("Rust");, for example. Deref coercion makes it possible to call hello
with a reference to a value of type MyBox<String>, as shown in Listing 15-12.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
hello with a reference to a MyBox<String> value, which works because of deref coercionHere we’re calling the hello function with the argument &m, which is a
reference to a MyBox<String> value. Because we implemented the Deref trait
on MyBox<T> in Listing 15-10, Rust can turn &MyBox<String> into &String
by calling deref. The standard library provides an implementation of Deref
on String that returns a string slice, and this is in the API documentation
for Deref. Rust calls deref again to turn the &String into &str, which
matches the hello function’s definition.
If Rust didn’t implement deref coercion, we would have to write the code in
Listing 15-13 instead of the code in Listing 15-12 to call hello with a value
of type &MyBox<String>.
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
The (*m) dereferences the MyBox<String> into a String. Then, the & and
[..] take a string slice of the String that is equal to the whole string to
match the signature of hello. This code without deref coercions is harder to
read, write, and understand with all of these symbols involved. Deref coercion
allows Rust to handle these conversions for us automatically.
When the Deref trait is defined for the types involved, Rust will analyze the
types and use Deref::deref as many times as necessary to get a reference to
match the parameter’s type. The number of times that Deref::deref needs to be
inserted is resolved at compile time, so there is no runtime penalty for taking
advantage of deref coercion!
Handling Deref Coercion with Mutable References
Similar to how you use the Deref trait to override the * operator on
immutable references, you can use the DerefMut trait to override the *
operator on mutable references.
Rust does deref coercion when it finds types and trait implementations in three cases:
- From
&Tto&UwhenT: Deref<Target=U> - From
&mut Tto&mut UwhenT: DerefMut<Target=U> - From
&mut Tto&UwhenT: Deref<Target=U>
The first two cases are the same except that the second implements mutability.
The first case states that if you have a &T, and T implements Deref to
some type U, you can get a &U transparently. The second case states that
the same deref coercion happens for mutable references.
The third case is trickier: Rust will also coerce a mutable reference to an immutable one. But the reverse is not possible: Immutable references will never coerce to mutable references. Because of the borrowing rules, if you have a mutable reference, that mutable reference must be the only reference to that data (otherwise, the program wouldn’t compile). Converting one mutable reference to one immutable reference will never break the borrowing rules. Converting an immutable reference to a mutable reference would require that the initial immutable reference is the only immutable reference to that data, but the borrowing rules don’t guarantee that. Therefore, Rust can’t make the assumption that converting an immutable reference to a mutable reference is possible.
รันโค้ดตอน cleanup ด้วย trait Drop
Running Code on Cleanup with the Drop Trait
The second trait important to the smart pointer pattern is Drop, which lets
you customize what happens when a value is about to go out of scope. You can
provide an implementation for the Drop trait on any type, and that code can
be used to release resources like files or network connections.
We’re introducing Drop in the context of smart pointers because the
functionality of the Drop trait is almost always used when implementing a
smart pointer. For example, when a Box<T> is dropped, it will deallocate the
space on the heap that the box points to.
In some languages, for some types, the programmer must call code to free memory or resources every time they finish using an instance of those types. Examples include file handles, sockets, and locks. If the programmer forgets, the system might become overloaded and crash. In Rust, you can specify that a particular bit of code be run whenever a value goes out of scope, and the compiler will insert this code automatically. As a result, you don’t need to be careful about placing cleanup code everywhere in a program that an instance of a particular type is finished with—you still won’t leak resources!
You specify the code to run when a value goes out of scope by implementing the
Drop trait. The Drop trait requires you to implement one method named
drop that takes a mutable reference to self. To see when Rust calls drop,
let’s implement drop with println! statements for now.
Listing 15-14 shows a CustomSmartPointer struct whose only custom
functionality is that it will print Dropping CustomSmartPointer! when the
instance goes out of scope, to show when Rust runs the drop method.
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created");
}
CustomSmartPointer struct that implements the Drop trait where we would put our cleanup codeThe Drop trait is included in the prelude, so we don’t need to bring it into
scope. We implement the Drop trait on CustomSmartPointer and provide an
implementation for the drop method that calls println!. The body of the
drop method is where you would place any logic that you wanted to run when an
instance of your type goes out of scope. We’re printing some text here to
demonstrate visually when Rust will call drop.
In main, we create two instances of CustomSmartPointer and then print
CustomSmartPointers created. At the end of main, our instances of
CustomSmartPointer will go out of scope, and Rust will call the code we put
in the drop method, printing our final message. Note that we didn’t need to
call the drop method explicitly.
When we run this program, we’ll see the following output:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
Running `target/debug/drop-example`
CustomSmartPointers created
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
Rust automatically called drop for us when our instances went out of scope,
calling the code we specified. Variables are dropped in the reverse order of
their creation, so d was dropped before c. This example’s purpose is to
give you a visual guide to how the drop method works; usually you would
specify the cleanup code that your type needs to run rather than a print
message.
Unfortunately, it’s not straightforward to disable the automatic drop
functionality. Disabling drop isn’t usually necessary; the whole point of the
Drop trait is that it’s taken care of automatically. Occasionally, however,
you might want to clean up a value early. One example is when using smart
pointers that manage locks: You might want to force the drop method that
releases the lock so that other code in the same scope can acquire the lock.
Rust doesn’t let you call the Drop trait’s drop method manually; instead,
you have to call the std::mem::drop function provided by the standard library
if you want to force a value to be dropped before the end of its scope.
Trying to call the Drop trait’s drop method manually by modifying the
main function from Listing 15-14 won’t work, as shown in Listing 15-15.
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created");
c.drop();
println!("CustomSmartPointer dropped before the end of main");
}
drop method from the Drop trait manually to clean up earlyWhen we try to compile this code, we’ll get this error:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
error[E0040]: explicit use of destructor method
--> src/main.rs:16:7
|
16 | c.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
16 - c.drop();
16 + drop(c);
|
For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error
This error message states that we’re not allowed to explicitly call drop. The
error message uses the term destructor, which is the general programming term
for a function that cleans up an instance. A destructor is analogous to a
constructor, which creates an instance. The drop function in Rust is one
particular destructor.
Rust doesn’t let us call drop explicitly, because Rust would still
automatically call drop on the value at the end of main. This would cause a
double free error because Rust would be trying to clean up the same value twice.
We can’t disable the automatic insertion of drop when a value goes out of
scope, and we can’t call the drop method explicitly. So, if we need to force
a value to be cleaned up early, we use the std::mem::drop function.
The std::mem::drop function is different from the drop method in the Drop
trait. We call it by passing as an argument the value we want to force-drop.
The function is in the prelude, so we can modify main in Listing 15-15 to
call the drop function, as shown in Listing 15-16.
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created");
drop(c);
println!("CustomSmartPointer dropped before the end of main");
}
std::mem::drop to explicitly drop a value before it goes out of scopeRunning this code will print the following:
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main
The text Dropping CustomSmartPointer with data `some data`! is printed
between the CustomSmartPointer created and CustomSmartPointer dropped before the end of main text, showing that the drop method code is called to drop
c at that point.
You can use code specified in a Drop trait implementation in many ways to
make cleanup convenient and safe: For instance, you could use it to create your
own memory allocator! With the Drop trait and Rust’s ownership system, you
don’t have to remember to clean up, because Rust does it automatically.
You also don’t have to worry about problems resulting from accidentally
cleaning up values still in use: The ownership system that makes sure
references are always valid also ensures that drop gets called only once when
the value is no longer being used.
Now that we’ve examined Box<T> and some of the characteristics of smart
pointers, let’s look at a few other smart pointers defined in the standard
library.
Rc<T>: smart pointer แบบนับ reference
Rc<T>, the Reference-Counted Smart Pointer
In the majority of cases, ownership is clear: You know exactly which variable owns a given value. However, there are cases when a single value might have multiple owners. For example, in graph data structures, multiple edges might point to the same node, and that node is conceptually owned by all of the edges that point to it. A node shouldn’t be cleaned up unless it doesn’t have any edges pointing to it and so has no owners.
You have to enable multiple ownership explicitly by using the Rust type
Rc<T>, which is an abbreviation for reference counting. The Rc<T> type
keeps track of the number of references to a value to determine whether or not
the value is still in use. If there are zero references to a value, the value
can be cleaned up without any references becoming invalid.
Imagine Rc<T> as a TV in a family room. When one person enters to watch TV,
they turn it on. Others can come into the room and watch the TV. When the last
person leaves the room, they turn off the TV because it’s no longer being used.
If someone turns off the TV while others are still watching it, there would be
an uproar from the remaining TV watchers!
We use the Rc<T> type when we want to allocate some data on the heap for
multiple parts of our program to read and we can’t determine at compile time
which part will finish using the data last. If we knew which part would finish
last, we could just make that part the data’s owner, and the normal ownership
rules enforced at compile time would take effect.
Note that Rc<T> is only for use in single-threaded scenarios. When we discuss
concurrency in Chapter 16, we’ll cover how to do reference counting in
multithreaded programs.
Sharing Data
Let’s return to our cons list example in Listing 15-5. Recall that we defined
it using Box<T>. This time, we’ll create two lists that both share ownership
of a third list. Conceptually, this looks similar to Figure 15-3.
Figure 15-3: Two lists, b and c, sharing ownership of
a third list, a
We’ll create list a that contains 5 and then 10. Then, we’ll make two
more lists: b that starts with 3 and c that starts with 4. Both the b
and c lists will then continue on to the first a list containing 5 and
10. In other words, both lists will share the first list containing 5 and
10.
Trying to implement this scenario using our definition of List with Box<T>
won’t work, as shown in Listing 15-17.
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
Box<T> that try to share ownership of a third listWhen we compile this code, we get this error:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
|
note: if `List` implemented `Clone`, you could clone the value
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ consider implementing `Clone` for this type
...
10 | let b = Cons(3, Box::new(a));
| - you could clone this value
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
The Cons variants own the data they hold, so when we create the b list, a
is moved into b and b owns a. Then, when we try to use a again when
creating c, we’re not allowed to because a has been moved.
We could change the definition of Cons to hold references instead, but then
we would have to specify lifetime parameters. By specifying lifetime
parameters, we would be specifying that every element in the list will live at
least as long as the entire list. This is the case for the elements and lists
in Listing 15-17, but not in every scenario.
Instead, we’ll change our definition of List to use Rc<T> in place of
Box<T>, as shown in Listing 15-18. Each Cons variant will now hold a value
and an Rc<T> pointing to a List. When we create b, instead of taking
ownership of a, we’ll clone the Rc<List> that a is holding, thereby
increasing the number of references from one to two and letting a and b
share ownership of the data in that Rc<List>. We’ll also clone a when
creating c, increasing the number of references from two to three. Every time
we call Rc::clone, the reference count to the data within the Rc<List> will
increase, and the data won’t be cleaned up unless there are zero references to
it.
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
List that uses Rc<T>We need to add a use statement to bring Rc<T> into scope because it’s not
in the prelude. In main, we create the list holding 5 and 10 and store it
in a new Rc<List> in a. Then, when we create b and c, we call the
Rc::clone function and pass a reference to the Rc<List> in a as an
argument.
We could have called a.clone() rather than Rc::clone(&a), but Rust’s
convention is to use Rc::clone in this case. The implementation of
Rc::clone doesn’t make a deep copy of all the data like most types’
implementations of clone do. The call to Rc::clone only increments the
reference count, which doesn’t take much time. Deep copies of data can take a
lot of time. By using Rc::clone for reference counting, we can visually
distinguish between the deep-copy kinds of clones and the kinds of clones that
increase the reference count. When looking for performance problems in the
code, we only need to consider the deep-copy clones and can disregard calls to
Rc::clone.
Cloning to Increase the Reference Count
Let’s change our working example in Listing 15-18 so that we can see the
reference counts changing as we create and drop references to the Rc<List> in
a.
In Listing 15-19, we’ll change main so that it has an inner scope around list
c; then, we can see how the reference count changes when c goes out of
scope.
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
// --snip--
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("count after creating a = {}", Rc::strong_count(&a));
let b = Cons(3, Rc::clone(&a));
println!("count after creating b = {}", Rc::strong_count(&a));
{
let c = Cons(4, Rc::clone(&a));
println!("count after creating c = {}", Rc::strong_count(&a));
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}
At each point in the program where the reference count changes, we print the
reference count, which we get by calling the Rc::strong_count function. This
function is named strong_count rather than count because the Rc<T> type
also has a weak_count; we’ll see what weak_count is used for in “Preventing
Reference Cycles Using Weak<T>”.
This code prints the following:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
We can see that the Rc<List> in a has an initial reference count of 1;
then, each time we call clone, the count goes up by 1. When c goes out of
scope, the count goes down by 1. We don’t have to call a function to decrease
the reference count like we have to call Rc::clone to increase the reference
count: The implementation of the Drop trait decreases the reference count
automatically when an Rc<T> value goes out of scope.
What we can’t see in this example is that when b and then a go out of scope
at the end of main, the count is 0, and the Rc<List> is cleaned up
completely. Using Rc<T> allows a single value to have multiple owners, and
the count ensures that the value remains valid as long as any of the owners
still exist.
Via immutable references, Rc<T> allows you to share data between multiple
parts of your program for reading only. If Rc<T> allowed you to have multiple
mutable references too, you might violate one of the borrowing rules discussed
in Chapter 4: Multiple mutable borrows to the same place can cause data races
and inconsistencies. But being able to mutate data is very useful! In the next
section, we’ll discuss the interior mutability pattern and the RefCell<T>
type that you can use in conjunction with an Rc<T> to work with this
immutability restriction.
RefCell<T> และ Interior Mutability Pattern
RefCell<T> and the Interior Mutability Pattern
Interior mutability is a design pattern in Rust that allows you to mutate
data even when there are immutable references to that data; normally, this
action is disallowed by the borrowing rules. To mutate data, the pattern uses
unsafe code inside a data structure to bend Rust’s usual rules that govern
mutation and borrowing. Unsafe code indicates to the compiler that we’re
checking the rules manually instead of relying on the compiler to check them
for us; we will discuss unsafe code more in Chapter 20.
We can use types that use the interior mutability pattern only when we can
ensure that the borrowing rules will be followed at runtime, even though the
compiler can’t guarantee that. The unsafe code involved is then wrapped in a
safe API, and the outer type is still immutable.
Let’s explore this concept by looking at the RefCell<T> type that follows the
interior mutability pattern.
Enforcing Borrowing Rules at Runtime
Unlike Rc<T>, the RefCell<T> type represents single ownership over the data
it holds. So, what makes RefCell<T> different from a type like Box<T>?
Recall the borrowing rules you learned in Chapter 4:
- At any given time, you can have either one mutable reference or any number of immutable references (but not both).
- References must always be valid.
With references and Box<T>, the borrowing rules’ invariants are enforced at
compile time. With RefCell<T>, these invariants are enforced at runtime.
With references, if you break these rules, you’ll get a compiler error. With
RefCell<T>, if you break these rules, your program will panic and exit.
The advantages of checking the borrowing rules at compile time are that errors will be caught sooner in the development process, and there is no impact on runtime performance because all the analysis is completed beforehand. For those reasons, checking the borrowing rules at compile time is the best choice in the majority of cases, which is why this is Rust’s default.
The advantage of checking the borrowing rules at runtime instead is that certain memory-safe scenarios are then allowed, where they would’ve been disallowed by the compile-time checks. Static analysis, like the Rust compiler, is inherently conservative. Some properties of code are impossible to detect by analyzing the code: The most famous example is the Halting Problem, which is beyond the scope of this book but is an interesting topic to research.
Because some analysis is impossible, if the Rust compiler can’t be sure the
code complies with the ownership rules, it might reject a correct program; in
this way, it’s conservative. If Rust accepted an incorrect program, users
wouldn’t be able to trust the guarantees Rust makes. However, if Rust rejects a
correct program, the programmer will be inconvenienced, but nothing
catastrophic can occur. The RefCell<T> type is useful when you’re sure your
code follows the borrowing rules but the compiler is unable to understand and
guarantee that.
Similar to Rc<T>, RefCell<T> is only for use in single-threaded scenarios
and will give you a compile-time error if you try using it in a multithreaded
context. We’ll talk about how to get the functionality of RefCell<T> in a
multithreaded program in Chapter 16.
Here is a recap of the reasons to choose Box<T>, Rc<T>, or RefCell<T>:
Rc<T>enables multiple owners of the same data;Box<T>andRefCell<T>have single owners.Box<T>allows immutable or mutable borrows checked at compile time;Rc<T>allows only immutable borrows checked at compile time;RefCell<T>allows immutable or mutable borrows checked at runtime.- Because
RefCell<T>allows mutable borrows checked at runtime, you can mutate the value inside theRefCell<T>even when theRefCell<T>is immutable.
Mutating the value inside an immutable value is the interior mutability pattern. Let’s look at a situation in which interior mutability is useful and examine how it’s possible.
Using Interior Mutability
A consequence of the borrowing rules is that when you have an immutable value, you can’t borrow it mutably. For example, this code won’t compile:
fn main() {
let x = 5;
let y = &mut x;
}
If you tried to compile this code, you’d get the following error:
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
However, there are situations in which it would be useful for a value to mutate
itself in its methods but appear immutable to other code. Code outside the
value’s methods would not be able to mutate the value. Using RefCell<T> is
one way to get the ability to have interior mutability, but RefCell<T>
doesn’t get around the borrowing rules completely: The borrow checker in the
compiler allows this interior mutability, and the borrowing rules are checked
at runtime instead. If you violate the rules, you’ll get a panic! instead of
a compiler error.
Let’s work through a practical example where we can use RefCell<T> to mutate
an immutable value and see why that is useful.
Testing with Mock Objects
Sometimes during testing a programmer will use a type in place of another type, in order to observe particular behavior and assert that it’s implemented correctly. This placeholder type is called a test double. Think of it in the sense of a stunt double in filmmaking, where a person steps in and substitutes for an actor to do a particularly tricky scene. Test doubles stand in for other types when we’re running tests. Mock objects are specific types of test doubles that record what happens during a test so that you can assert that the correct actions took place.
Rust doesn’t have objects in the same sense as other languages have objects, and Rust doesn’t have mock object functionality built into the standard library as some other languages do. However, you can definitely create a struct that will serve the same purposes as a mock object.
Here’s the scenario we’ll test: We’ll create a library that tracks a value against a maximum value and sends messages based on how close to the maximum value the current value is. This library could be used to keep track of a user’s quota for the number of API calls they’re allowed to make, for example.
Our library will only provide the functionality of tracking how close to the
maximum a value is and what the messages should be at what times. Applications
that use our library will be expected to provide the mechanism for sending the
messages: The application could show the message to the user directly, send an
email, send a text message, or do something else. The library doesn’t need to
know that detail. All it needs is something that implements a trait we’ll
provide, called Messenger. Listing 15-20 shows the library code.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
One important part of this code is that the Messenger trait has one method
called send that takes an immutable reference to self and the text of the
message. This trait is the interface our mock object needs to implement so that
the mock can be used in the same way a real object is. The other important part
is that we want to test the behavior of the set_value method on the
LimitTracker. We can change what we pass in for the value parameter, but
set_value doesn’t return anything for us to make assertions on. We want to be
able to say that if we create a LimitTracker with something that implements
the Messenger trait and a particular value for max, the messenger is told
to send the appropriate messages when we pass different numbers for value.
We need a mock object that, instead of sending an email or text message when we
call send, will only keep track of the messages it’s told to send. We can
create a new instance of the mock object, create a LimitTracker that uses the
mock object, call the set_value method on LimitTracker, and then check that
the mock object has the messages we expect. Listing 15-21 shows an attempt to
implement a mock object to do just that, but the borrow checker won’t allow it.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
MockMessenger that isn’t allowed by the borrow checkerThis test code defines a MockMessenger struct that has a sent_messages
field with a Vec of String values to keep track of the messages it’s told
to send. We also define an associated function new to make it convenient to
create new MockMessenger values that start with an empty list of messages. We
then implement the Messenger trait for MockMessenger so that we can give a
MockMessenger to a LimitTracker. In the definition of the send method, we
take the message passed in as a parameter and store it in the MockMessenger
list of sent_messages.
In the test, we’re testing what happens when the LimitTracker is told to set
value to something that is more than 75 percent of the max value. First, we
create a new MockMessenger, which will start with an empty list of messages.
Then, we create a new LimitTracker and give it a reference to the new
MockMessenger and a max value of 100. We call the set_value method on
the LimitTracker with a value of 80, which is more than 75 percent of 100.
Then, we assert that the list of messages that the MockMessenger is keeping
track of should now have one message in it.
However, there’s one problem with this test, as shown here:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
We can’t modify the MockMessenger to keep track of the messages, because the
send method takes an immutable reference to self. We also can’t take the
suggestion from the error text to use &mut self in both the impl method and
the trait definition. We do not want to change the Messenger trait solely for
the sake of testing. Instead, we need to find a way to make our test code work
correctly with our existing design.
This is a situation in which interior mutability can help! We’ll store the
sent_messages within a RefCell<T>, and then the send method will be able
to modify sent_messages to store the messages we’ve seen. Listing 15-22 shows
what that looks like.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T> to mutate an inner value while the outer value is considered immutableThe sent_messages field is now of type RefCell<Vec<String>> instead of
Vec<String>. In the new function, we create a new RefCell<Vec<String>>
instance around the empty vector.
For the implementation of the send method, the first parameter is still an
immutable borrow of self, which matches the trait definition. We call
borrow_mut on the RefCell<Vec<String>> in self.sent_messages to get a
mutable reference to the value inside the RefCell<Vec<String>>, which is the
vector. Then, we can call push on the mutable reference to the vector to keep
track of the messages sent during the test.
The last change we have to make is in the assertion: To see how many items are
in the inner vector, we call borrow on the RefCell<Vec<String>> to get an
immutable reference to the vector.
Now that you’ve seen how to use RefCell<T>, let’s dig into how it works!
Tracking Borrows at Runtime
When creating immutable and mutable references, we use the & and &mut
syntax, respectively. With RefCell<T>, we use the borrow and borrow_mut
methods, which are part of the safe API that belongs to RefCell<T>. The
borrow method returns the smart pointer type Ref<T>, and borrow_mut
returns the smart pointer type RefMut<T>. Both types implement Deref, so we
can treat them like regular references.
The RefCell<T> keeps track of how many Ref<T> and RefMut<T> smart
pointers are currently active. Every time we call borrow, the RefCell<T>
increases its count of how many immutable borrows are active. When a Ref<T>
value goes out of scope, the count of immutable borrows goes down by 1. Just
like the compile-time borrowing rules, RefCell<T> lets us have many immutable
borrows or one mutable borrow at any point in time.
If we try to violate these rules, rather than getting a compiler error as we
would with references, the implementation of RefCell<T> will panic at
runtime. Listing 15-23 shows a modification of the implementation of send in
Listing 15-22. We’re deliberately trying to create two mutable borrows active
for the same scope to illustrate that RefCell<T> prevents us from doing this
at runtime.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T> will panicWe create a variable one_borrow for the RefMut<T> smart pointer returned
from borrow_mut. Then, we create another mutable borrow in the same way in
the variable two_borrow. This makes two mutable references in the same scope,
which isn’t allowed. When we run the tests for our library, the code in Listing
15-23 will compile without any errors, but the test will fail:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Notice that the code panicked with the message already borrowed: BorrowMutError. This is how RefCell<T> handles violations of the borrowing
rules at runtime.
Choosing to catch borrowing errors at runtime rather than compile time, as
we’ve done here, means you’d potentially be finding mistakes in your code later
in the development process: possibly not until your code was deployed to
production. Also, your code would incur a small runtime performance penalty as
a result of keeping track of the borrows at runtime rather than compile time.
However, using RefCell<T> makes it possible to write a mock object that can
modify itself to keep track of the messages it has seen while you’re using it
in a context where only immutable values are allowed. You can use RefCell<T>
despite its trade-offs to get more functionality than regular references
provide.
Allowing Multiple Owners of Mutable Data
A common way to use RefCell<T> is in combination with Rc<T>. Recall that
Rc<T> lets you have multiple owners of some data, but it only gives immutable
access to that data. If you have an Rc<T> that holds a RefCell<T>, you can
get a value that can have multiple owners and that you can mutate!
For example, recall the cons list example in Listing 15-18 where we used
Rc<T> to allow multiple lists to share ownership of another list. Because
Rc<T> holds only immutable values, we can’t change any of the values in the
list once we’ve created them. Let’s add in RefCell<T> for its ability to
change the values in the lists. Listing 15-24 shows that by using a
RefCell<T> in the Cons definition, we can modify the value stored in all
the lists.
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {a:?}");
println!("b after = {b:?}");
println!("c after = {c:?}");
}
Rc<RefCell<i32>> to create a List that we can mutateWe create a value that is an instance of Rc<RefCell<i32>> and store it in a
variable named value so that we can access it directly later. Then, we create
a List in a with a Cons variant that holds value. We need to clone
value so that both a and value have ownership of the inner 5 value
rather than transferring ownership from value to a or having a borrow
from value.
We wrap the list a in an Rc<T> so that when we create lists b and c,
they can both refer to a, which is what we did in Listing 15-18.
After we’ve created the lists in a, b, and c, we want to add 10 to the
value in value. We do this by calling borrow_mut on value, which uses the
automatic dereferencing feature we discussed in “Where’s the ->
Operator?” in Chapter 5 to dereference
the Rc<T> to the inner RefCell<T> value. The borrow_mut method returns a
RefMut<T> smart pointer, and we use the dereference operator on it and change
the inner value.
When we print a, b, and c, we can see that they all have the modified
value of 15 rather than 5:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
This technique is pretty neat! By using RefCell<T>, we have an outwardly
immutable List value. But we can use the methods on RefCell<T> that provide
access to its interior mutability so that we can modify our data when we need
to. The runtime checks of the borrowing rules protect us from data races, and
it’s sometimes worth trading a bit of speed for this flexibility in our data
structures. Note that RefCell<T> does not work for multithreaded code!
Mutex<T> is the thread-safe version of RefCell<T>, and we’ll discuss
Mutex<T> in Chapter 16.
Reference cycle ทำให้หน่วยความจำรั่วได้
Reference Cycles Can Leak Memory
Rust’s memory safety guarantees make it difficult, but not impossible, to
accidentally create memory that is never cleaned up (known as a memory leak).
Preventing memory leaks entirely is not one of Rust’s guarantees, meaning
memory leaks are memory safe in Rust. We can see that Rust allows memory leaks
by using Rc<T> and RefCell<T>: It’s possible to create references where
items refer to each other in a cycle. This creates memory leaks because the
reference count of each item in the cycle will never reach 0, and the values
will never be dropped.
Creating a Reference Cycle
Let’s look at how a reference cycle might happen and how to prevent it,
starting with the definition of the List enum and a tail method in Listing
15-25.
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {}
RefCell<T> so that we can modify what a Cons variant is referring toWe’re using another variation of the List definition from Listing 15-5. The
second element in the Cons variant is now RefCell<Rc<List>>, meaning that
instead of having the ability to modify the i32 value as we did in Listing
15-24, we want to modify the List value a Cons variant is pointing to.
We’re also adding a tail method to make it convenient for us to access the
second item if we have a Cons variant.
In Listing 15-26, we’re adding a main function that uses the definitions in
Listing 15-25. This code creates a list in a and a list in b that points to
the list in a. Then, it modifies the list in a to point to b, creating a
reference cycle. There are println! statements along the way to show what the
reference counts are at various points in this process.
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack.
// println!("a next item = {:?}", a.tail());
}
List values pointing to each otherWe create an Rc<List> instance holding a List value in the variable a
with an initial list of 5, Nil. We then create an Rc<List> instance holding
another List value in the variable b that contains the value 10 and
points to the list in a.
We modify a so that it points to b instead of Nil, creating a cycle. We
do that by using the tail method to get a reference to the
RefCell<Rc<List>> in a, which we put in the variable link. Then, we use
the borrow_mut method on the RefCell<Rc<List>> to change the value inside
from an Rc<List> that holds a Nil value to the Rc<List> in b.
When we run this code, keeping the last println! commented out for the
moment, we’ll get this output:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
Running `target/debug/cons-list`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
The reference count of the Rc<List> instances in both a and b is 2 after
we change the list in a to point to b. At the end of main, Rust drops the
variable b, which decreases the reference count of the b Rc<List>
instance from 2 to 1. The memory that Rc<List> has on the heap won’t be
dropped at this point because its reference count is 1, not 0. Then, Rust drops
a, which decreases the reference count of the a Rc<List> instance from 2
to 1 as well. This instance’s memory can’t be dropped either, because the other
Rc<List> instance still refers to it. The memory allocated to the list will
remain uncollected forever. To visualize this reference cycle, we’ve created
the diagram in Figure 15-4.
Figure 15-4: A reference cycle of lists a and b
pointing to each other
If you uncomment the last println! and run the program, Rust will try to
print this cycle with a pointing to b pointing to a and so forth until it
overflows the stack.
Compared to a real-world program, the consequences of creating a reference cycle in this example aren’t very dire: Right after we create the reference cycle, the program ends. However, if a more complex program allocated lots of memory in a cycle and held onto it for a long time, the program would use more memory than it needed and might overwhelm the system, causing it to run out of available memory.
Creating reference cycles is not easily done, but it’s not impossible either.
If you have RefCell<T> values that contain Rc<T> values or similar nested
combinations of types with interior mutability and reference counting, you must
ensure that you don’t create cycles; you can’t rely on Rust to catch them.
Creating a reference cycle would be a logic bug in your program that you should
use automated tests, code reviews, and other software development practices to
minimize.
Another solution for avoiding reference cycles is reorganizing your data
structures so that some references express ownership and some references don’t.
As a result, you can have cycles made up of some ownership relationships and
some non-ownership relationships, and only the ownership relationships affect
whether or not a value can be dropped. In Listing 15-25, we always want Cons
variants to own their list, so reorganizing the data structure isn’t possible.
Let’s look at an example using graphs made up of parent nodes and child nodes
to see when non-ownership relationships are an appropriate way to prevent
reference cycles.
Preventing Reference Cycles Using Weak<T>
So far, we’ve demonstrated that calling Rc::clone increases the
strong_count of an Rc<T> instance, and an Rc<T> instance is only cleaned
up if its strong_count is 0. You can also create a weak reference to the
value within an Rc<T> instance by calling Rc::downgrade and passing a
reference to the Rc<T>. Strong references are how you can share ownership
of an Rc<T> instance. Weak references don’t express an ownership
relationship, and their count doesn’t affect when an Rc<T> instance is
cleaned up. They won’t cause a reference cycle, because any cycle involving
some weak references will be broken once the strong reference count of values
involved is 0.
When you call Rc::downgrade, you get a smart pointer of type Weak<T>.
Instead of increasing the strong_count in the Rc<T> instance by 1, calling
Rc::downgrade increases the weak_count by 1. The Rc<T> type uses
weak_count to keep track of how many Weak<T> references exist, similar to
strong_count. The difference is the weak_count doesn’t need to be 0 for the
Rc<T> instance to be cleaned up.
Because the value that Weak<T> references might have been dropped, to do
anything with the value that a Weak<T> is pointing to you must make sure the
value still exists. Do this by calling the upgrade method on a Weak<T>
instance, which will return an Option<Rc<T>>. You’ll get a result of Some
if the Rc<T> value has not been dropped yet and a result of None if the
Rc<T> value has been dropped. Because upgrade returns an Option<Rc<T>>,
Rust will ensure that the Some case and the None case are handled, and
there won’t be an invalid pointer.
As an example, rather than using a list whose items know only about the next item, we’ll create a tree whose items know about their child items and their parent items.
Creating a Tree Data Structure
To start, we’ll build a tree with nodes that know about their child nodes.
We’ll create a struct named Node that holds its own i32 value as well as
references to its child Node values:
Filename: src/main.rs
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}
We want a Node to own its children, and we want to share that ownership with
variables so that we can access each Node in the tree directly. To do this,
we define the Vec<T> items to be values of type Rc<Node>. We also want to
modify which nodes are children of another node, so we have a RefCell<T> in
children around the Vec<Rc<Node>>.
Next, we’ll use our struct definition and create one Node instance named
leaf with the value 3 and no children, and another instance named branch
with the value 5 and leaf as one of its children, as shown in Listing 15-27.
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}
leaf node with no children and a branch node with leaf as one of its childrenWe clone the Rc<Node> in leaf and store that in branch, meaning the
Node in leaf now has two owners: leaf and branch. We can get from
branch to leaf through branch.children, but there’s no way to get from
leaf to branch. The reason is that leaf has no reference to branch and
doesn’t know they’re related. We want leaf to know that branch is its
parent. We’ll do that next.
Adding a Reference from a Child to Its Parent
To make the child node aware of its parent, we need to add a parent field to
our Node struct definition. The trouble is in deciding what the type of
parent should be. We know it can’t contain an Rc<T>, because that would
create a reference cycle with leaf.parent pointing to branch and
branch.children pointing to leaf, which would cause their strong_count
values to never be 0.
Thinking about the relationships another way, a parent node should own its children: If a parent node is dropped, its child nodes should be dropped as well. However, a child should not own its parent: If we drop a child node, the parent should still exist. This is a case for weak references!
So, instead of Rc<T>, we’ll make the type of parent use Weak<T>,
specifically a RefCell<Weak<Node>>. Now our Node struct definition looks
like this:
Filename: src/main.rs
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
A node will be able to refer to its parent node but doesn’t own its parent. In
Listing 15-28, we update main to use this new definition so that the leaf
node will have a way to refer to its parent, branch.
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
leaf node with a weak reference to its parent node, branchCreating the leaf node looks similar to Listing 15-27 with the exception of
the parent field: leaf starts out without a parent, so we create a new,
empty Weak<Node> reference instance.
At this point, when we try to get a reference to the parent of leaf by using
the upgrade method, we get a None value. We see this in the output from the
first println! statement:
leaf parent = None
When we create the branch node, it will also have a new Weak<Node>
reference in the parent field because branch doesn’t have a parent node. We
still have leaf as one of the children of branch. Once we have the Node
instance in branch, we can modify leaf to give it a Weak<Node> reference
to its parent. We use the borrow_mut method on the RefCell<Weak<Node>> in
the parent field of leaf, and then we use the Rc::downgrade function to
create a Weak<Node> reference to branch from the Rc<Node> in branch.
When we print the parent of leaf again, this time we’ll get a Some variant
holding branch: Now leaf can access its parent! When we print leaf, we
also avoid the cycle that eventually ended in a stack overflow like we had in
Listing 15-26; the Weak<Node> references are printed as (Weak):
leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
children: RefCell { value: [] } }] } })
The lack of infinite output indicates that this code didn’t create a reference
cycle. We can also tell this by looking at the values we get from calling
Rc::strong_count and Rc::weak_count.
Visualizing Changes to strong_count and weak_count
Let’s look at how the strong_count and weak_count values of the Rc<Node>
instances change by creating a new inner scope and moving the creation of
branch into that scope. By doing so, we can see what happens when branch is
created and then dropped when it goes out of scope. The modifications are shown
in Listing 15-29.
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
branch in an inner scope and examining strong and weak reference countsAfter leaf is created, its Rc<Node> has a strong count of 1 and a weak
count of 0. In the inner scope, we create branch and associate it with
leaf, at which point when we print the counts, the Rc<Node> in branch
will have a strong count of 1 and a weak count of 1 (for leaf.parent pointing
to branch with a Weak<Node>). When we print the counts in leaf, we’ll see
it will have a strong count of 2 because branch now has a clone of the
Rc<Node> of leaf stored in branch.children but will still have a weak
count of 0.
When the inner scope ends, branch goes out of scope and the strong count of
the Rc<Node> decreases to 0, so its Node is dropped. The weak count of 1
from leaf.parent has no bearing on whether or not Node is dropped, so we
don’t get any memory leaks!
If we try to access the parent of leaf after the end of the scope, we’ll get
None again. At the end of the program, the Rc<Node> in leaf has a strong
count of 1 and a weak count of 0 because the variable leaf is now the only
reference to the Rc<Node> again.
All of the logic that manages the counts and value dropping is built into
Rc<T> and Weak<T> and their implementations of the Drop trait. By
specifying that the relationship from a child to its parent should be a
Weak<T> reference in the definition of Node, you’re able to have parent
nodes point to child nodes and vice versa without creating a reference cycle
and memory leaks.
Summary
This chapter covered how to use smart pointers to make different guarantees and
trade-offs from those Rust makes by default with regular references. The
Box<T> type has a known size and points to data allocated on the heap. The
Rc<T> type keeps track of the number of references to data on the heap so
that the data can have multiple owners. The RefCell<T> type with its interior
mutability gives us a type that we can use when we need an immutable type but
need to change an inner value of that type; it also enforces the borrowing
rules at runtime instead of at compile time.
Also discussed were the Deref and Drop traits, which enable a lot of the
functionality of smart pointers. We explored reference cycles that can cause
memory leaks and how to prevent them using Weak<T>.
If this chapter has piqued your interest and you want to implement your own smart pointers, check out “The Rustonomicon” for more useful information.
Next, we’ll talk about concurrency in Rust. You’ll even learn about a few new smart pointers.
Fearless Concurrency
Handling concurrent programming safely and efficiently is another of Rust’s major goals. Concurrent programming, in which different parts of a program execute independently, and parallel programming, in which different parts of a program execute at the same time, are becoming increasingly important as more computers take advantage of their multiple processors. Historically, programming in these contexts has been difficult and error-prone. Rust hopes to change that.
Initially, the Rust team thought that ensuring memory safety and preventing concurrency problems were two separate challenges to be solved with different methods. Over time, the team discovered that the ownership and type systems are a powerful set of tools to help manage memory safety and concurrency problems! By leveraging ownership and type checking, many concurrency errors are compile-time errors in Rust rather than runtime errors. Therefore, rather than making you spend lots of time trying to reproduce the exact circumstances under which a runtime concurrency bug occurs, incorrect code will refuse to compile and present an error explaining the problem. As a result, you can fix your code while you’re working on it rather than potentially after it has been shipped to production. We’ve nicknamed this aspect of Rust fearless concurrency. Fearless concurrency allows you to write code that is free of subtle bugs and is easy to refactor without introducing new bugs.
Note: For simplicity’s sake, we’ll refer to many of the problems as concurrent rather than being more precise by saying concurrent and/or parallel. For this chapter, please mentally substitute concurrent and/or parallel whenever we use concurrent. In the next chapter, where the distinction matters more, we’ll be more specific.
Many languages are dogmatic about the solutions they offer for handling concurrent problems. For example, Erlang has elegant functionality for message-passing concurrency but has only obscure ways to share state between threads. Supporting only a subset of possible solutions is a reasonable strategy for higher-level languages because a higher-level language promises benefits from giving up some control to gain abstractions. However, lower-level languages are expected to provide the solution with the best performance in any given situation and have fewer abstractions over the hardware. Therefore, Rust offers a variety of tools for modeling problems in whatever way is appropriate for your situation and requirements.
Here are the topics we’ll cover in this chapter:
- How to create threads to run multiple pieces of code at the same time
- Message-passing concurrency, where channels send messages between threads
- Shared-state concurrency, where multiple threads have access to some piece of data
- The
SyncandSendtraits, which extend Rust’s concurrency guarantees to user-defined types as well as types provided by the standard library
ใช้ thread รันโค้ดพร้อมกัน
Using Threads to Run Code Simultaneously
In most current operating systems, an executed program’s code is run in a process, and the operating system will manage multiple processes at once. Within a program, you can also have independent parts that run simultaneously. The features that run these independent parts are called threads. For example, a web server could have multiple threads so that it can respond to more than one request at the same time.
Splitting the computation in your program into multiple threads to run multiple tasks at the same time can improve performance, but it also adds complexity. Because threads can run simultaneously, there’s no inherent guarantee about the order in which parts of your code on different threads will run. This can lead to problems, such as:
- Race conditions, in which threads are accessing data or resources in an inconsistent order
- Deadlocks, in which two threads are waiting for each other, preventing both threads from continuing
- Bugs that only happen in certain situations and are hard to reproduce and fix reliably
Rust attempts to mitigate the negative effects of using threads, but programming in a multithreaded context still takes careful thought and requires a code structure that is different from that in programs running in a single thread.
Programming languages implement threads in a few different ways, and many operating systems provide an API the programming language can call for creating new threads. The Rust standard library uses a 1:1 model of thread implementation, whereby a program uses one operating system thread per one language thread. There are crates that implement other models of threading that make different trade-offs to the 1:1 model. (Rust’s async system, which we will see in the next chapter, provides another approach to concurrency as well.)
Creating a New Thread with spawn
To create a new thread, we call the thread::spawn function and pass it a
closure (we talked about closures in Chapter 13) containing the code we want to
run in the new thread. The example in Listing 16-1 prints some text from a main
thread and other text from a new thread.
use std::thread;
use std::time::Duration;
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
Note that when the main thread of a Rust program completes, all spawned threads are shut down, whether or not they have finished running. The output from this program might be a little different every time, but it will look similar to the following:
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
The calls to thread::sleep force a thread to stop its execution for a short
duration, allowing a different thread to run. The threads will probably take
turns, but that isn’t guaranteed: It depends on how your operating system
schedules the threads. In this run, the main thread printed first, even though
the print statement from the spawned thread appears first in the code. And even
though we told the spawned thread to print until i is 9, it only got to 5
before the main thread shut down.
If you run this code and only see output from the main thread, or don’t see any overlap, try increasing the numbers in the ranges to create more opportunities for the operating system to switch between the threads.
Waiting for All Threads to Finish
The code in Listing 16-1 not only stops the spawned thread prematurely most of the time due to the main thread ending, but because there is no guarantee on the order in which threads run, we also can’t guarantee that the spawned thread will get to run at all!
We can fix the problem of the spawned thread not running or of it ending
prematurely by saving the return value of thread::spawn in a variable. The
return type of thread::spawn is JoinHandle<T>. A JoinHandle<T> is an
owned value that, when we call the join method on it, will wait for its
thread to finish. Listing 16-2 shows how to use the JoinHandle<T> of the
thread we created in Listing 16-1 and how to call join to make sure the
spawned thread finishes before main exits.
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
JoinHandle<T> from thread::spawn to guarantee the thread is run to completionCalling join on the handle blocks the thread currently running until the
thread represented by the handle terminates. Blocking a thread means that
thread is prevented from performing work or exiting. Because we’ve put the call
to join after the main thread’s for loop, running Listing 16-2 should
produce output similar to this:
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 3 from the main thread!
hi number 2 from the spawned thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
The two threads continue alternating, but the main thread waits because of the
call to handle.join() and does not end until the spawned thread is finished.
But let’s see what happens when we instead move handle.join() before the
for loop in main, like this:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {i} from the spawned thread!");
thread::sleep(Duration::from_millis(1));
}
});
handle.join().unwrap();
for i in 1..5 {
println!("hi number {i} from the main thread!");
thread::sleep(Duration::from_millis(1));
}
}
The main thread will wait for the spawned thread to finish and then run its
for loop, so the output won’t be interleaved anymore, as shown here:
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
Small details, such as where join is called, can affect whether or not your
threads run at the same time.
Using move Closures with Threads
We’ll often use the move keyword with closures passed to thread::spawn
because the closure will then take ownership of the values it uses from the
environment, thus transferring ownership of those values from one thread to
another. In “Capturing References or Moving Ownership” in Chapter 13, we discussed move in the context of closures. Now we’ll
concentrate more on the interaction between move and thread::spawn.
Notice in Listing 16-1 that the closure we pass to thread::spawn takes no
arguments: We’re not using any data from the main thread in the spawned
thread’s code. To use data from the main thread in the spawned thread, the
spawned thread’s closure must capture the values it needs. Listing 16-3 shows
an attempt to create a vector in the main thread and use it in the spawned
thread. However, this won’t work yet, as you’ll see in a moment.
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
The closure uses v, so it will capture v and make it part of the closure’s
environment. Because thread::spawn runs this closure in a new thread, we
should be able to access v inside that new thread. But when we compile this
example, we get the following error:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src/main.rs:6:32
|
6 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
7 | println!("Here's a vector: {v:?}");
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src/main.rs:6:18
|
6 | let handle = thread::spawn(|| {
| __________________^
7 | | println!("Here's a vector: {v:?}");
8 | | });
| |______^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
For more information about this error, try `rustc --explain E0373`.
error: could not compile `threads` (bin "threads") due to 1 previous error
Rust infers how to capture v, and because println! only needs a reference
to v, the closure tries to borrow v. However, there’s a problem: Rust can’t
tell how long the spawned thread will run, so it doesn’t know whether the
reference to v will always be valid.
Listing 16-4 provides a scenario that’s more likely to have a reference to v
that won’t be valid.
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector: {v:?}");
});
drop(v); // oh no!
handle.join().unwrap();
}
v from a main thread that drops vIf Rust allowed us to run this code, there’s a possibility that the spawned
thread would be immediately put in the background without running at all. The
spawned thread has a reference to v inside, but the main thread immediately
drops v, using the drop function we discussed in Chapter 15. Then, when the
spawned thread starts to execute, v is no longer valid, so a reference to it
is also invalid. Oh no!
To fix the compiler error in Listing 16-3, we can use the error message’s advice:
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
6 | let handle = thread::spawn(move || {
| ++++
By adding the move keyword before the closure, we force the closure to take
ownership of the values it’s using rather than allowing Rust to infer that it
should borrow the values. The modification to Listing 16-3 shown in Listing
16-5 will compile and run as we intend.
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector: {v:?}");
});
handle.join().unwrap();
}
move keyword to force a closure to take ownership of the values it usesWe might be tempted to try the same thing to fix the code in Listing 16-4 where
the main thread called drop by using a move closure. However, this fix will
not work because what Listing 16-4 is trying to do is disallowed for a
different reason. If we added move to the closure, we would move v into the
closure’s environment, and we could no longer call drop on it in the main
thread. We would get this compiler error instead:
$ cargo run
Compiling threads v0.1.0 (file:///projects/threads)
error[E0382]: use of moved value: `v`
--> src/main.rs:10:10
|
4 | let v = vec![1, 2, 3];
| - move occurs because `v` has type `Vec<i32>`, which does not implement the `Copy` trait
5 |
6 | let handle = thread::spawn(move || {
| ------- value moved into closure here
7 | println!("Here's a vector: {v:?}");
| - variable moved due to use in closure
...
10 | drop(v); // oh no!
| ^ value used here after move
|
help: consider cloning the value before moving it into the closure
|
6 ~ let value = v.clone();
7 ~ let handle = thread::spawn(move || {
8 ~ println!("Here's a vector: {value:?}");
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `threads` (bin "threads") due to 1 previous error
Rust’s ownership rules have saved us again! We got an error from the code in
Listing 16-3 because Rust was being conservative and only borrowing v for the
thread, which meant the main thread could theoretically invalidate the spawned
thread’s reference. By telling Rust to move ownership of v to the spawned
thread, we’re guaranteeing to Rust that the main thread won’t use v anymore.
If we change Listing 16-4 in the same way, we’re then violating the ownership
rules when we try to use v in the main thread. The move keyword overrides
Rust’s conservative default of borrowing; it doesn’t let us violate the
ownership rules.
Now that we’ve covered what threads are and the methods supplied by the thread API, let’s look at some situations in which we can use threads.
ส่งข้อมูลระหว่าง thread ด้วย message passing
Transfer Data Between Threads with Message Passing
One increasingly popular approach to ensuring safe concurrency is message passing, where threads or actors communicate by sending each other messages containing data. Here’s the idea in a slogan from the Go language documentation: “Do not communicate by sharing memory; instead, share memory by communicating.”
To accomplish message-sending concurrency, Rust’s standard library provides an implementation of channels. A channel is a general programming concept by which data is sent from one thread to another.
You can imagine a channel in programming as being like a directional channel of water, such as a stream or a river. If you put something like a rubber duck into a river, it will travel downstream to the end of the waterway.
A channel has two halves: a transmitter and a receiver. The transmitter half is the upstream location where you put the rubber duck into the river, and the receiver half is where the rubber duck ends up downstream. One part of your code calls methods on the transmitter with the data you want to send, and another part checks the receiving end for arriving messages. A channel is said to be closed if either the transmitter or receiver half is dropped.
Here, we’ll work up to a program that has one thread to generate values and send them down a channel, and another thread that will receive the values and print them out. We’ll be sending simple values between threads using a channel to illustrate the feature. Once you’re familiar with the technique, you could use channels for any threads that need to communicate with each other, such as a chat system or a system where many threads perform parts of a calculation and send the parts to one thread that aggregates the results.
First, in Listing 16-6, we’ll create a channel but not do anything with it. Note that this won’t compile yet because Rust can’t tell what type of values we want to send over the channel.
use std::sync::mpsc;
fn main() {
let (tx, rx) = mpsc::channel();
}
tx and rxWe create a new channel using the mpsc::channel function; mpsc stands for
multiple producer, single consumer. In short, the way Rust’s standard library
implements channels means a channel can have multiple sending ends that
produce values but only one receiving end that consumes those values. Imagine
multiple streams flowing together into one big river: Everything sent down any
of the streams will end up in one river at the end. We’ll start with a single
producer for now, but we’ll add multiple producers when we get this example
working.
The mpsc::channel function returns a tuple, the first element of which is the
sending end—the transmitter—and the second element of which is the receiving
end—the receiver. The abbreviations tx and rx are traditionally used in
many fields for transmitter and receiver, respectively, so we name our
variables as such to indicate each end. We’re using a let statement with a
pattern that destructures the tuples; we’ll discuss the use of patterns in
let statements and destructuring in Chapter 19. For now, know that using a
let statement in this way is a convenient approach to extract the pieces of
the tuple returned by mpsc::channel.
Let’s move the transmitting end into a spawned thread and have it send one string so that the spawned thread is communicating with the main thread, as shown in Listing 16-7. This is like putting a rubber duck in the river upstream or sending a chat message from one thread to another.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
}
tx to a spawned thread and sending "hi"Again, we’re using thread::spawn to create a new thread and then using move
to move tx into the closure so that the spawned thread owns tx. The spawned
thread needs to own the transmitter to be able to send messages through the
channel.
The transmitter has a send method that takes the value we want to send. The
send method returns a Result<T, E> type, so if the receiver has already
been dropped and there’s nowhere to send a value, the send operation will
return an error. In this example, we’re calling unwrap to panic in case of an
error. But in a real application, we would handle it properly: Return to
Chapter 9 to review strategies for proper error handling.
In Listing 16-8, we’ll get the value from the receiver in the main thread. This is like retrieving the rubber duck from the water at the end of the river or receiving a chat message.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
"hi" in the main thread and printing itThe receiver has two useful methods: recv and try_recv. We’re using recv,
short for receive, which will block the main thread’s execution and wait
until a value is sent down the channel. Once a value is sent, recv will
return it in a Result<T, E>. When the transmitter closes, recv will return
an error to signal that no more values will be coming.
The try_recv method doesn’t block, but will instead return a Result<T, E>
immediately: an Ok value holding a message if one is available and an Err
value if there aren’t any messages this time. Using try_recv is useful if
this thread has other work to do while waiting for messages: We could write a
loop that calls try_recv every so often, handles a message if one is
available, and otherwise does other work for a little while until checking
again.
We’ve used recv in this example for simplicity; we don’t have any other work
for the main thread to do other than wait for messages, so blocking the main
thread is appropriate.
When we run the code in Listing 16-8, we’ll see the value printed from the main thread:
Got: hi
Perfect!
Transferring Ownership Through Channels
The ownership rules play a vital role in message sending because they help you
write safe, concurrent code. Preventing errors in concurrent programming is the
advantage of thinking about ownership throughout your Rust programs. Let’s do
an experiment to show how channels and ownership work together to prevent
problems: We’ll try to use a val value in the spawned thread after we’ve
sent it down the channel. Try compiling the code in Listing 16-9 to see why
this code isn’t allowed.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
println!("val is {val}");
});
let received = rx.recv().unwrap();
println!("Got: {received}");
}
val after we’ve sent it down the channelHere, we try to print val after we’ve sent it down the channel via tx.send.
Allowing this would be a bad idea: Once the value has been sent to another
thread, that thread could modify or drop it before we try to use the value
again. Potentially, the other thread’s modifications could cause errors or
unexpected results due to inconsistent or nonexistent data. However, Rust gives
us an error if we try to compile the code in Listing 16-9:
$ cargo run
Compiling message-passing v0.1.0 (file:///projects/message-passing)
error[E0382]: borrow of moved value: `val`
--> src/main.rs:10:27
|
8 | let val = String::from("hi");
| --- move occurs because `val` has type `String`, which does not implement the `Copy` trait
9 | tx.send(val).unwrap();
| --- value moved here
10 | println!("val is {val}");
| ^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
error: could not compile `message-passing` (bin "message-passing") due to 1 previous error
Our concurrency mistake has caused a compile-time error. The send function
takes ownership of its parameter, and when the value is moved the receiver
takes ownership of it. This stops us from accidentally using the value again
after sending it; the ownership system checks that everything is okay.
Sending Multiple Values
The code in Listing 16-8 compiled and ran, but it didn’t clearly show us that two separate threads were talking to each other over the channel.
In Listing 16-10, we’ve made some modifications that will prove the code in Listing 16-8 is running concurrently: The spawned thread will now send multiple messages and pause for a second between each message.
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
}
This time, the spawned thread has a vector of strings that we want to send to
the main thread. We iterate over them, sending each individually, and pause
between each by calling the thread::sleep function with a Duration value of
one second.
In the main thread, we’re not calling the recv function explicitly anymore:
Instead, we’re treating rx as an iterator. For each value received, we’re
printing it. When the channel is closed, iteration will end.
When running the code in Listing 16-10, you should see the following output with a one-second pause in between each line:
Got: hi
Got: from
Got: the
Got: thread
Because we don’t have any code that pauses or delays in the for loop in the
main thread, we can tell that the main thread is waiting to receive values from
the spawned thread.
Creating Multiple Producers
Earlier we mentioned that mpsc was an acronym for multiple producer, single
consumer. Let’s put mpsc to use and expand the code in Listing 16-10 to
create multiple threads that all send values to the same receiver. We can do so
by cloning the transmitter, as shown in Listing 16-11.
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
// --snip--
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("thread"),
];
for val in vals {
tx1.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
thread::spawn(move || {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
for received in rx {
println!("Got: {received}");
}
// --snip--
}
This time, before we create the first spawned thread, we call clone on the
transmitter. This will give us a new transmitter we can pass to the first
spawned thread. We pass the original transmitter to a second spawned thread.
This gives us two threads, each sending different messages to the one receiver.
When you run the code, your output should look something like this:
Got: hi
Got: more
Got: from
Got: messages
Got: for
Got: the
Got: thread
Got: you
You might see the values in another order, depending on your system. This is
what makes concurrency interesting as well as difficult. If you experiment with
thread::sleep, giving it various values in the different threads, each run
will be more nondeterministic and create different output each time.
Now that we’ve looked at how channels work, let’s look at a different method of concurrency.
Shared-state concurrency
Shared-State Concurrency
Message passing is a fine way to handle concurrency, but it’s not the only way. Another method would be for multiple threads to access the same shared data. Consider this part of the slogan from the Go language documentation again: “Do not communicate by sharing memory.”
What would communicating by sharing memory look like? In addition, why would message-passing enthusiasts caution not to use memory sharing?
In a way, channels in any programming language are similar to single ownership because once you transfer a value down a channel, you should no longer use that value. Shared-memory concurrency is like multiple ownership: Multiple threads can access the same memory location at the same time. As you saw in Chapter 15, where smart pointers made multiple ownership possible, multiple ownership can add complexity because these different owners need managing. Rust’s type system and ownership rules greatly assist in getting this management correct. For an example, let’s look at mutexes, one of the more common concurrency primitives for shared memory.
Controlling Access with Mutexes
Mutex is an abbreviation for mutual exclusion, as in a mutex allows only one thread to access some data at any given time. To access the data in a mutex, a thread must first signal that it wants access by asking to acquire the mutex’s lock. The lock is a data structure that is part of the mutex that keeps track of who currently has exclusive access to the data. Therefore, the mutex is described as guarding the data it holds via the locking system.
Mutexes have a reputation for being difficult to use because you have to remember two rules:
- You must attempt to acquire the lock before using the data.
- When you’re done with the data that the mutex guards, you must unlock the data so that other threads can acquire the lock.
For a real-world metaphor for a mutex, imagine a panel discussion at a conference with only one microphone. Before a panelist can speak, they have to ask or signal that they want to use the microphone. When they get the microphone, they can talk for as long as they want to and then hand the microphone to the next panelist who requests to speak. If a panelist forgets to hand the microphone off when they’re finished with it, no one else is able to speak. If management of the shared microphone goes wrong, the panel won’t work as planned!
Management of mutexes can be incredibly tricky to get right, which is why so many people are enthusiastic about channels. However, thanks to Rust’s type system and ownership rules, you can’t get locking and unlocking wrong.
The API of Mutex<T>
As an example of how to use a mutex, let’s start by using a mutex in a single-threaded context, as shown in Listing 16-12.
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
}
println!("m = {m:?}");
}
Mutex<T> in a single-threaded context for simplicityAs with many types, we create a Mutex<T> using the associated function new.
To access the data inside the mutex, we use the lock method to acquire the
lock. This call will block the current thread so that it can’t do any work
until it’s our turn to have the lock.
The call to lock would fail if another thread holding the lock panicked. In
that case, no one would ever be able to get the lock, so we’ve chosen to
unwrap and have this thread panic if we’re in that situation.
After we’ve acquired the lock, we can treat the return value, named num in
this case, as a mutable reference to the data inside. The type system ensures
that we acquire a lock before using the value in m. The type of m is
Mutex<i32>, not i32, so we must call lock to be able to use the i32
value. We can’t forget; the type system won’t let us access the inner i32
otherwise.
The call to lock returns a type called MutexGuard, wrapped in a
LockResult that we handled with the call to unwrap. The MutexGuard type
implements Deref to point at our inner data; the type also has a Drop
implementation that releases the lock automatically when a MutexGuard goes
out of scope, which happens at the end of the inner scope. As a result, we
don’t risk forgetting to release the lock and blocking the mutex from being
used by other threads because the lock release happens automatically.
After dropping the lock, we can print the mutex value and see that we were able
to change the inner i32 to 6.
Shared Access to Mutex<T>
Now let’s try to share a value between multiple threads using Mutex<T>. We’ll
spin up 10 threads and have them each increment a counter value by 1, so the
counter goes from 0 to 10. The example in Listing 16-13 will have a compiler
error, and we’ll use that error to learn more about using Mutex<T> and how
Rust helps us use it correctly.
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Mutex<T>We create a counter variable to hold an i32 inside a Mutex<T>, as we did
in Listing 16-12. Next, we create 10 threads by iterating over a range of
numbers. We use thread::spawn and give all the threads the same closure: one
that moves the counter into the thread, acquires a lock on the Mutex<T> by
calling the lock method, and then adds 1 to the value in the mutex. When a
thread finishes running its closure, num will go out of scope and release the
lock so that another thread can acquire it.
In the main thread, we collect all the join handles. Then, as we did in Listing
16-2, we call join on each handle to make sure all the threads finish. At
that point, the main thread will acquire the lock and print the result of this
program.
We hinted that this example wouldn’t compile. Now let’s find out why!
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: borrow of moved value: `counter`
--> src/main.rs:21:29
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `std::sync::Mutex<i32>`, which does not implement the `Copy` trait
...
8 | for _ in 0..10 {
| -------------- inside of this loop
9 | let handle = thread::spawn(move || {
| ------- value moved into closure here, in previous iteration of loop
...
21 | println!("Result: {}", *counter.lock().unwrap());
| ^^^^^^^ value borrowed here after move
|
help: consider moving the expression out of the loop so it is only moved once
|
8 ~ let mut value = counter.lock();
9 ~ for _ in 0..10 {
10 | let handle = thread::spawn(move || {
11 ~ let mut num = value.unwrap();
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
The error message states that the counter value was moved in the previous
iteration of the loop. Rust is telling us that we can’t move the ownership of
lock counter into multiple threads. Let’s fix the compiler error with the
multiple-ownership method we discussed in Chapter 15.
Multiple Ownership with Multiple Threads
In Chapter 15, we gave a value to multiple owners by using the smart pointer
Rc<T> to create a reference-counted value. Let’s do the same here and see
what happens. We’ll wrap the Mutex<T> in Rc<T> in Listing 16-14 and clone
the Rc<T> before moving ownership to the thread.
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Rc<T> to allow multiple threads to own the Mutex<T>Once again, we compile and get… different errors! The compiler is teaching us a lot:
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `{closure@src/main.rs:11:36: 11:43}`
| | |
| | required by a bound introduced by this call
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________^ `Rc<std::sync::Mutex<i32>>` cannot be sent between threads safely
|
= help: within `{closure@src/main.rs:11:36: 11:43}`, the trait `Send` is not implemented for `Rc<std::sync::Mutex<i32>>`
note: required because it's used within this closure
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/std/src/thread/mod.rs:723:1
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` (bin "shared-state") due to 1 previous error
Wow, that error message is very wordy! Here’s the important part to focus on:
`Rc<Mutex<i32>>` cannot be sent between threads safely. The compiler is
also telling us the reason why: the trait `Send` is not implemented for `Rc<Mutex<i32>>`. We’ll talk about Send in the next section: It’s one of
the traits that ensures that the types we use with threads are meant for use in
concurrent situations.
Unfortunately, Rc<T> is not safe to share across threads. When Rc<T>
manages the reference count, it adds to the count for each call to clone and
subtracts from the count when each clone is dropped. But it doesn’t use any
concurrency primitives to make sure that changes to the count can’t be
interrupted by another thread. This could lead to wrong counts—subtle bugs that
could in turn lead to memory leaks or a value being dropped before we’re done
with it. What we need is a type that is exactly like Rc<T>, but that makes
changes to the reference count in a thread-safe way.
Atomic Reference Counting with Arc<T>
Fortunately, Arc<T> is a type like Rc<T> that is safe to use in
concurrent situations. The a stands for atomic, meaning it’s an atomically
reference-counted type. Atomics are an additional kind of concurrency
primitive that we won’t cover in detail here: See the standard library
documentation for std::sync::atomic for more
details. At this point, you just need to know that atomics work like primitive
types but are safe to share across threads.
You might then wonder why all primitive types aren’t atomic and why standard
library types aren’t implemented to use Arc<T> by default. The reason is that
thread safety comes with a performance penalty that you only want to pay when
you really need to. If you’re just performing operations on values within a
single thread, your code can run faster if it doesn’t have to enforce the
guarantees atomics provide.
Let’s return to our example: Arc<T> and Rc<T> have the same API, so we fix
our program by changing the use line, the call to new, and the call to
clone. The code in Listing 16-15 will finally compile and run.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Arc<T> to wrap the Mutex<T> to be able to share ownership across multiple threadsThis code will print the following:
Result: 10
We did it! We counted from 0 to 10, which may not seem very impressive, but it
did teach us a lot about Mutex<T> and thread safety. You could also use this
program’s structure to do more complicated operations than just incrementing a
counter. Using this strategy, you can divide a calculation into independent
parts, split those parts across threads, and then use a Mutex<T> to have each
thread update the final result with its part.
Note that if you are doing simple numerical operations, there are types simpler
than Mutex<T> types provided by the std::sync::atomic module of the
standard library. These types provide safe, concurrent,
atomic access to primitive types. We chose to use Mutex<T> with a primitive
type for this example so that we could concentrate on how Mutex<T> works.
Comparing RefCell<T>/Rc<T> and Mutex<T>/Arc<T>
You might have noticed that counter is immutable but that we could get a
mutable reference to the value inside it; this means Mutex<T> provides
interior mutability, as the Cell family does. In the same way we used
RefCell<T> in Chapter 15 to allow us to mutate contents inside an Rc<T>, we
use Mutex<T> to mutate contents inside an Arc<T>.
Another detail to note is that Rust can’t protect you from all kinds of logic
errors when you use Mutex<T>. Recall from Chapter 15 that using Rc<T> came
with the risk of creating reference cycles, where two Rc<T> values refer to
each other, causing memory leaks. Similarly, Mutex<T> comes with the risk of
creating deadlocks. These occur when an operation needs to lock two resources
and two threads have each acquired one of the locks, causing them to wait for
each other forever. If you’re interested in deadlocks, try creating a Rust
program that has a deadlock; then, research deadlock mitigation strategies for
mutexes in any language and have a go at implementing them in Rust. The
standard library API documentation for Mutex<T> and MutexGuard offers
useful information.
We’ll round out this chapter by talking about the Send and Sync traits and
how we can use them with custom types.
Concurrency ที่ขยายได้ด้วย Send และ Sync
Extensible Concurrency with Send and Sync
Interestingly, almost every concurrency feature we’ve talked about so far in this chapter has been part of the standard library, not the language. Your options for handling concurrency are not limited to the language or the standard library; you can write your own concurrency features or use those written by others.
However, among the key concurrency concepts that are embedded in the language
rather than the standard library are the std::marker traits Send and Sync.
Transferring Ownership Between Threads
The Send marker trait indicates that ownership of values of the type
implementing Send can be transferred between threads. Almost every Rust type
implements Send, but there are some exceptions, including Rc<T>: This
cannot implement Send because if you cloned an Rc<T> value and tried to
transfer ownership of the clone to another thread, both threads might update
the reference count at the same time. For this reason, Rc<T> is implemented
for use in single-threaded situations where you don’t want to pay the
thread-safe performance penalty.
Therefore, Rust’s type system and trait bounds ensure that you can never
accidentally send an Rc<T> value across threads unsafely. When we tried to do
this in Listing 16-14, we got the error the trait `Send` is not implemented for `Rc<Mutex<i32>>`. When we switched to Arc<T>, which does implement
Send, the code compiled.
Any type composed entirely of Send types is automatically marked as Send as
well. Almost all primitive types are Send, aside from raw pointers, which
we’ll discuss in Chapter 20.
Accessing from Multiple Threads
The Sync marker trait indicates that it is safe for the type implementing
Sync to be referenced from multiple threads. In other words, any type T
implements Sync if &T (an immutable reference to T) implements Send,
meaning the reference can be sent safely to another thread. Similar to Send,
primitive types all implement Sync, and types composed entirely of types that
implement Sync also implement Sync.
The smart pointer Rc<T> also doesn’t implement Sync for the same reasons
that it doesn’t implement Send. The RefCell<T> type (which we talked about
in Chapter 15) and the family of related Cell<T> types don’t implement
Sync. The implementation of borrow checking that RefCell<T> does at runtime
is not thread-safe. The smart pointer Mutex<T> implements Sync and can be
used to share access with multiple threads, as you saw in “Shared Access to
Mutex<T>”.
Implementing Send and Sync Manually Is Unsafe
Because types composed entirely of other types that implement the Send and
Sync traits also automatically implement Send and Sync, we don’t have to
implement those traits manually. As marker traits, they don’t even have any
methods to implement. They’re just useful for enforcing invariants related to
concurrency.
Manually implementing these traits involves implementing unsafe Rust code.
We’ll talk about using unsafe Rust code in Chapter 20; for now, the important
information is that building new concurrent types not made up of Send and
Sync parts requires careful thought to uphold the safety guarantees. “The
Rustonomicon” has more information about these guarantees and how to
uphold them.
Summary
This isn’t the last you’ll see of concurrency in this book: The next chapter focuses on async programming, and the project in Chapter 21 will use the concepts in this chapter in a more realistic situation than the smaller examples discussed here.
As mentioned earlier, because very little of how Rust handles concurrency is part of the language, many concurrency solutions are implemented as crates. These evolve more quickly than the standard library, so be sure to search online for the current, state-of-the-art crates to use in multithreaded situations.
The Rust standard library provides channels for message passing and smart
pointer types, such as Mutex<T> and Arc<T>, that are safe to use in
concurrent contexts. The type system and the borrow checker ensure that the
code using these solutions won’t end up with data races or invalid references.
Once you get your code to compile, you can rest assured that it will happily
run on multiple threads without the kinds of hard-to-track-down bugs common in
other languages. Concurrent programming is no longer a concept to be afraid of:
Go forth and make your programs concurrent, fearlessly!
Fundamentals of Asynchronous Programming: Async, Await, Futures, and Streams
Many operations we ask the computer to do can take a while to finish. It would be nice if we could do something else while we’re waiting for those long-running processes to complete. Modern computers offer two techniques for working on more than one operation at a time: parallelism and concurrency. Our programs’ logic, however, is written in a mostly linear fashion. We’d like to be able to specify the operations a program should perform and points at which a function could pause and some other part of the program could run instead, without needing to specify up front exactly the order and manner in which each bit of code should run. Asynchronous programming is an abstraction that lets us express our code in terms of potential pausing points and eventual results that takes care of the details of coordination for us.
This chapter builds on Chapter 16’s use of threads for parallelism and
concurrency by introducing an alternative approach to writing code: Rust’s
futures, streams, and the async and await syntax that let us express how
operations could be asynchronous, and the third-party crates that implement
asynchronous runtimes: code that manages and coordinates the execution of
asynchronous operations.
Let’s consider an example. Say you’re exporting a video you’ve created of a family celebration, an operation that could take anywhere from minutes to hours. The video export will use as much CPU and GPU power as it can. If you had only one CPU core and your operating system didn’t pause that export until it completed—that is, if it executed the export synchronously—you couldn’t do anything else on your computer while that task was running. That would be a pretty frustrating experience. Fortunately, your computer’s operating system can, and does, invisibly interrupt the export often enough to let you get other work done simultaneously.
Now say you’re downloading a video shared by someone else, which can also take a while but does not take up as much CPU time. In this case, the CPU has to wait for data to arrive from the network. While you can start reading the data once it starts to arrive, it might take some time for all of it to show up. Even once the data is all present, if the video is quite large, it could take at least a second or two to load it all. That might not sound like much, but it’s a very long time for a modern processor, which can perform billions of operations every second. Again, your operating system will invisibly interrupt your program to allow the CPU to perform other work while waiting for the network call to finish.
The video export is an example of a CPU-bound or compute-bound operation. It’s limited by the computer’s potential data processing speed within the CPU or GPU, and how much of that speed it can dedicate to the operation. The video download is an example of an I/O-bound operation, because it’s limited by the speed of the computer’s input and output; it can only go as fast as the data can be sent across the network.
In both of these examples, the operating system’s invisible interrupts provide a form of concurrency. That concurrency happens only at the level of the entire program, though: the operating system interrupts one program to let other programs get work done. In many cases, because we understand our programs at a much more granular level than the operating system does, we can spot opportunities for concurrency that the operating system can’t see.
For example, if we’re building a tool to manage file downloads, we should be able to write our program so that starting one download won’t lock up the UI, and users should be able to start multiple downloads at the same time. Many operating system APIs for interacting with the network are blocking, though; that is, they block the program’s progress until the data they’re processing is completely ready.
Note: This is how most function calls work, if you think about it. However, the term blocking is usually reserved for function calls that interact with files, the network, or other resources on the computer, because those are the cases where an individual program would benefit from the operation being non-blocking.
We could avoid blocking our main thread by spawning a dedicated thread to download each file. However, the overhead of the system resources used by those threads would eventually become a problem. It would be preferable if the call didn’t block in the first place, and instead we could define a number of tasks that we’d like our program to complete and allow the runtime to choose the best order and manner in which to run them.
That is exactly what Rust’s async (short for asynchronous) abstraction gives us. In this chapter, you’ll learn all about async as we cover the following topics:
- How to use Rust’s
asyncandawaitsyntax and execute asynchronous functions with a runtime - How to use the async model to solve some of the same challenges we looked at in Chapter 16
- How multithreading and async provide complementary solutions that you can combine in many cases
Before we see how async works in practice, though, we need to take a short detour to discuss the differences between parallelism and concurrency.
Parallelism and Concurrency
We’ve treated parallelism and concurrency as mostly interchangeable so far. Now we need to distinguish between them more precisely, because the differences will show up as we start working.
Consider the different ways a team could split up work on a software project. You could assign a single member multiple tasks, assign each member one task, or use a mix of the two approaches.
When an individual works on several different tasks before any of them is complete, this is concurrency. One way to implement concurrency is similar to having two different projects checked out on your computer, and when you get bored or stuck on one project, you switch to the other. You’re just one person, so you can’t make progress on both tasks at the exact same time, but you can multitask, making progress on one at a time by switching between them (see Figure 17-1).
When the team splits up a group of tasks by having each member take one task and work on it alone, this is parallelism. Each person on the team can make progress at the exact same time (see Figure 17-2).
In both of these workflows, you might have to coordinate between different tasks. Maybe you thought the task assigned to one person was totally independent from everyone else’s work, but it actually requires another person on the team to finish their task first. Some of the work could be done in parallel, but some of it was actually serial: it could only happen in a series, one task after the other, as in Figure 17-3.
Likewise, you might realize that one of your own tasks depends on another of your tasks. Now your concurrent work has also become serial.
Parallelism and concurrency can intersect with each other, too. If you learn that a colleague is stuck until you finish one of your tasks, you’ll probably focus all your efforts on that task to “unblock” your colleague. You and your coworker are no longer able to work in parallel, and you’re also no longer able to work concurrently on your own tasks.
The same basic dynamics come into play with software and hardware. On a machine with a single CPU core, the CPU can perform only one operation at a time, but it can still work concurrently. Using tools such as threads, processes, and async, the computer can pause one activity and switch to others before eventually cycling back to that first activity again. On a machine with multiple CPU cores, it can also do work in parallel. One core can be performing one task while another core performs a completely unrelated one, and those operations actually happen at the same time.
Running async code in Rust usually happens concurrently. Depending on the hardware, the operating system, and the async runtime we are using (more on async runtimes shortly), that concurrency may also use parallelism under the hood.
Now, let’s dive into how async programming in Rust actually works.
Future และ syntax ของ async
Futures and the Async Syntax
The key elements of asynchronous programming in Rust are futures and Rust’s
async and await keywords.
A future is a value that may not be ready now but will become ready at some
point in the future. (This same concept shows up in many languages, sometimes
under other names such as task or promise.) Rust provides a Future trait
as a building block so that different async operations can be implemented with
different data structures but with a common interface. In Rust, futures are
types that implement the Future trait. Each future holds its own information
about the progress that has been made and what “ready” means.
You can apply the async keyword to blocks and functions to specify that they
can be interrupted and resumed. Within an async block or async function, you
can use the await keyword to await a future (that is, wait for it to become
ready). Any point where you await a future within an async block or function is
a potential spot for that block or function to pause and resume. The process of
checking with a future to see if its value is available yet is called polling.
Some other languages, such as C# and JavaScript, also use async and await
keywords for async programming. If you’re familiar with those languages, you
may notice some significant differences in how Rust handles the syntax. That’s
for good reason, as we’ll see!
When writing async Rust, we use the async and await keywords most of the
time. Rust compiles them into equivalent code using the Future trait, much as
it compiles for loops into equivalent code using the Iterator trait.
Because Rust provides the Future trait, though, you can also implement it for
your own data types when you need to. Many of the functions we’ll see
throughout this chapter return types with their own implementations of
Future. We’ll return to the definition of the trait at the end of the chapter
and dig into more of how it works, but this is enough detail to keep us moving
forward.
This may all feel a bit abstract, so let’s write our first async program: a little web scraper. We’ll pass in two URLs from the command line, fetch both of them concurrently, and return the result of whichever one finishes first. This example will have a fair bit of new syntax, but don’t worry—we’ll explain everything you need to know as we go.
Our First Async Program
To keep the focus of this chapter on learning async rather than juggling parts
of the ecosystem, we’ve created the trpl crate (trpl is short for “The Rust
Programming Language”). It re-exports all the types, traits, and functions
you’ll need, primarily from the futures and
tokio crates. The futures crate is an official home
for Rust experimentation for async code, and it’s actually where the Future
trait was originally designed. Tokio is the most widely used async runtime in
Rust today, especially for web applications. There are other great runtimes out
there, and they may be more suitable for your purposes. We use the tokio
crate under the hood for trpl because it’s well tested and widely used.
In some cases, trpl also renames or wraps the original APIs to keep you
focused on the details relevant to this chapter. If you want to understand what
the crate does, we encourage you to check out its source code.
You’ll be able to see what crate each re-export comes from, and we’ve left
extensive comments explaining what the crate does.
Create a new binary project named hello-async and add the trpl crate as a
dependency:
$ cargo new hello-async
$ cd hello-async
$ cargo add trpl
Now we can use the various pieces provided by trpl to write our first async
program. We’ll build a little command line tool that fetches two web pages,
pulls the <title> element from each, and prints out the title of whichever
page finishes that whole process first.
Defining the page_title Function
Let’s start by writing a function that takes one page URL as a parameter, makes
a request to it, and returns the text of the <title> element (see Listing
17-1).
extern crate trpl; // required for mdbook test
fn main() {
// TODO: we'll add this next!
}
use trpl::Html;
async fn page_title(url: &str) -> Option<String> {
let response = trpl::get(url).await;
let response_text = response.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
First, we define a function named page_title and mark it with the async
keyword. Then we use the trpl::get function to fetch whatever URL is passed
in and add the await keyword to await the response. To get the text of the
response, we call its text method and once again await it with the await
keyword. Both of these steps are asynchronous. For the get function, we have
to wait for the server to send back the first part of its response, which will
include HTTP headers, cookies, and so on and can be delivered separately from
the response body. Especially if the body is very large, it can take some time
for it all to arrive. Because we have to wait for the entirety of the
response to arrive, the text method is also async.
We have to explicitly await both of these futures, because futures in Rust are
lazy: they don’t do anything until you ask them to with the await keyword.
(In fact, Rust will show a compiler warning if you don’t use a future.) This
might remind you of the discussion of iterators in the “Processing a Series of
Items with Iterators” section in Chapter 13.
Iterators do nothing unless you call their next method—whether directly or by
using for loops or methods such as map that use next under the hood.
Likewise, futures do nothing unless you explicitly ask them to. This laziness
allows Rust to avoid running async code until it’s actually needed.
Note: This is different from the behavior we saw when using thread::spawn
in the “Creating a New Thread with spawn”
section in Chapter 16, where the closure we passed to another thread started
running immediately. It’s also different from how many other languages
approach async. But it’s important for Rust to be able to provide its
performance guarantees, just as it is with iterators.
Once we have response_text, we can parse it into an instance of the Html
type using Html::parse. Instead of a raw string, we now have a data type we
can use to work with the HTML as a richer data structure. In particular, we can
use the select_first method to find the first instance of a given CSS
selector. By passing the string "title", we’ll get the first <title>
element in the document, if there is one. Because there may not be any matching
element, select_first returns an Option<ElementRef>. Finally, we use the
Option::map method, which lets us work with the item in the Option if it’s
present, and do nothing if it isn’t. (We could also use a match expression
here, but map is more idiomatic.) In the body of the function we supply to
map, we call inner_html on the title to get its content, which is a
String. When all is said and done, we have an Option<String>.
Notice that Rust’s await keyword goes after the expression you’re awaiting,
not before it. That is, it’s a postfix keyword. This may differ from what
you’re used to if you’ve used async in other languages, but in Rust it makes
chains of methods much nicer to work with. As a result, we could change the
body of page_title to chain the trpl::get and text function calls
together with await between them, as shown in Listing 17-2.
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
// TODO: we'll add this next!
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
await keywordWith that, we have successfully written our first async function! Before we add
some code in main to call it, let’s talk a little more about what we’ve
written and what it means.
When Rust sees a block marked with the async keyword, it compiles it into a
unique, anonymous data type that implements the Future trait. When Rust sees
a function marked with async, it compiles it into a non-async function
whose body is an async block. An async function’s return type is the type of
the anonymous data type the compiler creates for that async block.
Thus, writing async fn is equivalent to writing a function that returns a
future of the return type. To the compiler, a function definition such as the
async fn page_title in Listing 17-1 is roughly equivalent to a non-async
function defined like this:
#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test
use std::future::Future;
use trpl::Html;
fn page_title(url: &str) -> impl Future<Output = Option<String>> {
async move {
let text = trpl::get(url).await.text().await;
Html::parse(&text)
.select_first("title")
.map(|title| title.inner_html())
}
}
}
Let’s walk through each part of the transformed version:
- It uses the
impl Traitsyntax we discussed back in Chapter 10 in the “Traits as Parameters” section. - The returned value implements the
Futuretrait with an associated type ofOutput. Notice that theOutputtype isOption<String>, which is the same as the original return type from theasync fnversion ofpage_title. - All of the code called in the body of the original function is wrapped in
an
async moveblock. Remember that blocks are expressions. This whole block is the expression returned from the function. - This async block produces a value with the type
Option<String>, as just described. That value matches theOutputtype in the return type. This is just like other blocks you have seen. - The new function body is an
async moveblock because of how it uses theurlparameter. (We’ll talk much more aboutasyncversusasync movelater in the chapter.)
Now we can call page_title in main.
Executing an Async Function with a Runtime
To start, we’ll get the title for a single page, shown in Listing 17-3. Unfortunately, this code doesn’t compile yet.
extern crate trpl; // required for mdbook test
use trpl::Html;
async fn main() {
let args: Vec<String> = std::env::args().collect();
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
page_title function from main with a user-supplied argumentWe follow the same pattern we used to get command line arguments in the
“Accepting Command Line Arguments” section in
Chapter 12. Then we pass the URL argument to page_title and await the result.
Because the value produced by the future is an Option<String>, we use a
match expression to print different messages to account for whether the page
had a <title>.
The only place we can use the await keyword is in async functions or blocks,
and Rust won’t let us mark the special main function as async.
error[E0752]: `main` function is not allowed to be `async`
--> src/main.rs:6:1
|
6 | async fn main() {
| ^^^^^^^^^^^^^^^ `main` function is not allowed to be `async`
The reason main can’t be marked async is that async code needs a runtime:
a Rust crate that manages the details of executing asynchronous code. A
program’s main function can initialize a runtime, but it’s not a runtime
itself. (We’ll see more about why this is the case in a bit.) Every Rust
program that executes async code has at least one place where it sets up a
runtime that executes the futures.
Most languages that support async bundle a runtime, but Rust does not. Instead, there are many different async runtimes available, each of which makes different tradeoffs suitable to the use case it targets. For example, a high-throughput web server with many CPU cores and a large amount of RAM has very different needs than a microcontroller with a single core, a small amount of RAM, and no heap allocation ability. The crates that provide those runtimes also often supply async versions of common functionality such as file or network I/O.
Here, and throughout the rest of this chapter, we’ll use the block_on
function from the trpl crate, which takes a future as an argument and blocks
the current thread until this future runs to completion. Behind the scenes,
calling block_on sets up a runtime using the tokio crate that’s used to run
the future passed in (the trpl crate’s block_on behavior is similar to
other runtime crates’ block_on functions). Once the future completes,
block_on returns whatever value the future produced.
We could pass the future returned by page_title directly to block_on and,
once it completed, we could match on the resulting Option<String> as we tried
to do in Listing 17-3. However, for most of the examples in the chapter (and
most async code in the real world), we’ll be doing more than just one async
function call, so instead we’ll pass an async block and explicitly await the
result of the page_title call, as in Listing 17-4.
extern crate trpl; // required for mdbook test
use trpl::Html;
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let url = &args[1];
match page_title(url).await {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
})
}
async fn page_title(url: &str) -> Option<String> {
let response_text = trpl::get(url).await.text().await;
Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html())
}
trpl::block_onWhen we run this code, we get the behavior we expected initially:
$ cargo run -- "https://www.rust-lang.org"
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s
Running `target/debug/async_await 'https://www.rust-lang.org'`
The title for https://www.rust-lang.org was
Rust Programming Language
Phew—we finally have some working async code! But before we add the code to race two sites against each other, let’s briefly turn our attention back to how futures work.
Each await point—that is, every place where the code uses the await
keyword—represents a place where control is handed back to the runtime. To make
that work, Rust needs to keep track of the state involved in the async block so
that the runtime could kick off some other work and then come back when it’s
ready to try advancing the first one again. This is an invisible state machine,
as if you’d written an enum like this to save the current state at each await
point:
#![allow(unused)]
fn main() {
extern crate trpl; // required for mdbook test
enum PageTitleFuture<'a> {
Initial { url: &'a str },
GetAwaitPoint { url: &'a str },
TextAwaitPoint { response: trpl::Response },
}
}
Writing the code to transition between each state by hand would be tedious and error-prone, however, especially when you need to add more functionality and more states to the code later. Fortunately, the Rust compiler creates and manages the state machine data structures for async code automatically. The normal borrowing and ownership rules around data structures all still apply, and happily, the compiler also handles checking those for us and provides useful error messages. We’ll work through a few of those later in the chapter.
Ultimately, something has to execute this state machine, and that something is a runtime. (This is why you may come across mentions of executors when looking into runtimes: an executor is the part of a runtime responsible for executing the async code.)
Now you can see why the compiler stopped us from making main itself an async
function back in Listing 17-3. If main were an async function, something else
would need to manage the state machine for whatever future main returned, but
main is the starting point for the program! Instead, we called the
trpl::block_on function in main to set up a runtime and run the future
returned by the async block until it’s done.
Note: Some runtimes provide macros so you can write an async main
function. Those macros rewrite async fn main() { ... } to be a normal fn main, which does the same thing we did by hand in Listing 17-4: call a
function that runs a future to completion the way trpl::block_on does.
Now let’s put these pieces together and see how we can write concurrent code.
Racing Two URLs Against Each Other Concurrently
In Listing 17-5, we call page_title with two different URLs passed in from the
command line and race them by selecting whichever future finishes first.
extern crate trpl; // required for mdbook test
use trpl::{Either, Html};
fn main() {
let args: Vec<String> = std::env::args().collect();
trpl::block_on(async {
let title_fut_1 = page_title(&args[1]);
let title_fut_2 = page_title(&args[2]);
let (url, maybe_title) =
match trpl::select(title_fut_1, title_fut_2).await {
Either::Left(left) => left,
Either::Right(right) => right,
};
println!("{url} returned first");
match maybe_title {
Some(title) => println!("Its page title was: '{title}'"),
None => println!("It had no title."),
}
})
}
async fn page_title(url: &str) -> (&str, Option<String>) {
let response_text = trpl::get(url).await.text().await;
let title = Html::parse(&response_text)
.select_first("title")
.map(|title| title.inner_html());
(url, title)
}
page_title for two URLs to see which returns firstWe begin by calling page_title for each of the user-supplied URLs. We save
the resulting futures as title_fut_1 and title_fut_2. Remember, these don’t
do anything yet, because futures are lazy and we haven’t yet awaited them. Then
we pass the futures to trpl::select, which returns a value to indicate which
of the futures passed to it finishes first.
Note: Under the hood, trpl::select is built on a more general select
function defined in the futures crate. The futures crate’s select
function can do a lot of things that the trpl::select function can’t, but
it also has some additional complexity that we can skip over for now.
Either future can legitimately “win,” so it doesn’t make sense to return a
Result. Instead, trpl::select returns a type we haven’t seen before,
trpl::Either. The Either type is somewhat similar to a Result in that it
has two cases. Unlike Result, though, there is no notion of success or
failure baked into Either. Instead, it uses Left and Right to indicate
“one or the other”:
#![allow(unused)]
fn main() {
enum Either<A, B> {
Left(A),
Right(B),
}
}
The select function returns Left with that future’s output if the first
argument wins, and Right with the second future argument’s output if that
one wins. This matches the order the arguments appear in when calling the
function: the first argument is to the left of the second argument.
We also update page_title to return the same URL passed in. That way, if the
page that returns first does not have a <title> we can resolve, we can still
print a meaningful message. With that information available, we wrap up by
updating our println! output to indicate both which URL finished first and
what, if any, the <title> is for the web page at that URL.
You have built a small working web scraper now! Pick a couple URLs and run the command line tool. You may discover that some sites are consistently faster than others, while in other cases the faster site varies from run to run. More importantly, you’ve learned the basics of working with futures, so now we can dig deeper into what we can do with async.
ใช้ async ทำ concurrency
Applying Concurrency with Async
In this section, we’ll apply async to some of the same concurrency challenges we tackled with threads in Chapter 16. Because we already talked about a lot of the key ideas there, in this section we’ll focus on what’s different between threads and futures.
In many cases, the APIs for working with concurrency using async are very similar to those for using threads. In other cases, they end up being quite different. Even when the APIs look similar between threads and async, they often have different behavior—and they nearly always have different performance characteristics.
Creating a New Task with spawn_task
The first operation we tackled in the “Creating a New Thread with
spawn” section in Chapter 16 was counting up on
two separate threads. Let’s do the same using async. The trpl crate supplies
a spawn_task function that looks very similar to the thread::spawn API, and
a sleep function that is an async version of the thread::sleep API. We can
use these together to implement the counting example, as shown in Listing 17-6.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
}
As our starting point, we set up our main function with trpl::block_on so
that our top-level function can be async.
Note: From this point forward in the chapter, every example will include this
exact same wrapping code with trpl::block_on in main, so we’ll often skip it
just as we do with main. Remember to include it in your code!
Then we write two loops within that block, each containing a trpl::sleep
call, which waits for half a second (500 milliseconds) before sending the next
message. We put one loop in the body of a trpl::spawn_task and the other in a
top-level for loop. We also add an await after the sleep calls.
This code behaves similarly to the thread-based implementation—including the fact that you may see the messages appear in a different order in your own terminal when you run it:
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
This version stops as soon as the for loop in the body of the main async
block finishes, because the task spawned by spawn_task is shut down when the
main function ends. If you want it to run all the way to the task’s
completion, you will need to use a join handle to wait for the first task to
complete. With threads, we used the join method to “block” until the thread
was done running. In Listing 17-7, we can use await to do the same thing,
because the task handle itself is a future. Its Output type is a Result, so
we also unwrap it after awaiting it.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let handle = trpl::spawn_task(async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
});
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
handle.await.unwrap();
});
}
await with a join handle to run a task to completionThis updated version runs until both loops finish:
hi number 1 from the second task!
hi number 1 from the first task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
So far, it looks like async and threads give us similar outcomes, just with
different syntax: using await instead of calling join on the join handle,
and awaiting the sleep calls.
The bigger difference is that we didn’t need to spawn another operating system
thread to do this. In fact, we don’t even need to spawn a task here. Because
async blocks compile to anonymous futures, we can put each loop in an async
block and have the runtime run them both to completion using the trpl::join
function.
In the “Waiting for All Threads to Finish”
section in Chapter 16, we showed how to use the join method on the
JoinHandle type returned when you call std::thread::spawn. The trpl::join
function is similar, but for futures. When you give it two futures, it produces
a single new future whose output is a tuple containing the output of each
future you passed in once they both complete. Thus, in Listing 17-8, we use
trpl::join to wait for both fut1 and fut2 to finish. We do not await
fut1 and fut2 but instead the new future produced by trpl::join. We
ignore the output, because it’s just a tuple containing two unit values.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let fut1 = async {
for i in 1..10 {
println!("hi number {i} from the first task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};
let fut2 = async {
for i in 1..5 {
println!("hi number {i} from the second task!");
trpl::sleep(Duration::from_millis(500)).await;
}
};
trpl::join(fut1, fut2).await;
});
}
trpl::join to await two anonymous futuresWhen we run this, we see both futures run to completion:
hi number 1 from the first task!
hi number 1 from the second task!
hi number 2 from the first task!
hi number 2 from the second task!
hi number 3 from the first task!
hi number 3 from the second task!
hi number 4 from the first task!
hi number 4 from the second task!
hi number 5 from the first task!
hi number 6 from the first task!
hi number 7 from the first task!
hi number 8 from the first task!
hi number 9 from the first task!
Now, you’ll see the exact same order every time, which is very different from
what we saw with threads and with trpl::spawn_task in Listing 17-7. That is
because the trpl::join function is fair, meaning it checks each future
equally often, alternating between them, and never lets one race ahead if the
other is ready. With threads, the operating system decides which thread to
check and how long to let it run. With async Rust, the runtime decides which
task to check. (In practice, the details get complicated because an async
runtime might use operating system threads under the hood as part of how it
manages concurrency, so guaranteeing fairness can be more work for a
runtime—but it’s still possible!) Runtimes don’t have to guarantee fairness for
any given operation, and they often offer different APIs to let you choose
whether or not you want fairness.
Try some of these variations on awaiting the futures and see what they do:
- Remove the async block from around either or both of the loops.
- Await each async block immediately after defining it.
- Wrap only the first loop in an async block, and await the resulting future after the body of second loop.
For an extra challenge, see if you can figure out what the output will be in each case before running the code!
Sending Data Between Two Tasks Using Message Passing
Sharing data between futures will also be familiar: we’ll use message passing again, but this time with async versions of the types and functions. We’ll take a slightly different path than we did in the “Transfer Data Between Threads with Message Passing” section in Chapter 16 to illustrate some of the key differences between thread-based and futures-based concurrency. In Listing 17-9, we’ll begin with just a single async block—not spawning a separate task as we spawned a separate thread.
extern crate trpl; // required for mdbook test
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let val = String::from("hi");
tx.send(val).unwrap();
let received = rx.recv().await.unwrap();
println!("received '{received}'");
});
}
tx and rxHere, we use trpl::channel, an async version of the multiple-producer,
single-consumer channel API we used with threads back in Chapter 16. The async
version of the API is only a little different from the thread-based version: it
uses a mutable rather than an immutable receiver rx, and its recv method
produces a future we need to await rather than producing the value directly.
Now we can send messages from the sender to the receiver. Notice that we don’t
have to spawn a separate thread or even a task; we merely need to await the
rx.recv call.
The synchronous Receiver::recv method in std::mpsc::channel blocks until it
receives a message. The trpl::Receiver::recv method does not, because it is
async. Instead of blocking, it hands control back to the runtime until either a
message is received or the send side of the channel closes. By contrast, we
don’t await the send call, because it doesn’t block. It doesn’t need to,
because the channel we’re sending it into is unbounded.
Note: Because all of this async code runs in an async block in a
trpl::block_on call, everything within it can avoid blocking. However, the
code outside it will block on the block_on function returning. That’s the
whole point of the trpl::block_on function: it lets you choose where to
block on some set of async code, and thus where to transition between sync
and async code.
Notice two things about this example. First, the message will arrive right away. Second, although we use a future here, there’s no concurrency yet. Everything in the listing happens in sequence, just as it would if there were no futures involved.
Let’s address the first part by sending a series of messages and sleeping in between them, as shown in Listing 17-10.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
}
await between each messageIn addition to sending the messages, we need to receive them. In this case,
because we know how many messages are coming in, we could do that manually by
calling rx.recv().await four times. In the real world, though, we’ll generally
be waiting on some unknown number of messages, so we need to keep waiting
until we determine that there are no more messages.
In Listing 16-10, we used a for loop to process all the items received from a
synchronous channel. Rust doesn’t yet have a way to use a for loop with an
asynchronously produced series of items, however, so we need to use a loop we
haven’t seen before: the while let conditional loop. This is the loop version
of the if let construct we saw back in the “Concise Control Flow with if let and let...else” section in Chapter 6. The loop
will continue executing as long as the pattern it specifies continues to match
the value.
The rx.recv call produces a future, which we await. The runtime will pause
the future until it is ready. Once a message arrives, the future will resolve
to Some(message) as many times as a message arrives. When the channel closes,
regardless of whether any messages have arrived, the future will instead
resolve to None to indicate that there are no more values and thus we should
stop polling—that is, stop awaiting.
The while let loop pulls all of this together. If the result of calling
rx.recv().await is Some(message), we get access to the message and we can
use it in the loop body, just as we could with if let. If the result is
None, the loop ends. Every time the loop completes, it hits the await point
again, so the runtime pauses it again until another message arrives.
The code now successfully sends and receives all of the messages. Unfortunately, there are still a couple of problems. For one thing, the messages do not arrive at half-second intervals. They arrive all at once, 2 seconds (2,000 milliseconds) after we start the program. For another, this program also never exits! Instead, it waits forever for new messages. You will need to shut it down using ctrl-C.
Code Within One Async Block Executes Linearly
Let’s start by examining why the messages come in all at once after the full
delay, rather than coming in with delays between each one. Within a given async
block, the order in which await keywords appear in the code is also the order
in which they’re executed when the program runs.
There’s only one async block in Listing 17-10, so everything in it runs
linearly. There’s still no concurrency. All the tx.send calls happen,
interspersed with all of the trpl::sleep calls and their associated await
points. Only then does the while let loop get to go through any of the
await points on the recv calls.
To get the behavior we want, where the sleep delay happens between each
message, we need to put the tx and rx operations in their own async blocks,
as shown in Listing 17-11. Then the runtime can execute each of them separately
using trpl::join, just as in Listing 17-8. Once again, we await the result of
calling trpl::join, not the individual futures. If we awaited the individual
futures in sequence, we would just end up back in a sequential flow—exactly
what we’re trying not to do.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx_fut = async {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
});
}
send and recv into their own async blocks and awaiting the futures for those blocksWith the updated code in Listing 17-11, the messages get printed at 500-millisecond intervals, rather than all in a rush after 2 seconds.
Moving Ownership Into an Async Block
The program still never exits, though, because of the way the while let loop
interacts with trpl::join:
- The future returned from
trpl::joincompletes only once both futures passed to it have completed. - The
tx_futfuture completes once it finishes sleeping after sending the last message invals. - The
rx_futfuture won’t complete until thewhile letloop ends. - The
while letloop won’t end until awaitingrx.recvproducesNone. - Awaiting
rx.recvwill returnNoneonly once the other end of the channel is closed. - The channel will close only if we call
rx.closeor when the sender side,tx, is dropped. - We don’t call
rx.closeanywhere, andtxwon’t be dropped until the outermost async block passed totrpl::block_onends. - The block can’t end because it is blocked on
trpl::joincompleting, which takes us back to the top of this list.
Right now, the async block where we send the messages only borrows tx
because sending a message doesn’t require ownership, but if we could move
tx into that async block, it would be dropped once that block ends. In the
“Capturing References or Moving Ownership”
section in Chapter 13, you learned how to use the move keyword with closures,
and, as discussed in the “Using move Closures with
Threads” section in Chapter 16, we often need to
move data into closures when working with threads. The same basic dynamics
apply to async blocks, so the move keyword works with async blocks just as it
does with closures.
In Listing 17-12, we change the block used to send messages from async to
async move.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx_fut = async move {
// --snip--
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
trpl::join(tx_fut, rx_fut).await;
});
}
When we run this version of the code, it shuts down gracefully after the last message is sent and received. Next, let’s see what would need to change to send data from more than one future.
Joining a Number of Futures with the join! Macro
This async channel is also a multiple-producer channel, so we can call clone
on tx if we want to send messages from multiple futures, as shown in Listing
17-13.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_millis(500)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_millis(1500)).await;
}
};
trpl::join!(tx1_fut, tx_fut, rx_fut);
});
}
First, we clone tx, creating tx1 outside the first async block. We move
tx1 into that block just as we did before with tx. Then, later, we move the
original tx into a new async block, where we send more messages on a
slightly slower delay. We happen to put this new async block after the async
block for receiving messages, but it could go before it just as well. The key is
the order in which the futures are awaited, not in which they’re created.
Both of the async blocks for sending messages need to be async move blocks so
that both tx and tx1 get dropped when those blocks finish. Otherwise, we’ll
end up back in the same infinite loop we started out in.
Finally, we switch from trpl::join to trpl::join! to handle the additional
future: the join! macro awaits an arbitrary number of futures where we know
the number of futures at compile time. We’ll discuss awaiting a collection of
an unknown number of futures later in this chapter.
Now we see all the messages from both sending futures, and because the sending futures use slightly different delays after sending, the messages are also received at those different intervals:
received 'hi'
received 'more'
received 'from'
received 'the'
received 'messages'
received 'future'
received 'for'
received 'you'
We’ve explored how to use message passing to send data between futures, how code within an async block runs sequentially, how to move ownership into an async block, and how to join multiple futures. Next, let’s discuss how and why to tell the runtime it can switch to another task.
จัดการ future จำนวนเท่าใดก็ได้
Yielding Control to the Runtime
Recall from the “Our First Async Program” section that at each await point, Rust gives a runtime a chance to pause the task and switch to another one if the future being awaited isn’t ready. The inverse is also true: Rust only pauses async blocks and hands control back to a runtime at an await point. Everything between await points is synchronous.
That means if you do a bunch of work in an async block without an await point, that future will block any other futures from making progress. You may sometimes hear this referred to as one future starving other futures. In some cases, that may not be a big deal. However, if you are doing some kind of expensive setup or long-running work, or if you have a future that will keep doing some particular task indefinitely, you’ll need to think about when and where to hand control back to the runtime.
Let’s simulate a long-running operation to illustrate the starvation problem,
then explore how to solve it. Listing 17-14 introduces a slow function.
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
// We will call `slow` here later
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
thread::sleep to simulate slow operationsThis code uses std::thread::sleep instead of trpl::sleep so that calling
slow will block the current thread for some number of milliseconds. We can
use slow to stand in for real-world operations that are both long-running and
blocking.
In Listing 17-15, we use slow to emulate doing this kind of CPU-bound work in
a pair of futures.
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let a = async {
println!("'a' started.");
slow("a", 30);
slow("a", 10);
slow("a", 20);
trpl::sleep(Duration::from_millis(50)).await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
slow("b", 10);
slow("b", 15);
slow("b", 350);
trpl::sleep(Duration::from_millis(50)).await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
slow function to simulate slow operationsEach future hands control back to the runtime only after carrying out a bunch of slow operations. If you run this code, you will see this output:
'a' started.
'a' ran for 30ms
'a' ran for 10ms
'a' ran for 20ms
'b' started.
'b' ran for 75ms
'b' ran for 10ms
'b' ran for 15ms
'b' ran for 350ms
'a' finished.
As with Listing 17-5 where we used trpl::select to race futures fetching two
URLs, select still finishes as soon as a is done. There’s no interleaving
between the calls to slow in the two futures, though. The a future does all
of its work until the trpl::sleep call is awaited, then the b future does
all of its work until its own trpl::sleep call is awaited, and finally the
a future completes. To allow both futures to make progress between their slow
tasks, we need await points so we can hand control back to the runtime. That
means we need something we can await!
We can already see this kind of handoff happening in Listing 17-15: if we
removed the trpl::sleep at the end of the a future, it would complete
without the b future running at all. Let’s try using the trpl::sleep
function as a starting point for letting operations switch off making progress,
as shown in Listing 17-16.
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let one_ms = Duration::from_millis(1);
let a = async {
println!("'a' started.");
slow("a", 30);
trpl::sleep(one_ms).await;
slow("a", 10);
trpl::sleep(one_ms).await;
slow("a", 20);
trpl::sleep(one_ms).await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::sleep(one_ms).await;
slow("b", 10);
trpl::sleep(one_ms).await;
slow("b", 15);
trpl::sleep(one_ms).await;
slow("b", 350);
trpl::sleep(one_ms).await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
trpl::sleep to let operations switch off making progressWe’ve added trpl::sleep calls with await points between each call to slow.
Now the two futures’ work is interleaved:
'a' started.
'a' ran for 30ms
'b' started.
'b' ran for 75ms
'a' ran for 10ms
'b' ran for 10ms
'a' ran for 20ms
'b' ran for 15ms
'a' finished.
The a future still runs for a bit before handing off control to b, because
it calls slow before ever calling trpl::sleep, but after that the futures
swap back and forth each time one of them hits an await point. In this case, we
have done that after every call to slow, but we could break up the work in
whatever way makes the most sense to us.
We don’t really want to sleep here, though: we want to make progress as fast
as we can. We just need to hand back control to the runtime. We can do that
directly, using the trpl::yield_now function. In Listing 17-17, we replace
all those trpl::sleep calls with trpl::yield_now.
extern crate trpl; // required for mdbook test
use std::{thread, time::Duration};
fn main() {
trpl::block_on(async {
let a = async {
println!("'a' started.");
slow("a", 30);
trpl::yield_now().await;
slow("a", 10);
trpl::yield_now().await;
slow("a", 20);
trpl::yield_now().await;
println!("'a' finished.");
};
let b = async {
println!("'b' started.");
slow("b", 75);
trpl::yield_now().await;
slow("b", 10);
trpl::yield_now().await;
slow("b", 15);
trpl::yield_now().await;
slow("b", 350);
trpl::yield_now().await;
println!("'b' finished.");
};
trpl::select(a, b).await;
});
}
fn slow(name: &str, ms: u64) {
thread::sleep(Duration::from_millis(ms));
println!("'{name}' ran for {ms}ms");
}
yield_now to let operations switch off making progressThis code is both clearer about the actual intent and can be significantly
faster than using sleep, because timers such as the one used by sleep often
have limits on how granular they can be. The version of sleep we are using,
for example, will always sleep for at least a millisecond, even if we pass it a
Duration of one nanosecond. Again, modern computers are fast: they can do a
lot in one millisecond!
This means that async can be useful even for compute-bound tasks, depending on what else your program is doing, because it provides a useful tool for structuring the relationships between different parts of the program (but at a cost of the overhead of the async state machine). This is a form of cooperative multitasking, where each future has the power to determine when it hands over control via await points. Each future therefore also has the responsibility to avoid blocking for too long. In some Rust-based embedded operating systems, this is the only kind of multitasking!
In real-world code, you won’t usually be alternating function calls with await points on every single line, of course. While yielding control in this way is relatively inexpensive, it’s not free. In many cases, trying to break up a compute-bound task might make it significantly slower, so sometimes it’s better for overall performance to let an operation block briefly. Always measure to see what your code’s actual performance bottlenecks are. The underlying dynamic is important to keep in mind, though, if you are seeing a lot of work happening in serial that you expected to happen concurrently!
Building Our Own Async Abstractions
We can also compose futures together to create new patterns. For example, we can
build a timeout function with async building blocks we already have. When
we’re done, the result will be another building block we could use to create
still more async abstractions.
Listing 17-18 shows how we would expect this timeout to work with a slow
future.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
timeout to run a slow operation with a time limitLet’s implement this! To begin, let’s think about the API for timeout:
- It needs to be an async function itself so we can await it.
- Its first parameter should be a future to run. We can make it generic to allow it to work with any future.
- Its second parameter will be the maximum time to wait. If we use a
Duration, that will make it easy to pass along totrpl::sleep. - It should return a
Result. If the future completes successfully, theResultwill beOkwith the value produced by the future. If the timeout elapses first, theResultwill beErrwith the duration that the timeout waited for.
Listing 17-19 shows this declaration.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
// Here is where our implementation will go!
}
timeoutThat satisfies our goals for the types. Now let’s think about the behavior we
need: we want to race the future passed in against the duration. We can use
trpl::sleep to make a timer future from the duration, and use trpl::select
to run that timer with the future the caller passes in.
In Listing 17-20, we implement timeout by matching on the result of awaiting
trpl::select.
extern crate trpl; // required for mdbook test
use std::time::Duration;
use trpl::Either;
// --snip--
fn main() {
trpl::block_on(async {
let slow = async {
trpl::sleep(Duration::from_secs(5)).await;
"Finally finished"
};
match timeout(slow, Duration::from_secs(2)).await {
Ok(message) => println!("Succeeded with '{message}'"),
Err(duration) => {
println!("Failed after {} seconds", duration.as_secs())
}
}
});
}
async fn timeout<F: Future>(
future_to_try: F,
max_time: Duration,
) -> Result<F::Output, Duration> {
match trpl::select(future_to_try, trpl::sleep(max_time)).await {
Either::Left(output) => Ok(output),
Either::Right(_) => Err(max_time),
}
}
timeout with select and sleepThe implementation of trpl::select is not fair: it always polls arguments in
the order in which they are passed (other select implementations will
randomly choose which argument to poll first). Thus, we pass future_to_try to
select first so it gets a chance to complete even if max_time is a very
short duration. If future_to_try finishes first, select will return Left
with the output from future_to_try. If timer finishes first, select will
return Right with the timer’s output of ().
If the future_to_try succeeds and we get a Left(output), we return
Ok(output). If the sleep timer elapses instead and we get a Right(()), we
ignore the () with _ and return Err(max_time) instead.
With that, we have a working timeout built out of two other async helpers. If
we run our code, it will print the failure mode after the timeout:
Failed after 2 seconds
Because futures compose with other futures, you can build really powerful tools using smaller async building blocks. For example, you can use this same approach to combine timeouts with retries, and in turn use those with operations such as network calls (such as those in Listing 17-5).
In practice, you’ll usually work directly with async and await, and
secondarily with functions such as select and macros such as the join!
macro to control how the outermost futures are executed.
We’ve now seen a number of ways to work with multiple futures at the same time. Up next, we’ll look at how we can work with multiple futures in a sequence over time with streams.
Stream: future ที่ต่อกันเป็นลำดับ
Streams: Futures in Sequence
Recall how we used the receiver for our async channel earlier in this chapter
in the “Message Passing” section. The async
recv method produces a sequence of items over time. This is an instance of a
much more general pattern known as a stream. Many concepts are naturally
represented as streams: items becoming available in a queue, chunks of data
being pulled incrementally from the filesystem when the full data set is too
large for the computer’s memory, or data arriving over the network over time.
Because streams are futures, we can use them with any other kind of future and
combine them in interesting ways. For example, we can batch up events to avoid
triggering too many network calls, set timeouts on sequences of long-running
operations, or throttle user interface events to avoid doing needless work.
We saw a sequence of items back in Chapter 13, when we looked at the Iterator
trait in “The Iterator Trait and the next Method” section, but there are two differences between iterators and the
async channel receiver. The first difference is time: iterators are
synchronous, while the channel receiver is asynchronous. The second difference
is the API. When working directly with Iterator, we call its synchronous
next method. With the trpl::Receiver stream in particular, we called an
asynchronous recv method instead. Otherwise, these APIs feel very similar,
and that similarity isn’t a coincidence. A stream is like an asynchronous form
of iteration. Whereas the trpl::Receiver specifically waits to receive
messages, though, the general-purpose stream API is much broader: it provides
the next item the way Iterator does, but asynchronously.
The similarity between iterators and streams in Rust means we can actually
create a stream from any iterator. As with an iterator, we can work with a
stream by calling its next method and then awaiting the output, as in Listing
17-21, which won’t compile yet.
extern crate trpl; // required for mdbook test
fn main() {
trpl::block_on(async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
while let Some(value) = stream.next().await {
println!("The value was: {value}");
}
});
}
We start with an array of numbers, which we convert to an iterator and then
call map on to double all the values. Then we convert the iterator into a
stream using the trpl::stream_from_iter function. Next, we loop over the
items in the stream as they arrive with the while let loop.
Unfortunately, when we try to run the code, it doesn’t compile but instead
reports that there’s no next method available:
error[E0599]: no method named `next` found for struct `tokio_stream::iter::Iter` in the current scope
--> src/main.rs:10:40
|
10 | while let Some(value) = stream.next().await {
| ^^^^
|
= help: items from traits can only be used if the trait is in scope
help: the following traits which provide `next` are implemented but not in scope; perhaps you want to import one of them
|
1 + use crate::trpl::StreamExt;
|
1 + use futures_util::stream::stream::StreamExt;
|
1 + use std::iter::Iterator;
|
1 + use std::str::pattern::Searcher;
|
help: there is a method `try_next` with a similar name
|
10 | while let Some(value) = stream.try_next().await {
| ~~~~~~~~
As this output explains, the reason for the compiler error is that we need the
right trait in scope to be able to use the next method. Given our discussion
so far, you might reasonably expect that trait to be Stream, but it’s
actually StreamExt. Short for extension, Ext is a common pattern in the
Rust community for extending one trait with another.
The Stream trait defines a low-level interface that effectively combines the
Iterator and Future traits. StreamExt supplies a higher-level set of APIs
on top of Stream, including the next method as well as other utility
methods similar to those provided by the Iterator trait. Stream and
StreamExt are not yet part of Rust’s standard library, but most ecosystem
crates use similar definitions.
The fix to the compiler error is to add a use statement for
trpl::StreamExt, as in Listing 17-22.
extern crate trpl; // required for mdbook test
use trpl::StreamExt;
fn main() {
trpl::block_on(async {
let values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// --snip--
let iter = values.iter().map(|n| n * 2);
let mut stream = trpl::stream_from_iter(iter);
while let Some(value) = stream.next().await {
println!("The value was: {value}");
}
});
}
With all those pieces put together, this code works the way we want! What’s
more, now that we have StreamExt in scope, we can use all of its utility
methods, just as with iterators.
Trait ที่เกี่ยวกับ async
A Closer Look at the Traits for Async
Throughout the chapter, we’ve used the Future, Stream, and StreamExt
traits in various ways. So far, though, we’ve avoided getting too far into the
details of how they work or how they fit together, which is fine most of the
time for your day-to-day Rust work. Sometimes, though, you’ll encounter
situations where you’ll need to understand a few more of these traits’ details,
along with the Pin type and the Unpin trait. In this section, we’ll dig in
just enough to help in those scenarios, still leaving the really deep dive
for other documentation.
The Future Trait
Let’s start by taking a closer look at how the Future trait works. Here’s how
Rust defines it:
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}
That trait definition includes a bunch of new types and also some syntax we haven’t seen before, so let’s walk through the definition piece by piece.
First, Future’s associated type Output says what the future resolves to.
This is analogous to the Item associated type for the Iterator trait.
Second, Future has the poll method, which takes a special Pin reference
for its self parameter and a mutable reference to a Context type, and
returns a Poll<Self::Output>. We’ll talk more about Pin and Context in a
moment. For now, let’s focus on what the method returns, the Poll type:
#![allow(unused)]
fn main() {
pub enum Poll<T> {
Ready(T),
Pending,
}
}
This Poll type is similar to an Option. It has one variant that has a value,
Ready(T), and one that does not, Pending. Poll means something quite
different from Option, though! The Pending variant indicates that the future
still has work to do, so the caller will need to check again later. The Ready
variant indicates that the Future has finished its work and the T value is
available.
Note: It’s rare to need to call poll directly, but if you do need to, keep
in mind that with most futures, the caller should not call poll again after
the future has returned Ready. Many futures will panic if polled again after
becoming ready. Futures that are safe to poll again will say so explicitly in
their documentation. This is similar to how Iterator::next behaves.
When you see code that uses await, Rust compiles it under the hood to code
that calls poll. If you look back at Listing 17-4, where we printed out the
page title for a single URL once it resolved, Rust compiles it into something
kind of (although not exactly) like this:
match page_title(url).poll() {
Ready(page_title) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// But what goes here?
}
}
What should we do when the future is still Pending? We need some way to try
again, and again, and again, until the future is finally ready. In other words,
we need a loop:
let mut page_title_fut = page_title(url);
loop {
match page_title_fut.poll() {
Ready(value) => match page_title {
Some(title) => println!("The title for {url} was {title}"),
None => println!("{url} had no title"),
}
Pending => {
// continue
}
}
}
If Rust compiled it to exactly that code, though, every await would be
blocking—exactly the opposite of what we were going for! Instead, Rust ensures
that the loop can hand off control to something that can pause work on this
future to work on other futures and then check this one again later. As we’ve
seen, that something is an async runtime, and this scheduling and coordination
work is one of its main jobs.
In the “Sending Data Between Two Tasks Using Message
Passing” section, we described waiting on
rx.recv. The recv call returns a future, and awaiting the future polls it.
We noted that a runtime will pause the future until it’s ready with either
Some(message) or None when the channel closes. With our deeper
understanding of the Future trait, and specifically Future::poll, we can
see how that works. The runtime knows the future isn’t ready when it returns
Poll::Pending. Conversely, the runtime knows the future is ready and
advances it when poll returns Poll::Ready(Some(message)) or
Poll::Ready(None).
The exact details of how a runtime does that are beyond the scope of this book, but the key is to see the basic mechanics of futures: a runtime polls each future it is responsible for, putting the future back to sleep when it is not yet ready.
The Pin Type and the Unpin Trait
Back in Listing 17-13, we used the trpl::join! macro to await three
futures. However, it’s common to have a collection such as a vector containing
some number futures that won’t be known until runtime. Let’s change Listing
17-13 to the code in Listing 17-23 that puts the three futures into a vector
and calls the trpl::join_all function instead, which won’t compile yet.
extern crate trpl; // required for mdbook test
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = async move {
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let rx_fut = async {
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
};
let tx_fut = async move {
// --snip--
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
};
let futures: Vec<Box<dyn Future<Output = ()>>> =
vec![Box::new(tx1_fut), Box::new(rx_fut), Box::new(tx_fut)];
trpl::join_all(futures).await;
});
}
We put each future within a Box to make them into trait objects, just as
we did in the “Returning Errors from run” section in Chapter 12. (We’ll cover
trait objects in detail in Chapter 18.) Using trait objects lets us treat each
of the anonymous futures produced by these types as the same type, because all
of them implement the Future trait.
This might be surprising. After all, none of the async blocks returns anything,
so each one produces a Future<Output = ()>. Remember that Future is a
trait, though, and that the compiler creates a unique enum for each async
block, even when they have identical output types. Just as you can’t put two
different handwritten structs in a Vec, you can’t mix compiler-generated
enums.
Then we pass the collection of futures to the trpl::join_all function and
await the result. However, this doesn’t compile; here’s the relevant part of
the error messages.
error[E0277]: `dyn Future<Output = ()>` cannot be unpinned
--> src/main.rs:48:33
|
48 | trpl::join_all(futures).await;
| ^^^^^ the trait `Unpin` is not implemented for `dyn Future<Output = ()>`
|
= note: consider using the `pin!` macro
consider using `Box::pin` if you need to access the pinned value outside of the current scope
= note: required for `Box<dyn Future<Output = ()>>` to implement `Future`
note: required by a bound in `futures_util::future::join_all::JoinAll`
--> file:///home/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.30/src/future/join_all.rs:29:8
|
27 | pub struct JoinAll<F>
| ------- required by a bound in this struct
28 | where
29 | F: Future,
| ^^^^^^ required by this bound in `JoinAll`
The note in this error message tells us that we should use the pin! macro to
pin the values, which means putting them inside the Pin type that
guarantees the values won’t be moved in memory. The error message says pinning
is required because dyn Future<Output = ()> needs to implement the Unpin
trait and it currently does not.
The trpl::join_all function returns a struct called JoinAll. That struct is
generic over a type F, which is constrained to implement the Future trait.
Directly awaiting a future with await pins the future implicitly. That’s why
we don’t need to use pin! everywhere we want to await futures.
However, we’re not directly awaiting a future here. Instead, we construct a new
future, JoinAll, by passing a collection of futures to the join_all function.
The signature for join_all requires that the types of the items in the
collection all implement the Future trait, and Box<T> implements Future
only if the T it wraps is a future that implements the Unpin trait.
That’s a lot to absorb! To really understand it, let’s dive a little further
into how the Future trait actually works, in particular around pinning. Look
again at the definition of the Future trait:
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
type Output;
// Required method
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
}
The cx parameter and its Context type are the key to how a runtime actually
knows when to check any given future while still being lazy. Again, the details
of how that works are beyond the scope of this chapter, and you generally only
need to think about this when writing a custom Future implementation. We’ll
focus instead on the type for self, as this is the first time we’ve seen a
method where self has a type annotation. A type annotation for self works
like type annotations for other function parameters but with two key
differences:
- It tells Rust what type
selfmust be for the method to be called. - It can’t be just any type. It’s restricted to the type on which the method is
implemented, a reference or smart pointer to that type, or a
Pinwrapping a reference to that type.
We’ll see more on this syntax in Chapter 18. For now,
it’s enough to know that if we want to poll a future to check whether it is
Pending or Ready(Output), we need a Pin-wrapped mutable reference to the
type.
Pin is a wrapper for pointer-like types such as &, &mut, Box, and Rc.
(Technically, Pin works with types that implement the Deref or DerefMut
traits, but this is effectively equivalent to working only with references and
smart pointers.) Pin is not a pointer itself and doesn’t have any behavior of
its own like Rc and Arc do with reference counting; it’s purely a tool the
compiler can use to enforce constraints on pointer usage.
Recalling that await is implemented in terms of calls to poll starts to
explain the error message we saw earlier, but that was in terms of Unpin, not
Pin. So how exactly does Pin relate to Unpin, and why does Future need
self to be in a Pin type to call poll?
Remember from earlier in this chapter that a series of await points in a future get compiled into a state machine, and the compiler makes sure that state machine follows all of Rust’s normal rules around safety, including borrowing and ownership. To make that work, Rust looks at what data is needed between one await point and either the next await point or the end of the async block. It then creates a corresponding variant in the compiled state machine. Each variant gets the access it needs to the data that will be used in that section of the source code, whether by taking ownership of that data or by getting a mutable or immutable reference to it.
So far, so good: if we get anything wrong about the ownership or references in
a given async block, the borrow checker will tell us. When we want to move
around the future that corresponds to that block—like moving it into a Vec to
pass to join_all—things get trickier.
When we move a future—whether by pushing it into a data structure to use as an
iterator with join_all or by returning it from a function—that actually means
moving the state machine Rust creates for us. And unlike most other types in
Rust, the futures Rust creates for async blocks can end up with references to
themselves in the fields of any given variant, as shown in the simplified illustration in Figure 17-4.
By default, though, any object that has a reference to itself is unsafe to move, because references always point to the actual memory address of whatever they refer to (see Figure 17-5). If you move the data structure itself, those internal references will be left pointing to the old location. However, that memory location is now invalid. For one thing, its value will not be updated when you make changes to the data structure. For another—more important—thing, the computer is now free to reuse that memory for other purposes! You could end up reading completely unrelated data later.
Theoretically, the Rust compiler could try to update every reference to an object whenever it gets moved, but that could add a lot of performance overhead, especially if a whole web of references needs updating. If we could instead make sure the data structure in question doesn’t move in memory, we wouldn’t have to update any references. This is exactly what Rust’s borrow checker is for: in safe code, it prevents you from moving any item with an active reference to it.
Pin builds on that to give us the exact guarantee we need. When we pin a
value by wrapping a pointer to that value in Pin, it can no longer move. Thus,
if you have Pin<Box<SomeType>>, you actually pin the SomeType value, not
the Box pointer. Figure 17-6 illustrates this process.
In fact, the Box pointer can still move around freely. Remember: we care about
making sure the data ultimately being referenced stays in place. If a pointer
moves around, but the data it points to is in the same place, as in Figure
17-7, there’s no potential problem. (As an independent exercise, look at the docs
for the types as well as the std::pin module and try to work out how you’d do
this with a Pin wrapping a Box.) The key is that the self-referential type
itself cannot move, because it is still pinned.
However, most types are perfectly safe to move around, even if they happen to be
behind a Pin pointer. We only need to think about pinning when items have
internal references. Primitive values such as numbers and Booleans are safe
because they obviously don’t have any internal references.
Neither do most types you normally work with in Rust. You can move around
a Vec, for example, without worrying. Given what we have seen so far, if
you have a Pin<Vec<String>>, you’d have to do everything via the safe but
restrictive APIs provided by Pin, even though a Vec<String> is always safe
to move if there are no other references to it. We need a way to tell the
compiler that it’s fine to move items around in cases like this—and that’s
where Unpin comes into play.
Unpin is a marker trait, similar to the Send and Sync traits we saw in
Chapter 16, and thus has no functionality of its own. Marker traits exist only
to tell the compiler it’s safe to use the type implementing a given trait in a
particular context. Unpin informs the compiler that a given type does not
need to uphold any guarantees about whether the value in question can be safely
moved.
Just as with Send and Sync, the compiler implements Unpin automatically
for all types where it can prove it is safe. A special case, again similar to
Send and Sync, is where Unpin is not implemented for a type. The
notation for this is impl !Unpin for SomeType, where
SomeType is the name of a type that does need to uphold
those guarantees to be safe whenever a pointer to that type is used in a Pin.
In other words, there are two things to keep in mind about the relationship
between Pin and Unpin. First, Unpin is the “normal” case, and !Unpin is
the special case. Second, whether a type implements Unpin or !Unpin only
matters when you’re using a pinned pointer to that type like Pin<&mut
SomeType>.
To make that concrete, think about a String: it has a length and the Unicode
characters that make it up. We can wrap a String in Pin, as seen in Figure
17-8. However, String automatically implements Unpin, as do most other types
in Rust.
As a result, we can do things that would be illegal if String implemented
!Unpin instead, such as replacing one string with another at the exact same
location in memory as in Figure 17-9. This doesn’t violate the Pin contract,
because String has no internal references that make it unsafe to move around.
That is precisely why it implements Unpin rather than !Unpin.
Now we know enough to understand the errors reported for that join_all call
from back in Listing 17-23. We originally tried to move the futures produced by
async blocks into a Vec<Box<dyn Future<Output = ()>>>, but as we’ve seen,
those futures may have internal references, so they don’t automatically
implement Unpin. Once we pin them, we can pass the resulting Pin type into
the Vec, confident that the underlying data in the futures will not be
moved. Listing 17-24 shows how to fix the code by calling the pin! macro
where each of the three futures are defined and adjusting the trait object type.
extern crate trpl; // required for mdbook test
use std::pin::{Pin, pin};
// --snip--
use std::time::Duration;
fn main() {
trpl::block_on(async {
let (tx, mut rx) = trpl::channel();
let tx1 = tx.clone();
let tx1_fut = pin!(async move {
// --snip--
let vals = vec![
String::from("hi"),
String::from("from"),
String::from("the"),
String::from("future"),
];
for val in vals {
tx1.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
});
let rx_fut = pin!(async {
// --snip--
while let Some(value) = rx.recv().await {
println!("received '{value}'");
}
});
let tx_fut = pin!(async move {
// --snip--
let vals = vec![
String::from("more"),
String::from("messages"),
String::from("for"),
String::from("you"),
];
for val in vals {
tx.send(val).unwrap();
trpl::sleep(Duration::from_secs(1)).await;
}
});
let futures: Vec<Pin<&mut dyn Future<Output = ()>>> =
vec![tx1_fut, rx_fut, tx_fut];
trpl::join_all(futures).await;
});
}
This example now compiles and runs, and we could add or remove futures from the vector at runtime and join them all.
Pin and Unpin are mostly important for building lower-level libraries, or
when you’re building a runtime itself, rather than for day-to-day Rust code.
When you see these traits in error messages, though, now you’ll have a better
idea of how to fix your code!
Note: This combination of Pin and Unpin makes it possible to safely
implement a whole class of complex types in Rust that would otherwise prove
challenging because they’re self-referential. Types that require Pin show up
most commonly in async Rust today, but every once in a while, you might see
them in other contexts, too.
The specifics of how Pin and Unpin work, and the rules they’re required
to uphold, are covered extensively in the API documentation for std::pin, so
if you’re interested in learning more, that’s a great place to start.
If you want to understand how things work under the hood in even more detail, see Chapters 2 and 4 of Asynchronous Programming in Rust.
The Stream Trait
Now that you have a deeper grasp on the Future, Pin, and Unpin traits, we
can turn our attention to the Stream trait. As you learned earlier in the
chapter, streams are similar to asynchronous iterators. Unlike Iterator and
Future, however, Stream has no definition in the standard library as of
this writing, but there is a very common definition from the futures crate
used throughout the ecosystem.
Let’s review the definitions of the Iterator and Future traits before
looking at how a Stream trait might merge them together. From Iterator, we
have the idea of a sequence: its next method provides an
Option<Self::Item>. From Future, we have the idea of readiness over time:
its poll method provides a Poll<Self::Output>. To represent a sequence of
items that become ready over time, we define a Stream trait that puts those
features together:
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>
) -> Poll<Option<Self::Item>>;
}
}
The Stream trait defines an associated type called Item for the type of the
items produced by the stream. This is similar to Iterator, where there may be
zero to many items, and unlike Future, where there is always a single
Output, even if it’s the unit type ().
Stream also defines a method to get those items. We call it poll_next, to
make it clear that it polls in the same way Future::poll does and produces a
sequence of items in the same way Iterator::next does. Its return type
combines Poll with Option. The outer type is Poll, because it has to be
checked for readiness, just as a future does. The inner type is Option,
because it needs to signal whether there are more messages, just as an iterator
does.
Something very similar to this definition will likely end up as part of Rust’s standard library. In the meantime, it’s part of the toolkit of most runtimes, so you can rely on it, and everything we cover next should generally apply!
In the examples we saw in the “Streams: Futures in Sequence” section, though, we didn’t use poll_next or Stream, but
instead used next and StreamExt. We could work directly in terms of the
poll_next API by hand-writing our own Stream state machines, of course,
just as we could work with futures directly via their poll method. Using
await is much nicer, though, and the StreamExt trait supplies the next
method so we can do just that:
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::task::{Context, Poll};
trait Stream {
type Item;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>>;
}
trait StreamExt: Stream {
async fn next(&mut self) -> Option<Self::Item>
where
Self: Unpin;
// other methods...
}
}
Note: The actual definition we used earlier in the chapter looks slightly different than this, because it supports versions of Rust that did not yet support using async functions in traits. As a result, it looks like this:
fn next(&mut self) -> Next<'_, Self> where Self: Unpin;
That Next type is a struct that implements Future and allows us to name
the lifetime of the reference to self with Next<'_, Self>, so that await
can work with this method.
The StreamExt trait is also the home of all the interesting methods available
to use with streams. StreamExt is automatically implemented for every type
that implements Stream, but these traits are defined separately to enable the
community to iterate on convenience APIs without affecting the foundational
trait.
In the version of StreamExt used in the trpl crate, the trait not only
defines the next method but also supplies a default implementation of next
that correctly handles the details of calling Stream::poll_next. This means
that even when you need to write your own streaming data type, you only have
to implement Stream, and then anyone who uses your data type can use
StreamExt and its methods with it automatically.
That’s all we’re going to cover for the lower-level details on these traits. To wrap up, let’s consider how futures (including streams), tasks, and threads all fit together!
Future, Task และ Thread
Putting It All Together: Futures, Tasks, and Threads
As we saw in Chapter 16, threads provide one approach to concurrency. We’ve seen another approach in this chapter: using async with futures and streams. If you’re wondering when to choose one method over the other, the answer is: it depends! And in many cases, the choice isn’t threads or async but rather threads and async.
Many operating systems have supplied threading-based concurrency models for decades now, and many programming languages support them as a result. However, these models are not without their tradeoffs. On many operating systems, they use a fair bit of memory for each thread. Threads are also only an option when your operating system and hardware support them. Unlike mainstream desktop and mobile computers, some embedded systems don’t have an OS at all, so they also don’t have threads.
The async model provides a different—and ultimately complementary—set of
tradeoffs. In the async model, concurrent operations don’t require their own
threads. Instead, they can run on tasks, as when we used trpl::spawn_task to
kick off work from a synchronous function in the streams section. A task is
similar to a thread, but instead of being managed by the operating system, it’s
managed by library-level code: the runtime.
There’s a reason the APIs for spawning threads and spawning tasks are so similar. Threads act as a boundary for sets of synchronous operations; concurrency is possible between threads. Tasks act as a boundary for sets of asynchronous operations; concurrency is possible both between and within tasks, because a task can switch between futures in its body. Finally, futures are Rust’s most granular unit of concurrency, and each future may represent a tree of other futures. The runtime—specifically, its executor—manages tasks, and tasks manage futures. In that regard, tasks are similar to lightweight, runtime-managed threads with added capabilities that come from being managed by a runtime instead of by the operating system.
This doesn’t mean that async tasks are always better than threads (or vice
versa). Concurrency with threads is in some ways a simpler programming model
than concurrency with async. That can be a strength or a weakness. Threads are
somewhat “fire and forget”; they have no native equivalent to a future, so they
simply run to completion without being interrupted except by the operating
system itself.
And it turns out that threads and tasks often work
very well together, because tasks can (at least in some runtimes) be moved
around between threads. In fact, under the hood, the runtime we’ve been
using—including the spawn_blocking and spawn_task functions—is multithreaded
by default! Many runtimes use an approach called work stealing to
transparently move tasks around between threads, based on how the threads are
currently being utilized, to improve the system’s overall performance. That
approach actually requires threads and tasks, and therefore futures.
When thinking about which method to use when, consider these rules of thumb:
- If the work is very parallelizable (that is, CPU-bound), such as processing a bunch of data where each part can be processed separately, threads are a better choice.
- If the work is very concurrent (that is, I/O-bound), such as handling messages from a bunch of different sources that may come in at different intervals or different rates, async is a better choice.
And if you need both parallelism and concurrency, you don’t have to choose between threads and async. You can use them together freely, letting each play the part it’s best at. For example, Listing 17-25 shows a fairly common example of this kind of mix in real-world Rust code.
extern crate trpl; // for mdbook test
use std::{thread, time::Duration};
fn main() {
let (tx, mut rx) = trpl::channel();
thread::spawn(move || {
for i in 1..11 {
tx.send(i).unwrap();
thread::sleep(Duration::from_secs(1));
}
});
trpl::block_on(async {
while let Some(message) = rx.recv().await {
println!("{message}");
}
});
}
We begin by creating an async channel, then spawning a thread that takes
ownership of the sender side of the channel using the move keyword. Within
the thread, we send the numbers 1 through 10, sleeping for a second between
each. Finally, we run a future created with an async block passed to
trpl::block_on just as we have throughout the chapter. In that future, we
await those messages, just as in the other message-passing examples we have
seen.
To return to the scenario we opened the chapter with, imagine running a set of video encoding tasks using a dedicated thread (because video encoding is compute-bound) but notifying the UI that those operations are done with an async channel. There are countless examples of these kinds of combinations in real-world use cases.
Summary
This isn’t the last you’ll see of concurrency in this book. The project in Chapter 21 will apply these concepts in a more realistic situation than the simpler examples discussed here and compare problem-solving with threading versus tasks and futures more directly.
No matter which of these approaches you choose, Rust gives you the tools you need to write safe, fast, concurrent code—whether for a high-throughput web server or an embedded operating system.
Next, we’ll talk about idiomatic ways to model problems and structure solutions as your Rust programs get bigger. In addition, we’ll discuss how Rust’s idioms relate to those you might be familiar with from object-oriented programming.
Object-Oriented Programming Features
Object-oriented programming (OOP) is a way of modeling programs. Objects as a programmatic concept were introduced in the programming language Simula in the 1960s. Those objects influenced Alan Kay’s programming architecture in which objects pass messages to each other. To describe this architecture, he coined the term object-oriented programming in 1967. Many competing definitions describe what OOP is, and by some of these definitions Rust is object oriented but by others it is not. In this chapter, we’ll explore certain characteristics that are commonly considered object oriented and how those characteristics translate to idiomatic Rust. We’ll then show you how to implement an object-oriented design pattern in Rust and discuss the trade-offs of doing so versus implementing a solution using some of Rust’s strengths instead.
คุณลักษณะของภาษา object-oriented
Characteristics of Object-Oriented Languages
There is no consensus in the programming community about what features a language must have to be considered object oriented. Rust is influenced by many programming paradigms, including OOP; for example, we explored the features that came from functional programming in Chapter 13. Arguably, OOP languages share certain common characteristics—namely, objects, encapsulation, and inheritance. Let’s look at what each of those characteristics means and whether Rust supports it.
Objects Contain Data and Behavior
The book Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Addison-Wesley, 1994), colloquially referred to as The Gang of Four book, is a catalog of object-oriented design patterns. It defines OOP in this way:
Object-oriented programs are made up of objects. An object packages both data and the procedures that operate on that data. The procedures are typically called methods or operations.
Using this definition, Rust is object oriented: Structs and enums have data,
and impl blocks provide methods on structs and enums. Even though structs and
enums with methods aren’t called objects, they provide the same
functionality, according to the Gang of Four’s definition of objects.
Encapsulation That Hides Implementation Details
Another aspect commonly associated with OOP is the idea of encapsulation, which means that the implementation details of an object aren’t accessible to code using that object. Therefore, the only way to interact with an object is through its public API; code using the object shouldn’t be able to reach into the object’s internals and change data or behavior directly. This enables the programmer to change and refactor an object’s internals without needing to change the code that uses the object.
We discussed how to control encapsulation in Chapter 7: We can use the pub
keyword to decide which modules, types, functions, and methods in our code
should be public, and by default everything else is private. For example, we
can define a struct AveragedCollection that has a field containing a vector
of i32 values. The struct can also have a field that contains the average of
the values in the vector, meaning the average doesn’t have to be computed on
demand whenever anyone needs it. In other words, AveragedCollection will
cache the calculated average for us. Listing 18-1 has the definition of the
AveragedCollection struct.
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
AveragedCollection struct that maintains a list of integers and the average of the items in the collectionThe struct is marked pub so that other code can use it, but the fields within
the struct remain private. This is important in this case because we want to
ensure that whenever a value is added or removed from the list, the average is
also updated. We do this by implementing add, remove, and average methods
on the struct, as shown in Listing 18-2.
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}
pub fn average(&self) -> f64 {
self.average
}
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
add, remove, and average on AveragedCollectionThe public methods add, remove, and average are the only ways to access
or modify data in an instance of AveragedCollection. When an item is added to
list using the add method or removed using the remove method, the
implementations of each call the private update_average method that handles
updating the average field as well.
We leave the list and average fields private so that there is no way for
external code to add or remove items to or from the list field directly;
otherwise, the average field might become out of sync when the list
changes. The average method returns the value in the average field,
allowing external code to read the average but not modify it.
Because we’ve encapsulated the implementation details of the struct
AveragedCollection, we can easily change aspects, such as the data structure,
in the future. For instance, we could use a HashSet<i32> instead of a
Vec<i32> for the list field. As long as the signatures of the add,
remove, and average public methods stayed the same, code using
AveragedCollection wouldn’t need to change. If we made list public instead,
this wouldn’t necessarily be the case: HashSet<i32> and Vec<i32> have
different methods for adding and removing items, so the external code would
likely have to change if it were modifying list directly.
If encapsulation is a required aspect for a language to be considered object
oriented, then Rust meets that requirement. The option to use pub or not for
different parts of code enables encapsulation of implementation details.
Inheritance as a Type System and as Code Sharing
Inheritance is a mechanism whereby an object can inherit elements from another object’s definition, thus gaining the parent object’s data and behavior without you having to define them again.
If a language must have inheritance to be object oriented, then Rust is not such a language. There is no way to define a struct that inherits the parent struct’s fields and method implementations without using a macro.
However, if you’re used to having inheritance in your programming toolbox, you can use other solutions in Rust, depending on your reason for reaching for inheritance in the first place.
You would choose inheritance for two main reasons. One is for reuse of code:
You can implement particular behavior for one type, and inheritance enables you
to reuse that implementation for a different type. You can do this in a limited
way in Rust code using default trait method implementations, which you saw in
Listing 10-14 when we added a default implementation of the summarize method
on the Summary trait. Any type implementing the Summary trait would have
the summarize method available on it without any further code. This is
similar to a parent class having an implementation of a method and an
inheriting child class also having the implementation of the method. We can
also override the default implementation of the summarize method when we
implement the Summary trait, which is similar to a child class overriding the
implementation of a method inherited from a parent class.
The other reason to use inheritance relates to the type system: to enable a child type to be used in the same places as the parent type. This is also called polymorphism, which means that you can substitute multiple objects for each other at runtime if they share certain characteristics.
Polymorphism
To many people, polymorphism is synonymous with inheritance. But it’s actually a more general concept that refers to code that can work with data of multiple types. For inheritance, those types are generally subclasses.
Rust instead uses generics to abstract over different possible types and trait bounds to impose constraints on what those types must provide. This is sometimes called bounded parametric polymorphism.
Rust has chosen a different set of trade-offs by not offering inheritance. Inheritance is often at risk of sharing more code than necessary. Subclasses shouldn’t always share all characteristics of their parent class but will do so with inheritance. This can make a program’s design less flexible. It also introduces the possibility of calling methods on subclasses that don’t make sense or that cause errors because the methods don’t apply to the subclass. In addition, some languages will only allow single inheritance (meaning a subclass can only inherit from one class), further restricting the flexibility of a program’s design.
For these reasons, Rust takes the different approach of using trait objects instead of inheritance to achieve polymorphism at runtime. Let’s look at how trait objects work.
ใช้ trait object เพื่อนามธรรมพฤติกรรมร่วม
Using Trait Objects to Abstract over Shared Behavior
In Chapter 8, we mentioned that one limitation of vectors is that they can
store elements of only one type. We created a workaround in Listing 8-9 where
we defined a SpreadsheetCell enum that had variants to hold integers, floats,
and text. This meant we could store different types of data in each cell and
still have a vector that represented a row of cells. This is a perfectly good
solution when our interchangeable items are a fixed set of types that we know
when our code is compiled.
However, sometimes we want our library user to be able to extend the set of
types that are valid in a particular situation. To show how we might achieve
this, we’ll create an example graphical user interface (GUI) tool that iterates
through a list of items, calling a draw method on each one to draw it to the
screen—a common technique for GUI tools. We’ll create a library crate called
gui that contains the structure of a GUI library. This crate might include
some types for people to use, such as Button or TextField. In addition,
gui users will want to create their own types that can be drawn: For
instance, one programmer might add an Image, and another might add a
SelectBox.
At the time of writing the library, we can’t know and define all the types
other programmers might want to create. But we do know that gui needs to keep
track of many values of different types, and it needs to call a draw method
on each of these differently typed values. It doesn’t need to know exactly what
will happen when we call the draw method, just that the value will have that
method available for us to call.
To do this in a language with inheritance, we might define a class named
Component that has a method named draw on it. The other classes, such as
Button, Image, and SelectBox, would inherit from Component and thus
inherit the draw method. They could each override the draw method to define
their custom behavior, but the framework could treat all of the types as if
they were Component instances and call draw on them. But because Rust
doesn’t have inheritance, we need another way to structure the gui library to
allow users to create new types compatible with the library.
Defining a Trait for Common Behavior
To implement the behavior that we want gui to have, we’ll define a trait
named Draw that will have one method named draw. Then, we can define a
vector that takes a trait object. A trait object points to both an instance
of a type implementing our specified trait and a table used to look up trait
methods on that type at runtime. We create a trait object by specifying some
sort of pointer, such as a reference or a Box<T> smart pointer, then the
dyn keyword, and then specifying the relevant trait. (We’ll talk about the
reason trait objects must use a pointer in “Dynamically Sized Types and the
Sized Trait” in Chapter 20.) We can use
trait objects in place of a generic or concrete type. Wherever we use a trait
object, Rust’s type system will ensure at compile time that any value used in
that context will implement the trait object’s trait. Consequently, we don’t
need to know all the possible types at compile time.
We’ve mentioned that, in Rust, we refrain from calling structs and enums
“objects” to distinguish them from other languages’ objects. In a struct or
enum, the data in the struct fields and the behavior in impl blocks are
separated, whereas in other languages, the data and behavior combined into one
concept is often labeled an object. Trait objects differ from objects in other
languages in that we can’t add data to a trait object. Trait objects aren’t as
generally useful as objects in other languages: Their specific purpose is to
allow abstraction across common behavior.
Listing 18-3 shows how to define a trait named Draw with one method named
draw.
pub trait Draw {
fn draw(&self);
}
Draw traitThis syntax should look familiar from our discussions on how to define traits
in Chapter 10. Next comes some new syntax: Listing 18-4 defines a struct named
Screen that holds a vector named components. This vector is of type
Box<dyn Draw>, which is a trait object; it’s a stand-in for any type inside a
Box that implements the Draw trait.
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
Screen struct with a components field holding a vector of trait objects that implement the Draw traitOn the Screen struct, we’ll define a method named run that will call the
draw method on each of its components, as shown in Listing 18-5.
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
run method on Screen that calls the draw method on each componentThis works differently from defining a struct that uses a generic type
parameter with trait bounds. A generic type parameter can be substituted with
only one concrete type at a time, whereas trait objects allow for multiple
concrete types to fill in for the trait object at runtime. For example, we
could have defined the Screen struct using a generic type and a trait bound,
as in Listing 18-6.
pub trait Draw {
fn draw(&self);
}
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
Screen struct and its run method using generics and trait boundsThis restricts us to a Screen instance that has a list of components all of
type Button or all of type TextField. If you’ll only ever have homogeneous
collections, using generics and trait bounds is preferable because the
definitions will be monomorphized at compile time to use the concrete types.
On the other hand, with the method using trait objects, one Screen instance
can hold a Vec<T> that contains a Box<Button> as well as a
Box<TextField>. Let’s look at how this works, and then we’ll talk about the
runtime performance implications.
Implementing the Trait
Now we’ll add some types that implement the Draw trait. We’ll provide the
Button type. Again, actually implementing a GUI library is beyond the scope
of this book, so the draw method won’t have any useful implementation in its
body. To imagine what the implementation might look like, a Button struct
might have fields for width, height, and label, as shown in Listing 18-7.
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}
Button struct that implements the Draw traitThe width, height, and label fields on Button will differ from the
fields on other components; for example, a TextField type might have those
same fields plus a placeholder field. Each of the types we want to draw on
the screen will implement the Draw trait but will use different code in the
draw method to define how to draw that particular type, as Button has here
(without the actual GUI code, as mentioned). The Button type, for instance,
might have an additional impl block containing methods related to what
happens when a user clicks the button. These kinds of methods won’t apply to
types like TextField.
If someone using our library decides to implement a SelectBox struct that has
width, height, and options fields, they would implement the Draw trait
on the SelectBox type as well, as shown in Listing 18-8.
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
fn main() {}
gui and implementing the Draw trait on a SelectBox structOur library’s user can now write their main function to create a Screen
instance. To the Screen instance, they can add a SelectBox and a Button
by putting each in a Box<T> to become a trait object. They can then call the
run method on the Screen instance, which will call draw on each of the
components. Listing 18-9 shows this implementation.
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
use gui::{Button, Screen};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
When we wrote the library, we didn’t know that someone might add the
SelectBox type, but our Screen implementation was able to operate on the
new type and draw it because SelectBox implements the Draw trait, which
means it implements the draw method.
This concept—of being concerned only with the messages a value responds to
rather than the value’s concrete type—is similar to the concept of duck
typing in dynamically typed languages: If it walks like a duck and quacks like
a duck, then it must be a duck! In the implementation of run on Screen in
Listing 18-5, run doesn’t need to know what the concrete type of each
component is. It doesn’t check whether a component is an instance of a Button
or a SelectBox, it just calls the draw method on the component. By
specifying Box<dyn Draw> as the type of the values in the components
vector, we’ve defined Screen to need values that we can call the draw
method on.
The advantage of using trait objects and Rust’s type system to write code similar to code using duck typing is that we never have to check whether a value implements a particular method at runtime or worry about getting errors if a value doesn’t implement a method but we call it anyway. Rust won’t compile our code if the values don’t implement the traits that the trait objects need.
For example, Listing 18-10 shows what happens if we try to create a Screen
with a String as a component.
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
We’ll get this error because String doesn’t implement the Draw trait:
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast from `Box<String>` to `Box<dyn Draw>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error
This error lets us know that either we’re passing something to Screen that we
didn’t mean to pass and so should pass a different type, or we should implement
Draw on String so that Screen is able to call draw on it.
Performing Dynamic Dispatch
Recall in “Performance of Code Using Generics” in Chapter 10 our discussion on the monomorphization process performed on generics by the compiler: The compiler generates nongeneric implementations of functions and methods for each concrete type that we use in place of a generic type parameter. The code that results from monomorphization is doing static dispatch, which is when the compiler knows what method you’re calling at compile time. This is opposed to dynamic dispatch, which is when the compiler can’t tell at compile time which method you’re calling. In dynamic dispatch cases, the compiler emits code that at runtime will know which method to call.
When we use trait objects, Rust must use dynamic dispatch. The compiler doesn’t know all the types that might be used with the code that’s using trait objects, so it doesn’t know which method implemented on which type to call. Instead, at runtime, Rust uses the pointers inside the trait object to know which method to call. This lookup incurs a runtime cost that doesn’t occur with static dispatch. Dynamic dispatch also prevents the compiler from choosing to inline a method’s code, which in turn prevents some optimizations, and Rust has some rules about where you can and cannot use dynamic dispatch, called dyn compatibility. Those rules are beyond the scope of this discussion, but you can read more about them in the reference. However, we did get extra flexibility in the code that we wrote in Listing 18-5 and were able to support in Listing 18-9, so it’s a trade-off to consider.
Implement design pattern แบบ OOP
Implementing an Object-Oriented Design Pattern
The state pattern is an object-oriented design pattern. The crux of the pattern is that we define a set of states a value can have internally. The states are represented by a set of state objects, and the value’s behavior changes based on its state. We’re going to work through an example of a blog post struct that has a field to hold its state, which will be a state object from the set “draft,” “review,” or “published.”
The state objects share functionality: In Rust, of course, we use structs and traits rather than objects and inheritance. Each state object is responsible for its own behavior and for governing when it should change into another state. The value that holds a state object knows nothing about the different behavior of the states or when to transition between states.
The advantage of using the state pattern is that, when the business requirements of the program change, we won’t need to change the code of the value holding the state or the code that uses the value. We’ll only need to update the code inside one of the state objects to change its rules or perhaps add more state objects.
First, we’re going to implement the state pattern in a more traditional object-oriented way. Then, we’ll use an approach that’s a bit more natural in Rust. Let’s dig in to incrementally implement a blog post workflow using the state pattern.
The final functionality will look like this:
- A blog post starts as an empty draft.
- When the draft is done, a review of the post is requested.
- When the post is approved, it gets published.
- Only published blog posts return content to print so that unapproved posts can’t accidentally be published.
Any other changes attempted on a post should have no effect. For example, if we try to approve a draft blog post before we’ve requested a review, the post should remain an unpublished draft.
Attempting Traditional Object-Oriented Style
There are infinite ways to structure code to solve the same problem, each with different trade-offs. This section’s implementation is more of a traditional object-oriented style, which is possible to write in Rust, but doesn’t take advantage of some of Rust’s strengths. Later, we’ll demonstrate a different solution that still uses the object-oriented design pattern but is structured in a way that might look less familiar to programmers with object-oriented experience. We’ll compare the two solutions to experience the trade-offs of designing Rust code differently than code in other languages.
Listing 18-11 shows this workflow in code form: This is an example usage of the
API we’ll implement in a library crate named blog. This won’t compile yet
because we haven’t implemented the blog crate.
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
blog crate to haveWe want to allow the user to create a new draft blog post with Post::new. We
want to allow text to be added to the blog post. If we try to get the post’s
content immediately, before approval, we shouldn’t get any text because the
post is still a draft. We’ve added assert_eq! in the code for demonstration
purposes. An excellent unit test for this would be to assert that a draft blog
post returns an empty string from the content method, but we’re not going to
write tests for this example.
Next, we want to enable a request for a review of the post, and we want
content to return an empty string while waiting for the review. When the post
receives approval, it should get published, meaning the text of the post will
be returned when content is called.
Notice that the only type we’re interacting with from the crate is the Post
type. This type will use the state pattern and will hold a value that will be
one of three state objects representing the various states a post can be
in—draft, review, or published. Changing from one state to another will be
managed internally within the Post type. The states change in response to the
methods called by our library’s users on the Post instance, but they don’t
have to manage the state changes directly. Also, users can’t make a mistake
with the states, such as publishing a post before it’s reviewed.
Defining Post and Creating a New Instance
Let’s get started on the implementation of the library! We know we need a
public Post struct that holds some content, so we’ll start with the
definition of the struct and an associated public new function to create an
instance of Post, as shown in Listing 18-12. We’ll also make a private
State trait that will define the behavior that all state objects for a Post
must have.
Then, Post will hold a trait object of Box<dyn State> inside an Option<T>
in a private field named state to hold the state object. You’ll see why the
Option<T> is necessary in a bit.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}
trait State {}
struct Draft {}
impl State for Draft {}
Post struct and a new function that creates a new Post instance, a State trait, and a Draft structThe State trait defines the behavior shared by different post states. The
state objects are Draft, PendingReview, and Published, and they will all
implement the State trait. For now, the trait doesn’t have any methods, and
we’ll start by defining just the Draft state because that is the state we
want a post to start in.
When we create a new Post, we set its state field to a Some value that
holds a Box. This Box points to a new instance of the Draft struct. This
ensures that whenever we create a new instance of Post, it will start out as
a draft. Because the state field of Post is private, there is no way to
create a Post in any other state! In the Post::new function, we set the
content field to a new, empty String.
Storing the Text of the Post Content
We saw in Listing 18-11 that we want to be able to call a method named
add_text and pass it a &str that is then added as the text content of the
blog post. We implement this as a method, rather than exposing the content
field as pub, so that later we can implement a method that will control how
the content field’s data is read. The add_text method is pretty
straightforward, so let’s add the implementation in Listing 18-13 to the impl Post block.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
trait State {}
struct Draft {}
impl State for Draft {}
add_text method to add text to a post’s contentThe add_text method takes a mutable reference to self because we’re
changing the Post instance that we’re calling add_text on. We then call
push_str on the String in content and pass the text argument to add to
the saved content. This behavior doesn’t depend on the state the post is in,
so it’s not part of the state pattern. The add_text method doesn’t interact
with the state field at all, but it is part of the behavior we want to
support.
Ensuring That the Content of a Draft Post Is Empty
Even after we’ve called add_text and added some content to our post, we still
want the content method to return an empty string slice because the post is
still in the draft state, as shown by the first assert_eq! in Listing 18-11.
For now, let’s implement the content method with the simplest thing that will
fulfill this requirement: always returning an empty string slice. We’ll change
this later once we implement the ability to change a post’s state so that it
can be published. So far, posts can only be in the draft state, so the post
content should always be empty. Listing 18-14 shows this placeholder
implementation.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
}
trait State {}
struct Draft {}
impl State for Draft {}
content method on Post that always returns an empty string sliceWith this added content method, everything in Listing 18-11 through the first
assert_eq! works as intended.
Requesting a Review, Which Changes the Post’s State
Next, we need to add functionality to request a review of a post, which should
change its state from Draft to PendingReview. Listing 18-15 shows this code.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
}
request_review methods on Post and the State traitWe give Post a public method named request_review that will take a mutable
reference to self. Then, we call an internal request_review method on the
current state of Post, and this second request_review method consumes the
current state and returns a new state.
We add the request_review method to the State trait; all types that
implement the trait will now need to implement the request_review method.
Note that rather than having self, &self, or &mut self as the first
parameter of the method, we have self: Box<Self>. This syntax means the
method is only valid when called on a Box holding the type. This syntax takes
ownership of Box<Self>, invalidating the old state so that the state value of
the Post can transform into a new state.
To consume the old state, the request_review method needs to take ownership
of the state value. This is where the Option in the state field of Post
comes in: We call the take method to take the Some value out of the state
field and leave a None in its place because Rust doesn’t let us have
unpopulated fields in structs. This lets us move the state value out of
Post rather than borrowing it. Then, we’ll set the post’s state value to
the result of this operation.
We need to set state to None temporarily rather than setting it directly
with code like self.state = self.state.request_review(); to get ownership of
the state value. This ensures that Post can’t use the old state value
after we’ve transformed it into a new state.
The request_review method on Draft returns a new, boxed instance of a new
PendingReview struct, which represents the state when a post is waiting for a
review. The PendingReview struct also implements the request_review method
but doesn’t do any transformations. Rather, it returns itself because when we
request a review on a post already in the PendingReview state, it should stay
in the PendingReview state.
Now we can start seeing the advantages of the state pattern: The
request_review method on Post is the same no matter its state value. Each
state is responsible for its own rules.
We’ll leave the content method on Post as is, returning an empty string
slice. We can now have a Post in the PendingReview state as well as in the
Draft state, but we want the same behavior in the PendingReview state.
Listing 18-11 now works up to the second assert_eq! call!
Adding approve to Change content’s Behavior
The approve method will be similar to the request_review method: It will
set state to the value that the current state says it should have when that
state is approved, as shown in Listing 18-16.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
""
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
approve method on Post and the State traitWe add the approve method to the State trait and add a new struct that
implements State, the Published state.
Similar to the way request_review on PendingReview works, if we call the
approve method on a Draft, it will have no effect because approve will
return self. When we call approve on PendingReview, it returns a new,
boxed instance of the Published struct. The Published struct implements the
State trait, and for both the request_review method and the approve
method, it returns itself because the post should stay in the Published state
in those cases.
Now we need to update the content method on Post. We want the value
returned from content to depend on the current state of the Post, so we’re
going to have the Post delegate to a content method defined on its state,
as shown in Listing 18-17.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
// --snip--
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
// --snip--
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
content method on Post to delegate to a content method on StateBecause the goal is to keep all of these rules inside the structs that
implement State, we call a content method on the value in state and pass
the post instance (that is, self) as an argument. Then, we return the value
that’s returned from using the content method on the state value.
We call the as_ref method on the Option because we want a reference to the
value inside the Option rather than ownership of the value. Because state is
an Option<Box<dyn State>>, when we call as_ref, an Option<&Box<dyn State>> is returned. If we didn’t call as_ref, we would get an error because
we can’t move state out of the borrowed &self of the function parameter.
We then call the unwrap method, which we know will never panic because we
know the methods on Post ensure that state will always contain a Some
value when those methods are done. This is one of the cases we talked about in
the “When You Have More Information Than the
Compiler” section of Chapter 9 when we
know that a None value is never possible, even though the compiler isn’t able
to understand that.
At this point, when we call content on the &Box<dyn State>, deref coercion
will take effect on the & and the Box so that the content method will
ultimately be called on the type that implements the State trait. That means
we need to add content to the State trait definition, and that is where
we’ll put the logic for what content to return depending on which state we
have, as shown in Listing 18-18.
pub struct Post {
state: Option<Box<dyn State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self)
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State>;
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
// --snip--
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<dyn State> {
Box::new(PendingReview {})
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
// --snip--
fn request_review(self: Box<Self>) -> Box<dyn State> {
self
}
fn approve(self: Box<Self>) -> Box<dyn State> {
self
}
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
content method to the State traitWe add a default implementation for the content method that returns an empty
string slice. That means we don’t need to implement content on the Draft
and PendingReview structs. The Published struct will override the content
method and return the value in post.content. While convenient, having the
content method on State determine the content of the Post is blurring
the lines between the responsibility of State and the responsibility of
Post.
Note that we need lifetime annotations on this method, as we discussed in
Chapter 10. We’re taking a reference to a post as an argument and returning a
reference to part of that post, so the lifetime of the returned reference is
related to the lifetime of the post argument.
And we’re done—all of Listing 18-11 now works! We’ve implemented the state
pattern with the rules of the blog post workflow. The logic related to the
rules lives in the state objects rather than being scattered throughout Post.
Why Not An Enum?
You may have been wondering why we didn’t use an enum with the different
possible post states as variants. That’s certainly a possible solution; try it
and compare the end results to see which you prefer! One disadvantage of using
an enum is that every place that checks the value of the enum will need a
match expression or similar to handle every possible variant. This could get
more repetitive than this trait object solution.
Evaluating the State Pattern
We’ve shown that Rust is capable of implementing the object-oriented state
pattern to encapsulate the different kinds of behavior a post should have in
each state. The methods on Post know nothing about the various behaviors.
Because of the way we organized the code, we have to look in only one place to
know the different ways a published post can behave: the implementation of the
State trait on the Published struct.
If we were to create an alternative implementation that didn’t use the state
pattern, we might instead use match expressions in the methods on Post or
even in the main code that checks the state of the post and changes behavior
in those places. That would mean we would have to look in several places to
understand all the implications of a post being in the published state.
With the state pattern, the Post methods and the places we use Post don’t
need match expressions, and to add a new state, we would only need to add a
new struct and implement the trait methods on that one struct in one location.
The implementation using the state pattern is easy to extend to add more functionality. To see the simplicity of maintaining code that uses the state pattern, try a few of these suggestions:
- Add a
rejectmethod that changes the post’s state fromPendingReviewback toDraft. - Require two calls to
approvebefore the state can be changed toPublished. - Allow users to add text content only when a post is in the
Draftstate. Hint: have the state object responsible for what might change about the content but not responsible for modifying thePost.
One downside of the state pattern is that, because the states implement the
transitions between states, some of the states are coupled to each other. If we
add another state between PendingReview and Published, such as Scheduled,
we would have to change the code in PendingReview to transition to
Scheduled instead. It would be less work if PendingReview didn’t need to
change with the addition of a new state, but that would mean switching to
another design pattern.
Another downside is that we’ve duplicated some logic. To eliminate some of the
duplication, we might try to make default implementations for the
request_review and approve methods on the State trait that return self.
However, this wouldn’t work: When using State as a trait object, the trait
doesn’t know what the concrete self will be exactly, so the return type isn’t
known at compile time. (This is one of the dyn compatibility rules mentioned
earlier.)
Other duplication includes the similar implementations of the request_review
and approve methods on Post. Both methods use Option::take with the
state field of Post, and if state is Some, they delegate to the wrapped
value’s implementation of the same method and set the new value of the state
field to the result. If we had a lot of methods on Post that followed this
pattern, we might consider defining a macro to eliminate the repetition (see
the “Macros” section in Chapter 20).
By implementing the state pattern exactly as it’s defined for object-oriented
languages, we’re not taking as full advantage of Rust’s strengths as we could.
Let’s look at some changes we can make to the blog crate that can make
invalid states and transitions into compile-time errors.
Encoding States and Behavior as Types
We’ll show you how to rethink the state pattern to get a different set of trade-offs. Rather than encapsulating the states and transitions completely so that outside code has no knowledge of them, we’ll encode the states into different types. Consequently, Rust’s type-checking system will prevent attempts to use draft posts where only published posts are allowed by issuing a compiler error.
Let’s consider the first part of main in Listing 18-11:
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
We still enable the creation of new posts in the draft state using Post::new
and the ability to add text to the post’s content. But instead of having a
content method on a draft post that returns an empty string, we’ll make it so
that draft posts don’t have the content method at all. That way, if we try to
get a draft post’s content, we’ll get a compiler error telling us the method
doesn’t exist. As a result, it will be impossible for us to accidentally
display draft post content in production because that code won’t even compile.
Listing 18-19 shows the definition of a Post struct and a DraftPost struct,
as well as methods on each.
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
Post with a content method and a DraftPost without a content methodBoth the Post and DraftPost structs have a private content field that
stores the blog post text. The structs no longer have the state field because
we’re moving the encoding of the state to the types of the structs. The Post
struct will represent a published post, and it has a content method that
returns the content.
We still have a Post::new function, but instead of returning an instance of
Post, it returns an instance of DraftPost. Because content is private and
there aren’t any functions that return Post, it’s not possible to create an
instance of Post right now.
The DraftPost struct has an add_text method, so we can add text to
content as before, but note that DraftPost does not have a content method
defined! So now the program ensures that all posts start as draft posts, and
draft posts don’t have their content available for display. Any attempt to get
around these constraints will result in a compiler error.
So, how do we get a published post? We want to enforce the rule that a draft
post has to be reviewed and approved before it can be published. A post in the
pending review state should still not display any content. Let’s implement
these constraints by adding another struct, PendingReviewPost, defining the
request_review method on DraftPost to return a PendingReviewPost and
defining an approve method on PendingReviewPost to return a Post, as
shown in Listing 18-20.
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
// --snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
content: self.content,
}
}
}
pub struct PendingReviewPost {
content: String,
}
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
PendingReviewPost that gets created by calling request_review on DraftPost and an approve method that turns a PendingReviewPost into a published PostThe request_review and approve methods take ownership of self, thus
consuming the DraftPost and PendingReviewPost instances and transforming
them into a PendingReviewPost and a published Post, respectively. This way,
we won’t have any lingering DraftPost instances after we’ve called
request_review on them, and so forth. The PendingReviewPost struct doesn’t
have a content method defined on it, so attempting to read its content
results in a compiler error, as with DraftPost. Because the only way to get a
published Post instance that does have a content method defined is to call
the approve method on a PendingReviewPost, and the only way to get a
PendingReviewPost is to call the request_review method on a DraftPost,
we’ve now encoded the blog post workflow into the type system.
But we also have to make some small changes to main. The request_review and
approve methods return new instances rather than modifying the struct they’re
called on, so we need to add more let post = shadowing assignments to save
the returned instances. We also can’t have the assertions about the draft and
pending review posts’ contents be empty strings, nor do we need them: We can’t
compile code that tries to use the content of posts in those states any longer.
The updated code in main is shown in Listing 18-21.
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
let post = post.request_review();
let post = post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
main to use the new implementation of the blog post workflowThe changes we needed to make to main to reassign post mean that this
implementation doesn’t quite follow the object-oriented state pattern anymore:
The transformations between the states are no longer encapsulated entirely
within the Post implementation. However, our gain is that invalid states are
now impossible because of the type system and the type checking that happens at
compile time! This ensures that certain bugs, such as display of the content of
an unpublished post, will be discovered before they make it to production.
Try the tasks suggested at the start of this section on the blog crate as it
is after Listing 18-21 to see what you think about the design of this version
of the code. Note that some of the tasks might be completed already in this
design.
We’ve seen that even though Rust is capable of implementing object-oriented design patterns, other patterns, such as encoding state into the type system, are also available in Rust. These patterns have different trade-offs. Although you might be very familiar with object-oriented patterns, rethinking the problem to take advantage of Rust’s features can provide benefits, such as preventing some bugs at compile time. Object-oriented patterns won’t always be the best solution in Rust due to certain features, like ownership, that object-oriented languages don’t have.
Summary
Regardless of whether you think Rust is an object-oriented language after reading this chapter, you now know that you can use trait objects to get some object-oriented features in Rust. Dynamic dispatch can give your code some flexibility in exchange for a bit of runtime performance. You can use this flexibility to implement object-oriented patterns that can help your code’s maintainability. Rust also has other features, like ownership, that object-oriented languages don’t have. An object-oriented pattern won’t always be the best way to take advantage of Rust’s strengths, but it is an available option.
Next, we’ll look at patterns, which are another of Rust’s features that enable lots of flexibility. We’ve looked at them briefly throughout the book but haven’t seen their full capability yet. Let’s go!
Patterns and Matching
Patterns are a special syntax in Rust for matching against the structure of
types, both complex and simple. Using patterns in conjunction with match
expressions and other constructs gives you more control over a program’s
control flow. A pattern consists of some combination of the following:
- Literals
- Destructured arrays, enums, structs, or tuples
- Variables
- Wildcards
- Placeholders
Some example patterns include x, (a, 3), and Some(Color::Red). In the
contexts in which patterns are valid, these components describe the shape of
data. Our program then matches values against the patterns to determine whether
it has the correct shape of data to continue running a particular piece of code.
To use a pattern, we compare it to some value. If the pattern matches the
value, we use the value parts in our code. Recall the match expressions in
Chapter 6 that used patterns, such as the coin-sorting machine example. If the
value fits the shape of the pattern, we can use the named pieces. If it
doesn’t, the code associated with the pattern won’t run.
This chapter is a reference on all things related to patterns. We’ll cover the valid places to use patterns, the difference between refutable and irrefutable patterns, and the different kinds of pattern syntax that you might see. By the end of the chapter, you’ll know how to use patterns to express many concepts in a clear way.
ที่ ๆ ใช้ pattern ได้ทั้งหมด
All the Places Patterns Can Be Used
Patterns pop up in a number of places in Rust, and you’ve been using them a lot without realizing it! This section discusses all the places where patterns are valid.
match Arms
As discussed in Chapter 6, we use patterns in the arms of match expressions.
Formally, match expressions are defined as the keyword match, a value to
match on, and one or more match arms that consist of a pattern and an
expression to run if the value matches that arm’s pattern, like this:
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
For example, here’s the match expression from Listing 6-5 that matches on an
Option<i32> value in the variable x:
match x {
None => None,
Some(i) => Some(i + 1),
}
The patterns in this match expression are the None and Some(i) to the
left of each arrow.
One requirement for match expressions is that they need to be exhaustive in
the sense that all possibilities for the value in the match expression must
be accounted for. One way to ensure that you’ve covered every possibility is to
have a catch-all pattern for the last arm: For example, a variable name
matching any value can never fail and thus covers every remaining case.
The particular pattern _ will match anything, but it never binds to a
variable, so it’s often used in the last match arm. The _ pattern can be
useful when you want to ignore any value not specified, for example. We’ll
cover the _ pattern in more detail in “Ignoring Values in a
Pattern” later in this chapter.
let Statements
Prior to this chapter, we had only explicitly discussed using patterns with
match and if let, but in fact, we’ve used patterns in other places as well,
including in let statements. For example, consider this straightforward
variable assignment with let:
#![allow(unused)]
fn main() {
let x = 5;
}
Every time you’ve used a let statement like this you’ve been using patterns,
although you might not have realized it! More formally, a let statement looks
like this:
let PATTERN = EXPRESSION;
In statements like let x = 5; with a variable name in the PATTERN slot, the
variable name is just a particularly simple form of a pattern. Rust compares
the expression against the pattern and assigns any names it finds. So, in the
let x = 5; example, x is a pattern that means “bind what matches here to
the variable x.” Because the name x is the whole pattern, this pattern
effectively means “bind everything to the variable x, whatever the value is.”
To see the pattern-matching aspect of let more clearly, consider Listing
19-1, which uses a pattern with let to destructure a tuple.
fn main() {
let (x, y, z) = (1, 2, 3);
}
Here, we match a tuple against a pattern. Rust compares the value (1, 2, 3)
to the pattern (x, y, z) and sees that the value matches the pattern—that is,
it sees that the number of elements is the same in both—so Rust binds 1 to
x, 2 to y, and 3 to z. You can think of this tuple pattern as nesting
three individual variable patterns inside it.
If the number of elements in the pattern doesn’t match the number of elements in the tuple, the overall type won’t match and we’ll get a compiler error. For example, Listing 19-2 shows an attempt to destructure a tuple with three elements into two variables, which won’t work.
fn main() {
let (x, y) = (1, 2, 3);
}
Attempting to compile this code results in this type error:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
--> src/main.rs:2:9
|
2 | let (x, y) = (1, 2, 3);
| ^^^^^^ --------- this expression has type `({integer}, {integer}, {integer})`
| |
| expected a tuple with 3 elements, found one with 2 elements
|
= note: expected tuple `({integer}, {integer}, {integer})`
found tuple `(_, _)`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
To fix the error, we could ignore one or more of the values in the tuple using
_ or .., as you’ll see in the “Ignoring Values in a
Pattern” section. If the problem
is that we have too many variables in the pattern, the solution is to make the
types match by removing variables so that the number of variables equals the
number of elements in the tuple.
Conditional if let Expressions
In Chapter 6, we discussed how to use if let expressions mainly as a shorter
way to write the equivalent of a match that only matches one case.
Optionally, if let can have a corresponding else containing code to run if
the pattern in the if let doesn’t match.
Listing 19-3 shows that it’s also possible to mix and match if let, else if, and else if let expressions. Doing so gives us more flexibility than a
match expression in which we can express only one value to compare with the
patterns. Also, Rust doesn’t require that the conditions in a series of if let, else if, and else if let arms relate to each other.
The code in Listing 19-3 determines what color to make your background based on a series of checks for several conditions. For this example, we’ve created variables with hardcoded values that a real program might receive from user input.
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color {
println!("Using your favorite color, {color}, as the background");
} else if is_tuesday {
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
}
if let, else if, else if let, and elseIf the user specifies a favorite color, that color is used as the background. If no favorite color is specified and today is Tuesday, the background color is green. Otherwise, if the user specifies their age as a string and we can parse it as a number successfully, the color is either purple or orange depending on the value of the number. If none of these conditions apply, the background color is blue.
This conditional structure lets us support complex requirements. With the
hardcoded values we have here, this example will print Using purple as the background color.
You can see that if let can also introduce new variables that shadow existing
variables in the same way that match arms can: The line if let Ok(age) = age
introduces a new age variable that contains the value inside the Ok variant,
shadowing the existing age variable. This means we need to place the if age > 30 condition within that block: We can’t combine these two conditions into if let Ok(age) = age && age > 30. The new age we want to compare to 30 isn’t
valid until the new scope starts with the curly bracket.
The downside of using if let expressions is that the compiler doesn’t check
for exhaustiveness, whereas with match expressions it does. If we omitted the
last else block and therefore missed handling some cases, the compiler would
not alert us to the possible logic bug.
while let Conditional Loops
Similar in construction to if let, the while let conditional loop allows a
while loop to run for as long as a pattern continues to match. In Listing
19-4, we show a while let loop that waits on messages sent between threads,
but in this case checking a Result instead of an Option.
fn main() {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for val in [1, 2, 3] {
tx.send(val).unwrap();
}
});
while let Ok(value) = rx.recv() {
println!("{value}");
}
}
while let loop to print values for as long as rx.recv() returns OkThis example prints 1, 2, and then 3. The recv method takes the first
message out of the receiver side of the channel and returns an Ok(value). When
we first saw recv back in Chapter 16, we unwrapped the error directly, or
we interacted with it as an iterator using a for loop. As Listing 19-4 shows,
though, we can also use while let, because the recv method returns an Ok
each time a message arrives, as long as the sender exists, and then produces an
Err once the sender side disconnects.
for Loops
In a for loop, the value that directly follows the keyword for is a
pattern. For example, in for x in y, the x is the pattern. Listing 19-5
demonstrates how to use a pattern in a for loop to destructure, or break
apart, a tuple as part of the for loop.
fn main() {
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{value} is at index {index}");
}
}
for loop to destructure a tupleThe code in Listing 19-5 will print the following:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2
We adapt an iterator using the enumerate method so that it produces a value
and the index for that value, placed into a tuple. The first value produced is
the tuple (0, 'a'). When this value is matched to the pattern (index, value), index will be 0 and value will be 'a', printing the first line of
the output.
Function Parameters
Function parameters can also be patterns. The code in Listing 19-6, which
declares a function named foo that takes one parameter named x of type
i32, should by now look familiar.
fn foo(x: i32) {
// code goes here
}
fn main() {}
The x part is a pattern! As we did with let, we could match a tuple in a
function’s arguments to the pattern. Listing 19-7 splits the values in a tuple
as we pass it to a function.
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({x}, {y})");
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
This code prints Current location: (3, 5). The values &(3, 5) match the
pattern &(x, y), so x is the value 3 and y is the value 5.
We can also use patterns in closure parameter lists in the same way as in function parameter lists because closures are similar to functions, as discussed in Chapter 13.
At this point, you’ve seen several ways to use patterns, but patterns don’t work the same in every place we can use them. In some places, the patterns must be irrefutable; in other circumstances, they can be refutable. We’ll discuss these two concepts next.
Refutability: pattern ที่อาจ match ไม่สำเร็จ
Refutability: Whether a Pattern Might Fail to Match
Patterns come in two forms: refutable and irrefutable. Patterns that will match
for any possible value passed are irrefutable. An example would be x in the
statement let x = 5; because x matches anything and therefore cannot fail
to match. Patterns that can fail to match for some possible value are
refutable. An example would be Some(x) in the expression if let Some(x) = a_value because if the value in the a_value variable is None rather than
Some, the Some(x) pattern will not match.
Function parameters, let statements, and for loops can only accept
irrefutable patterns because the program cannot do anything meaningful when
values don’t match. The if let and while let expressions and the
let...else statement accept refutable and irrefutable patterns, but the
compiler warns against irrefutable patterns because, by definition, they’re
intended to handle possible failure: The functionality of a conditional is in
its ability to perform differently depending on success or failure.
In general, you shouldn’t have to worry about the distinction between refutable and irrefutable patterns; however, you do need to be familiar with the concept of refutability so that you can respond when you see it in an error message. In those cases, you’ll need to change either the pattern or the construct you’re using the pattern with, depending on the intended behavior of the code.
Let’s look at an example of what happens when we try to use a refutable pattern
where Rust requires an irrefutable pattern and vice versa. Listing 19-8 shows a
let statement, but for the pattern, we’ve specified Some(x), a refutable
pattern. As you might expect, this code will not compile.
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value;
}
letIf some_option_value were a None value, it would fail to match the pattern
Some(x), meaning the pattern is refutable. However, the let statement can
only accept an irrefutable pattern because there is nothing valid the code can
do with a None value. At compile time, Rust will complain that we’ve tried to
use a refutable pattern where an irrefutable pattern is required:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0005]: refutable pattern in local binding
--> src/main.rs:3:9
|
3 | let Some(x) = some_option_value;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
|
3 | let Some(x) = some_option_value else { todo!() };
| ++++++++++++++++
For more information about this error, try `rustc --explain E0005`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error
Because we didn’t cover (and couldn’t cover!) every valid value with the
pattern Some(x), Rust rightfully produces a compiler error.
If we have a refutable pattern where an irrefutable pattern is needed, we can
fix it by changing the code that uses the pattern: Instead of using let, we
can use let...else. Then, if the pattern doesn’t match, the code in the curly
brackets will handle the value. Listing 19-9 shows how to fix the code in
Listing 19-8.
fn main() {
let some_option_value: Option<i32> = None;
let Some(x) = some_option_value else {
return;
};
}
let...else and a block with refutable patterns instead of letWe’ve given the code an out! This code is perfectly valid, although it means we
cannot use an irrefutable pattern without receiving a warning. If we give
let...else a pattern that will always match, such as x, as shown in Listing
19-10, the compiler will give a warning.
fn main() {
let x = 5 else {
return;
};
}
let...elseRust complains that it doesn’t make sense to use let...else with an
irrefutable pattern:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
warning: irrefutable `let...else` pattern
--> src/main.rs:2:5
|
2 | let x = 5 else {
| ^^^^^^^^^
|
= note: this pattern will always match, so the `else` clause is useless
= help: consider removing the `else` clause
= note: `#[warn(irrefutable_let_patterns)]` on by default
warning: `patterns` (bin "patterns") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s
Running `target/debug/patterns`
For this reason, match arms must use refutable patterns, except for the last
arm, which should match any remaining values with an irrefutable pattern. Rust
allows us to use an irrefutable pattern in a match with only one arm, but
this syntax isn’t particularly useful and could be replaced with a simpler
let statement.
Now that you know where to use patterns and the difference between refutable and irrefutable patterns, let’s cover all the syntax we can use to create patterns.
Syntax ของ pattern
Pattern Syntax
In this section, we gather all the syntax that is valid in patterns and discuss why and when you might want to use each one.
Matching Literals
As you saw in Chapter 6, you can match patterns against literals directly. The following code gives some examples:
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
This code prints one because the value in x is 1. This syntax is useful
when you want your code to take an action if it gets a particular concrete
value.
Matching Named Variables
Named variables are irrefutable patterns that match any value, and we’ve used
them many times in this book. However, there is a complication when you use
named variables in match, if let, or while let expressions. Because each
of these kinds of expressions starts a new scope, variables declared as part of
a pattern inside these expressions will shadow those with the same name outside
the constructs, as is the case with all variables. In Listing 19-11, we declare
a variable named x with the value Some(5) and a variable y with the value
10. We then create a match expression on the value x. Look at the
patterns in the match arms and println! at the end, and try to figure out
what the code will print before running this code or reading further.
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
match expression with an arm that introduces a new variable which shadows an existing variable yLet’s walk through what happens when the match expression runs. The pattern
in the first match arm doesn’t match the defined value of x, so the code
continues.
The pattern in the second match arm introduces a new variable named y that
will match any value inside a Some value. Because we’re in a new scope inside
the match expression, this is a new y variable, not the y we declared at
the beginning with the value 10. This new y binding will match any value
inside a Some, which is what we have in x. Therefore, this new y binds to
the inner value of the Some in x. That value is 5, so the expression for
that arm executes and prints Matched, y = 5.
If x had been a None value instead of Some(5), the patterns in the first
two arms wouldn’t have matched, so the value would have matched to the
underscore. We didn’t introduce the x variable in the pattern of the
underscore arm, so the x in the expression is still the outer x that hasn’t
been shadowed. In this hypothetical case, the match would print Default case, x = None.
When the match expression is done, its scope ends, and so does the scope of
the inner y. The last println! produces at the end: x = Some(5), y = 10.
To create a match expression that compares the values of the outer x and
y, rather than introducing a new variable that shadows the existing y
variable, we would need to use a match guard conditional instead. We’ll talk
about match guards later in the “Adding Conditionals with Match
Guards” section.
Matching Multiple Patterns
In match expressions, you can match multiple patterns using the | syntax,
which is the pattern or operator. For example, in the following code, we match
the value of x against the match arms, the first of which has an or option,
meaning if the value of x matches either of the values in that arm, that
arm’s code will run:
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
This code prints one or two.
Matching Ranges of Values with ..=
The ..= syntax allows us to match to an inclusive range of values. In the
following code, when a pattern matches any of the values within the given
range, that arm will execute:
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}
If x is 1, 2, 3, 4, or 5, the first arm will match. This syntax is
more convenient for multiple match values than using the | operator to
express the same idea; if we were to use |, we would have to specify 1 | 2 | 3 | 4 | 5. Specifying a range is much shorter, especially if we want to match,
say, any number between 1 and 1,000!
The compiler checks that the range isn’t empty at compile time, and because the
only types for which Rust can tell if a range is empty or not are char and
numeric values, ranges are only allowed with numeric or char values.
Here is an example using ranges of char values:
fn main() {
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
Rust can tell that 'c' is within the first pattern’s range and prints early ASCII letter.
Destructuring to Break Apart Values
We can also use patterns to destructure structs, enums, and tuples to use different parts of these values. Let’s walk through each value.
Structs
Listing 19-12 shows a Point struct with two fields, x and y, that we can
break apart using a pattern with a let statement.
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
}
This code creates the variables a and b that match the values of the x
and y fields of the p struct. This example shows that the names of the
variables in the pattern don’t have to match the field names of the struct.
However, it’s common to match the variable names to the field names to make it
easier to remember which variables came from which fields. Because of this
common usage, and because writing let Point { x: x, y: y } = p; contains a
lot of duplication, Rust has a shorthand for patterns that match struct fields:
You only need to list the name of the struct field, and the variables created
from the pattern will have the same names. Listing 19-13 behaves in the same
way as the code in Listing 19-12, but the variables created in the let
pattern are x and y instead of a and b.
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
This code creates the variables x and y that match the x and y fields
of the p variable. The outcome is that the variables x and y contain the
values from the p struct.
We can also destructure with literal values as part of the struct pattern rather than creating variables for all the fields. Doing so allows us to test some of the fields for particular values while creating variables to destructure the other fields.
In Listing 19-14, we have a match expression that separates Point values
into three cases: points that lie directly on the x axis (which is true when
y = 0), on the y axis (x = 0), or on neither axis.
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {x}"),
Point { x: 0, y } => println!("On the y axis at {y}"),
Point { x, y } => {
println!("On neither axis: ({x}, {y})");
}
}
}
The first arm will match any point that lies on the x axis by specifying that
the y field matches if its value matches the literal 0. The pattern still
creates an x variable that we can use in the code for this arm.
Similarly, the second arm matches any point on the y axis by specifying that
the x field matches if its value is 0 and creates a variable y for the
value of the y field. The third arm doesn’t specify any literals, so it
matches any other Point and creates variables for both the x and y fields.
In this example, the value p matches the second arm by virtue of x
containing a 0, so this code will print On the y axis at 7.
Remember that a match expression stops checking arms once it has found the
first matching pattern, so even though Point { x: 0, y: 0 } is on the x axis
and the y axis, this code would only print On the x axis at 0.
Enums
We’ve destructured enums in this book (for example, Listing 6-5 in Chapter 6),
but we haven’t yet explicitly discussed that the pattern to destructure an enum
corresponds to the way the data stored within the enum is defined. As an
example, in Listing 19-15, we use the Message enum from Listing 6-2 and write
a match with patterns that will destructure each inner value.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.");
}
Message::Move { x, y } => {
println!("Move in the x direction {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
}
}
This code will print Change color to red 0, green 160, and blue 255. Try
changing the value of msg to see the code from the other arms run.
For enum variants without any data, like Message::Quit, we can’t destructure
the value any further. We can only match on the literal Message::Quit value,
and no variables are in that pattern.
For struct-like enum variants, such as Message::Move, we can use a pattern
similar to the pattern we specify to match structs. After the variant name, we
place curly brackets and then list the fields with variables so that we break
apart the pieces to use in the code for this arm. Here we use the shorthand
form as we did in Listing 19-13.
For tuple-like enum variants, like Message::Write that holds a tuple with one
element and Message::ChangeColor that holds a tuple with three elements, the
pattern is similar to the pattern we specify to match tuples. The number of
variables in the pattern must match the number of elements in the variant we’re
matching.
Nested Structs and Enums
So far, our examples have all been matching structs or enums one level deep,
but matching can work on nested items too! For example, we can refactor the
code in Listing 19-15 to support RGB and HSV colors in the ChangeColor
message, as shown in Listing 19-16.
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}");
}
_ => (),
}
}
The pattern of the first arm in the match expression matches a
Message::ChangeColor enum variant that contains a Color::Rgb variant; then,
the pattern binds to the three inner i32 values. The pattern of the second
arm also matches a Message::ChangeColor enum variant, but the inner enum
matches Color::Hsv instead. We can specify these complex conditions in one
match expression, even though two enums are involved.
Structs and Tuples
We can mix, match, and nest destructuring patterns in even more complex ways. The following example shows a complicated destructure where we nest structs and tuples inside a tuple and destructure all the primitive values out:
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
This code lets us break complex types into their component parts so that we can use the values we’re interested in separately.
Destructuring with patterns is a convenient way to use pieces of values, such as the value from each field in a struct, separately from each other.
Ignoring Values in a Pattern
You’ve seen that it’s sometimes useful to ignore values in a pattern, such as
in the last arm of a match, to get a catch-all that doesn’t actually do
anything but does account for all remaining possible values. There are a few
ways to ignore entire values or parts of values in a pattern: using the _
pattern (which you’ve seen), using the _ pattern within another pattern,
using a name that starts with an underscore, or using .. to ignore remaining
parts of a value. Let’s explore how and why to use each of these patterns.
An Entire Value with _
We’ve used the underscore as a wildcard pattern that will match any value but
not bind to the value. This is especially useful as the last arm in a match
expression, but we can also use it in any pattern, including function
parameters, as shown in Listing 19-17.
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
_ in a function signatureThis code will completely ignore the value 3 passed as the first argument,
and will print This code only uses the y parameter: 4.
In most cases when you no longer need a particular function parameter, you would change the signature so that it doesn’t include the unused parameter. Ignoring a function parameter can be especially useful in cases when, for example, you’re implementing a trait when you need a certain type signature but the function body in your implementation doesn’t need one of the parameters. You then avoid getting a compiler warning about unused function parameters, as you would if you used a name instead.
Parts of a Value with a Nested _
We can also use _ inside another pattern to ignore just part of a value, for
example, when we want to test for only part of a value but have no use for the
other parts in the corresponding code we want to run. Listing 19-18 shows code
responsible for managing a setting’s value. The business requirements are that
the user should not be allowed to overwrite an existing customization of a
setting but can unset the setting and give it a value if it is currently unset.
fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {setting_value:?}");
}
Some variants when we don’t need to use the value inside the SomeThis code will print Can't overwrite an existing customized value and then
setting is Some(5). In the first match arm, we don’t need to match on or use
the values inside either Some variant, but we do need to test for the case
when setting_value and new_setting_value are the Some variant. In that
case, we print the reason for not changing setting_value, and it doesn’t get
changed.
In all other cases (if either setting_value or new_setting_value is None)
expressed by the _ pattern in the second arm, we want to allow
new_setting_value to become setting_value.
We can also use underscores in multiple places within one pattern to ignore particular values. Listing 19-19 shows an example of ignoring the second and fourth values in a tuple of five items.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}");
}
}
}
This code will print Some numbers: 2, 8, 32, and the values 4 and 16 will
be ignored.
An Unused Variable by Starting Its Name with _
If you create a variable but don’t use it anywhere, Rust will usually issue a warning because an unused variable could be a bug. However, sometimes it’s useful to be able to create a variable you won’t use yet, such as when you’re prototyping or just starting a project. In this situation, you can tell Rust not to warn you about the unused variable by starting the name of the variable with an underscore. In Listing 19-20, we create two unused variables, but when we compile this code, we should only get a warning about one of them.
fn main() {
let _x = 5;
let y = 10;
}
Here, we get a warning about not using the variable y, but we don’t get a
warning about not using _x.
Note that there is a subtle difference between using only _ and using a name
that starts with an underscore. The syntax _x still binds the value to the
variable, whereas _ doesn’t bind at all. To show a case where this
distinction matters, Listing 19-21 will provide us with an error.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
We’ll receive an error because the s value will still be moved into _s,
which prevents us from using s again. However, using the underscore by itself
doesn’t ever bind to the value. Listing 19-22 will compile without any errors
because s doesn’t get moved into _.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
}
This code works just fine because we never bind s to anything; it isn’t moved.
Remaining Parts of a Value with ..
With values that have many parts, we can use the .. syntax to use specific
parts and ignore the rest, avoiding the need to list underscores for each
ignored value. The .. pattern ignores any parts of a value that we haven’t
explicitly matched in the rest of the pattern. In Listing 19-23, we have a
Point struct that holds a coordinate in three-dimensional space. In the
match expression, we want to operate only on the x coordinate and ignore
the values in the y and z fields.
fn main() {
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {x}"),
}
}
Point except for x by using ..We list the x value and then just include the .. pattern. This is quicker
than having to list y: _ and z: _, particularly when we’re working with
structs that have lots of fields in situations where only one or two fields are
relevant.
The syntax .. will expand to as many values as it needs to be. Listing 19-24
shows how to use .. with a tuple.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
In this code, the first and last values are matched with first and last.
The .. will match and ignore everything in the middle.
However, using .. must be unambiguous. If it is unclear which values are
intended for matching and which should be ignored, Rust will give us an error.
Listing 19-25 shows an example of using .. ambiguously, so it will not
compile.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
.. in an ambiguous wayWhen we compile this example, we get this error:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
It’s impossible for Rust to determine how many values in the tuple to ignore
before matching a value with second and then how many further values to
ignore thereafter. This code could mean that we want to ignore 2, bind
second to 4, and then ignore 8, 16, and 32; or that we want to ignore
2 and 4, bind second to 8, and then ignore 16 and 32; and so forth.
The variable name second doesn’t mean anything special to Rust, so we get a
compiler error because using .. in two places like this is ambiguous.
Adding Conditionals with Match Guards
A match guard is an additional if condition, specified after the pattern in
a match arm, that must also match for that arm to be chosen. Match guards are
useful for expressing more complex ideas than a pattern alone allows. Note,
however, that they are only available in match expressions, not if let or
while let expressions.
The condition can use variables created in the pattern. Listing 19-26 shows a
match where the first arm has the pattern Some(x) and also has a match
guard of if x % 2 == 0 (which will be true if the number is even).
fn main() {
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("The number {x} is even"),
Some(x) => println!("The number {x} is odd"),
None => (),
}
}
This example will print The number 4 is even. When num is compared to the
pattern in the first arm, it matches because Some(4) matches Some(x). Then,
the match guard checks whether the remainder of dividing x by 2 is equal to
0, and because it is, the first arm is selected.
If num had been Some(5) instead, the match guard in the first arm would
have been false because the remainder of 5 divided by 2 is 1, which is not
equal to 0. Rust would then go to the second arm, which would match because the
second arm doesn’t have a match guard and therefore matches any Some variant.
There is no way to express the if x % 2 == 0 condition within a pattern, so
the match guard gives us the ability to express this logic. The downside of
this additional expressiveness is that the compiler doesn’t try to check for
exhaustiveness when match guard expressions are involved.
When discussing Listing 19-11, we mentioned that we could use match guards to
solve our pattern-shadowing problem. Recall that we created a new variable
inside the pattern in the match expression instead of using the variable
outside the match. That new variable meant we couldn’t test against the value
of the outer variable. Listing 19-27 shows how we can use a match guard to fix
this problem.
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {n}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
This code will now print Default case, x = Some(5). The pattern in the second
match arm doesn’t introduce a new variable y that would shadow the outer y,
meaning we can use the outer y in the match guard. Instead of specifying the
pattern as Some(y), which would have shadowed the outer y, we specify
Some(n). This creates a new variable n that doesn’t shadow anything because
there is no n variable outside the match.
The match guard if n == y is not a pattern and therefore doesn’t introduce new
variables. This y is the outer y rather than a new y shadowing it, and
we can look for a value that has the same value as the outer y by comparing
n to y.
You can also use the or operator | in a match guard to specify multiple
patterns; the match guard condition will apply to all the patterns. Listing
19-28 shows the precedence when combining a pattern that uses | with a match
guard. The important part of this example is that the if y match guard
applies to 4, 5, and 6, even though it might look like if y only
applies to 6.
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
The match condition states that the arm only matches if the value of x is
equal to 4, 5, or 6 and if y is true. When this code runs, the
pattern of the first arm matches because x is 4, but the match guard if y
is false, so the first arm is not chosen. The code moves on to the second
arm, which does match, and this program prints no. The reason is that the
if condition applies to the whole pattern 4 | 5 | 6, not just to the last
value 6. In other words, the precedence of a match guard in relation to a
pattern behaves like this:
(4 | 5 | 6) if y => ...
rather than this:
4 | 5 | (6 if y) => ...
After running the code, the precedence behavior is evident: If the match guard
were applied only to the final value in the list of values specified using the
| operator, the arm would have matched, and the program would have printed
yes.
Using @ Bindings
The at operator @ lets us create a variable that holds a value at the same
time we’re testing that value for a pattern match. In Listing 19-29, we want to
test that a Message::Hello id field is within the range 3..=7. We also
want to bind the value to the variable id so that we can use it in the code
associated with the arm.
fn main() {
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id @ 3..=7 } => {
println!("Found an id in range: {id}")
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
}
@ to bind to a value in a pattern while also testing itThis example will print Found an id in range: 5. By specifying id @ before
the range 3..=7, we’re capturing whatever value matched the range in a
variable named id while also testing that the value matched the range pattern.
In the second arm, where we only have a range specified in the pattern, the code
associated with the arm doesn’t have a variable that contains the actual value
of the id field. The id field’s value could have been 10, 11, or 12, but
the code that goes with that pattern doesn’t know which it is. The pattern code
isn’t able to use the value from the id field because we haven’t saved the
id value in a variable.
In the last arm, where we’ve specified a variable without a range, we do have
the value available to use in the arm’s code in a variable named id. The
reason is that we’ve used the struct field shorthand syntax. But we haven’t
applied any test to the value in the id field in this arm, as we did with the
first two arms: Any value would match this pattern.
Using @ lets us test a value and save it in a variable within one pattern.
Summary
Rust’s patterns are very useful in distinguishing between different kinds of
data. When used in match expressions, Rust ensures that your patterns cover
every possible value, or your program won’t compile. Patterns in let
statements and function parameters make those constructs more useful, enabling
the destructuring of values into smaller parts and assigning those parts to
variables. We can create simple or complex patterns to suit our needs.
Next, for the penultimate chapter of the book, we’ll look at some advanced aspects of a variety of Rust’s features.
Advanced Features
By now, you’ve learned the most commonly used parts of the Rust programming language. Before we do one more project, in Chapter 21, we’ll look at a few aspects of the language you might run into every once in a while but may not use every day. You can use this chapter as a reference for when you encounter any unknowns. The features covered here are useful in very specific situations. Although you might not reach for them often, we want to make sure you have a grasp of all the features Rust has to offer.
In this chapter, we’ll cover:
- Unsafe Rust: How to opt out of some of Rust’s guarantees and take responsibility for manually upholding those guarantees
- Advanced traits: Associated types, default type parameters, fully qualified syntax, supertraits, and the newtype pattern in relation to traits
- Advanced types: More about the newtype pattern, type aliases, the never type, and dynamically sized types
- Advanced functions and closures: Function pointers and returning closures
- Macros: Ways to define code that defines more code at compile time
It’s a panoply of Rust features with something for everyone! Let’s dive in!
Unsafe Rust
Unsafe Rust
All the code we’ve discussed so far has had Rust’s memory safety guarantees enforced at compile time. However, Rust has a second language hidden inside it that doesn’t enforce these memory safety guarantees: It’s called unsafe Rust and works just like regular Rust but gives us extra superpowers.
Unsafe Rust exists because, by nature, static analysis is conservative. When the compiler tries to determine whether or not code upholds the guarantees, it’s better for it to reject some valid programs than to accept some invalid programs. Although the code might be okay, if the Rust compiler doesn’t have enough information to be confident, it will reject the code. In these cases, you can use unsafe code to tell the compiler, “Trust me, I know what I’m doing.” Be warned, however, that you use unsafe Rust at your own risk: If you use unsafe code incorrectly, problems can occur due to memory unsafety, such as null pointer dereferencing.
Another reason Rust has an unsafe alter ego is that the underlying computer hardware is inherently unsafe. If Rust didn’t let you do unsafe operations, you couldn’t do certain tasks. Rust needs to allow you to do low-level systems programming, such as directly interacting with the operating system or even writing your own operating system. Working with low-level systems programming is one of the goals of the language. Let’s explore what we can do with unsafe Rust and how to do it.
Performing Unsafe Superpowers
To switch to unsafe Rust, use the unsafe keyword and then start a new block
that holds the unsafe code. You can take five actions in unsafe Rust that you
can’t in safe Rust, which we call unsafe superpowers. Those superpowers
include the ability to:
- Dereference a raw pointer.
- Call an unsafe function or method.
- Access or modify a mutable static variable.
- Implement an unsafe trait.
- Access fields of
unions.
It’s important to understand that unsafe doesn’t turn off the borrow checker
or disable any of Rust’s other safety checks: If you use a reference in unsafe
code, it will still be checked. The unsafe keyword only gives you access to
these five features that are then not checked by the compiler for memory
safety. You’ll still get some degree of safety inside an unsafe block.
In addition, unsafe does not mean the code inside the block is necessarily
dangerous or that it will definitely have memory safety problems: The intent is
that as the programmer, you’ll ensure that the code inside an unsafe block
will access memory in a valid way.
People are fallible and mistakes will happen, but by requiring these five
unsafe operations to be inside blocks annotated with unsafe, you’ll know that
any errors related to memory safety must be within an unsafe block. Keep
unsafe blocks small; you’ll be thankful later when you investigate memory
bugs.
To isolate unsafe code as much as possible, it’s best to enclose such code
within a safe abstraction and provide a safe API, which we’ll discuss later in
the chapter when we examine unsafe functions and methods. Parts of the standard
library are implemented as safe abstractions over unsafe code that has been
audited. Wrapping unsafe code in a safe abstraction prevents uses of unsafe
from leaking out into all the places that you or your users might want to use
the functionality implemented with unsafe code, because using a safe
abstraction is safe.
Let’s look at each of the five unsafe superpowers in turn. We’ll also look at some abstractions that provide a safe interface to unsafe code.
Dereferencing a Raw Pointer
In Chapter 4, in the “Dangling References” section, we mentioned that the compiler ensures that references are always
valid. Unsafe Rust has two new types called raw pointers that are similar to
references. As with references, raw pointers can be immutable or mutable and
are written as *const T and *mut T, respectively. The asterisk isn’t the
dereference operator; it’s part of the type name. In the context of raw
pointers, immutable means that the pointer can’t be directly assigned to
after being dereferenced.
Different from references and smart pointers, raw pointers:
- Are allowed to ignore the borrowing rules by having both immutable and mutable pointers or multiple mutable pointers to the same location
- Aren’t guaranteed to point to valid memory
- Are allowed to be null
- Don’t implement any automatic cleanup
By opting out of having Rust enforce these guarantees, you can give up guaranteed safety in exchange for greater performance or the ability to interface with another language or hardware where Rust’s guarantees don’t apply.
Listing 20-1 shows how to create an immutable and a mutable raw pointer.
fn main() {
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
}
Notice that we don’t include the unsafe keyword in this code. We can create
raw pointers in safe code; we just can’t dereference raw pointers outside an
unsafe block, as you’ll see in a bit.
We’ve created raw pointers by using the raw borrow operators: &raw const num
creates a *const i32 immutable raw pointer, and &raw mut num creates a *mut i32 mutable raw pointer. Because we created them directly from a local
variable, we know these particular raw pointers are valid, but we can’t make
that assumption about just any raw pointer.
To demonstrate this, next we’ll create a raw pointer whose validity we can’t be
so certain of, using the keyword as to cast a value instead of using the raw
borrow operator. Listing 20-2 shows how to create a raw pointer to an arbitrary
location in memory. Trying to use arbitrary memory is undefined: There might be
data at that address or there might not, the compiler might optimize the code
so that there is no memory access, or the program might terminate with a
segmentation fault. Usually, there is no good reason to write code like this,
especially in cases where you can use a raw borrow operator instead, but it is
possible.
fn main() {
let address = 0x012345usize;
let r = address as *const i32;
}
Recall that we can create raw pointers in safe code, but we can’t dereference
raw pointers and read the data being pointed to. In Listing 20-3, we use the
dereference operator * on a raw pointer that requires an unsafe block.
fn main() {
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
unsafe {
println!("r1 is: {}", *r1);
println!("r2 is: {}", *r2);
}
}
unsafe blockCreating a pointer does no harm; it’s only when we try to access the value that it points at that we might end up dealing with an invalid value.
Note also that in Listings 20-1 and 20-3, we created *const i32 and *mut i32 raw pointers that both pointed to the same memory location, where num is
stored. If we instead tried to create an immutable and a mutable reference to
num, the code would not have compiled because Rust’s ownership rules don’t
allow a mutable reference at the same time as any immutable references. With
raw pointers, we can create a mutable pointer and an immutable pointer to the
same location and change data through the mutable pointer, potentially creating
a data race. Be careful!
With all of these dangers, why would you ever use raw pointers? One major use case is when interfacing with C code, as you’ll see in the next section. Another case is when building up safe abstractions that the borrow checker doesn’t understand. We’ll introduce unsafe functions and then look at an example of a safe abstraction that uses unsafe code.
Calling an Unsafe Function or Method
The second type of operation you can perform in an unsafe block is calling
unsafe functions. Unsafe functions and methods look exactly like regular
functions and methods, but they have an extra unsafe before the rest of the
definition. The unsafe keyword in this context indicates the function has
requirements we need to uphold when we call this function, because Rust can’t
guarantee we’ve met these requirements. By calling an unsafe function within an
unsafe block, we’re saying that we’ve read this function’s documentation and
we take responsibility for upholding the function’s contracts.
Here is an unsafe function named dangerous that doesn’t do anything in its
body:
fn main() {
unsafe fn dangerous() {}
unsafe {
dangerous();
}
}
We must call the dangerous function within a separate unsafe block. If we
try to call dangerous without the unsafe block, we’ll get an error:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe block
--> src/main.rs:4:5
|
4 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
|
= note: consult the function's documentation for information on how to avoid undefined behavior
For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
With the unsafe block, we’re asserting to Rust that we’ve read the function’s
documentation, we understand how to use it properly, and we’ve verified that
we’re fulfilling the contract of the function.
To perform unsafe operations in the body of an unsafe function, you still
need to use an unsafe block, just as within a regular function, and the
compiler will warn you if you forget. This helps us keep unsafe blocks as
small as possible, as unsafe operations may not be needed across the whole
function body.
Creating a Safe Abstraction over Unsafe Code
Just because a function contains unsafe code doesn’t mean we need to mark the
entire function as unsafe. In fact, wrapping unsafe code in a safe function is
a common abstraction. As an example, let’s study the split_at_mut function
from the standard library, which requires some unsafe code. We’ll explore how
we might implement it. This safe method is defined on mutable slices: It takes
one slice and makes it two by splitting the slice at the index given as an
argument. Listing 20-4 shows how to use split_at_mut.
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
}
split_at_mut functionWe can’t implement this function using only safe Rust. An attempt might look
something like Listing 20-5, which won’t compile. For simplicity, we’ll
implement split_at_mut as a function rather than a method and only for slices
of i32 values rather than for a generic type T.
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert!(mid <= len);
(&mut values[..mid], &mut values[mid..])
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
}
split_at_mut using only safe RustThis function first gets the total length of the slice. Then, it asserts that the index given as a parameter is within the slice by checking whether it’s less than or equal to the length. The assertion means that if we pass an index that is greater than the length to split the slice at, the function will panic before it attempts to use that index.
Then, we return two mutable slices in a tuple: one from the start of the
original slice to the mid index and another from mid to the end of the
slice.
When we try to compile the code in Listing 20-5, we’ll get an error:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:6:31
|
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6 | (&mut values[..mid], &mut values[mid..])
| --------------------------^^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*values` is borrowed for `'1`
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
Rust’s borrow checker can’t understand that we’re borrowing different parts of the slice; it only knows that we’re borrowing from the same slice twice. Borrowing different parts of a slice is fundamentally okay because the two slices aren’t overlapping, but Rust isn’t smart enough to know this. When we know code is okay, but Rust doesn’t, it’s time to reach for unsafe code.
Listing 20-6 shows how to use an unsafe block, a raw pointer, and some calls
to unsafe functions to make the implementation of split_at_mut work.
use std::slice;
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
}
split_at_mut functionRecall from “The Slice Type” section in
Chapter 4 that a slice is a pointer to some data and the length of the slice.
We use the len method to get the length of a slice and the as_mut_ptr
method to access the raw pointer of a slice. In this case, because we have a
mutable slice to i32 values, as_mut_ptr returns a raw pointer with the type
*mut i32, which we’ve stored in the variable ptr.
We keep the assertion that the mid index is within the slice. Then, we get to
the unsafe code: The slice::from_raw_parts_mut function takes a raw pointer
and a length, and it creates a slice. We use this function to create a slice
that starts from ptr and is mid items long. Then, we call the add method
on ptr with mid as an argument to get a raw pointer that starts at mid,
and we create a slice using that pointer and the remaining number of items
after mid as the length.
The function slice::from_raw_parts_mut is unsafe because it takes a raw
pointer and must trust that this pointer is valid. The add method on raw
pointers is also unsafe because it must trust that the offset location is also
a valid pointer. Therefore, we had to put an unsafe block around our calls to
slice::from_raw_parts_mut and add so that we could call them. By looking at
the code and by adding the assertion that mid must be less than or equal to
len, we can tell that all the raw pointers used within the unsafe block
will be valid pointers to data within the slice. This is an acceptable and
appropriate use of unsafe.
Note that we don’t need to mark the resultant split_at_mut function as
unsafe, and we can call this function from safe Rust. We’ve created a safe
abstraction to the unsafe code with an implementation of the function that uses
unsafe code in a safe way, because it creates only valid pointers from the
data this function has access to.
In contrast, the use of slice::from_raw_parts_mut in Listing 20-7 would
likely crash when the slice is used. This code takes an arbitrary memory
location and creates a slice 10,000 items long.
fn main() {
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}
We don’t own the memory at this arbitrary location, and there is no guarantee
that the slice this code creates contains valid i32 values. Attempting to use
values as though it’s a valid slice results in undefined behavior.
Using extern Functions to Call External Code
Sometimes your Rust code might need to interact with code written in another
language. For this, Rust has the keyword extern that facilitates the creation
and use of a Foreign Function Interface (FFI), which is a way for a
programming language to define functions and enable a different (foreign)
programming language to call those functions.
Listing 20-8 demonstrates how to set up an integration with the abs function
from the C standard library. Functions declared within extern blocks are
generally unsafe to call from Rust code, so extern blocks must also be marked
unsafe. The reason is that other languages don’t enforce Rust’s rules and
guarantees, and Rust can’t check them, so responsibility falls on the
programmer to ensure safety.
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
extern function defined in another languageWithin the unsafe extern "C" block, we list the names and signatures of
external functions from another language we want to call. The "C" part
defines which application binary interface (ABI) the external function uses:
The ABI defines how to call the function at the assembly level. The "C" ABI
is the most common and follows the C programming language’s ABI. Information
about all the ABIs Rust supports is available in the Rust Reference.
Every item declared within an unsafe extern block is implicitly unsafe.
However, some FFI functions are safe to call. For example, the abs function
from C’s standard library does not have any memory safety considerations, and we
know it can be called with any i32. In cases like this, we can use the safe
keyword to say that this specific function is safe to call even though it is in
an unsafe extern block. Once we make that change, calling it no longer
requires an unsafe block, as shown in Listing 20-9.
unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}
fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
safe within an unsafe extern block and calling it safelyMarking a function as safe does not inherently make it safe! Instead, it is
like a promise you are making to Rust that it is safe. It is still your
responsibility to make sure that promise is kept!
Calling Rust Functions from Other Languages
We can also use extern to create an interface that allows other languages to
call Rust functions. Instead of creating a whole extern block, we add the
extern keyword and specify the ABI to use just before the fn keyword for
the relevant function. We also need to add an #[unsafe(no_mangle)] annotation
to tell the Rust compiler not to mangle the name of this function. Mangling
is when a compiler changes the name we’ve given a function to a different name
that contains more information for other parts of the compilation process to
consume but is less human readable. Every programming language compiler mangles
names slightly differently, so for a Rust function to be nameable by other
languages, we must disable the Rust compiler’s name mangling. This is unsafe
because there might be name collisions across libraries without the built-in
mangling, so it is our responsibility to make sure the name we choose is safe
to export without mangling.
In the following example, we make the call_from_c function accessible from C
code, after it’s compiled to a shared library and linked from C:
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
This usage of extern requires unsafe only in the attribute, not on the
extern block.
Accessing or Modifying a Mutable Static Variable
In this book, we’ve not yet talked about global variables, which Rust does support but which can be problematic with Rust’s ownership rules. If two threads are accessing the same mutable global variable, it can cause a data race.
In Rust, global variables are called static variables. Listing 20-10 shows an example declaration and use of a static variable with a string slice as a value.
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("value is: {HELLO_WORLD}");
}
Static variables are similar to constants, which we discussed in the
“Declaring Constants” section in Chapter 3. The
names of static variables are in SCREAMING_SNAKE_CASE by convention. Static
variables can only store references with the 'static lifetime, which means
the Rust compiler can figure out the lifetime and we aren’t required to
annotate it explicitly. Accessing an immutable static variable is safe.
A subtle difference between constants and immutable static variables is that
values in a static variable have a fixed address in memory. Using the value
will always access the same data. Constants, on the other hand, are allowed to
duplicate their data whenever they’re used. Another difference is that static
variables can be mutable. Accessing and modifying mutable static variables is
unsafe. Listing 20-11 shows how to declare, access, and modify a mutable
static variable named COUNTER.
static mut COUNTER: u32 = 0;
/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
unsafe {
// SAFETY: This is only called from a single thread in `main`.
add_to_count(3);
println!("COUNTER: {}", *(&raw const COUNTER));
}
}
As with regular variables, we specify mutability using the mut keyword. Any
code that reads or writes from COUNTER must be within an unsafe block. The
code in Listing 20-11 compiles and prints COUNTER: 3 as we would expect
because it’s single threaded. Having multiple threads access COUNTER would
likely result in data races, so it is undefined behavior. Therefore, we need to
mark the entire function as unsafe and document the safety limitation so that
anyone calling the function knows what they are and are not allowed to do
safely.
Whenever we write an unsafe function, it is idiomatic to write a comment
starting with SAFETY and explaining what the caller needs to do to call the
function safely. Likewise, whenever we perform an unsafe operation, it is
idiomatic to write a comment starting with SAFETY to explain how the safety
rules are upheld.
Additionally, the compiler will deny by default any attempt to create
references to a mutable static variable through a compiler lint. You must
either explicitly opt out of that lint’s protections by adding an
#[allow(static_mut_refs)] annotation or access the mutable static variable
via a raw pointer created with one of the raw borrow operators. That includes
cases where the reference is created invisibly, as when it is used in the
println! in this code listing. Requiring references to static mutable
variables to be created via raw pointers helps make the safety requirements for
using them more obvious.
With mutable data that is globally accessible, it’s difficult to ensure that there are no data races, which is why Rust considers mutable static variables to be unsafe. Where possible, it’s preferable to use the concurrency techniques and thread-safe smart pointers we discussed in Chapter 16 so that the compiler checks that data access from different threads is done safely.
Implementing an Unsafe Trait
We can use unsafe to implement an unsafe trait. A trait is unsafe when at
least one of its methods has some invariant that the compiler can’t verify. We
declare that a trait is unsafe by adding the unsafe keyword before trait
and marking the implementation of the trait as unsafe too, as shown in
Listing 20-12.
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
fn main() {}
By using unsafe impl, we’re promising that we’ll uphold the invariants that
the compiler can’t verify.
As an example, recall the Send and Sync marker traits we discussed in the
“Extensible Concurrency with Send and Sync”
section in Chapter 16: The compiler implements these traits automatically if
our types are composed entirely of other types that implement Send and
Sync. If we implement a type that contains a type that does not implement
Send or Sync, such as raw pointers, and we want to mark that type as Send
or Sync, we must use unsafe. Rust can’t verify that our type upholds the
guarantees that it can be safely sent across threads or accessed from multiple
threads; therefore, we need to do those checks manually and indicate as such
with unsafe.
Accessing Fields of a Union
The final action that works only with unsafe is accessing fields of a union.
A union is similar to a struct, but only one declared field is used in a
particular instance at one time. Unions are primarily used to interface with
unions in C code. Accessing union fields is unsafe because Rust can’t guarantee
the type of the data currently being stored in the union instance. You can
learn more about unions in the Rust Reference.
Using Miri to Check Unsafe Code
When writing unsafe code, you might want to check that what you have written actually is safe and correct. One of the best ways to do that is to use Miri, an official Rust tool for detecting undefined behavior. Whereas the borrow checker is a static tool that works at compile time, Miri is a dynamic tool that works at runtime. It checks your code by running your program, or its test suite, and detecting when you violate the rules it understands about how Rust should work.
Using Miri requires a nightly build of Rust (which we talk about more in
Appendix G: How Rust is Made and “Nightly Rust”). You
can install both a nightly version of Rust and the Miri tool by typing rustup +nightly component add miri. This does not change what version of Rust your
project uses; it only adds the tool to your system so you can use it when you
want to. You can run Miri on a project by typing cargo +nightly miri run or
cargo +nightly miri test.
For an example of how helpful this can be, consider what happens when we run it against Listing 20-7.
$ cargo +nightly miri run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/unsafe-example`
warning: integer-to-pointer cast
--> src/main.rs:5:13
|
5 | let r = address as *mut i32;
| ^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
|
= help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program
= help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation
= help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead
= help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics
= help: alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning
= note: BACKTRACE:
= note: inside `main` at src/main.rs:5:13: 5:32
error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 40000 bytes, but got 0x1234[noalloc] which is a dangling pointer (it has no provenance)
--> src/main.rs:7:35
|
7 | let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: BACKTRACE:
= note: inside `main` at src/main.rs:7:35: 7:70
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error; 1 warning emitted
Miri correctly warns us that we’re casting an integer to a pointer, which might be a problem, but Miri can’t determine whether a problem exists because it doesn’t know how the pointer originated. Then, Miri returns an error where Listing 20-7 has undefined behavior because we have a dangling pointer. Thanks to Miri, we now know there is a risk of undefined behavior, and we can think about how to make the code safe. In some cases, Miri can even make recommendations about how to fix errors.
Miri doesn’t catch everything you might get wrong when writing unsafe code. Miri is a dynamic analysis tool, so it only catches problems with code that actually gets run. That means you will need to use it in conjunction with good testing techniques to increase your confidence about the unsafe code you have written. Miri also does not cover every possible way your code can be unsound.
Put another way: If Miri does catch a problem, you know there’s a bug, but just because Miri doesn’t catch a bug doesn’t mean there isn’t a problem. It can catch a lot, though. Try running it on the other examples of unsafe code in this chapter and see what it says!
You can learn more about Miri at its GitHub repository.
Using Unsafe Code Correctly
Using unsafe to use one of the five superpowers just discussed isn’t wrong or
even frowned upon, but it is trickier to get unsafe code correct because the
compiler can’t help uphold memory safety. When you have a reason to use
unsafe code, you can do so, and having the explicit unsafe annotation makes
it easier to track down the source of problems when they occur. Whenever you
write unsafe code, you can use Miri to help you be more confident that the code
you have written upholds Rust’s rules.
For a much deeper exploration of how to work effectively with unsafe Rust, read
Rust’s official guide for unsafe, The Rustonomicon.
Trait ขั้นสูง
Advanced Traits
We first covered traits in the “Defining Shared Behavior with Traits” section in Chapter 10, but we didn’t discuss the more advanced details. Now that you know more about Rust, we can get into the nitty-gritty.
Defining Traits with Associated Types
Associated types connect a type placeholder with a trait such that the trait method definitions can use these placeholder types in their signatures. The implementor of a trait will specify the concrete type to be used instead of the placeholder type for the particular implementation. That way, we can define a trait that uses some types without needing to know exactly what those types are until the trait is implemented.
We’ve described most of the advanced features in this chapter as being rarely needed. Associated types are somewhere in the middle: They’re used more rarely than features explained in the rest of the book but more commonly than many of the other features discussed in this chapter.
One example of a trait with an associated type is the Iterator trait that the
standard library provides. The associated type is named Item and stands in
for the type of the values the type implementing the Iterator trait is
iterating over. The definition of the Iterator trait is as shown in Listing
20-13.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Iterator trait that has an associated type ItemThe type Item is a placeholder, and the next method’s definition shows that
it will return values of type Option<Self::Item>. Implementors of the
Iterator trait will specify the concrete type for Item, and the next
method will return an Option containing a value of that concrete type.
Associated types might seem like a similar concept to generics, in that the
latter allow us to define a function without specifying what types it can
handle. To examine the difference between the two concepts, we’ll look at an
implementation of the Iterator trait on a type named Counter that specifies
the Item type is u32:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
This syntax seems comparable to that of generics. So, why not just define the
Iterator trait with generics, as shown in Listing 20-14?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
Iterator trait using genericsThe difference is that when using generics, as in Listing 20-14, we must
annotate the types in each implementation; because we can also implement
Iterator<String> for Counter or any other type, we could have multiple
implementations of Iterator for Counter. In other words, when a trait has a
generic parameter, it can be implemented for a type multiple times, changing
the concrete types of the generic type parameters each time. When we use the
next method on Counter, we would have to provide type annotations to
indicate which implementation of Iterator we want to use.
With associated types, we don’t need to annotate types, because we can’t
implement a trait on a type multiple times. In Listing 20-13 with the
definition that uses associated types, we can choose what the type of Item
will be only once because there can be only one impl Iterator for Counter. We
don’t have to specify that we want an iterator of u32 values everywhere we
call next on Counter.
Associated types also become part of the trait’s contract: Implementors of the trait must provide a type to stand in for the associated type placeholder. Associated types often have a name that describes how the type will be used, and documenting the associated type in the API documentation is a good practice.
Using Default Generic Parameters and Operator Overloading
When we use generic type parameters, we can specify a default concrete type for
the generic type. This eliminates the need for implementors of the trait to
specify a concrete type if the default type works. You specify a default type
when declaring a generic type with the <PlaceholderType=ConcreteType> syntax.
A great example of a situation where this technique is useful is with operator
overloading, in which you customize the behavior of an operator (such as +)
in particular situations.
Rust doesn’t allow you to create your own operators or overload arbitrary
operators. But you can overload the operations and corresponding traits listed
in std::ops by implementing the traits associated with the operator. For
example, in Listing 20-15, we overload the + operator to add two Point
instances together. We do this by implementing the Add trait on a Point
struct.
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
Add trait to overload the + operator for Point instancesThe add method adds the x values of two Point instances and the y
values of two Point instances to create a new Point. The Add trait has an
associated type named Output that determines the type returned from the add
method.
The default generic type in this code is within the Add trait. Here is its
definition:
#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
}
This code should look generally familiar: a trait with one method and an
associated type. The new part is Rhs=Self: This syntax is called default
type parameters. The Rhs generic type parameter (short for “right-hand
side”) defines the type of the rhs parameter in the add method. If we don’t
specify a concrete type for Rhs when we implement the Add trait, the type
of Rhs will default to Self, which will be the type we’re implementing
Add on.
When we implemented Add for Point, we used the default for Rhs because we
wanted to add two Point instances. Let’s look at an example of implementing
the Add trait where we want to customize the Rhs type rather than using the
default.
We have two structs, Millimeters and Meters, holding values in different
units. This thin wrapping of an existing type in another struct is known as the
newtype pattern, which we describe in more detail in the “Implementing
External Traits with the Newtype Pattern” section. We
want to add values in millimeters to values in meters and have the
implementation of Add do the conversion correctly. We can implement Add for
Millimeters with Meters as the Rhs, as shown in Listing 20-16.
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
Add trait on Millimeters to add Millimeters and MetersTo add Millimeters and Meters, we specify impl Add<Meters> to set the
value of the Rhs type parameter instead of using the default of Self.
You’ll use default type parameters in two main ways:
- To extend a type without breaking existing code
- To allow customization in specific cases most users won’t need
The standard library’s Add trait is an example of the second purpose:
Usually, you’ll add two like types, but the Add trait provides the ability to
customize beyond that. Using a default type parameter in the Add trait
definition means you don’t have to specify the extra parameter most of the
time. In other words, a bit of implementation boilerplate isn’t needed, making
it easier to use the trait.
The first purpose is similar to the second but in reverse: If you want to add a type parameter to an existing trait, you can give it a default to allow extension of the functionality of the trait without breaking the existing implementation code.
Disambiguating Between Identically Named Methods
Nothing in Rust prevents a trait from having a method with the same name as another trait’s method, nor does Rust prevent you from implementing both traits on one type. It’s also possible to implement a method directly on the type with the same name as methods from traits.
When calling methods with the same name, you’ll need to tell Rust which one you
want to use. Consider the code in Listing 20-17 where we’ve defined two traits,
Pilot and Wizard, that both have a method called fly. We then implement
both traits on a type Human that already has a method named fly implemented
on it. Each fly method does something different.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {}
fly method and are implemented on the Human type, and a fly method is implemented on Human directly.When we call fly on an instance of Human, the compiler defaults to calling
the method that is directly implemented on the type, as shown in Listing 20-18.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
person.fly();
}
fly on an instance of HumanRunning this code will print *waving arms furiously*, showing that Rust
called the fly method implemented on Human directly.
To call the fly methods from either the Pilot trait or the Wizard trait,
we need to use more explicit syntax to specify which fly method we mean.
Listing 20-19 demonstrates this syntax.
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
fly method we want to callSpecifying the trait name before the method name clarifies to Rust which
implementation of fly we want to call. We could also write
Human::fly(&person), which is equivalent to the person.fly() that we used
in Listing 20-19, but this is a bit longer to write if we don’t need to
disambiguate.
Running this code prints the following:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
Because the fly method takes a self parameter, if we had two types that
both implement one trait, Rust could figure out which implementation of a
trait to use based on the type of self.
However, associated functions that are not methods don’t have a self
parameter. When there are multiple types or traits that define non-method
functions with the same function name, Rust doesn’t always know which type you
mean unless you use fully qualified syntax. For example, in Listing 20-20, we
create a trait for an animal shelter that wants to name all baby dogs Spot. We
make an Animal trait with an associated non-method function baby_name. The
Animal trait is implemented for the struct Dog, on which we also provide an
associated non-method function baby_name directly.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
We implement the code for naming all puppies Spot in the baby_name associated
function that is defined on Dog. The Dog type also implements the trait
Animal, which describes characteristics that all animals have. Baby dogs are
called puppies, and that is expressed in the implementation of the Animal
trait on Dog in the baby_name function associated with the Animal trait.
In main, we call the Dog::baby_name function, which calls the associated
function defined on Dog directly. This code prints the following:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
This output isn’t what we wanted. We want to call the baby_name function that
is part of the Animal trait that we implemented on Dog so that the code
prints A baby dog is called a puppy. The technique of specifying the trait
name that we used in Listing 20-19 doesn’t help here; if we change main to
the code in Listing 20-21, we’ll get a compilation error.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
baby_name function from the Animal trait, but Rust doesn’t know which implementation to useBecause Animal::baby_name doesn’t have a self parameter, and there could be
other types that implement the Animal trait, Rust can’t figure out which
implementation of Animal::baby_name we want. We’ll get this compiler error:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
To disambiguate and tell Rust that we want to use the implementation of
Animal for Dog as opposed to the implementation of Animal for some other
type, we need to use fully qualified syntax. Listing 20-22 demonstrates how to
use fully qualified syntax.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
baby_name function from the Animal trait as implemented on DogWe’re providing Rust with a type annotation within the angle brackets, which
indicates we want to call the baby_name method from the Animal trait as
implemented on Dog by saying that we want to treat the Dog type as an
Animal for this function call. This code will now print what we want:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
In general, fully qualified syntax is defined as follows:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
For associated functions that aren’t methods, there would not be a receiver:
There would only be the list of other arguments. You could use fully qualified
syntax everywhere that you call functions or methods. However, you’re allowed
to omit any part of this syntax that Rust can figure out from other information
in the program. You only need to use this more verbose syntax in cases where
there are multiple implementations that use the same name and Rust needs help
to identify which implementation you want to call.
Using Supertraits
Sometimes you might write a trait definition that depends on another trait: For a type to implement the first trait, you want to require that type to also implement the second trait. You would do this so that your trait definition can make use of the associated items of the second trait. The trait your trait definition is relying on is called a supertrait of your trait.
For example, let’s say we want to make an OutlinePrint trait with an
outline_print method that will print a given value formatted so that it’s
framed in asterisks. That is, given a Point struct that implements the
standard library trait Display to result in (x, y), when we call
outline_print on a Point instance that has 1 for x and 3 for y, it
should print the following:
**********
* *
* (1, 3) *
* *
**********
In the implementation of the outline_print method, we want to use the
Display trait’s functionality. Therefore, we need to specify that the
OutlinePrint trait will work only for types that also implement Display and
provide the functionality that OutlinePrint needs. We can do that in the
trait definition by specifying OutlinePrint: Display. This technique is
similar to adding a trait bound to the trait. Listing 20-23 shows an
implementation of the OutlinePrint trait.
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
fn main() {}
OutlinePrint trait that requires the functionality from DisplayBecause we’ve specified that OutlinePrint requires the Display trait, we
can use the to_string function that is automatically implemented for any type
that implements Display. If we tried to use to_string without adding a
colon and specifying the Display trait after the trait name, we’d get an
error saying that no method named to_string was found for the type &Self in
the current scope.
Let’s see what happens when we try to implement OutlinePrint on a type that
doesn’t implement Display, such as the Point struct:
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
We get an error saying that Display is required but not implemented:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ the trait `std::fmt::Display` is not implemented for `Point`
|
note: required by a bound in `OutlinePrint::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
To fix this, we implement Display on Point and satisfy the constraint that
OutlinePrint requires, like so:
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
Then, implementing the OutlinePrint trait on Point will compile
successfully, and we can call outline_print on a Point instance to display
it within an outline of asterisks.
Implementing External Traits with the Newtype Pattern
In the “Implementing a Trait on a Type” section in Chapter 10, we mentioned the orphan rule that states we’re only allowed to implement a trait on a type if either the trait or the type, or both, are local to our crate. It’s possible to get around this restriction using the newtype pattern, which involves creating a new type in a tuple struct. (We covered tuple structs in the “Creating Different Types with Tuple Structs” section in Chapter 5.) The tuple struct will have one field and be a thin wrapper around the type for which we want to implement a trait. Then, the wrapper type is local to our crate, and we can implement the trait on the wrapper. Newtype is a term that originates from the Haskell programming language. There is no runtime performance penalty for using this pattern, and the wrapper type is elided at compile time.
As an example, let’s say we want to implement Display on Vec<T>, which the
orphan rule prevents us from doing directly because the Display trait and the
Vec<T> type are defined outside our crate. We can make a Wrapper struct
that holds an instance of Vec<T>; then, we can implement Display on
Wrapper and use the Vec<T> value, as shown in Listing 20-24.
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {w}");
}
Wrapper type around Vec<String> to implement DisplayThe implementation of Display uses self.0 to access the inner Vec<T>
because Wrapper is a tuple struct and Vec<T> is the item at index 0 in the
tuple. Then, we can use the functionality of the Display trait on Wrapper.
The downside of using this technique is that Wrapper is a new type, so it
doesn’t have the methods of the value it’s holding. We would have to implement
all the methods of Vec<T> directly on Wrapper such that the methods
delegate to self.0, which would allow us to treat Wrapper exactly like a
Vec<T>. If we wanted the new type to have every method the inner type has,
implementing the Deref trait on the Wrapper to return the inner type would
be a solution (we discussed implementing the Deref trait in the “Treating
Smart Pointers Like Regular References”
section in Chapter 15). If we didn’t want the Wrapper type to have all the
methods of the inner type—for example, to restrict the Wrapper type’s
behavior—we would have to implement just the methods we do want manually.
This newtype pattern is also useful even when traits are not involved. Let’s switch focus and look at some advanced ways to interact with Rust’s type system.
Type ขั้นสูง
Advanced Types
The Rust type system has some features that we’ve so far mentioned but haven’t
yet discussed. We’ll start by discussing newtypes in general as we examine why
they are useful as types. Then, we’ll move on to type aliases, a feature
similar to newtypes but with slightly different semantics. We’ll also discuss
the ! type and dynamically sized types.
Type Safety and Abstraction with the Newtype Pattern
This section assumes you’ve read the earlier section “Implementing External
Traits with the Newtype Pattern”. The newtype pattern
is also useful for tasks beyond those we’ve discussed so far, including
statically enforcing that values are never confused and indicating the units of
a value. You saw an example of using newtypes to indicate units in Listing
20-16: Recall that the Millimeters and Meters structs wrapped u32 values
in a newtype. If we wrote a function with a parameter of type Millimeters, we
wouldn’t be able to compile a program that accidentally tried to call that
function with a value of type Meters or a plain u32.
We can also use the newtype pattern to abstract away some implementation details of a type: The new type can expose a public API that is different from the API of the private inner type.
Newtypes can also hide internal implementation. For example, we could provide a
People type to wrap a HashMap<i32, String> that stores a person’s ID
associated with their name. Code using People would only interact with the
public API we provide, such as a method to add a name string to the People
collection; that code wouldn’t need to know that we assign an i32 ID to names
internally. The newtype pattern is a lightweight way to achieve encapsulation
to hide implementation details, which we discussed in the “Encapsulation that
Hides Implementation
Details”
section in Chapter 18.
Type Synonyms and Type Aliases
Rust provides the ability to declare a type alias to give an existing type
another name. For this we use the type keyword. For example, we can create
the alias Kilometers to i32 like so:
fn main() {
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!("x + y = {}", x + y);
}
Now the alias Kilometers is a synonym for i32; unlike the Millimeters
and Meters types we created in Listing 20-16, Kilometers is not a separate,
new type. Values that have the type Kilometers will be treated the same as
values of type i32:
fn main() {
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!("x + y = {}", x + y);
}
Because Kilometers and i32 are the same type, we can add values of both
types and can pass Kilometers values to functions that take i32
parameters. However, using this method, we don’t get the type-checking benefits
that we get from the newtype pattern discussed earlier. In other words, if we
mix up Kilometers and i32 values somewhere, the compiler will not give us
an error.
The main use case for type synonyms is to reduce repetition. For example, we might have a lengthy type like this:
Box<dyn Fn() + Send + 'static>
Writing this lengthy type in function signatures and as type annotations all over the code can be tiresome and error-prone. Imagine having a project full of code like that in Listing 20-25.
fn main() {
let f: Box<dyn Fn() + Send + 'static> = Box::new(|| println!("hi"));
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) {
// --snip--
}
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
// --snip--
Box::new(|| ())
}
}
A type alias makes this code more manageable by reducing the repetition. In
Listing 20-26, we’ve introduced an alias named Thunk for the verbose type and
can replace all uses of the type with the shorter alias Thunk.
fn main() {
type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println!("hi"));
fn takes_long_type(f: Thunk) {
// --snip--
}
fn returns_long_type() -> Thunk {
// --snip--
Box::new(|| ())
}
}
Thunk, to reduce repetitionThis code is much easier to read and write! Choosing a meaningful name for a type alias can help communicate your intent as well (thunk is a word for code to be evaluated at a later time, so it’s an appropriate name for a closure that gets stored).
Type aliases are also commonly used with the Result<T, E> type for reducing
repetition. Consider the std::io module in the standard library. I/O
operations often return a Result<T, E> to handle situations when operations
fail to work. This library has a std::io::Error struct that represents all
possible I/O errors. Many of the functions in std::io will be returning
Result<T, E> where the E is std::io::Error, such as these functions in
the Write trait:
use std::fmt;
use std::io::Error;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
fn flush(&mut self) -> Result<(), Error>;
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}
The Result<..., Error> is repeated a lot. As such, std::io has this type
alias declaration:
use std::fmt;
type Result<T> = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
Because this declaration is in the std::io module, we can use the fully
qualified alias std::io::Result<T>; that is, a Result<T, E> with the E
filled in as std::io::Error. The Write trait function signatures end up
looking like this:
use std::fmt;
type Result<T> = std::result::Result<T, std::io::Error>;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}
The type alias helps in two ways: It makes code easier to write and it gives
us a consistent interface across all of std::io. Because it’s an alias, it’s
just another Result<T, E>, which means we can use any methods that work on
Result<T, E> with it, as well as special syntax like the ? operator.
The Never Type That Never Returns
Rust has a special type named ! that’s known in type theory lingo as the
empty type because it has no values. We prefer to call it the never type
because it stands in the place of the return type when a function will never
return. Here is an example:
fn bar() -> ! {
// --snip--
panic!();
}
This code is read as “the function bar returns never.” Functions that return
never are called diverging functions. We can’t create values of the type !,
so bar can never possibly return.
But what use is a type you can never create values for? Recall the code from Listing 2-5, part of the number-guessing game; we’ve reproduced a bit of it here in Listing 20-27.
use std::cmp::Ordering;
use std::io;
use rand::Rng;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1..=100);
println!("The secret number is: {secret_number}");
loop {
println!("Please input your guess.");
let mut guess = String::new();
// --snip--
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("You guessed: {guess}");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
match with an arm that ends in continueAt the time, we skipped over some details in this code. In “The match
Control Flow Construct”
section in Chapter 6, we discussed that match arms must all return the same
type. So, for example, the following code doesn’t work:
fn main() {
let guess = "3";
let guess = match guess.trim().parse() {
Ok(_) => 5,
Err(_) => "hello",
};
}
The type of guess in this code would have to be an integer and a string,
and Rust requires that guess have only one type. So, what does continue
return? How were we allowed to return a u32 from one arm and have another arm
that ends with continue in Listing 20-27?
As you might have guessed, continue has a ! value. That is, when Rust
computes the type of guess, it looks at both match arms, the former with a
value of u32 and the latter with a ! value. Because ! can never have a
value, Rust decides that the type of guess is u32.
The formal way of describing this behavior is that expressions of type ! can
be coerced into any other type. We’re allowed to end this match arm with
continue because continue doesn’t return a value; instead, it moves control
back to the top of the loop, so in the Err case, we never assign a value to
guess.
The never type is useful with the panic! macro as well. Recall the unwrap
function that we call on Option<T> values to produce a value or panic with
this definition:
enum Option<T> {
Some(T),
None,
}
use crate::Option::*;
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called `Option::unwrap()` on a `None` value"),
}
}
}
In this code, the same thing happens as in the match in Listing 20-27: Rust
sees that val has the type T and panic! has the type !, so the result
of the overall match expression is T. This code works because panic!
doesn’t produce a value; it ends the program. In the None case, we won’t be
returning a value from unwrap, so this code is valid.
One final expression that has the type ! is a loop:
fn main() {
print!("forever ");
loop {
print!("and ever ");
}
}
Here, the loop never ends, so ! is the value of the expression. However, this
wouldn’t be true if we included a break, because the loop would terminate
when it got to the break.
Dynamically Sized Types and the Sized Trait
Rust needs to know certain details about its types, such as how much space to allocate for a value of a particular type. This leaves one corner of its type system a little confusing at first: the concept of dynamically sized types. Sometimes referred to as DSTs or unsized types, these types let us write code using values whose size we can know only at runtime.
Let’s dig into the details of a dynamically sized type called str, which
we’ve been using throughout the book. That’s right, not &str, but str on
its own, is a DST. In many cases, such as when storing text entered by a user,
we can’t know how long the string is until runtime. That means we can’t create
a variable of type str, nor can we take an argument of type str. Consider
the following code, which does not work:
fn main() {
let s1: str = "Hello there!";
let s2: str = "How's it going?";
}
Rust needs to know how much memory to allocate for any value of a particular
type, and all values of a type must use the same amount of memory. If Rust
allowed us to write this code, these two str values would need to take up the
same amount of space. But they have different lengths: s1 needs 12 bytes of
storage and s2 needs 15. This is why it’s not possible to create a variable
holding a dynamically sized type.
So, what do we do? In this case, you already know the answer: We make the type
of s1 and s2 string slice (&str) rather than str. Recall from the
“String Slices” section in Chapter 4 that the
slice data structure only stores the starting position and the length of the
slice. So, although &T is a single value that stores the memory address of
where the T is located, a string slice is two values: the address of the
str and its length. As such, we can know the size of a string slice value at
compile time: It’s twice the length of a usize. That is, we always know the
size of a string slice, no matter how long the string it refers to is. In
general, this is the way in which dynamically sized types are used in Rust:
They have an extra bit of metadata that stores the size of the dynamic
information. The golden rule of dynamically sized types is that we must always
put values of dynamically sized types behind a pointer of some kind.
We can combine str with all kinds of pointers: for example, Box<str> or
Rc<str>. In fact, you’ve seen this before but with a different dynamically
sized type: traits. Every trait is a dynamically sized type we can refer to by
using the name of the trait. In the “Using Trait Objects to Abstract over
Shared Behavior” section in Chapter 18, we mentioned that to use traits as trait
objects, we must put them behind a pointer, such as &dyn Trait or Box<dyn Trait> (Rc<dyn Trait> would work too).
To work with DSTs, Rust provides the Sized trait to determine whether or not
a type’s size is known at compile time. This trait is automatically implemented
for everything whose size is known at compile time. In addition, Rust
implicitly adds a bound on Sized to every generic function. That is, a
generic function definition like this:
fn generic<T>(t: T) {
// --snip--
}
is actually treated as though we had written this:
fn generic<T: Sized>(t: T) {
// --snip--
}
By default, generic functions will work only on types that have a known size at compile time. However, you can use the following special syntax to relax this restriction:
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
A trait bound on ?Sized means “T may or may not be Sized,” and this
notation overrides the default that generic types must have a known size at
compile time. The ?Trait syntax with this meaning is only available for
Sized, not any other traits.
Also note that we switched the type of the t parameter from T to &T.
Because the type might not be Sized, we need to use it behind some kind of
pointer. In this case, we’ve chosen a reference.
Next, we’ll talk about functions and closures!
Function และ closure ขั้นสูง
Advanced Functions and Closures
This section explores some advanced features related to functions and closures, including function pointers and returning closures.
Function Pointers
We’ve talked about how to pass closures to functions; you can also pass regular
functions to functions! This technique is useful when you want to pass a
function you’ve already defined rather than defining a new closure. Functions
coerce to the type fn (with a lowercase f), not to be confused with the
Fn closure trait. The fn type is called a function pointer. Passing
functions with function pointers will allow you to use functions as arguments
to other functions.
The syntax for specifying that a parameter is a function pointer is similar to
that of closures, as shown in Listing 20-28, where we’ve defined a function
add_one that adds 1 to its parameter. The function do_twice takes two
parameters: a function pointer to any function that takes an i32 parameter
and returns an i32, and one i32 value. The do_twice function calls the
function f twice, passing it the arg value, then adds the two function call
results together. The main function calls do_twice with the arguments
add_one and 5.
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {answer}");
}
fn type to accept a function pointer as an argumentThis code prints The answer is: 12. We specify that the parameter f in
do_twice is an fn that takes one parameter of type i32 and returns an
i32. We can then call f in the body of do_twice. In main, we can pass
the function name add_one as the first argument to do_twice.
Unlike closures, fn is a type rather than a trait, so we specify fn as the
parameter type directly rather than declaring a generic type parameter with one
of the Fn traits as a trait bound.
Function pointers implement all three of the closure traits (Fn, FnMut, and
FnOnce), meaning you can always pass a function pointer as an argument for a
function that expects a closure. It’s best to write functions using a generic
type and one of the closure traits so that your functions can accept either
functions or closures.
That said, one example of where you would want to only accept fn and not
closures is when interfacing with external code that doesn’t have closures: C
functions can accept functions as arguments, but C doesn’t have closures.
As an example of where you could use either a closure defined inline or a named
function, let’s look at a use of the map method provided by the Iterator
trait in the standard library. To use the map method to turn a vector of
numbers into a vector of strings, we could use a closure, as in Listing 20-29.
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(|i| i.to_string()).collect();
}
map method to convert numbers to stringsOr we could name a function as the argument to map instead of the closure.
Listing 20-30 shows what this would look like.
fn main() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings: Vec<String> =
list_of_numbers.iter().map(ToString::to_string).collect();
}
String::to_string function with the map method to convert numbers to stringsNote that we must use the fully qualified syntax that we talked about in the
“Advanced Traits” section because there are
multiple functions available named to_string.
Here, we’re using the to_string function defined in the ToString trait,
which the standard library has implemented for any type that implements
Display.
Recall from the “Enum Values” section in Chapter 6 that the name of each enum variant that we define also becomes an initializer function. We can use these initializer functions as function pointers that implement the closure traits, which means we can specify the initializer functions as arguments for methods that take closures, as seen in Listing 20-31.
fn main() {
enum Status {
Value(u32),
Stop,
}
let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
map method to create a Status instance from numbersHere, we create Status::Value instances using each u32 value in the range
that map is called on by using the initializer function of Status::Value.
Some people prefer this style and some people prefer to use closures. They
compile to the same code, so use whichever style is clearer to you.
Returning Closures
Closures are represented by traits, which means you can’t return closures
directly. In most cases where you might want to return a trait, you can instead
use the concrete type that implements the trait as the return value of the
function. However, you can’t usually do that with closures because they don’t
have a concrete type that is returnable; you’re not allowed to use the function
pointer fn as a return type if the closure captures any values from its
scope, for example.
Instead, you will normally use the impl Trait syntax we learned about in
Chapter 10. You can return any function type, using Fn, FnOnce, and FnMut.
For example, the code in Listing 20-32 will compile just fine.
#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
}
impl Trait syntaxHowever, as we noted in the “Inferring and Annotating Closure Types” section in Chapter 13, each closure is also its own distinct type. If you need to work with multiple functions that have the same signature but different implementations, you will need to use a trait object for them. Consider what happens if you write code like that shown in Listing 20-33.
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}
fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
move |x| x + init
}
Vec<T> of closures defined by functions that return impl Fn typesHere we have two functions, returns_closure and returns_initialized_closure,
which both return impl Fn(i32) -> i32. Notice that the closures that they
return are different, even though they implement the same type. If we try to
compile this, Rust lets us know that it won’t work:
$ cargo build
Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
--> src/main.rs:2:44
|
2 | let handlers = vec![returns_closure(), returns_initialized_closure(123)];
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
9 | fn returns_closure() -> impl Fn(i32) -> i32 {
| ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
| ------------------- the found opaque type
|
= note: expected opaque type `impl Fn(i32) -> i32`
found opaque type `impl Fn(i32) -> i32`
= note: distinct uses of `impl Trait` result in different opaque types
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions-example` (bin "functions-example") due to 1 previous error
The error message tells us that whenever we return an impl Trait, Rust
creates a unique opaque type, a type where we cannot see into the details of
what Rust constructs for us, nor can we guess the type Rust will generate to
write ourselves. So, even though these functions return closures that implement
the same trait, Fn(i32) -> i32, the opaque types Rust generates for each are
distinct. (This is similar to how Rust produces different concrete types for
distinct async blocks even when they have the same output type, as we saw in
“The Pin Type and the Unpin Trait” in
Chapter 17.) We have seen a solution to this problem a few times now: We can
use a trait object, as in Listing 20-34.
fn main() {
let handlers = vec![returns_closure(), returns_initialized_closure(123)];
for handler in handlers {
let output = handler(5);
println!("{output}");
}
}
fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
Box::new(|x| x + 1)
}
fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
Box::new(move |x| x + init)
}
Vec<T> of closures defined by functions that return Box<dyn Fn> so that they have the same typeThis code will compile just fine. For more about trait objects, refer to the section “Using Trait Objects To Abstract over Shared Behavior” in Chapter 18.
Next, let’s look at macros!
Macro
Macros
We’ve used macros like println! throughout this book, but we haven’t fully
explored what a macro is and how it works. The term macro refers to a family
of features in Rust—declarative macros with macro_rules! and three kinds of
procedural macros:
- Custom
#[derive]macros that specify code added with thederiveattribute used on structs and enums - Attribute-like macros that define custom attributes usable on any item
- Function-like macros that look like function calls but operate on the tokens specified as their argument
We’ll talk about each of these in turn, but first, let’s look at why we even need macros when we already have functions.
The Difference Between Macros and Functions
Fundamentally, macros are a way of writing code that writes other code, which
is known as metaprogramming. In Appendix C, we discuss the derive
attribute, which generates an implementation of various traits for you. We’ve
also used the println! and vec! macros throughout the book. All of these
macros expand to produce more code than the code you’ve written manually.
Metaprogramming is useful for reducing the amount of code you have to write and maintain, which is also one of the roles of functions. However, macros have some additional powers that functions don’t have.
A function signature must declare the number and type of parameters the
function has. Macros, on the other hand, can take a variable number of
parameters: We can call println!("hello") with one argument or
println!("hello {}", name) with two arguments. Also, macros are expanded
before the compiler interprets the meaning of the code, so a macro can, for
example, implement a trait on a given type. A function can’t, because it gets
called at runtime and a trait needs to be implemented at compile time.
The downside to implementing a macro instead of a function is that macro definitions are more complex than function definitions because you’re writing Rust code that writes Rust code. Due to this indirection, macro definitions are generally more difficult to read, understand, and maintain than function definitions.
Another important difference between macros and functions is that you must define macros or bring them into scope before you call them in a file, as opposed to functions you can define anywhere and call anywhere.
Declarative Macros for General Metaprogramming
The most widely used form of macros in Rust is the declarative macro. These
are also sometimes referred to as “macros by example,” “macro_rules! macros,”
or just plain “macros.” At their core, declarative macros allow you to write
something similar to a Rust match expression. As discussed in Chapter 6,
match expressions are control structures that take an expression, compare the
resultant value of the expression to patterns, and then run the code associated
with the matching pattern. Macros also compare a value to patterns that are
associated with particular code: In this situation, the value is the literal
Rust source code passed to the macro; the patterns are compared with the
structure of that source code; and the code associated with each pattern, when
matched, replaces the code passed to the macro. This all happens during
compilation.
To define a macro, you use the macro_rules! construct. Let’s explore how to
use macro_rules! by looking at how the vec! macro is defined. Chapter 8
covered how we can use the vec! macro to create a new vector with particular
values. For example, the following macro creates a new vector containing three
integers:
#![allow(unused)]
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
}
We could also use the vec! macro to make a vector of two integers or a vector
of five string slices. We wouldn’t be able to use a function to do the same
because we wouldn’t know the number or type of values up front.
Listing 20-35 shows a slightly simplified definition of the vec! macro.
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
vec! macro definitionNote: The actual definition of the vec! macro in the standard library
includes code to pre-allocate the correct amount of memory up front. That code
is an optimization that we don’t include here, to make the example simpler.
The #[macro_export] annotation indicates that this macro should be made
available whenever the crate in which the macro is defined is brought into
scope. Without this annotation, the macro can’t be brought into scope.
We then start the macro definition with macro_rules! and the name of the
macro we’re defining without the exclamation mark. The name, in this case
vec, is followed by curly brackets denoting the body of the macro definition.
The structure in the vec! body is similar to the structure of a match
expression. Here we have one arm with the pattern ( $( $x:expr ),* ),
followed by => and the block of code associated with this pattern. If the
pattern matches, the associated block of code will be emitted. Given that this
is the only pattern in this macro, there is only one valid way to match; any
other pattern will result in an error. More complex macros will have more than
one arm.
Valid pattern syntax in macro definitions is different from the pattern syntax covered in Chapter 19 because macro patterns are matched against Rust code structure rather than values. Let’s walk through what the pattern pieces in Listing 20-29 mean; for the full macro pattern syntax, see the Rust Reference.
First, we use a set of parentheses to encompass the whole pattern. We use a
dollar sign ($) to declare a variable in the macro system that will contain
the Rust code matching the pattern. The dollar sign makes it clear this is a
macro variable as opposed to a regular Rust variable. Next comes a set of
parentheses that captures values that match the pattern within the parentheses
for use in the replacement code. Within $() is $x:expr, which matches any
Rust expression and gives the expression the name $x.
The comma following $() indicates that a literal comma separator character
must appear between each instance of the code that matches the code in $().
The * specifies that the pattern matches zero or more of whatever precedes
the *.
When we call this macro with vec![1, 2, 3];, the $x pattern matches three
times with the three expressions 1, 2, and 3.
Now let’s look at the pattern in the body of the code associated with this arm:
temp_vec.push() within $()* is generated for each part that matches $()
in the pattern zero or more times depending on how many times the pattern
matches. The $x is replaced with each expression matched. When we call this
macro with vec![1, 2, 3];, the code generated that replaces this macro call
will be the following:
{
let mut temp_vec = Vec::new();
temp_vec.push(1);
temp_vec.push(2);
temp_vec.push(3);
temp_vec
}
We’ve defined a macro that can take any number of arguments of any type and can generate code to create a vector containing the specified elements.
To learn more about how to write macros, consult the online documentation or other resources, such as “The Little Book of Rust Macros” started by Daniel Keep and continued by Lukas Wirth.
Procedural Macros for Generating Code from Attributes
The second form of macros is the procedural macro, which acts more like a
function (and is a type of procedure). Procedural macros accept some code as
an input, operate on that code, and produce some code as an output rather than
matching against patterns and replacing the code with other code as declarative
macros do. The three kinds of procedural macros are custom derive,
attribute-like, and function-like, and all work in a similar fashion.
When creating procedural macros, the definitions must reside in their own crate
with a special crate type. This is for complex technical reasons that we hope
to eliminate in the future. In Listing 20-36, we show how to define a
procedural macro, where some_attribute is a placeholder for using a specific
macro variety.
use proc_macro::TokenStream;
#[some_attribute]
pub fn some_name(input: TokenStream) -> TokenStream {
}
The function that defines a procedural macro takes a TokenStream as an input
and produces a TokenStream as an output. The TokenStream type is defined by
the proc_macro crate that is included with Rust and represents a sequence of
tokens. This is the core of the macro: The source code that the macro is
operating on makes up the input TokenStream, and the code the macro produces
is the output TokenStream. The function also has an attribute attached to it
that specifies which kind of procedural macro we’re creating. We can have
multiple kinds of procedural macros in the same crate.
Let’s look at the different kinds of procedural macros. We’ll start with a
custom derive macro and then explain the small dissimilarities that make the
other forms different.
Custom derive Macros
Let’s create a crate named hello_macro that defines a trait named
HelloMacro with one associated function named hello_macro. Rather than
making our users implement the HelloMacro trait for each of their types,
we’ll provide a procedural macro so that users can annotate their type with
#[derive(HelloMacro)] to get a default implementation of the hello_macro
function. The default implementation will print Hello, Macro! My name is TypeName! where TypeName is the name of the type on which this trait has
been defined. In other words, we’ll write a crate that enables another
programmer to write code like Listing 20-37 using our crate.
use hello_macro::HelloMacro;
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello_macro();
}
This code will print Hello, Macro! My name is Pancakes! when we’re done. The
first step is to make a new library crate, like this:
$ cargo new hello_macro --lib
Next, in Listing 20-38, we’ll define the HelloMacro trait and its associated
function.
pub trait HelloMacro {
fn hello_macro();
}
derive macroWe have a trait and its function. At this point, our crate user could implement the trait to achieve the desired functionality, as in Listing 20-39.
use hello_macro::HelloMacro;
struct Pancakes;
impl HelloMacro for Pancakes {
fn hello_macro() {
println!("Hello, Macro! My name is Pancakes!");
}
}
fn main() {
Pancakes::hello_macro();
}
HelloMacro traitHowever, they would need to write the implementation block for each type they
wanted to use with hello_macro; we want to spare them from having to do this
work.
Additionally, we can’t yet provide the hello_macro function with default
implementation that will print the name of the type the trait is implemented
on: Rust doesn’t have reflection capabilities, so it can’t look up the type’s
name at runtime. We need a macro to generate code at compile time.
The next step is to define the procedural macro. At the time of this writing,
procedural macros need to be in their own crate. Eventually, this restriction
might be lifted. The convention for structuring crates and macro crates is as
follows: For a crate named foo, a custom derive procedural macro crate is
called foo_derive. Let’s start a new crate called hello_macro_derive inside
our hello_macro project:
$ cargo new hello_macro_derive --lib
Our two crates are tightly related, so we create the procedural macro crate
within the directory of our hello_macro crate. If we change the trait
definition in hello_macro, we’ll have to change the implementation of the
procedural macro in hello_macro_derive as well. The two crates will need to
be published separately, and programmers using these crates will need to add
both as dependencies and bring them both into scope. We could instead have the
hello_macro crate use hello_macro_derive as a dependency and re-export the
procedural macro code. However, the way we’ve structured the project makes it
possible for programmers to use hello_macro even if they don’t want the
derive functionality.
We need to declare the hello_macro_derive crate as a procedural macro crate.
We’ll also need functionality from the syn and quote crates, as you’ll see
in a moment, so we need to add them as dependencies. Add the following to the
Cargo.toml file for hello_macro_derive:
[lib]
proc-macro = true
[dependencies]
syn = "2.0"
quote = "1.0"
To start defining the procedural macro, place the code in Listing 20-40 into
your src/lib.rs file for the hello_macro_derive crate. Note that this code
won’t compile until we add a definition for the impl_hello_macro function.
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate.
let ast = syn::parse(input).unwrap();
// Build the trait implementation.
impl_hello_macro(&ast)
}
Notice that we’ve split the code into the hello_macro_derive function, which
is responsible for parsing the TokenStream, and the impl_hello_macro
function, which is responsible for transforming the syntax tree: This makes
writing a procedural macro more convenient. The code in the outer function
(hello_macro_derive in this case) will be the same for almost every
procedural macro crate you see or create. The code you specify in the body of
the inner function (impl_hello_macro in this case) will be different
depending on your procedural macro’s purpose.
We’ve introduced three new crates: proc_macro, syn,
and quote. The proc_macro crate comes with Rust,
so we didn’t need to add that to the dependencies in Cargo.toml. The
proc_macro crate is the compiler’s API that allows us to read and manipulate
Rust code from our code.
The syn crate parses Rust code from a string into a data structure that we
can perform operations on. The quote crate turns syn data structures back
into Rust code. These crates make it much simpler to parse any sort of Rust
code we might want to handle: Writing a full parser for Rust code is no simple
task.
The hello_macro_derive function will be called when a user of our library
specifies #[derive(HelloMacro)] on a type. This is possible because we’ve
annotated the hello_macro_derive function here with proc_macro_derive and
specified the name HelloMacro, which matches our trait name; this is the
convention most procedural macros follow.
The hello_macro_derive function first converts the input from a
TokenStream to a data structure that we can then interpret and perform
operations on. This is where syn comes into play. The parse function in
syn takes a TokenStream and returns a DeriveInput struct representing the
parsed Rust code. Listing 20-41 shows the relevant parts of the DeriveInput
struct we get from parsing the struct Pancakes; string.
DeriveInput {
// --snip--
ident: Ident {
ident: "Pancakes",
span: #0 bytes(95..103)
},
data: Struct(
DataStruct {
struct_token: Struct,
fields: Unit,
semi_token: Some(
Semi
)
}
)
}
DeriveInput instance we get when parsing the code that has the macro’s attribute in Listing 20-37The fields of this struct show that the Rust code we’ve parsed is a unit struct
with the ident (identifier, meaning the name) of Pancakes. There are more
fields on this struct for describing all sorts of Rust code; check the syn
documentation for DeriveInput for more information.
Soon we’ll define the impl_hello_macro function, which is where we’ll build
the new Rust code we want to include. But before we do, note that the output
for our derive macro is also a TokenStream. The returned TokenStream is
added to the code that our crate users write, so when they compile their crate,
they’ll get the extra functionality that we provide in the modified
TokenStream.
You might have noticed that we’re calling unwrap to cause the
hello_macro_derive function to panic if the call to the syn::parse function
fails here. It’s necessary for our procedural macro to panic on errors because
proc_macro_derive functions must return TokenStream rather than Result to
conform to the procedural macro API. We’ve simplified this example by using
unwrap; in production code, you should provide more specific error messages
about what went wrong by using panic! or expect.
Now that we have the code to turn the annotated Rust code from a TokenStream
into a DeriveInput instance, let’s generate the code that implements the
HelloMacro trait on the annotated type, as shown in Listing 20-42.
use proc_macro::TokenStream;
use quote::quote;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Construct a representation of Rust code as a syntax tree
// that we can manipulate
let ast = syn::parse(input).unwrap();
// Build the trait implementation
impl_hello_macro(&ast)
}
fn impl_hello_macro(ast: &syn::DeriveInput) -> TokenStream {
let name = &ast.ident;
let generated = quote! {
impl HelloMacro for #name {
fn hello_macro() {
println!("Hello, Macro! My name is {}!", stringify!(#name));
}
}
};
generated.into()
}
HelloMacro trait using the parsed Rust codeWe get an Ident struct instance containing the name (identifier) of the
annotated type using ast.ident. The struct in Listing 20-41 shows that when
we run the impl_hello_macro function on the code in Listing 20-37, the
ident we get will have the ident field with a value of "Pancakes". Thus,
the name variable in Listing 20-42 will contain an Ident struct instance
that, when printed, will be the string "Pancakes", the name of the struct in
Listing 20-37.
The quote! macro lets us define the Rust code that we want to return. The
compiler expects something different from the direct result of the quote!
macro’s execution, so we need to convert it to a TokenStream. We do this by
calling the into method, which consumes this intermediate representation and
returns a value of the required TokenStream type.
The quote! macro also provides some very cool templating mechanics: We can
enter #name, and quote! will replace it with the value in the variable
name. You can even do some repetition similar to the way regular macros work.
Check out the quote crate’s docs for a thorough introduction.
We want our procedural macro to generate an implementation of our HelloMacro
trait for the type the user annotated, which we can get by using #name. The
trait implementation has the one function hello_macro, whose body contains the
functionality we want to provide: printing Hello, Macro! My name is and then
the name of the annotated type.
The stringify! macro used here is built into Rust. It takes a Rust
expression, such as 1 + 2, and at compile time turns the expression into a
string literal, such as "1 + 2". This is different from format! or
println!, which are macros that evaluate the expression and then turn the
result into a String. There is a possibility that the #name input might be
an expression to print literally, so we use stringify!. Using stringify!
also saves an allocation by converting #name to a string literal at compile
time.
At this point, cargo build should complete successfully in both hello_macro
and hello_macro_derive. Let’s hook up these crates to the code in Listing
20-37 to see the procedural macro in action! Create a new binary project in
your projects directory using cargo new pancakes. We need to add
hello_macro and hello_macro_derive as dependencies in the pancakes
crate’s Cargo.toml. If you’re publishing your versions of hello_macro and
hello_macro_derive to crates.io, they
would be regular dependencies; if not, you can specify them as path
dependencies as follows:
[dependencies]
hello_macro = { path = "../hello_macro" }
hello_macro_derive = { path = "../hello_macro/hello_macro_derive" }
Put the code in Listing 20-37 into src/main.rs, and run cargo run: It
should print Hello, Macro! My name is Pancakes!. The implementation of the
HelloMacro trait from the procedural macro was included without the
pancakes crate needing to implement it; the #[derive(HelloMacro)] added the
trait implementation.
Next, let’s explore how the other kinds of procedural macros differ from custom
derive macros.
Attribute-Like Macros
Attribute-like macros are similar to custom derive macros, but instead of
generating code for the derive attribute, they allow you to create new
attributes. They’re also more flexible: derive only works for structs and
enums; attributes can be applied to other items as well, such as functions.
Here’s an example of using an attribute-like macro. Say you have an attribute
named route that annotates functions when using a web application framework:
#[route(GET, "/")]
fn index() {
This #[route] attribute would be defined by the framework as a procedural
macro. The signature of the macro definition function would look like this:
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
Here, we have two parameters of type TokenStream. The first is for the
contents of the attribute: the GET, "/" part. The second is the body of the
item the attribute is attached to: in this case, fn index() {} and the rest
of the function’s body.
Other than that, attribute-like macros work the same way as custom derive
macros: You create a crate with the proc-macro crate type and implement a
function that generates the code you want!
Function-Like Macros
Function-like macros define macros that look like function calls. Similarly to
macro_rules! macros, they’re more flexible than functions; for example, they
can take an unknown number of arguments. However, macro_rules! macros can
only be defined using the match-like syntax we discussed in the “Declarative
Macros for General Metaprogramming” section earlier.
Function-like macros take a TokenStream parameter, and their definition
manipulates that TokenStream using Rust code as the other two types of
procedural macros do. An example of a function-like macro is an sql! macro
that might be called like so:
let sql = sql!(SELECT * FROM posts WHERE id=1);
This macro would parse the SQL statement inside it and check that it’s
syntactically correct, which is much more complex processing than a
macro_rules! macro can do. The sql! macro would be defined like this:
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
This definition is similar to the custom derive macro’s signature: We receive
the tokens that are inside the parentheses and return the code we wanted to
generate.
Summary
Whew! Now you have some Rust features in your toolbox that you likely won’t use often, but you’ll know they’re available in very particular circumstances. We’ve introduced several complex topics so that when you encounter them in error message suggestions or in other people’s code, you’ll be able to recognize these concepts and syntax. Use this chapter as a reference to guide you to solutions.
Next, we’ll put everything we’ve discussed throughout the book into practice and do one more project!
Final Project: Building a Multithreaded Web Server
It’s been a long journey, but we’ve reached the end of the book. In this chapter, we’ll build one more project together to demonstrate some of the concepts we covered in the final chapters, as well as recap some earlier lessons.
For our final project, we’ll make a web server that says “Hello!” and looks like Figure 21-1 in a web browser.
Here is our plan for building the web server:
- Learn a bit about TCP and HTTP.
- Listen for TCP connections on a socket.
- Parse a small number of HTTP requests.
- Create a proper HTTP response.
- Improve the throughput of our server with a thread pool.
Figure 21-1: Our final shared project
Before we get started, we should mention two details. First, the method we’ll use won’t be the best way to build a web server with Rust. Community members have published a number of production-ready crates available at crates.io that provide more complete web server and thread pool implementations than we’ll build. However, our intention in this chapter is to help you learn, not to take the easy route. Because Rust is a systems programming language, we can choose the level of abstraction we want to work with and can go to a lower level than is possible or practical in other languages.
Second, we will not be using async and await here. Building a thread pool is a big enough challenge on its own, without adding in building an async runtime! However, we will note how async and await might be applicable to some of the same problems we will see in this chapter. Ultimately, as we noted back in Chapter 17, many async runtimes use thread pools for managing their work.
We’ll therefore write the basic HTTP server and thread pool manually so that you can learn the general ideas and techniques behind the crates you might use in the future.
เขียน web server แบบ single-threaded
Building a Single-Threaded Web Server
We’ll start by getting a single-threaded web server working. Before we begin, let’s look at a quick overview of the protocols involved in building web servers. The details of these protocols are beyond the scope of this book, but a brief overview will give you the information you need.
The two main protocols involved in web servers are Hypertext Transfer Protocol (HTTP) and Transmission Control Protocol (TCP). Both protocols are request-response protocols, meaning a client initiates requests and a server listens to the requests and provides a response to the client. The contents of those requests and responses are defined by the protocols.
TCP is the lower-level protocol that describes the details of how information gets from one server to another but doesn’t specify what that information is. HTTP builds on top of TCP by defining the contents of the requests and responses. It’s technically possible to use HTTP with other protocols, but in the vast majority of cases, HTTP sends its data over TCP. We’ll work with the raw bytes of TCP and HTTP requests and responses.
Listening to the TCP Connection
Our web server needs to listen to a TCP connection, so that’s the first part
we’ll work on. The standard library offers a std::net module that lets us do
this. Let’s make a new project in the usual fashion:
$ cargo new hello
Created binary (application) `hello` project
$ cd hello
Now enter the code in Listing 21-1 in src/main.rs to start. This code will
listen at the local address 127.0.0.1:7878 for incoming TCP streams. When it
gets an incoming stream, it will print Connection established!.
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("Connection established!");
}
}
Using TcpListener, we can listen for TCP connections at the address
127.0.0.1:7878. In the address, the section before the colon is an IP address
representing your computer (this is the same on every computer and doesn’t
represent the authors’ computer specifically), and 7878 is the port. We’ve
chosen this port for two reasons: HTTP isn’t normally accepted on this port, so
our server is unlikely to conflict with any other web server you might have
running on your machine, and 7878 is rust typed on a telephone.
The bind function in this scenario works like the new function in that it
will return a new TcpListener instance. The function is called bind
because, in networking, connecting to a port to listen to is known as “binding
to a port.”
The bind function returns a Result<T, E>, which indicates that it’s
possible for binding to fail, for example, if we ran two instances of our
program and so had two programs listening to the same port. Because we’re
writing a basic server just for learning purposes, we won’t worry about
handling these kinds of errors; instead, we use unwrap to stop the program if
errors happen.
The incoming method on TcpListener returns an iterator that gives us a
sequence of streams (more specifically, streams of type TcpStream). A single
stream represents an open connection between the client and the server.
Connection is the name for the full request and response process in which a
client connects to the server, the server generates a response, and the server
closes the connection. As such, we will read from the TcpStream to see what
the client sent and then write our response to the stream to send data back to
the client. Overall, this for loop will process each connection in turn and
produce a series of streams for us to handle.
For now, our handling of the stream consists of calling unwrap to terminate
our program if the stream has any errors; if there aren’t any errors, the
program prints a message. We’ll add more functionality for the success case in
the next listing. The reason we might receive errors from the incoming method
when a client connects to the server is that we’re not actually iterating over
connections. Instead, we’re iterating over connection attempts. The
connection might not be successful for a number of reasons, many of them
operating system specific. For example, many operating systems have a limit to
the number of simultaneous open connections they can support; new connection
attempts beyond that number will produce an error until some of the open
connections are closed.
Let’s try running this code! Invoke cargo run in the terminal and then load
127.0.0.1:7878 in a web browser. The browser should show an error message
like “Connection reset” because the server isn’t currently sending back any
data. But when you look at your terminal, you should see several messages that
were printed when the browser connected to the server!
Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
Sometimes you’ll see multiple messages printed for one browser request; the reason might be that the browser is making a request for the page as well as a request for other resources, like the favicon.ico icon that appears in the browser tab.
It could also be that the browser is trying to connect to the server multiple
times because the server isn’t responding with any data. When stream goes out
of scope and is dropped at the end of the loop, the connection is closed as
part of the drop implementation. Browsers sometimes deal with closed
connections by retrying, because the problem might be temporary.
Browsers also sometimes open multiple connections to the server without sending any requests so that if they do later send requests, those requests can happen more quickly. When this occurs, our server will see each connection, regardless of whether there are any requests over that connection. Many versions of Chrome-based browsers do this, for example; you can disable that optimization by using private browsing mode or using a different browser.
The important factor is that we’ve successfully gotten a handle to a TCP connection!
Remember to stop the program by pressing ctrl-C when
you’re done running a particular version of the code. Then, restart the program
by invoking the cargo run command after you’ve made each set of code changes
to make sure you’re running the newest code.
Reading the Request
Let’s implement the functionality to read the request from the browser! To
separate the concerns of first getting a connection and then taking some action
with the connection, we’ll start a new function for processing connections. In
this new handle_connection function, we’ll read data from the TCP stream and
print it so that we can see the data being sent from the browser. Change the
code to look like Listing 21-2.
use std::{
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
println!("Request: {http_request:#?}");
}
TcpStream and printing the dataWe bring std::io::BufReader and std::io::prelude into scope to get access
to traits and types that let us read from and write to the stream. In the for
loop in the main function, instead of printing a message that says we made a
connection, we now call the new handle_connection function and pass the
stream to it.
In the handle_connection function, we create a new BufReader instance that
wraps a reference to the stream. The BufReader adds buffering by managing
calls to the std::io::Read trait methods for us.
We create a variable named http_request to collect the lines of the request
the browser sends to our server. We indicate that we want to collect these
lines in a vector by adding the Vec<_> type annotation.
BufReader implements the std::io::BufRead trait, which provides the lines
method. The lines method returns an iterator of Result<String, std::io::Error> by splitting the stream of data whenever it sees a newline
byte. To get each String, we map and unwrap each Result. The Result
might be an error if the data isn’t valid UTF-8 or if there was a problem
reading from the stream. Again, a production program should handle these errors
more gracefully, but we’re choosing to stop the program in the error case for
simplicity.
The browser signals the end of an HTTP request by sending two newline characters in a row, so to get one request from the stream, we take lines until we get a line that is the empty string. Once we’ve collected the lines into the vector, we’re printing them out using pretty debug formatting so that we can take a look at the instructions the web browser is sending to our server.
Let’s try this code! Start the program and make a request in a web browser again. Note that we’ll still get an error page in the browser, but our program’s output in the terminal will now look similar to this:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello`
Request: [
"GET / HTTP/1.1",
"Host: 127.0.0.1:7878",
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip, deflate, br",
"DNT: 1",
"Connection: keep-alive",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Cache-Control: max-age=0",
]
Depending on your browser, you might get slightly different output. Now that
we’re printing the request data, we can see why we get multiple connections
from one browser request by looking at the path after GET in the first line
of the request. If the repeated connections are all requesting /, we know the
browser is trying to fetch / repeatedly because it’s not getting a response
from our program.
Let’s break down this request data to understand what the browser is asking of our program.
Looking More Closely at an HTTP Request
HTTP is a text-based protocol, and a request takes this format:
Method Request-URI HTTP-Version CRLF
headers CRLF
message-body
The first line is the request line that holds information about what the
client is requesting. The first part of the request line indicates the method
being used, such as GET or POST, which describes how the client is making
this request. Our client used a GET request, which means it is asking for
information.
The next part of the request line is /, which indicates the uniform resource identifier (URI) the client is requesting: A URI is almost, but not quite, the same as a uniform resource locator (URL). The difference between URIs and URLs isn’t important for our purposes in this chapter, but the HTTP spec uses the term URI, so we can just mentally substitute URL for URI here.
The last part is the HTTP version the client uses, and then the request line
ends in a CRLF sequence. (CRLF stands for carriage return and line feed,
which are terms from the typewriter days!) The CRLF sequence can also be
written as \r\n, where \r is a carriage return and \n is a line feed. The
CRLF sequence separates the request line from the rest of the request data.
Note that when the CRLF is printed, we see a new line start rather than \r\n.
Looking at the request line data we received from running our program so far,
we see that GET is the method, / is the request URI, and HTTP/1.1 is the
version.
After the request line, the remaining lines starting from Host: onward are
headers. GET requests have no body.
Try making a request from a different browser or asking for a different address, such as 127.0.0.1:7878/test, to see how the request data changes.
Now that we know what the browser is asking for, let’s send back some data!
Writing a Response
We’re going to implement sending data in response to a client request. Responses have the following format:
HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
The first line is a status line that contains the HTTP version used in the response, a numeric status code that summarizes the result of the request, and a reason phrase that provides a text description of the status code. After the CRLF sequence are any headers, another CRLF sequence, and the body of the response.
Here is an example response that uses HTTP version 1.1 and has a status code of 200, an OK reason phrase, no headers, and no body:
HTTP/1.1 200 OK\r\n\r\n
The status code 200 is the standard success response. The text is a tiny
successful HTTP response. Let’s write this to the stream as our response to a
successful request! From the handle_connection function, remove the
println! that was printing the request data and replace it with the code in
Listing 21-3.
use std::{
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
let response = "HTTP/1.1 200 OK\r\n\r\n";
stream.write_all(response.as_bytes()).unwrap();
}
The first new line defines the response variable that holds the success
message’s data. Then, we call as_bytes on our response to convert the
string data to bytes. The write_all method on stream takes a &[u8] and
sends those bytes directly down the connection. Because the write_all
operation could fail, we use unwrap on any error result as before. Again, in
a real application, you would add error handling here.
With these changes, let’s run our code and make a request. We’re no longer printing any data to the terminal, so we won’t see any output other than the output from Cargo. When you load 127.0.0.1:7878 in a web browser, you should get a blank page instead of an error. You’ve just handcoded receiving an HTTP request and sending a response!
Returning Real HTML
Let’s implement the functionality for returning more than a blank page. Create the new file hello.html in the root of your project directory, not in the src directory. You can input any HTML you want; Listing 21-4 shows one possibility.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Hello!</h1>
<p>Hi from Rust</p>
</body>
</html>
This is a minimal HTML5 document with a heading and some text. To return this
from the server when a request is received, we’ll modify handle_connection as
shown in Listing 21-5 to read the HTML file, add it to the response as a body,
and send it.
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
// --snip--
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let http_request: Vec<_> = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
We’ve added fs to the use statement to bring the standard library’s
filesystem module into scope. The code for reading the contents of a file to a
string should look familiar; we used it when we read the contents of a file for
our I/O project in Listing 12-4.
Next, we use format! to add the file’s contents as the body of the success
response. To ensure a valid HTTP response, we add the Content-Length header,
which is set to the size of our response body—in this case, the size of
hello.html.
Run this code with cargo run and load 127.0.0.1:7878 in your browser; you
should see your HTML rendered!
Currently, we’re ignoring the request data in http_request and just sending
back the contents of the HTML file unconditionally. That means if you try
requesting 127.0.0.1:7878/something-else in your browser, you’ll still get
back this same HTML response. At the moment, our server is very limited and
does not do what most web servers do. We want to customize our responses
depending on the request and only send back the HTML file for a well-formed
request to /.
Validating the Request and Selectively Responding
Right now, our web server will return the HTML in the file no matter what the
client requested. Let’s add functionality to check that the browser is
requesting / before returning the HTML file and to return an error if the
browser requests anything else. For this we need to modify handle_connection,
as shown in Listing 21-6. This new code checks the content of the request
received against what we know a request for / looks like and adds if and
else blocks to treat requests differently.
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
// --snip--
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
if request_line == "GET / HTTP/1.1" {
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
} else {
// some other request
}
}
We’re only going to be looking at the first line of the HTTP request, so rather
than reading the entire request into a vector, we’re calling next to get the
first item from the iterator. The first unwrap takes care of the Option and
stops the program if the iterator has no items. The second unwrap handles the
Result and has the same effect as the unwrap that was in the map added in
Listing 21-2.
Next, we check the request_line to see if it equals the request line of a GET
request to the / path. If it does, the if block returns the contents of our
HTML file.
If the request_line does not equal the GET request to the / path, it
means we’ve received some other request. We’ll add code to the else block in
a moment to respond to all other requests.
Run this code now and request 127.0.0.1:7878; you should get the HTML in hello.html. If you make any other request, such as 127.0.0.1:7878/something-else, you’ll get a connection error like those you saw when running the code in Listing 21-1 and Listing 21-2.
Now let’s add the code in Listing 21-7 to the else block to return a response
with the status code 404, which signals that the content for the request was
not found. We’ll also return some HTML for a page to render in the browser
indicating the response to the end user.
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
if request_line == "GET / HTTP/1.1" {
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
// --snip--
} else {
let status_line = "HTTP/1.1 404 NOT FOUND";
let contents = fs::read_to_string("404.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
}
}
Here, our response has a status line with status code 404 and the reason phrase
NOT FOUND. The body of the response will be the HTML in the file 404.html.
You’ll need to create a 404.html file next to hello.html for the error
page; again, feel free to use any HTML you want, or use the example HTML in
Listing 21-8.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
<h1>Oops!</h1>
<p>Sorry, I don't know what you're asking for.</p>
</body>
</html>
With these changes, run your server again. Requesting 127.0.0.1:7878 should return the contents of hello.html, and any other request, like 127.0.0.1:7878/foo, should return the error HTML from 404.html.
Refactoring
At the moment, the if and else blocks have a lot of repetition: They’re
both reading files and writing the contents of the files to the stream. The
only differences are the status line and the filename. Let’s make the code more
concise by pulling out those differences into separate if and else lines
that will assign the values of the status line and the filename to variables;
we can then use those variables unconditionally in the code to read the file
and write the response. Listing 21-9 shows the resultant code after replacing
the large if and else blocks.
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
// --snip--
fn handle_connection(mut stream: TcpStream) {
// --snip--
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
("HTTP/1.1 200 OK", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND", "404.html")
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
if and else blocks to contain only the code that differs between the two casesNow the if and else blocks only return the appropriate values for the
status line and filename in a tuple; we then use destructuring to assign these
two values to status_line and filename using a pattern in the let
statement, as discussed in Chapter 19.
The previously duplicated code is now outside the if and else blocks and
uses the status_line and filename variables. This makes it easier to see
the difference between the two cases, and it means we have only one place to
update the code if we want to change how the file reading and response writing
work. The behavior of the code in Listing 21-9 will be the same as that in
Listing 21-7.
Awesome! We now have a simple web server in approximately 40 lines of Rust code that responds to one request with a page of content and responds to all other requests with a 404 response.
Currently, our server runs in a single thread, meaning it can only serve one request at a time. Let’s examine how that can be a problem by simulating some slow requests. Then, we’ll fix it so that our server can handle multiple requests at once.
เปลี่ยนจาก single-threaded เป็น multithreaded
From a Single-Threaded to a Multithreaded Server
Right now, the server will process each request in turn, meaning it won’t process a second connection until the first connection is finished processing. If the server received more and more requests, this serial execution would be less and less optimal. If the server receives a request that takes a long time to process, subsequent requests will have to wait until the long request is finished, even if the new requests can be processed quickly. We’ll need to fix this, but first we’ll look at the problem in action.
Simulating a Slow Request
We’ll look at how a slowly processing request can affect other requests made to our current server implementation. Listing 21-10 implements handling a request to /sleep with a simulated slow response that will cause the server to sleep for five seconds before responding.
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
// --snip--
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
// --snip--
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
// --snip--
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
We switched from if to match now that we have three cases. We need to
explicitly match on a slice of request_line to pattern-match against the
string literal values; match doesn’t do automatic referencing and
dereferencing, like the equality method does.
The first arm is the same as the if block from Listing 21-9. The second arm
matches a request to /sleep. When that request is received, the server will
sleep for five seconds before rendering the successful HTML page. The third arm
is the same as the else block from Listing 21-9.
You can see how primitive our server is: Real libraries would handle the recognition of multiple requests in a much less verbose way!
Start the server using cargo run. Then, open two browser windows: one for
http://127.0.0.1:7878 and the other for http://127.0.0.1:7878/sleep. If you
enter the / URI a few times, as before, you’ll see it respond quickly. But if
you enter /sleep and then load /, you’ll see that / waits until sleep
has slept for its full five seconds before loading.
There are multiple techniques we could use to avoid requests backing up behind a slow request, including using async as we did Chapter 17; the one we’ll implement is a thread pool.
Improving Throughput with a Thread Pool
A thread pool is a group of spawned threads that are ready and waiting to handle a task. When the program receives a new task, it assigns one of the threads in the pool to the task, and that thread will process the task. The remaining threads in the pool are available to handle any other tasks that come in while the first thread is processing. When the first thread is done processing its task, it’s returned to the pool of idle threads, ready to handle a new task. A thread pool allows you to process connections concurrently, increasing the throughput of your server.
We’ll limit the number of threads in the pool to a small number to protect us from DoS attacks; if we had our program create a new thread for each request as it came in, someone making 10 million requests to our server could wreak havoc by using up all our server’s resources and grinding the processing of requests to a halt.
Rather than spawning unlimited threads, then, we’ll have a fixed number of
threads waiting in the pool. Requests that come in are sent to the pool for
processing. The pool will maintain a queue of incoming requests. Each of the
threads in the pool will pop off a request from this queue, handle the request,
and then ask the queue for another request. With this design, we can process up
to N requests concurrently, where N is the number of threads. If each
thread is responding to a long-running request, subsequent requests can still
back up in the queue, but we’ve increased the number of long-running requests
we can handle before reaching that point.
This technique is just one of many ways to improve the throughput of a web server. Other options you might explore are the fork/join model, the single-threaded async I/O model, and the multithreaded async I/O model. If you’re interested in this topic, you can read more about other solutions and try to implement them; with a low-level language like Rust, all of these options are possible.
Before we begin implementing a thread pool, let’s talk about what using the pool should look like. When you’re trying to design code, writing the client interface first can help guide your design. Write the API of the code so that it’s structured in the way you want to call it; then, implement the functionality within that structure rather than implementing the functionality and then designing the public API.
Similar to how we used test-driven development in the project in Chapter 12, we’ll use compiler-driven development here. We’ll write the code that calls the functions we want, and then we’ll look at errors from the compiler to determine what we should change next to get the code to work. Before we do that, however, we’ll explore the technique we’re not going to use as a starting point.
Spawning a Thread for Each Request
First, let’s explore how our code might look if it did create a new thread for every connection. As mentioned earlier, this isn’t our final plan due to the problems with potentially spawning an unlimited number of threads, but it is a starting point to get a working multithreaded server first. Then, we’ll add the thread pool as an improvement, and contrasting the two solutions will be easier.
Listing 21-11 shows the changes to make to main to spawn a new thread to
handle each stream within the for loop.
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
thread::spawn(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
As you learned in Chapter 16, thread::spawn will create a new thread and then
run the code in the closure in the new thread. If you run this code and load
/sleep in your browser, then / in two more browser tabs, you’ll indeed see
that the requests to / don’t have to wait for /sleep to finish. However, as
we mentioned, this will eventually overwhelm the system because you’d be making
new threads without any limit.
You may also recall from Chapter 17 that this is exactly the kind of situation where async and await really shine! Keep that in mind as we build the thread pool and think about how things would look different or the same with async.
Creating a Finite Number of Threads
We want our thread pool to work in a similar, familiar way so that switching
from threads to a thread pool doesn’t require large changes to the code that
uses our API. Listing 21-12 shows the hypothetical interface for a ThreadPool
struct we want to use instead of thread::spawn.
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
ThreadPool interfaceWe use ThreadPool::new to create a new thread pool with a configurable number
of threads, in this case four. Then, in the for loop, pool.execute has a
similar interface as thread::spawn in that it takes a closure that the pool
should run for each stream. We need to implement pool.execute so that it
takes the closure and gives it to a thread in the pool to run. This code won’t
yet compile, but we’ll try so that the compiler can guide us in how to fix it.
Building ThreadPool Using Compiler-Driven Development
Make the changes in Listing 21-12 to src/main.rs, and then let’s use the
compiler errors from cargo check to drive our development. Here is the first
error we get:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
--> src/main.rs:11:16
|
11 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^ use of undeclared type `ThreadPool`
For more information about this error, try `rustc --explain E0433`.
error: could not compile `hello` (bin "hello") due to 1 previous error
Great! This error tells us we need a ThreadPool type or module, so we’ll
build one now. Our ThreadPool implementation will be independent of the kind
of work our web server is doing. So, let’s switch the hello crate from a
binary crate to a library crate to hold our ThreadPool implementation. After
we change to a library crate, we could also use the separate thread pool
library for any work we want to do using a thread pool, not just for serving
web requests.
Create a src/lib.rs file that contains the following, which is the simplest
definition of a ThreadPool struct that we can have for now:
pub struct ThreadPool;
Then, edit the main.rs file to bring ThreadPool into scope from the library
crate by adding the following code to the top of src/main.rs:
use hello::ThreadPool;
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming() {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
This code still won’t work, but let’s check it again to get the next error that we need to address:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope
--> src/main.rs:12:28
|
12 | let pool = ThreadPool::new(4);
| ^^^ function or associated item not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
This error indicates that next we need to create an associated function named
new for ThreadPool. We also know that new needs to have one parameter
that can accept 4 as an argument and should return a ThreadPool instance.
Let’s implement the simplest new function that will have those
characteristics:
pub struct ThreadPool;
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
}
We chose usize as the type of the size parameter because we know that a
negative number of threads doesn’t make any sense. We also know we’ll use this
4 as the number of elements in a collection of threads, which is what the
usize type is for, as discussed in the “Integer Types” section in Chapter 3.
Let’s check the code again:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope
--> src/main.rs:17:14
|
17 | pool.execute(|| {
| -----^^^^^^^ method not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
Now the error occurs because we don’t have an execute method on ThreadPool.
Recall from the “Creating a Finite Number of
Threads” section that we
decided our thread pool should have an interface similar to thread::spawn. In
addition, we’ll implement the execute function so that it takes the closure
it’s given and gives it to an idle thread in the pool to run.
We’ll define the execute method on ThreadPool to take a closure as a
parameter. Recall from the “Moving Captured Values Out of
Closures” in Chapter 13 that we can
take closures as parameters with three different traits: Fn, FnMut, and
FnOnce. We need to decide which kind of closure to use here. We know we’ll
end up doing something similar to the standard library thread::spawn
implementation, so we can look at what bounds the signature of thread::spawn
has on its parameter. The documentation shows us the following:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
The F type parameter is the one we’re concerned with here; the T type
parameter is related to the return value, and we’re not concerned with that. We
can see that spawn uses FnOnce as the trait bound on F. This is probably
what we want as well, because we’ll eventually pass the argument we get in
execute to spawn. We can be further confident that FnOnce is the trait we
want to use because the thread for running a request will only execute that
request’s closure one time, which matches the Once in FnOnce.
The F type parameter also has the trait bound Send and the lifetime bound
'static, which are useful in our situation: We need Send to transfer the
closure from one thread to another and 'static because we don’t know how long
the thread will take to execute. Let’s create an execute method on
ThreadPool that will take a generic parameter of type F with these bounds:
pub struct ThreadPool;
impl ThreadPool {
// --snip--
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
We still use the () after FnOnce because this FnOnce represents a closure
that takes no parameters and returns the unit type (). Just like function
definitions, the return type can be omitted from the signature, but even if we
have no parameters, we still need the parentheses.
Again, this is the simplest implementation of the execute method: It does
nothing, but we’re only trying to make our code compile. Let’s check it again:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.24s
It compiles! But note that if you try cargo run and make a request in the
browser, you’ll see the errors in the browser that we saw at the beginning of
the chapter. Our library isn’t actually calling the closure passed to execute
yet!
Note: A saying you might hear about languages with strict compilers, such as Haskell and Rust, is “If the code compiles, it works.” But this saying is not universally true. Our project compiles, but it does absolutely nothing! If we were building a real, complete project, this would be a good time to start writing unit tests to check that the code compiles and has the behavior we want.
Consider: What would be different here if we were going to execute a future instead of a closure?
Validating the Number of Threads in new
We aren’t doing anything with the parameters to new and execute. Let’s
implement the bodies of these functions with the behavior we want. To start,
let’s think about new. Earlier we chose an unsigned type for the size
parameter because a pool with a negative number of threads makes no sense.
However, a pool with zero threads also makes no sense, yet zero is a perfectly
valid usize. We’ll add code to check that size is greater than zero before
we return a ThreadPool instance, and we’ll have the program panic if it
receives a zero by using the assert! macro, as shown in Listing 21-13.
pub struct ThreadPool;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
ThreadPool
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
ThreadPool::new to panic if size is zeroWe’ve also added some documentation for our ThreadPool with doc comments.
Note that we followed good documentation practices by adding a section that
calls out the situations in which our function can panic, as discussed in
Chapter 14. Try running cargo doc --open and clicking the ThreadPool struct
to see what the generated docs for new look like!
Instead of adding the assert! macro as we’ve done here, we could change new
into build and return a Result like we did with Config::build in the I/O
project in Listing 12-9. But we’ve decided in this case that trying to create a
thread pool without any threads should be an unrecoverable error. If you’re
feeling ambitious, try to write a function named build with the following
signature to compare with the new function:
pub fn build(size: usize) -> Result<ThreadPool, PoolCreationError> {
Creating Space to Store the Threads
Now that we have a way to know we have a valid number of threads to store in
the pool, we can create those threads and store them in the ThreadPool struct
before returning the struct. But how do we “store” a thread? Let’s take another
look at the thread::spawn signature:
pub fn spawn<F, T>(f: F) -> JoinHandle<T>
where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
The spawn function returns a JoinHandle<T>, where T is the type that the
closure returns. Let’s try using JoinHandle too and see what happens. In our
case, the closures we’re passing to the thread pool will handle the connection
and not return anything, so T will be the unit type ().
The code in Listing 21-14 will compile, but it doesn’t create any threads yet.
We’ve changed the definition of ThreadPool to hold a vector of
thread::JoinHandle<()> instances, initialized the vector with a capacity of
size, set up a for loop that will run some code to create the threads, and
returned a ThreadPool instance containing them.
use std::thread;
pub struct ThreadPool {
threads: Vec<thread::JoinHandle<()>>,
}
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let mut threads = Vec::with_capacity(size);
for _ in 0..size {
// create some threads and store them in the vector
}
ThreadPool { threads }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
ThreadPool to hold the threadsWe’ve brought std::thread into scope in the library crate because we’re
using thread::JoinHandle as the type of the items in the vector in
ThreadPool.
Once a valid size is received, our ThreadPool creates a new vector that can
hold size items. The with_capacity function performs the same task as
Vec::new but with an important difference: It pre-allocates space in the
vector. Because we know we need to store size elements in the vector, doing
this allocation up front is slightly more efficient than using Vec::new,
which resizes itself as elements are inserted.
When you run cargo check again, it should succeed.
Sending Code from the ThreadPool to a Thread
We left a comment in the for loop in Listing 21-14 regarding the creation of
threads. Here, we’ll look at how we actually create threads. The standard
library provides thread::spawn as a way to create threads, and
thread::spawn expects to get some code the thread should run as soon as the
thread is created. However, in our case, we want to create the threads and have
them wait for code that we’ll send later. The standard library’s
implementation of threads doesn’t include any way to do that; we have to
implement it manually.
We’ll implement this behavior by introducing a new data structure between the
ThreadPool and the threads that will manage this new behavior. We’ll call
this data structure Worker, which is a common term in pooling
implementations. The Worker picks up code that needs to be run and runs the
code in its thread.
Think of people working in the kitchen at a restaurant: The workers wait until orders come in from customers, and then they’re responsible for taking those orders and filling them.
Instead of storing a vector of JoinHandle<()> instances in the thread pool,
we’ll store instances of the Worker struct. Each Worker will store a single
JoinHandle<()> instance. Then, we’ll implement a method on Worker that will
take a closure of code to run and send it to the already running thread for
execution. We’ll also give each Worker an id so that we can distinguish
between the different instances of Worker in the pool when logging or
debugging.
Here is the new process that will happen when we create a ThreadPool. We’ll
implement the code that sends the closure to the thread after we have Worker
set up in this way:
- Define a
Workerstruct that holds anidand aJoinHandle<()>. - Change
ThreadPoolto hold a vector ofWorkerinstances. - Define a
Worker::newfunction that takes anidnumber and returns aWorkerinstance that holds theidand a thread spawned with an empty closure. - In
ThreadPool::new, use theforloop counter to generate anid, create a newWorkerwith thatid, and store theWorkerin the vector.
If you’re up for a challenge, try implementing these changes on your own before looking at the code in Listing 21-15.
Ready? Here is Listing 21-15 with one way to make the preceding modifications.
use std::thread;
pub struct ThreadPool {
workers: Vec<Worker>,
}
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id));
}
ThreadPool { workers }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});
Worker { id, thread }
}
}
ThreadPool to hold Worker instances instead of holding threads directlyWe’ve changed the name of the field on ThreadPool from threads to workers
because it’s now holding Worker instances instead of JoinHandle<()>
instances. We use the counter in the for loop as an argument to
Worker::new, and we store each new Worker in the vector named workers.
External code (like our server in src/main.rs) doesn’t need to know the
implementation details regarding using a Worker struct within ThreadPool,
so we make the Worker struct and its new function private. The
Worker::new function uses the id we give it and stores a JoinHandle<()>
instance that is created by spawning a new thread using an empty closure.
Note: If the operating system can’t create a thread because there aren’t
enough system resources, thread::spawn will panic. That will cause our
whole server to panic, even though the creation of some threads might
succeed. For simplicity’s sake, this behavior is fine, but in a production
thread pool implementation, you’d likely want to use
std::thread::Builder and its
spawn method that returns Result instead.
This code will compile and will store the number of Worker instances we
specified as an argument to ThreadPool::new. But we’re still not processing
the closure that we get in execute. Let’s look at how to do that next.
Sending Requests to Threads via Channels
The next problem we’ll tackle is that the closures given to thread::spawn do
absolutely nothing. Currently, we get the closure we want to execute in the
execute method. But we need to give thread::spawn a closure to run when we
create each Worker during the creation of the ThreadPool.
We want the Worker structs that we just created to fetch the code to run from
a queue held in the ThreadPool and send that code to its thread to run.
The channels we learned about in Chapter 16—a simple way to communicate between
two threads—would be perfect for this use case. We’ll use a channel to function
as the queue of jobs, and execute will send a job from the ThreadPool to
the Worker instances, which will send the job to its thread. Here is the plan:
- The
ThreadPoolwill create a channel and hold on to the sender. - Each
Workerwill hold on to the receiver. - We’ll create a new
Jobstruct that will hold the closures we want to send down the channel. - The
executemethod will send the job it wants to execute through the sender. - In its thread, the
Workerwill loop over its receiver and execute the closures of any jobs it receives.
Let’s start by creating a channel in ThreadPool::new and holding the sender
in the ThreadPool instance, as shown in Listing 21-16. The Job struct
doesn’t hold anything for now but will be the type of item we’re sending down
the channel.
use std::{sync::mpsc, thread};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id));
}
ThreadPool { workers, sender }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize) -> Worker {
let thread = thread::spawn(|| {});
Worker { id, thread }
}
}
ThreadPool to store the sender of a channel that transmits Job instancesIn ThreadPool::new, we create our new channel and have the pool hold the
sender. This will successfully compile.
Let’s try passing a receiver of the channel into each Worker as the thread
pool creates the channel. We know we want to use the receiver in the thread that
the Worker instances spawn, so we’ll reference the receiver parameter in the
closure. The code in Listing 21-17 won’t quite compile yet.
use std::{sync::mpsc, thread};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, receiver));
}
ThreadPool { workers, sender }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
// --snip--
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
WorkerWe’ve made some small and straightforward changes: We pass the receiver into
Worker::new, and then we use it inside the closure.
When we try to check this code, we get this error:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0382]: use of moved value: `receiver`
--> src/lib.rs:26:42
|
21 | let (sender, receiver) = mpsc::channel();
| -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
25 | for id in 0..size {
| ----------------- inside of this loop
26 | workers.push(Worker::new(id, receiver));
| ^^^^^^^^ value moved here, in previous iteration of loop
|
note: consider changing this parameter type in method `new` to borrow instead if owning the value isn't necessary
--> src/lib.rs:47:33
|
47 | fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
| --- in this method ^^^^^^^^^^^^^^^^^^^ this parameter takes ownership of the value
help: consider moving the expression out of the loop so it is only moved once
|
25 ~ let mut value = Worker::new(id, receiver);
26 ~ for id in 0..size {
27 ~ workers.push(value);
|
For more information about this error, try `rustc --explain E0382`.
error: could not compile `hello` (lib) due to 1 previous error
The code is trying to pass receiver to multiple Worker instances. This
won’t work, as you’ll recall from Chapter 16: The channel implementation that
Rust provides is multiple producer, single consumer. This means we can’t
just clone the consuming end of the channel to fix this code. We also don’t
want to send a message multiple times to multiple consumers; we want one list
of messages with multiple Worker instances such that each message gets
processed once.
Additionally, taking a job off the channel queue involves mutating the
receiver, so the threads need a safe way to share and modify receiver;
otherwise, we might get race conditions (as covered in Chapter 16).
Recall the thread-safe smart pointers discussed in Chapter 16: To share
ownership across multiple threads and allow the threads to mutate the value, we
need to use Arc<Mutex<T>>. The Arc type will let multiple Worker instances
own the receiver, and Mutex will ensure that only one Worker gets a job from
the receiver at a time. Listing 21-18 shows the changes we need to make.
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
// --snip--
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
struct Job;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
// --snip--
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
}
// --snip--
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
// --snip--
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
Worker instances using Arc and MutexIn ThreadPool::new, we put the receiver in an Arc and a Mutex. For each
new Worker, we clone the Arc to bump the reference count so that the
Worker instances can share ownership of the receiver.
With these changes, the code compiles! We’re getting there!
Implementing the execute Method
Let’s finally implement the execute method on ThreadPool. We’ll also change
Job from a struct to a type alias for a trait object that holds the type of
closure that execute receives. As discussed in the “Type Synonyms and Type
Aliases” section in Chapter 20, type aliases
allow us to make long types shorter for ease of use. Look at Listing 21-19.
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
// --snip--
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
// --snip--
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
// --snip--
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(|| {
receiver;
});
Worker { id, thread }
}
}
Job type alias for a Box that holds each closure and then sending the job down the channelAfter creating a new Job instance using the closure we get in execute, we
send that job down the sending end of the channel. We’re calling unwrap on
send for the case that sending fails. This might happen if, for example, we
stop all our threads from executing, meaning the receiving end has stopped
receiving new messages. At the moment, we can’t stop our threads from
executing: Our threads continue executing as long as the pool exists. The
reason we use unwrap is that we know the failure case won’t happen, but the
compiler doesn’t know that.
But we’re not quite done yet! In the Worker, our closure being passed to
thread::spawn still only references the receiving end of the channel.
Instead, we need the closure to loop forever, asking the receiving end of the
channel for a job and running the job when it gets one. Let’s make the change
shown in Listing 21-20 to Worker::new.
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
Worker instance’s threadHere, we first call lock on the receiver to acquire the mutex, and then we
call unwrap to panic on any errors. Acquiring a lock might fail if the mutex
is in a poisoned state, which can happen if some other thread panicked while
holding the lock rather than releasing the lock. In this situation, calling
unwrap to have this thread panic is the correct action to take. Feel free to
change this unwrap to an expect with an error message that is meaningful to
you.
If we get the lock on the mutex, we call recv to receive a Job from the
channel. A final unwrap moves past any errors here as well, which might occur
if the thread holding the sender has shut down, similar to how the send
method returns Err if the receiver shuts down.
The call to recv blocks, so if there is no job yet, the current thread will
wait until a job becomes available. The Mutex<T> ensures that only one
Worker thread at a time is trying to request a job.
Our thread pool is now in a working state! Give it a cargo run and make some
requests:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
warning: field `workers` is never read
--> src/lib.rs:7:5
|
6 | pub struct ThreadPool {
| ---------- field in this struct
7 | workers: Vec<Worker>,
| ^^^^^^^
|
= note: `#[warn(dead_code)]` on by default
warning: fields `id` and `thread` are never read
--> src/lib.rs:48:5
|
47 | struct Worker {
| ------ fields in this struct
48 | id: usize,
| ^^
49 | thread: thread::JoinHandle<()>,
| ^^^^^^
warning: `hello` (lib) generated 2 warnings
Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.91s
Running `target/debug/hello`
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Worker 1 got a job; executing.
Worker 3 got a job; executing.
Worker 0 got a job; executing.
Worker 2 got a job; executing.
Success! We now have a thread pool that executes connections asynchronously. There are never more than four threads created, so our system won’t get overloaded if the server receives a lot of requests. If we make a request to /sleep, the server will be able to serve other requests by having another thread run them.
Note: If you open /sleep in multiple browser windows simultaneously, they might load one at a time in five-second intervals. Some web browsers execute multiple instances of the same request sequentially for caching reasons. This limitation is not caused by our web server.
This is a good time to pause and consider how the code in Listings 21-18, 21-19, and 21-20 would be different if we were using futures instead of a closure for the work to be done. What types would change? How would the method signatures be different, if at all? What parts of the code would stay the same?
After learning about the while let loop in Chapter 17 and Chapter 19, you
might be wondering why we didn’t write the Worker thread code as shown in
Listing 21-21.
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
// --snip--
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
while let Ok(job) = receiver.lock().unwrap().recv() {
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
Worker::new using while letThis code compiles and runs but doesn’t result in the desired threading
behavior: A slow request will still cause other requests to wait to be
processed. The reason is somewhat subtle: The Mutex struct has no public
unlock method because the ownership of the lock is based on the lifetime of
the MutexGuard<T> within the LockResult<MutexGuard<T>> that the lock
method returns. At compile time, the borrow checker can then enforce the rule
that a resource guarded by a Mutex cannot be accessed unless we hold the
lock. However, this implementation can also result in the lock being held
longer than intended if we aren’t mindful of the lifetime of the
MutexGuard<T>.
The code in Listing 21-20 that uses let job = receiver.lock().unwrap().recv().unwrap(); works because with let, any
temporary values used in the expression on the right-hand side of the equal
sign are immediately dropped when the let statement ends. However, while let (and if let and match) does not drop temporary values until the end of
the associated block. In Listing 21-21, the lock remains held for the duration
of the call to job(), meaning other Worker instances cannot receive jobs.
Graceful shutdown และ cleanup
Graceful Shutdown and Cleanup
The code in Listing 21-20 is responding to requests asynchronously through the
use of a thread pool, as we intended. We get some warnings about the workers,
id, and thread fields that we’re not using in a direct way that reminds us
we’re not cleaning up anything. When we use the less elegant
ctrl-C method to halt the main thread, all other threads
are stopped immediately as well, even if they’re in the middle of serving a
request.
Next, then, we’ll implement the Drop trait to call join on each of the
threads in the pool so that they can finish the requests they’re working on
before closing. Then, we’ll implement a way to tell the threads they should
stop accepting new requests and shut down. To see this code in action, we’ll
modify our server to accept only two requests before gracefully shutting down
its thread pool.
One thing to notice as we go: None of this affects the parts of the code that handle executing the closures, so everything here would be the same if we were using a thread pool for an async runtime.
Implementing the Drop Trait on ThreadPool
Let’s start with implementing Drop on our thread pool. When the pool is
dropped, our threads should all join to make sure they finish their work.
Listing 21-22 shows a first attempt at a Drop implementation; this code won’t
quite work yet.
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
First, we loop through each of the thread pool workers. We use &mut for this
because self is a mutable reference, and we also need to be able to mutate
worker. For each worker, we print a message saying that this particular
Worker instance is shutting down, and then we call join on that Worker
instance’s thread. If the call to join fails, we use unwrap to make Rust
panic and go into an ungraceful shutdown.
Here is the error we get when we compile this code:
$ cargo check
Checking hello v0.1.0 (file:///projects/hello)
error[E0507]: cannot move out of `worker.thread` which is behind a mutable reference
--> src/lib.rs:52:13
|
52 | worker.thread.join().unwrap();
| ^^^^^^^^^^^^^ ------ `worker.thread` moved due to this method call
| |
| move occurs because `worker.thread` has type `JoinHandle<()>`, which does not implement the `Copy` trait
|
note: `JoinHandle::<T>::join` takes ownership of the receiver `self`, which moves `worker.thread`
--> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/std/src/thread/mod.rs:1921:17
For more information about this error, try `rustc --explain E0507`.
error: could not compile `hello` (lib) due to 1 previous error
The error tells us we can’t call join because we only have a mutable borrow
of each worker and join takes ownership of its argument. To solve this
issue, we need to move the thread out of the Worker instance that owns
thread so that join can consume the thread. One way to do this is to take
the same approach we took in Listing 18-15. If Worker held an
Option<thread::JoinHandle<()>>, we could call the take method on the
Option to move the value out of the Some variant and leave a None variant
in its place. In other words, a Worker that is running would have a Some
variant in thread, and when we wanted to clean up a Worker, we’d replace
Some with None so that the Worker wouldn’t have a thread to run.
However, the only time this would come up would be when dropping the
Worker. In exchange, we’d have to deal with an
Option<thread::JoinHandle<()>> anywhere we accessed worker.thread.
Idiomatic Rust uses Option quite a bit, but when you find yourself wrapping
something you know will always be present in an Option as a workaround like
this, it’s a good idea to look for alternative approaches to make your code
cleaner and less error-prone.
In this case, a better alternative exists: the Vec::drain method. It accepts
a range parameter to specify which items to remove from the vector and returns
an iterator of those items. Passing the .. range syntax will remove every
value from the vector.
So, we need to update the ThreadPool drop implementation like this:
#![allow(unused)]
fn main() {
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: mpsc::Sender<Job>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
for worker in self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
}
This resolves the compiler error and does not require any other changes to our code. Note that, because drop can be called when panicking, the unwrap could also panic and cause a double panic, which immediately crashes the program and ends any cleanup in progress. This is fine for an example program, but it isn’t recommended for production code.
Signaling to the Threads to Stop Listening for Jobs
With all the changes we’ve made, our code compiles without any warnings.
However, the bad news is that this code doesn’t function the way we want it to
yet. The key is the logic in the closures run by the threads of the Worker
instances: At the moment, we call join, but that won’t shut down the threads,
because they loop forever looking for jobs. If we try to drop our
ThreadPool with our current implementation of drop, the main thread will
block forever, waiting for the first thread to finish.
To fix this problem, we’ll need a change in the ThreadPool drop
implementation and then a change in the Worker loop.
First, we’ll change the ThreadPool drop implementation to explicitly drop
the sender before waiting for the threads to finish. Listing 21-23 shows the
changes to ThreadPool to explicitly drop sender. Unlike with the thread,
here we do need to use an Option to be able to move sender out of
ThreadPool with Option::take.
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
// --snip--
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
// --snip--
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {id} got a job; executing.");
job();
}
});
Worker { id, thread }
}
}
sender before joining the Worker threadsDropping sender closes the channel, which indicates no more messages will be
sent. When that happens, all the calls to recv that the Worker instances do
in the infinite loop will return an error. In Listing 21-24, we change the
Worker loop to gracefully exit the loop in that case, which means the threads
will finish when the ThreadPool drop implementation calls join on them.
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in self.workers.drain(..) {
println!("Shutting down worker {}", worker.id);
worker.thread.join().unwrap();
}
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let message = receiver.lock().unwrap().recv();
match message {
Ok(job) => {
println!("Worker {id} got a job; executing.");
job();
}
Err(_) => {
println!("Worker {id} disconnected; shutting down.");
break;
}
}
}
});
Worker { id, thread }
}
}
recv returns an errorTo see this code in action, let’s modify main to accept only two requests
before gracefully shutting down the server, as shown in Listing 21-25.
use hello::ThreadPool;
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming().take(2) {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
println!("Shutting down.");
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
You wouldn’t want a real-world web server to shut down after serving only two requests. This code just demonstrates that the graceful shutdown and cleanup is in working order.
The take method is defined in the Iterator trait and limits the iteration
to the first two items at most. The ThreadPool will go out of scope at the
end of main, and the drop implementation will run.
Start the server with cargo run and make three requests. The third request
should error, and in your terminal, you should see output similar to this:
$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `target/debug/hello`
Worker 0 got a job; executing.
Shutting down.
Shutting down worker 0
Worker 3 got a job; executing.
Worker 1 disconnected; shutting down.
Worker 2 disconnected; shutting down.
Worker 3 disconnected; shutting down.
Worker 0 disconnected; shutting down.
Shutting down worker 1
Shutting down worker 2
Shutting down worker 3
You might see a different ordering of Worker IDs and messages printed. We can
see how this code works from the messages: Worker instances 0 and 3 got the
first two requests. The server stopped accepting connections after the second
connection, and the Drop implementation on ThreadPool starts executing
before Worker 3 even starts its job. Dropping the sender disconnects all the
Worker instances and tells them to shut down. The Worker instances each
print a message when they disconnect, and then the thread pool calls join to
wait for each Worker thread to finish.
Notice one interesting aspect of this particular execution: The ThreadPool
dropped the sender, and before any Worker received an error, we tried to
join Worker 0. Worker 0 had not yet gotten an error from recv, so the main
thread blocked, waiting for Worker 0 to finish. In the meantime, Worker 3
received a job and then all threads received an error. When Worker 0 finished,
the main thread waited for the rest of the Worker instances to finish. At that
point, they had all exited their loops and stopped.
Congrats! We’ve now completed our project; we have a basic web server that uses a thread pool to respond asynchronously. We’re able to perform a graceful shutdown of the server, which cleans up all the threads in the pool.
Here’s the full code for reference:
use hello::ThreadPool;
use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
let pool = ThreadPool::new(4);
for stream in listener.incoming().take(2) {
let stream = stream.unwrap();
pool.execute(|| {
handle_connection(stream);
});
}
println!("Shutting down.");
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
let (status_line, filename) = match &request_line[..] {
"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),
"GET /sleep HTTP/1.1" => {
thread::sleep(Duration::from_secs(5));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}
use std::{
sync::{Arc, Mutex, mpsc},
thread,
};
pub struct ThreadPool {
workers: Vec<Worker>,
sender: Option<mpsc::Sender<Job>>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {
/// Create a new ThreadPool.
///
/// The size is the number of threads in the pool.
///
/// # Panics
///
/// The `new` function will panic if the size is zero.
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool {
workers,
sender: Some(sender),
}
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.as_ref().unwrap().send(job).unwrap();
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
drop(self.sender.take());
for worker in &mut self.workers {
println!("Shutting down worker {}", worker.id);
if let Some(thread) = worker.thread.take() {
thread.join().unwrap();
}
}
}
}
struct Worker {
id: usize,
thread: Option<thread::JoinHandle<()>>,
}
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || {
loop {
let message = receiver.lock().unwrap().recv();
match message {
Ok(job) => {
println!("Worker {id} got a job; executing.");
job();
}
Err(_) => {
println!("Worker {id} disconnected; shutting down.");
break;
}
}
}
});
Worker {
id,
thread: Some(thread),
}
}
}
We could do more here! If you want to continue enhancing this project, here are some ideas:
- Add more documentation to
ThreadPooland its public methods. - Add tests of the library’s functionality.
- Change calls to
unwrapto more robust error handling. - Use
ThreadPoolto perform some task other than serving web requests. - Find a thread pool crate on crates.io and implement a similar web server using the crate instead. Then, compare its API and robustness to the thread pool we implemented.
Summary
Well done! You’ve made it to the end of the book! We want to thank you for joining us on this tour of Rust. You’re now ready to implement your own Rust projects and help with other people’s projects. Keep in mind that there is a welcoming community of other Rustaceans who would love to help you with any challenges you encounter on your Rust journey.
Appendix
The following sections contain reference material you may find useful in your Rust journey.
A - Keyword
Appendix A: Keywords
The following lists contain keywords that are reserved for current or future use by the Rust language. As such, they cannot be used as identifiers (except as raw identifiers, as we discuss in the “Raw Identifiers” section). Identifiers are names of functions, variables, parameters, struct fields, modules, crates, constants, macros, static values, attributes, types, traits, or lifetimes.
Keywords Currently in Use
The following is a list of keywords currently in use, with their functionality described.
as: Perform primitive casting, disambiguate the specific trait containing an item, or rename items inusestatements.async: Return aFutureinstead of blocking the current thread.await: Suspend execution until the result of aFutureis ready.break: Exit a loop immediately.const: Define constant items or constant raw pointers.continue: Continue to the next loop iteration.crate: In a module path, refers to the crate root.dyn: Dynamic dispatch to a trait object.else: Fallback forifandif letcontrol flow constructs.enum: Define an enumeration.extern: Link an external function or variable.false: Boolean false literal.fn: Define a function or the function pointer type.for: Loop over items from an iterator, implement a trait, or specify a higher ranked lifetime.if: Branch based on the result of a conditional expression.impl: Implement inherent or trait functionality.in: Part offorloop syntax.let: Bind a variable.loop: Loop unconditionally.match: Match a value to patterns.mod: Define a module.move: Make a closure take ownership of all its captures.mut: Denote mutability in references, raw pointers, or pattern bindings.pub: Denote public visibility in struct fields,implblocks, or modules.ref: Bind by reference.return: Return from function.Self: A type alias for the type we are defining or implementing.self: Method subject or current module.static: Global variable or lifetime lasting the entire program execution.struct: Define a structure.super: Parent module of the current module.trait: Define a trait.true: Boolean true literal.type: Define a type alias or associated type.union: Define a union; is a keyword only when used in a union declaration.unsafe: Denote unsafe code, functions, traits, or implementations.use: Bring symbols into scope.where: Denote clauses that constrain a type.while: Loop conditionally based on the result of an expression.
Keywords Reserved for Future Use
The following keywords do not yet have any functionality but are reserved by Rust for potential future use:
abstractbecomeboxdofinalgenmacrooverrideprivtrytypeofunsizedvirtualyield
Raw Identifiers
Raw identifiers are the syntax that lets you use keywords where they wouldn’t
normally be allowed. You use a raw identifier by prefixing a keyword with r#.
For example, match is a keyword. If you try to compile the following function
that uses match as its name:
Filename: src/main.rs
fn match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
you’ll get this error:
error: expected identifier, found keyword `match`
--> src/main.rs:4:4
|
4 | fn match(needle: &str, haystack: &str) -> bool {
| ^^^^^ expected identifier, found keyword
The error shows that you can’t use the keyword match as the function
identifier. To use match as a function name, you need to use the raw
identifier syntax, like this:
Filename: src/main.rs
fn r#match(needle: &str, haystack: &str) -> bool {
haystack.contains(needle)
}
fn main() {
assert!(r#match("foo", "foobar"));
}
This code will compile without any errors. Note the r# prefix on the function
name in its definition as well as where the function is called in main.
Raw identifiers allow you to use any word you choose as an identifier, even if
that word happens to be a reserved keyword. This gives us more freedom to choose
identifier names, as well as lets us integrate with programs written in a
language where these words aren’t keywords. In addition, raw identifiers allow
you to use libraries written in a different Rust edition than your crate uses.
For example, try isn’t a keyword in the 2015 edition but is in the 2018, 2021,
and 2024 editions. If you depend on a library that is written using the 2015
edition and has a try function, you’ll need to use the raw identifier syntax,
r#try in this case, to call that function from your code on later editions.
See Appendix E for more information on editions.
B - Operator และสัญลักษณ์
Appendix B: Operators and Symbols
This appendix contains a glossary of Rust’s syntax, including operators and other symbols that appear by themselves or in the context of paths, generics, trait bounds, macros, attributes, comments, tuples, and brackets.
Operators
Table B-1 contains the operators in Rust, an example of how the operator would appear in context, a short explanation, and whether that operator is overloadable. If an operator is overloadable, the relevant trait to use to overload that operator is listed.
Table B-1: Operators
| Operator | Example | Explanation | Overloadable? |
|---|---|---|---|
! | ident!(...), ident!{...}, ident![...] | Macro expansion | |
! | !expr | Bitwise or logical complement | Not |
!= | expr != expr | Nonequality comparison | PartialEq |
% | expr % expr | Arithmetic remainder | Rem |
%= | var %= expr | Arithmetic remainder and assignment | RemAssign |
& | &expr, &mut expr | Borrow | |
& | &type, &mut type, &'a type, &'a mut type | Borrowed pointer type | |
& | expr & expr | Bitwise AND | BitAnd |
&= | var &= expr | Bitwise AND and assignment | BitAndAssign |
&& | expr && expr | Short-circuiting logical AND | |
* | expr * expr | Arithmetic multiplication | Mul |
*= | var *= expr | Arithmetic multiplication and assignment | MulAssign |
* | *expr | Dereference | Deref |
* | *const type, *mut type | Raw pointer | |
+ | trait + trait, 'a + trait | Compound type constraint | |
+ | expr + expr | Arithmetic addition | Add |
+= | var += expr | Arithmetic addition and assignment | AddAssign |
, | expr, expr | Argument and element separator | |
- | - expr | Arithmetic negation | Neg |
- | expr - expr | Arithmetic subtraction | Sub |
-= | var -= expr | Arithmetic subtraction and assignment | SubAssign |
-> | fn(...) -> type, |…| -> type | Function and closure return type | |
. | expr.ident | Field access | |
. | expr.ident(expr, ...) | Method call | |
. | expr.0, expr.1, and so on | Tuple indexing | |
.. | .., expr.., ..expr, expr..expr | Right-exclusive range literal | PartialOrd |
..= | ..=expr, expr..=expr | Right-inclusive range literal | PartialOrd |
.. | ..expr | Struct literal update syntax | |
.. | variant(x, ..), struct_type { x, .. } | “And the rest” pattern binding | |
... | expr...expr | (Deprecated, use ..= instead) In a pattern: inclusive range pattern | |
/ | expr / expr | Arithmetic division | Div |
/= | var /= expr | Arithmetic division and assignment | DivAssign |
: | pat: type, ident: type | Constraints | |
: | ident: expr | Struct field initializer | |
: | 'a: loop {...} | Loop label | |
; | expr; | Statement and item terminator | |
; | [...; len] | Part of fixed-size array syntax | |
<< | expr << expr | Left-shift | Shl |
<<= | var <<= expr | Left-shift and assignment | ShlAssign |
< | expr < expr | Less than comparison | PartialOrd |
<= | expr <= expr | Less than or equal to comparison | PartialOrd |
= | var = expr, ident = type | Assignment/equivalence | |
== | expr == expr | Equality comparison | PartialEq |
=> | pat => expr | Part of match arm syntax | |
> | expr > expr | Greater than comparison | PartialOrd |
>= | expr >= expr | Greater than or equal to comparison | PartialOrd |
>> | expr >> expr | Right-shift | Shr |
>>= | var >>= expr | Right-shift and assignment | ShrAssign |
@ | ident @ pat | Pattern binding | |
^ | expr ^ expr | Bitwise exclusive OR | BitXor |
^= | var ^= expr | Bitwise exclusive OR and assignment | BitXorAssign |
| | pat | pat | Pattern alternatives | |
| | expr | expr | Bitwise OR | BitOr |
|= | var |= expr | Bitwise OR and assignment | BitOrAssign |
|| | expr || expr | Short-circuiting logical OR | |
? | expr? | Error propagation |
Non-operator Symbols
The following tables contain all symbols that don’t function as operators; that is, they don’t behave like a function or method call.
Table B-2 shows symbols that appear on their own and are valid in a variety of locations.
Table B-2: Stand-alone Syntax
| Symbol | Explanation |
|---|---|
'ident | Named lifetime or loop label |
Digits immediately followed by u8, i32, f64, usize, and so on | Numeric literal of specific type |
"..." | String literal |
r"...", r#"..."#, r##"..."##, and so on | Raw string literal; escape characters not processed |
b"..." | Byte string literal; constructs an array of bytes instead of a string |
br"...", br#"..."#, br##"..."##, and so on | Raw byte string literal; combination of raw and byte string literal |
'...' | Character literal |
b'...' | ASCII byte literal |
|…| expr | Closure |
! | Always-empty bottom type for diverging functions |
_ | “Ignored” pattern binding; also used to make integer literals readable |
Table B-3 shows symbols that appear in the context of a path through the module hierarchy to an item.
Table B-3: Path-Related Syntax
| Symbol | Explanation |
|---|---|
ident::ident | Namespace path |
::path | Path relative to the crate root (that is, an explicitly absolute path) |
self::path | Path relative to the current module (that is, an explicitly relative path) |
super::path | Path relative to the parent of the current module |
type::ident, <type as trait>::ident | Associated constants, functions, and types |
<type>::... | Associated item for a type that cannot be directly named (for example, <&T>::..., <[T]>::..., and so on) |
trait::method(...) | Disambiguating a method call by naming the trait that defines it |
type::method(...) | Disambiguating a method call by naming the type for which it’s defined |
<type as trait>::method(...) | Disambiguating a method call by naming the trait and type |
Table B-4 shows symbols that appear in the context of using generic type parameters.
Table B-4: Generics
| Symbol | Explanation |
|---|---|
path<...> | Specifies parameters to a generic type in a type (for example, Vec<u8>) |
path::<...>, method::<...> | Specifies parameters to a generic type, function, or method in an expression; often referred to as turbofish (for example, "42".parse::<i32>()) |
fn ident<...> ... | Define generic function |
struct ident<...> ... | Define generic structure |
enum ident<...> ... | Define generic enumeration |
impl<...> ... | Define generic implementation |
for<...> type | Higher ranked lifetime bounds |
type<ident=type> | A generic type where one or more associated types have specific assignments (for example, Iterator<Item=T>) |
Table B-5 shows symbols that appear in the context of constraining generic type parameters with trait bounds.
Table B-5: Trait Bound Constraints
| Symbol | Explanation |
|---|---|
T: U | Generic parameter T constrained to types that implement U |
T: 'a | Generic type T must outlive lifetime 'a (meaning the type cannot transitively contain any references with lifetimes shorter than 'a) |
T: 'static | Generic type T contains no borrowed references other than 'static ones |
'b: 'a | Generic lifetime 'b must outlive lifetime 'a |
T: ?Sized | Allow generic type parameter to be a dynamically sized type |
'a + trait, trait + trait | Compound type constraint |
Table B-6 shows symbols that appear in the context of calling or defining macros and specifying attributes on an item.
Table B-6: Macros and Attributes
| Symbol | Explanation |
|---|---|
#[meta] | Outer attribute |
#![meta] | Inner attribute |
$ident | Macro substitution |
$ident:kind | Macro metavariable |
$(...)... | Macro repetition |
ident!(...), ident!{...}, ident![...] | Macro invocation |
Table B-7 shows symbols that create comments.
Table B-7: Comments
| Symbol | Explanation |
|---|---|
// | Line comment |
//! | Inner line doc comment |
/// | Outer line doc comment |
/*...*/ | Block comment |
/*!...*/ | Inner block doc comment |
/**...*/ | Outer block doc comment |
Table B-8 shows the contexts in which parentheses are used.
Table B-8: Parentheses
| Symbol | Explanation |
|---|---|
() | Empty tuple (aka unit), both literal and type |
(expr) | Parenthesized expression |
(expr,) | Single-element tuple expression |
(type,) | Single-element tuple type |
(expr, ...) | Tuple expression |
(type, ...) | Tuple type |
expr(expr, ...) | Function call expression; also used to initialize tuple structs and tuple enum variants |
Table B-9 shows the contexts in which curly brackets are used.
Table B-9: Curly Brackets
| Context | Explanation |
|---|---|
{...} | Block expression |
Type {...} | Struct literal |
Table B-10 shows the contexts in which square brackets are used.
Table B-10: Square Brackets
| Context | Explanation |
|---|---|
[...] | Array literal |
[expr; len] | Array literal containing len copies of expr |
[type; len] | Array type containing len instances of type |
expr[expr] | Collection indexing; overloadable (Index, IndexMut) |
expr[..], expr[a..], expr[..b], expr[a..b] | Collection indexing pretending to be collection slicing, using Range, RangeFrom, RangeTo, or RangeFull as the “index” |
C - Derivable Trait
Appendix C: Derivable Traits
In various places in the book, we’ve discussed the derive attribute, which
you can apply to a struct or enum definition. The derive attribute generates
code that will implement a trait with its own default implementation on the
type you’ve annotated with the derive syntax.
In this appendix, we provide a reference of all the traits in the standard
library that you can use with derive. Each section covers:
- What operators and methods deriving this trait will enable
- What the implementation of the trait provided by
derivedoes - What implementing the trait signifies about the type
- The conditions in which you’re allowed or not allowed to implement the trait
- Examples of operations that require the trait
If you want different behavior from that provided by the derive attribute,
consult the standard library documentation
for each trait for details on how to manually implement them.
The traits listed here are the only ones defined by the standard library that
can be implemented on your types using derive. Other traits defined in the
standard library don’t have sensible default behavior, so it’s up to you to
implement them in the way that makes sense for what you’re trying to accomplish.
An example of a trait that can’t be derived is Display, which handles
formatting for end users. You should always consider the appropriate way to
display a type to an end user. What parts of the type should an end user be
allowed to see? What parts would they find relevant? What format of the data
would be most relevant to them? The Rust compiler doesn’t have this insight, so
it can’t provide appropriate default behavior for you.
The list of derivable traits provided in this appendix is not comprehensive:
Libraries can implement derive for their own traits, making the list of
traits you can use derive with truly open ended. Implementing derive
involves using a procedural macro, which is covered in the “Custom derive
Macros” section in Chapter 20.
Debug for Programmer Output
The Debug trait enables debug formatting in format strings, which you
indicate by adding :? within {} placeholders.
The Debug trait allows you to print instances of a type for debugging
purposes, so you and other programmers using your type can inspect an instance
at a particular point in a program’s execution.
The Debug trait is required, for example, in the use of the assert_eq!
macro. This macro prints the values of instances given as arguments if the
equality assertion fails so that programmers can see why the two instances
weren’t equal.
PartialEq and Eq for Equality Comparisons
The PartialEq trait allows you to compare instances of a type to check for
equality and enables use of the == and != operators.
Deriving PartialEq implements the eq method. When PartialEq is derived on
structs, two instances are equal only if all fields are equal, and the
instances are not equal if any fields are not equal. When derived on enums,
each variant is equal to itself and not equal to the other variants.
The PartialEq trait is required, for example, with the use of the
assert_eq! macro, which needs to be able to compare two instances of a type
for equality.
The Eq trait has no methods. Its purpose is to signal that for every value of
the annotated type, the value is equal to itself. The Eq trait can only be
applied to types that also implement PartialEq, although not all types that
implement PartialEq can implement Eq. One example of this is floating-point
number types: The implementation of floating-point numbers states that two
instances of the not-a-number (NaN) value are not equal to each other.
An example of when Eq is required is for keys in a HashMap<K, V> so that
the HashMap<K, V> can tell whether two keys are the same.
PartialOrd and Ord for Ordering Comparisons
The PartialOrd trait allows you to compare instances of a type for sorting
purposes. A type that implements PartialOrd can be used with the <, >,
<=, and >= operators. You can only apply the PartialOrd trait to types
that also implement PartialEq.
Deriving PartialOrd implements the partial_cmp method, which returns an
Option<Ordering> that will be None when the values given don’t produce an
ordering. An example of a value that doesn’t produce an ordering, even though
most values of that type can be compared, is the NaN floating point value.
Calling partial_cmp with any floating-point number and the NaN
floating-point value will return None.
When derived on structs, PartialOrd compares two instances by comparing the
value in each field in the order in which the fields appear in the struct
definition. When derived on enums, variants of the enum declared earlier in the
enum definition are considered less than the variants listed later.
The PartialOrd trait is required, for example, for the gen_range method
from the rand crate that generates a random value in the range specified by a
range expression.
The Ord trait allows you to know that for any two values of the annotated
type, a valid ordering will exist. The Ord trait implements the cmp method,
which returns an Ordering rather than an Option<Ordering> because a valid
ordering will always be possible. You can only apply the Ord trait to types
that also implement PartialOrd and Eq (and Eq requires PartialEq). When
derived on structs and enums, cmp behaves the same way as the derived
implementation for partial_cmp does with PartialOrd.
An example of when Ord is required is when storing values in a BTreeSet<T>,
a data structure that stores data based on the sort order of the values.
Clone and Copy for Duplicating Values
The Clone trait allows you to explicitly create a deep copy of a value, and
the duplication process might involve running arbitrary code and copying heap
data. See the “Variables and Data Interacting with
Clone” section in
Chapter 4 for more information on Clone.
Deriving Clone implements the clone method, which when implemented for the
whole type, calls clone on each of the parts of the type. This means all the
fields or values in the type must also implement Clone to derive Clone.
An example of when Clone is required is when calling the to_vec method on a
slice. The slice doesn’t own the type instances it contains, but the vector
returned from to_vec will need to own its instances, so to_vec calls
clone on each item. Thus, the type stored in the slice must implement Clone.
The Copy trait allows you to duplicate a value by only copying bits stored on
the stack; no arbitrary code is necessary. See the “Stack-Only Data:
Copy” section in Chapter 4 for more
information on Copy.
The Copy trait doesn’t define any methods to prevent programmers from
overloading those methods and violating the assumption that no arbitrary code
is being run. That way, all programmers can assume that copying a value will be
very fast.
You can derive Copy on any type whose parts all implement Copy. A type that
implements Copy must also implement Clone because a type that implements
Copy has a trivial implementation of Clone that performs the same task as
Copy.
The Copy trait is rarely required; types that implement Copy have
optimizations available, meaning you don’t have to call clone, which makes
the code more concise.
Everything possible with Copy you can also accomplish with Clone, but the
code might be slower or have to use clone in places.
Hash for Mapping a Value to a Value of Fixed Size
The Hash trait allows you to take an instance of a type of arbitrary size and
map that instance to a value of fixed size using a hash function. Deriving
Hash implements the hash method. The derived implementation of the hash
method combines the result of calling hash on each of the parts of the type,
meaning all fields or values must also implement Hash to derive Hash.
An example of when Hash is required is in storing keys in a HashMap<K, V>
to store data efficiently.
Default for Default Values
The Default trait allows you to create a default value for a type. Deriving
Default implements the default function. The derived implementation of the
default function calls the default function on each part of the type,
meaning all fields or values in the type must also implement Default to
derive Default.
The Default::default function is commonly used in combination with the struct
update syntax discussed in the “Creating Instances from Other Instances with
Struct Update
Syntax” section in Chapter 5. You can customize a few fields of a struct and
then set and use a default value for the rest of the fields by using
..Default::default().
The Default trait is required when you use the method unwrap_or_default on
Option<T> instances, for example. If the Option<T> is None, the method
unwrap_or_default will return the result of Default::default for the type
T stored in the Option<T>.
D - เครื่องมือพัฒนาที่มีประโยชน์
Appendix D: Useful Development Tools
In this appendix, we talk about some useful development tools that the Rust project provides. We’ll look at automatic formatting, quick ways to apply warning fixes, a linter, and integrating with IDEs.
Automatic Formatting with rustfmt
The rustfmt tool reformats your code according to the community code style.
Many collaborative projects use rustfmt to prevent arguments about which
style to use when writing Rust: Everyone formats their code using the tool.
Rust installations include rustfmt by default, so you should already have the
programs rustfmt and cargo-fmt on your system. These two commands are
analogous to rustc and cargo in that rustfmt allows finer grained control
and cargo-fmt understands conventions of a project that uses Cargo. To format
any Cargo project, enter the following:
$ cargo fmt
Running this command reformats all the Rust code in the current crate. This
should only change the code style, not the code semantics. For more information
on rustfmt, see its documentation.
Fix Your Code with rustfix
The rustfix tool is included with Rust installations and can automatically
fix compiler warnings that have a clear way to correct the problem that’s
likely what you want. You’ve probably seen compiler warnings before. For
example, consider this code:
Filename: src/main.rs
fn main() {
let mut x = 42;
println!("{x}");
}
Here, we’re defining the variable x as mutable, but we never actually mutate
it. Rust warns us about that:
$ cargo build
Compiling myprogram v0.1.0 (file:///projects/myprogram)
warning: variable does not need to be mutable
--> src/main.rs:2:9
|
2 | let mut x = 0;
| ----^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` on by default
The warning suggests that we remove the mut keyword. We can automatically
apply that suggestion using the rustfix tool by running the command cargo fix:
$ cargo fix
Checking myprogram v0.1.0 (file:///projects/myprogram)
Fixing src/main.rs (1 fix)
Finished dev [unoptimized + debuginfo] target(s) in 0.59s
When we look at src/main.rs again, we’ll see that cargo fix has changed the
code:
Filename: src/main.rs
fn main() {
let x = 42;
println!("{x}");
}
The variable x is now immutable, and the warning no longer appears.
You can also use the cargo fix command to transition your code between
different Rust editions. Editions are covered in Appendix E.
More Lints with Clippy
The Clippy tool is a collection of lints to analyze your code so that you can catch common mistakes and improve your Rust code. Clippy is included with standard Rust installations.
To run Clippy’s lints on any Cargo project, enter the following:
$ cargo clippy
For example, say you write a program that uses an approximation of a mathematical constant, such as pi, as this program does:
fn main() {
let x = 3.1415;
let r = 8.0;
println!("the area of the circle is {}", x * r * r);
}
Running cargo clippy on this project results in this error:
error: approximate value of `f{32, 64}::consts::PI` found
--> src/main.rs:2:13
|
2 | let x = 3.1415;
| ^^^^^^
|
= note: `#[deny(clippy::approx_constant)]` on by default
= help: consider using the constant directly
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#approx_constant
This error lets you know that Rust already has a more precise PI constant
defined, and that your program would be more correct if you used the constant
instead. You would then change your code to use the PI constant.
The following code doesn’t result in any errors or warnings from Clippy:
fn main() {
let x = std::f64::consts::PI;
let r = 8.0;
println!("the area of the circle is {}", x * r * r);
}
For more information on Clippy, see its documentation.
IDE Integration Using rust-analyzer
To help with IDE integration, the Rust community recommends using
rust-analyzer. This tool is a set of
compiler-centric utilities that speak Language Server Protocol, which is a specification for IDEs and programming languages to
communicate with each other. Different clients can use rust-analyzer, such as
the Rust analyzer plug-in for Visual Studio Code.
Visit the rust-analyzer project’s home page
for installation instructions, then install the language server support in your
particular IDE. Your IDE will gain capabilities such as autocompletion, jump to
definition, and inline errors.
E - Edition
Appendix E: Editions
In Chapter 1, you saw that cargo new adds a bit of metadata to your
Cargo.toml file about an edition. This appendix talks about what that means!
The Rust language and compiler have a six-week release cycle, meaning users get a constant stream of new features. Other programming languages release larger changes less often; Rust releases smaller updates more frequently. After a while, all of these tiny changes add up. But from release to release, it can be difficult to look back and say, “Wow, between Rust 1.10 and Rust 1.31, Rust has changed a lot!”
Every three years or so, the Rust team produces a new Rust edition. Each edition brings together the features that have landed into a clear package with fully updated documentation and tooling. New editions ship as part of the usual six-week release process.
Editions serve different purposes for different people:
- For active Rust users, a new edition brings together incremental changes into an easy-to-understand package.
- For non-users, a new edition signals that some major advancements have landed, which might make Rust worth another look.
- For those developing Rust, a new edition provides a rallying point for the project as a whole.
At the time of this writing, four Rust editions are available: Rust 2015, Rust 2018, Rust 2021, and Rust 2024. This book is written using Rust 2024 edition idioms.
The edition key in Cargo.toml indicates which edition the compiler should
use for your code. If the key doesn’t exist, Rust uses 2015 as the edition
value for backward compatibility reasons.
Each project can opt in to an edition other than the default 2015 edition. Editions can contain incompatible changes, such as including a new keyword that conflicts with identifiers in code. However, unless you opt in to those changes, your code will continue to compile even as you upgrade the Rust compiler version you use.
All Rust compiler versions support any edition that existed prior to that compiler’s release, and they can link crates of any supported editions together. Edition changes only affect the way the compiler initially parses code. Therefore, if you’re using Rust 2015 and one of your dependencies uses Rust 2018, your project will compile and be able to use that dependency. The opposite situation, where your project uses Rust 2018 and a dependency uses Rust 2015, works as well.
To be clear: Most features will be available on all editions. Developers using any Rust edition will continue to see improvements as new stable releases are made. However, in some cases, mainly when new keywords are added, some new features might only be available in later editions. You will need to switch editions if you want to take advantage of such features.
For more details, see The Rust Edition Guide. This is a
complete book that enumerates the differences between editions and explains how
to automatically upgrade your code to a new edition via cargo fix.
F - ฉบับแปลของหนังสือ
Appendix F: Translations of the Book
For resources in languages other than English. Most are still in progress; see the Translations label to help or let us know about a new translation!
- Português (BR)
- Português (PT)
- 简体中文: KaiserY/trpl-zh-cn, gnu4cn/rust-lang-Zh_CN
- 正體中文
- Українська
- Español, alternate, Español por RustLangES
- Русский
- 한국어
- 日本語
- Français
- Polski
- Cebuano
- Tagalog
- Esperanto
- ελληνική
- Svenska
- Farsi, Persian (FA)
- Deutsch
- हिंदी
- ไทย
- Danske
- O’zbek
- Tiếng Việt
- Italiano
- বাংলা
G - วิธีพัฒนา Rust และ "Nightly Rust"
Appendix G - How Rust is Made and “Nightly Rust”
This appendix is about how Rust is made and how that affects you as a Rust developer.
Stability Without Stagnation
As a language, Rust cares a lot about the stability of your code. We want Rust to be a rock-solid foundation you can build on, and if things were constantly changing, that would be impossible. At the same time, if we can’t experiment with new features, we may not find out important flaws until after their release, when we can no longer change things.
Our solution to this problem is what we call “stability without stagnation”, and our guiding principle is this: you should never have to fear upgrading to a new version of stable Rust. Each upgrade should be painless, but should also bring you new features, fewer bugs, and faster compile times.
Choo, Choo! Release Channels and Riding the Trains
Rust development operates on a train schedule. That is, all development is done in the main branch of the Rust repository. Releases follow a software release train model, which has been used by Cisco IOS and other software projects. There are three release channels for Rust:
- Nightly
- Beta
- Stable
Most Rust developers primarily use the stable channel, but those who want to try out experimental new features may use nightly or beta.
Here’s an example of how the development and release process works: let’s assume that the Rust team is working on the release of Rust 1.5. That release happened in December of 2015, but it will provide us with realistic version numbers. A new feature is added to Rust: a new commit lands on the main branch. Each night, a new nightly version of Rust is produced. Every day is a release day, and these releases are created by our release infrastructure automatically. So as time passes, our releases look like this, once a night:
nightly: * - - * - - *
Every six weeks, it’s time to prepare a new release! The beta branch of the
Rust repository branches off from the main branch used by nightly. Now,
there are two releases:
nightly: * - - * - - *
|
beta: *
Most Rust users do not use beta releases actively, but test against beta in their CI system to help Rust discover possible regressions. In the meantime, there’s still a nightly release every night:
nightly: * - - * - - * - - * - - *
|
beta: *
Let’s say a regression is found. Good thing we had some time to test the beta
release before the regression snuck into a stable release! The fix is applied
to the main branch, so that nightly is fixed, and then the fix is backported to
the beta branch, and a new release of beta is produced:
nightly: * - - * - - * - - * - - * - - *
|
beta: * - - - - - - - - *
Six weeks after the first beta was created, it’s time for a stable release! The
stable branch is produced from the beta branch:
nightly: * - - * - - * - - * - - * - - * - * - *
|
beta: * - - - - - - - - *
|
stable: *
Hooray! Rust 1.5 is done! However, we’ve forgotten one thing: because the six
weeks have gone by, we also need a new beta of the next version of Rust, 1.6.
So after stable branches off of beta, the next version of beta branches
off of nightly again:
nightly: * - - * - - * - - * - - * - - * - * - *
| |
beta: * - - - - - - - - * *
|
stable: *
This is called the “train model” because every six weeks, a release “leaves the station”, but still has to take a journey through the beta channel before it arrives as a stable release.
Rust releases every six weeks, like clockwork. If you know the date of one Rust release, you can know the date of the next one: it’s six weeks later. A nice aspect of having releases scheduled every six weeks is that the next train is coming soon. If a feature happens to miss a particular release, there’s no need to worry: another one is happening in a short time! This helps reduce pressure to sneak possibly unpolished features in close to the release deadline.
Thanks to this process, you can always check out the next build of Rust and
verify for yourself that it’s easy to upgrade to: if a beta release doesn’t
work as expected, you can report it to the team and get it fixed before the
next stable release happens! Breakage in a beta release is relatively rare, but
rustc is still a piece of software, and bugs do exist.
Maintenance time
The Rust project supports the most recent stable version. When a new stable version is released, the old version reaches its end of life (EOL). This means each version is supported for six weeks.
Unstable Features
There’s one more catch with this release model: unstable features. Rust uses a technique called “feature flags” to determine what features are enabled in a given release. If a new feature is under active development, it lands on the main branch, and therefore, in nightly, but behind a feature flag. If you, as a user, wish to try out the work-in-progress feature, you can, but you must be using a nightly release of Rust and annotate your source code with the appropriate flag to opt in.
If you’re using a beta or stable release of Rust, you can’t use any feature flags. This is the key that allows us to get practical use with new features before we declare them stable forever. Those who wish to opt into the bleeding edge can do so, and those who want a rock-solid experience can stick with stable and know that their code won’t break. Stability without stagnation.
This book only contains information about stable features, as in-progress features are still changing, and surely they’ll be different between when this book was written and when they get enabled in stable builds. You can find documentation for nightly-only features online.
Rustup and the Role of Rust Nightly
Rustup makes it easy to change between different release channels of Rust, on a global or per-project basis. By default, you’ll have stable Rust installed. To install nightly, for example:
$ rustup toolchain install nightly
You can see all of the toolchains (releases of Rust and associated
components) you have installed with rustup as well. Here’s an example on one
of your authors’ Windows computer:
> rustup toolchain list
stable-x86_64-pc-windows-msvc (default)
beta-x86_64-pc-windows-msvc
nightly-x86_64-pc-windows-msvc
As you can see, the stable toolchain is the default. Most Rust users use stable
most of the time. You might want to use stable most of the time, but use
nightly on a specific project, because you care about a cutting-edge feature.
To do so, you can use rustup override in that project’s directory to set the
nightly toolchain as the one rustup should use when you’re in that directory:
$ cd ~/projects/needs-nightly
$ rustup override set nightly
Now, every time you call rustc or cargo inside of
~/projects/needs-nightly, rustup will make sure that you are using nightly
Rust, rather than your default of stable Rust. This comes in handy when you
have a lot of Rust projects!
The RFC Process and Teams
So how do you learn about these new features? Rust’s development model follows a Request For Comments (RFC) process. If you’d like an improvement in Rust, you can write up a proposal, called an RFC.
Anyone can write RFCs to improve Rust, and the proposals are reviewed and discussed by the Rust team, which is comprised of many topic subteams. There’s a full list of the teams on Rust’s website, which includes teams for each area of the project: language design, compiler implementation, infrastructure, documentation, and more. The appropriate team reads the proposal and the comments, writes some comments of their own, and eventually, there’s consensus to accept or reject the feature.
If the feature is accepted, an issue is opened on the Rust repository, and someone can implement it. The person who implements it very well may not be the person who proposed the feature in the first place! When the implementation is ready, it lands on the main branch behind a feature gate, as we discussed in the “Unstable Features” section.
After some time, once Rust developers who use nightly releases have been able to try out the new feature, team members will discuss the feature, how it’s worked out on nightly, and decide if it should make it into stable Rust or not. If the decision is to move forward, the feature gate is removed, and the feature is now considered stable! It rides the trains into a new stable release of Rust.