การกำหนดพฤติกรรมร่วมด้วยทริต (Defining Shared Behavior with Traits)
ทริต (trait) จะทำหน้าที่กำหนดฟังก์ชันการทำงานที่ประเภทข้อมูลใดประเภทข้อมูลหนึ่งมี และสามารถแบ่งปันให้กับประเภทข้อมูลอื่น ๆ ได้ด้วย เราสามารถประยุกต์ใช้งานทริตเพื่อเขียนกำหนดพฤติกรรมร่วม (shared behavior) ในรูปแบบเชิงนามธรรมได้ และเราสามารถใช้ ขอบเขตของทริต (trait bounds) เพื่อระบุเฉพาะเจาะจงว่าประเภทข้อมูลเจเนอริกนั้นสามารถเป็นประเภทข้อมูลใด ๆ ก็ได้ที่มีพฤติกรรมบางประการตามข้อกำหนด
หมายเหตุ: ทริตมีคุณลักษณะการทำงานที่คล้ายคลึงกับคุณสมบัติที่มักเรียกกันว่า อินเทอร์เฟซ (interfaces) ในภาษาอื่น ๆ แม้ว่าจะมีจุดแตกต่างกันอยู่บ้างบางส่วนก็ตาม
การนิยามทริต (Defining a Trait)
พฤติกรรมของประเภทข้อมูลจะประกอบไปด้วยเมธอดต่าง ๆ ที่เราสามารถสั่งเรียกใช้งานบนประเภทข้อมูลนั้นได้ ประเภทข้อมูลที่ต่างกันจะถือว่ามีพฤติกรรมร่วมกันหากเราสามารถสั่งเรียกใช้งานเมธอดเดียวกันบนประเภทข้อมูลเหล่านั้นได้ทั้งหมด คำนิยามของทริตจึงเป็นแนวทางในการนำซิกเนเจอร์ของเมธอดต่าง ๆ มารวมกลุ่มเข้าไว้ด้วยกันเพื่อกำหนดชุดพฤติกรรมที่จำเป็นสำหรับการบรรลุวัตถุประสงค์บางประการ
ตัวอย่างเช่น สมมติว่าเรามี struct หลายตัวที่ทำหน้าที่เก็บข้อมูลข้อความประเภทและขนาดที่ต่างกัน: ได้แก่ struct NewsArticle ที่ใช้จัดเก็บเนื้อข่าวที่รายงานจากพิกัดตำแหน่งใดพิกัดหนึ่ง และ struct SocialPost ที่เก็บข้อมูลตัวอักษรได้ไม่เกิน 280 ตัวอักษร พร้อมทั้งมีข้อมูลเมทาดาตา (metadata) บ่งบอกสถานะว่าเป็นโพสต์ใหม่ โพสต์ซ้ำ หรือเป็นโพสต์ตอบกลับโพสต์อื่น
เราต้องการสร้างเครตประเภทไลบรารีสำหรับรวบรวมสื่อข่าวสาร (media aggregator library crate) ชื่อว่า aggregator ซึ่งพร้อมทำหน้าที่แสดงผลสรุปของข้อมูลที่อาจจะจัดเก็บอยู่ภายใต้อินสแตนซ์ของ NewsArticle หรือ SocialPost ในการทำเช่นนี้ เราต้องการสรุปความจากข้อมูลแต่ละประเภท และเราจะดึงข้อมูลสรุปดังกล่าวโดยสั่งเรียกเมธอด summarize บนอินสแตนซ์เหล่านั้น ตัวอย่างที่ 10-12 แสดงนิยามของทริตสาธารณะ Summary ที่แสดงถึงพฤติกรรมนี้
pub trait Summary {
fn summarize(&self) -> String;
}
ที่จุดนี้ เราประกาศสร้างทริตขึ้นมาโดยใช้คีย์เวิร์ด trait ตามด้วยชื่อของทริต ซึ่งในที่นี้คือ Summary นอกจากนี้เรายังประกาศระบุทริตนี้เป็นแบบสาธารณะ pub เพื่อเปิดโอกาสให้เครตส์ (crates) อื่นที่อ้างอิงเครตนี้สามารถดึงทริตไปเขียนใช้งานได้ด้วยดังที่จะแสดงในตัวอย่างถัด ๆ ไป ภายในเครื่องหมายวงเล็บปีกกา เราจะเขียนประกาศตัวแปรซิกเนเจอร์ของเมธอดต่าง ๆ เพื่อแสดงชุดพฤติกรรมของประเภทข้อมูลที่จะนำทริตนี้ไปสืบทอดใช้งาน ซึ่งในที่นี้คือคำสั่ง fn summarize(&self) -> String
ต่อท้ายซิกเนเจอร์ของเมธอด แทนที่เราจะเขียนรายละเอียดการทำงานภายในเครื่องหมายวงเล็บปีกกา เราจะปิดท้ายด้วยเครื่องหมายอัฒภาค (semicolon) ชนิดประเภทข้อมูลใด ๆ ที่นำทริตนี้ไปสืบทอดประยุกต์ใช้จะต้องเป็นฝ่ายเขียนนิยามรายละเอียดการทำงานในส่วนบอดี้ของเมธอดด้วยตนเอง ตัวคอมไพเลอร์จะคอยบังคับตรวจสอบให้มั่นใจว่าประเภทข้อมูลใด ๆ ที่สืบทอดทริต Summary จะต้องมีคำนิยามเมธอด summarize ที่มีโครงสร้างซิกเนเจอร์ตรงกับเงื่อนไขนี้อย่างถูกต้องเป๊ะ
ภายในโครงสร้างของทริตหนึ่งสามารถเก็บเมธอดได้หลายตัว โดยซิกเนเจอร์ของเมธอดแต่ละตัวจะเขียนแสดงแยกหนึ่งบรรทัดต่อหนึ่งตัวและปิดท้ายด้วยเครื่องหมาย semicolon เสมอ
การสืบทอดทริตให้กับประเภทข้อมูล (Implementing a Trait on a Type)
เมื่อเรานิยามซิกเนเจอร์ที่ต้องการให้กับเมธอดของทริต Summary เรียบร้อยแล้ว เราจะสามารถเริ่มสืบทอดให้กับประเภทข้อมูลต่าง ๆ ในโครงการ media aggregator ของเราได้ ตัวอย่างที่ 10-13 แสดงการสืบทอดทริต Summary ให้กับ struct NewsArticle โดยดึงหัวข้อข่าว ผู้เขียน และสถานที่มาร้อยเรียงสร้างเป็นข้อความสำหรับส่งคืนค่าจาก summarize ส่วนในฝั่งของ struct SocialPost เราจะนิยามเมธอด summarize ให้ส่งค่าเป็นชื่อผู้ใช้ตามด้วยเนื้อความของโพสต์ทั้งหมด โดยตั้งสมมติฐานว่าโพสต์ดังกล่าวถูกจำกัดความยาวไว้ไม่เกิน 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)
}
}
การสืบทอดทริตให้กับประเภทข้อมูลใด ๆ จะมีหลักการเขียนคล้ายกับการสืบทอดเมธอดทั่วไป จุดต่างคือ หลังคำสำคัญ impl เราจะต้องใส่ชื่อทริตที่เราต้องการนำมาสืบทอดใช้งาน จากนั้นพิมพ์คีย์เวิร์ด for แล้วจึงระบุชื่อประเภทข้อมูลที่เราต้องการสร้างทริตนี้ให้ ภายในบล็อกคำสั่ง impl เราจะนำซิกเนเจอร์ของเมธอดต่าง ๆ ที่ถูกประกาศไว้ในนิยามทริตมาใส่ แทนที่จะใช้เครื่องหมาย semicolon ปิดท้ายแต่ละซิกเนเจอร์ เราจะเปลี่ยนมาเขียนบล็อกวงเล็บปีกกาและเติมรายละเอียดเนื้อความบอดี้ของเมธอดเพื่อกำหนดพฤติกรรมการทำงานเฉพาะทางที่ประเภทข้อมูลนั้นต้องการนำเสนอสำหรับทริต
เมื่อตัวไลบรารีได้นำเสนอทริต Summary ให้แก่ NewsArticle และ SocialPost เรียบร้อยแล้ว ผู้ใช้งานเครตนี้จะสามารถเรียกใช้งานเมธอดต่าง ๆ ของทริตบนอินสแตนซ์ของ NewsArticle และ SocialPost ได้ทันทีแบบเดียวกับการเรียกใช้งานเมธอดปกติทั่วไป จุดต่างเพียงอย่างเดียวคือตัวผู้ใช้งานต้องทำการดึงเอาตัวแปรทริตเข้ามาไว้ในขอบเขตการใช้งาน (scope) นอกเหนือจากตัวแปรประเภทข้อมูล ตัวอย่างเช่น โค้ดของ 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
เครตอื่น ๆ ที่ทำงานขึ้นตรงต่อเครต aggregator สามารถดึงทริต Summary เข้ามาไว้ในขอบเขตการใช้งานเพื่อสืบทอด Summary ให้กับประเภทข้อมูลของตนเองได้เช่นเดียวกัน อย่างไรก็ตาม มีข้อจำกัดประการหนึ่งที่ต้องตระหนักไว้คือ เราสามารถเลือกเขียนสืบทอดทริตให้กับประเภทข้อมูลใด ๆ ได้ก็ต่อเมื่อ ทริตดังกล่าว หรือประเภทข้อมูลดังกล่าว หรือทั้งคู่ มีฐานะเป็นทรัพยากรภายใน (local) ของเครตเราเองเท่านั้น ตัวอย่างเช่น เราสามารถนำทริตของไลบรารีมาตรฐานอย่าง Display มาสืบทอดให้ประเภทข้อมูลเฉพาะอย่าง SocialPost ได้เพราะ struct SocialPost เป็นทรัพยากรภายในเครต aggregator ของเรา หรือเราอาจสืบทอดทริต Summary ให้แก่โครงสร้างข้อมูล Vec<T> ได้เช่นกันเพราะทริต Summary เป็นทรัพยากรท้องถิ่นในเครต aggregator ของเรา
แต่ทว่าเราไม่สามารถสืบทอดทริตภายนอก (external traits) ให้กับประเภทข้อมูลภายนอก (external types) ได้ ตัวอย่างเช่น เราไม่สามารถเขียนสืบทอดทริต Display ให้แก่ Vec<T> ภายในเครต aggregator ของเราได้ เนื่องจากทั้งทริต Display และ Vec<T> ต่างก็ถูกกำหนดไว้ในไลบรารีมาตรฐานและไม่ได้มีฐานะเป็นทรัพยากรภายในเครต aggregator ข้อจำกัดนี้เป็นส่วนหนึ่งของกฎเกณฑ์ที่เรียกว่า ความสอดคล้อง (coherence) หรือที่เรียกเฉพาะเจาะจงว่า กฎกำพร้า (orphan rule - ได้ชื่อนี้มาเพราะประเภทข้อมูลแม่ไม่ได้อยู่ในเครตเดียวกัน) กฎนี้ช่วยให้มั่นใจได้ว่าโค้ดของผู้อื่นจะไม่ไปสร้างผลกระทบต่อโค้ดของคุณ และโค้ดของคุณจะไม่ไปทำลายโค้ดของคนอื่น หากปราศจากกฎนี้ เครตสองตัวอาจจะเขียนสืบทอดทริตเดียวกันให้ประเภทข้อมูลชนิดเดียวกัน ซึ่งจะส่งผลให้ Rust ตัดสินใจเลือกไม่ถูกว่าควรจะเรียกใช้งานโค้ดของเครตใด
การใช้งานพฤติกรรมเริ่มต้นของทริต (Using Default Implementations)
ในบางกรณี การกำหนดพฤติกรรมการทำงานเริ่มต้น (default behavior) ให้แก่เมธอดบางส่วนหรือทั้งหมดในทริตจะมีประโยชน์อย่างยิ่ง เพื่อช่วยลดความจำเป็นที่ต้องเขียนนิยามรายละเอียดการทำงานของทุกเมธอดซ้ำ ๆ สำหรับทุกประเภทข้อมูล จากนั้นเมื่อเรานำทริตไปสืบทอดให้กับประเภทข้อมูลเฉพาะ เราจะเลือกใช้การทำงานเริ่มต้นนั้น หรือจะเขียนนิยามใหม่ทับ (override) พฤติกรรมเริ่มต้นของเมธอดนั้น ๆ ก็ได้ตามต้องการ
ในตัวอย่างที่ 10-14 เราได้ระบุค่าข้อความสตริงเริ่มต้นสำหรับเมธอด summarize ของทริต Summary แทนที่จะเขียนเฉพาะประกาศซิกเนเจอร์แบบเดิมในตัวอย่างที่ 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)
}
}
หากต้องการนำเอาการทำงานเริ่มต้นนี้ไปใช้สำหรับสรุปผลลัพธ์ของอินสแตนซ์ NewsArticle เราเพียงแค่ประกาศบล็อกคำสั่ง impl ที่ว่างเปล่าดังนี้ impl Summary for NewsArticle {}
แม้ว่าเราจะไม่ได้ระบุเขียนรายละเอียดการทำงานของเมธอด summarize บน NewsArticle โดยตรง แต่ทว่าเนื่องจากเราเตรียมโครงสร้างการทำงานเริ่มต้นไว้แล้วและระบุว่า NewsArticle มีการสืบทอดทริต Summary ส่งผลให้เรายังคงสามารถสั่งเรียกใช้เมธอด summarize บนอินสแตนซ์ของ 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...)
การจัดทำโครงสร้างพฤติกรรมเริ่มต้นนี้ไม่ได้ส่งผลให้เราต้องไปแก้ไขโค้ดการสืบทอดทริต Summary บน struct SocialPost ในตัวอย่างที่ 10-13 แต่อย่างใด เนื่องจากไวยากรณ์ที่ใช้สำหรับเขียนทับ (overriding) พฤติกรรมเริ่มต้นนั้นมีลักษณะแบบเดียวกันเป๊ะกับไวยากรณ์สำหรับนิยามการสืบทอดเมธอดที่ไม่มีค่าเริ่มต้นอยู่ก่อน
พฤติกรรมเริ่มต้นของเมธอดสามารถเข้าไปสั่งเรียกใช้งานเมธอดตัวอื่น ๆ ภายในทริตเดียวกันได้ แม้ว่าเมธอดตัวอื่นนั้นจะไม่ได้นิยามพฤติกรรมเริ่มต้นไว้ก็ตาม ด้วยคุณสมบัตินี้ ทริตจึงพร้อมมอบชุดคำสั่งการทำงานที่มีประโยชน์จำนวนมากและช่วยให้นักพัฒนาเขียนสืบทอดส่วนที่เหลือเพียงเล็กน้อยเท่านั้น ตัวอย่างเช่น เราสามารถนิยามทริต Summary โดยระบุว่าจำเป็นต้องมีการเขียนสืบทอดเมธอด summarize_author จากนั้นจึงเขียนสร้างเมธอด summarize ที่มีพฤติกรรมเริ่มต้นคอยสั่งเรียกใช้งานเมธอด 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)
}
}
ในการใช้งานทริต Summary เวอร์ชันนี้ เราจึงจำเป็นต้องระบุนิยามเฉพาะของเมธอด 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)
}
}
หลังจากที่เราเขียนนิยามเมธอด summarize_author เรียบร้อยแล้ว เราจะสามารถสั่งเรียกใช้ summarize บนอินสแตนซ์ของ struct SocialPost ได้ทันที และตัวระบบพฤติกรรมเริ่มต้นของ summarize จะทำหน้าที่สืบค้นเรียกใช้งานเมธอด summarize_author ที่เราเขียนส่งขึ้นไปให้โดยอัตโนมัติ การที่เราสืบทอด summarize_author จึงทำให้ทริต 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) จากภายในเมธอดเวอร์ชันที่เราเขียนคำสั่งทับ (override) ของเมธอดตัวเดียวกันนั้นได้
การใช้งานทริตเป็นพารามิเตอร์ (Using Traits as Parameters)
เมื่อคุณรู้วิธีการกำหนดและสืบทอดทริตแล้ว เราสามารถนำทริตมาประยุกต์ใช้ในการเขียนนิยามฟังก์ชันที่ยินดีต้อนรับประเภทข้อมูลที่หลากหลายได้ เราจะดึงทริต Summary ที่เราเคยเขียนสืบทอดให้กับประเภทข้อมูล NewsArticle และ SocialPost ในตัวอย่างที่ 10-13 มาใช้ในการนิยามฟังก์ชัน notify ซึ่งทำหน้าที่เรียกเมธอด summarize บนตัวแปรพารามิเตอร์ item ซึ่งเป็นประเภทข้อมูลใด ๆ ก็ตามที่สืบทอดทริต Summary ในการเขียนคำสั่งดังกล่าวเราจะหันมาใช้งานไวยากรณ์ 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());
}
แทนที่จะระบุประเภทข้อมูลรูปธรรมแบบคงที่ให้แก่พารามิเตอร์ item เราจะใส่คีย์เวิร์ด impl นำหน้าชื่อของทริตแทน พารามิเตอร์นี้จึงยอมรับประเภทข้อมูลใด ๆ ก็ตามที่สืบทอดทริตตามที่ระบุไว้ ภายในส่วนการประมวลผลของฟังก์ชัน notify เราจะมีสิทธิ์สั่งเรียกใช้เมธอดต่าง ๆ บนตัวแปร item ที่ผูกอยู่กับทริต Summary เช่น เมธอด summarize เราสามารถส่งอินสแตนซ์ของ NewsArticle หรือ SocialPost เข้าไปในฟังก์ชัน notify ได้อย่างอิสระ ส่วนโค้ดใด ๆ ที่พยายามจะส่งข้อมูลประเภทอื่น เช่น String หรือ i32 เข้าไปในฟังก์ชันนี้จะไม่สามารถคอมไพล์ผ่านได้ เนื่องจากประเภทเหล่านั้นไม่ได้เขียนสืบทอดทริต Summary เอาไว้
ไวยากรณ์ขอบเขตของทริต (Trait Bound Syntax)
ไวยากรณ์ impl Trait ทำงานได้ดีในกรณีทั่วไปที่ตรงไปตรงมา แต่แท้จริงแล้วมันคือการลดรูปไวยากรณ์ (syntax sugar) ของรูปแบบที่ยาวกว่าซึ่งเรียกว่า ขอบเขตของทริต (trait bound) ซึ่งมีหน้าตาไวยากรณ์ดังนี้:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
ไวยากรณ์รูปแบบยาวนี้ให้ผลลัพธ์เทียบเท่ากับตัวอย่างในส่วนก่อนหน้าทุกประการแต่มีความเวิ่นเว้อกว่า เราจะวางตัวแปรขอบเขตของทริตร่วมกับการประกาศพารามิเตอร์ประเภทข้อมูลเจเนอริกไว้หลังเครื่องหมายทวิภาค (colon) ภายในวงเล็บสามเหลี่ยม
การใช้ไวยากรณ์ impl Trait จะสะดวกและให้โค้ดที่สั้นในกรณีทั่วไปที่ไม่ซับซ้อน ส่วนการเขียนแบบ trait bound เต็มรูปแบบจะทรงพลังและรองรับกรณีที่โครงสร้างเงื่อนไขซับซ้อนได้ดีกว่า ตัวอย่างเช่น เราอาจต้องการให้พารามิเตอร์สองตัวสืบทอดทริต Summary หากเขียนด้วยไวยากรณ์ impl Trait จะมีลักษณะดังนี้:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {
การเลือกใช้ impl Trait แบบนี้จะเหมาะสมหากเราประสงค์ให้ฟังก์ชันยอมรับให้ item1 และ item2 สามารถเก็บประเภทข้อมูลต่างชนิดกันได้ (ตราบใดที่ทั้งสองประเภทนั้นต่างก็สืบทอดทริต Summary) แต่ทว่าหากคุณต้องการบังคับให้พารามิเตอร์ทั้งสองตัวต้องมีประเภทข้อมูลที่เหมือนกันทุกประการ คุณจำเป็นต้องสลับมาเขียนขอบเขตของทริตแบบ trait bound ดังนี้:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
ประเภทข้อมูลเจเนอริก T ที่ระบุเป็นชนิดของพารามิเตอร์ item1 และ item2 จะทำหน้าที่บีบข้อจำกัดให้ตัวคอมไพเลอร์คอยเช็คว่าประเภทข้อมูลรูปธรรมของอาร์กิวเมนต์ที่ส่งเข้ามาสำหรับ item1 และ item2 จะต้องเป็นประเภทเดียวกันเท่านั้น
การระบุขอบเขตทริตหลายตัวร่วมกันด้วยเครื่องหมาย + (Multiple Trait Bounds with the + Syntax)
เราสามารถกำหนดขอบเขตของทริตหลายตัวเข้าด้วยกันได้ สมมติว่าเราต้องการให้ฟังก์ชัน notify สามารถแสดงผลข้อมูลข้อความผ่านตัวยึดอักขระนอกเหนือจากการสรุปความด้วยเมธอด summarize บนตัวแปร item ได้: เราจะเขียนระบุในคำนิยาม notify ว่าตัวแปร item จำเป็นต้องสืบทอดทริตทั้ง Display และ Summary ซึ่งเราประยุกต์ใช้งานเครื่องหมายบวก + ได้ดังนี้:
pub fn notify(item: &(impl Summary + Display)) {
ไวยากรณ์เครื่องหมายบวก + นี้ยังนำมาใช้งานกับขอบเขตทริตของประเภทเจเนอริกได้เช่นเดียวกัน:
pub fn notify<T: Summary + Display>(item: &T) {
เมื่อมีการระบุขอบเขตของทริตทั้งสองตัวเรียบร้อยแล้ว ภายในโครงสร้างการทำงานของ notify จะได้รับอนุญาตให้เรียกใช้เมธอด summarize และประยุกต์ใช้งานเครื่องหมาย {} เพื่อจัดการแสดงผลพารามิเตอร์ item ได้
การเขียนระบุขอบเขตของทริตให้สะอาดขึ้นด้วยบล็อก where (Clearer Trait Bounds with where Clauses)
การเขียนระบุขอบเขตของทริตจำกัดจำนวนมากเกินไปจะส่งผลเสียตามมา เนื่องจากพารามิเตอร์เจเนอริกแต่ละตัวต่างก็มีขอบเขตของทริตเป็นของตัวเอง ส่งผลให้ฟังก์ชันที่มีพารามิเตอร์เจเนอริกจำนวนหลายตัวจะมีข้อมูลขอบเขตของทริตอัดแน่นอยู่ระหว่างชื่อฟังก์ชันกับรายการพารามิเตอร์ ซึ่งจะบดบังทำให้ส่วนซิกเนเจอร์ของฟังก์ชันอ่านยากมาก ด้วยเหตุนี้ Rust จึงเปิดช่องทางไวยากรณ์ทางเลือกในการระบุขอบเขตทริตผ่านคำสั่งเงื่อนไข where ต่อท้ายบรรทัดซิกเนเจอร์ของฟังก์ชันแทน ดังนั้น แทนที่จะต้องเขียนโค้ดพะรุงพะรังแบบนี้:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
เราสามารถสลับมาใช้งานบล็อก where ได้ดังนี้:
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
unimplemented!()
}
ซิกเนเจอร์ของฟังก์ชันตัวใหม่นี้จะดูเป็นระเบียบเรียบร้อยกว่าเดิมมาก โดยชื่อของฟังก์ชัน รายการพารามิเตอร์ และชนิดประเภทข้อมูลส่งคืนจะเกาะกลุ่มอยู่ติดกัน มีลักษณะคล้ายกับฟังก์ชันทั่วไปที่ไม่ได้ใส่ขอบเขตจำกัดของทริตยาว ๆ
การส่งคืนประเภทข้อมูลที่สืบทอดทริต (Returning Types That Implement Traits)
เราสามารถนำไวยากรณ์ 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)
}
}
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 หมายความว่า ฟังก์ชัน returns_summarizable จะส่งคืนออบเจกต์ประเภทข้อมูลใดก็ปานที่ต้องมีการสืบทอดทริต Summary โดยไม่จำเป็นต้องเอ่ยชื่อประเภทข้อมูลรูปธรรมนั้นออกมาตรง ๆ ในตัวอย่างนี้ ฟังก์ชัน returns_summarizable ส่งค่ากลับออกมาเป็น SocialPost แต่โค้ดระดับบนที่เป็นคนเรียกใช้ฟังก์ชันไม่จำเป็นต้องรู้รายละเอียดส่วนลึกข้อนี้เลย
ความสามารถในการระบุชนิดข้อมูลส่งคืนด้วยเงื่อนไขทริตที่สืบทอดจะมีประโยชน์อย่างยิ่งสำหรับบริบทของโคลเชอร์ (closures) และตัววนลูป (iterators) ซึ่งเราจะมาศึกษาต่อในบทที่ 13 เนื่องจากกระบวนการของโคลเชอร์และตัววนลูปมักสร้างประเภทข้อมูลแปลกใหม่ที่คอมไพเลอร์เข้าใจเพียงผู้เดียว หรือสร้างประเภทข้อมูลที่มีความยาวจนเขียนอธิบายได้ลำบาก ไวยากรณ์ impl Trait จึงเข้ามาช่วยให้นักพัฒนาเขียนอธิบายได้อย่างสั้นกระชับว่าฟังก์ชันนี้จะส่งคืนข้อมูลบางประเภทที่สืบทอดทริต Iterator โดยไม่ต้องพิมพ์อธิบายชนิดประเภทข้อมูลที่ยาวเหยียด
อย่างไรก็ตาม เงื่อนไขคือคุณสามารถใช้ไวยากรณ์ impl Trait ได้ก็ต่อเมื่อตัวฟังก์ชันหลักมีการส่งคืนประเภทข้อมูลรูปธรรมเพียงชนิดเดียวเท่านั้น ตัวอย่างเช่น โค้ดชุดนี้ที่พยายามส่งคืนค่าเป็น NewsArticle หรือไม่ก็ SocialPost สลับกันตามเงื่อนไข แต่ระบุชนิดข้อมูลส่งคืนเป็น 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,
}
}
}
การส่งคืนค่าแบบสลับกันระหว่าง NewsArticle กับ SocialPost ภายใต้ไวยากรณ์นี้ยังไม่ได้รับสิทธิ์ให้ผ่านได้เนื่องจากข้อจำกัดในการประมวลผลไวยากรณ์ impl Trait ในขั้นตอนทำหน้าที่ของคอมไพเลอร์ เราจะมาเรียนรู้แนวทางการเขียนโครงสร้างฟังก์ชันเพื่อตอบสนองพฤเบตเป้าหมายความต้องการนี้ร่วมกันในหัวข้อ “การใช้งานทริตอ็อบเจกต์เพื่อสร้างรูปแบบนามธรรมทับพฤติกรรมร่วม (Using Trait Objects to Abstract over Shared Behavior)” ในบทที่ 18
การใช้ขอบเขตของทริตเพื่อเลือกสร้างเมธอดตามเงื่อนไข (Using Trait Bounds to Conditionally Implement Methods)
การเลือกใส่ขอบเขตของทริตจำกัดร่วมกับบล็อกคำสั่ง impl ที่มีพารามิเตอร์ประเภทข้อมูลเจเนอริก ช่วยให้เราสามารถสร้างเมธอดเฉพาะตามเงื่อนไขที่กำหนดให้แก่ประเภทข้อมูลที่สืบทอดทริตที่ระบุไว้ได้ ตัวอย่างเช่น ประเภทข้อมูล Pair<T> ในตัวอย่างที่ 10-15 จะสืบทอดฟังก์ชัน new เสมอเพื่อส่งคืนอินสแตนซ์ของ Pair<T> ออกมา (ย้อนความจากหัวข้อ “ไวยากรณ์ของเมธอด (Method Syntax)” ในบทที่ 5 ว่าคำคีย์เวิร์ด Self คือประเภทข้อมูลสมญา (type alias) สำหรับประเภทข้อมูลในบล็อกคำสั่ง impl ซึ่งในที่นี้คือ Pair<T>) แต่ทว่าในบล็อกคำสั่ง impl ถัดไป ตัว Pair<T> จะได้รับการยอมรับให้สร้างเมธอด cmp_display ขึ้นมาได้ก็ต่อเมื่อ ประเภทข้อมูลภายใน T มีคุณสมบัติสืบทอดทริต PartialOrd สำหรับการเปรียบเทียบขนาด และ สืบทอดทริต 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);
}
}
}
นอกจากนี้เรายังสามารถเขียนคำสั่งเพื่อสืบทอดทริตตัวหนึ่งให้แก่ประเภทข้อมูลใด ๆ ที่มีการสืบทอดทริตอีกตัวหนึ่งอยู่ก่อนหน้าแล้วตามเงื่อนไข การสืบทอดทริตให้กับทุกประเภทข้อมูลที่เป็นไปตามเงื่อนไขจำกัดเรียกว่า การสืบทอดคลุมทั้งหมด (blanket implementations) ซึ่งถูกประยุกต์ใช้งานอย่างกว้างขวางในคลังไลบรารีมาตรฐานของ Rust ตัวอย่างเช่น ไลบรารีมาตรฐานจะเขียนคำสั่งสืบทอดทริต ToString ให้แก่ทุกประเภทข้อมูลที่สืบทอดทริต Display อยู่ก่อน บล็อกคำสั่ง impl ในไลบรารีมาตรฐานมีโครงสร้างหน้าตาประมาณโค้ดบรรทัดนี้:
impl<T: Display> ToString for T {
// --snip--
}
เพราะไลบรารีมาตรฐานได้ทำ blanket implementation นี้ไว้ให้เรียบร้อยแล้ว เราจึงพร้อมสั่งเรียกใช้เมธอด to_string ที่นิยามอยู่ภายใต้ทริต ToString บนประเภทข้อมูลใด ๆ ก็ตามที่สืบทอดทริต Display ได้ทันที ตัวอย่างเช่น เราสามารถแปลงข้อมูลประเภทตัวเลขจำนวนเต็มให้เป็นออบเจกต์สตริง String ได้อย่างง่ายดายด้วยคำสั่งดังนี้ เนื่องจากจำนวนเต็มใน Rust มีคุณสมบัติสืบทอดทริต Display อยู่แล้ว:
#![allow(unused)]
fn main() {
let s = 3.to_string();
}
การเขียนโครงสร้าง blanket implementations จะแสดงรายละเอียดอธิบายไว้ในส่วนของเอกสารทริตตรงหัวข้อ “Implementors”
ทริตและขอบเขตของทริตช่วยให้นักพัฒนาเขียนโค้ดที่ใช้พารามิเตอร์ประเภทเจเนอริกเพื่อช่วยประหยัดบรรทัดโค้ดซ้ำซ้อน แต่ในขณะเดียวกันก็ช่วยส่งมอบข้อมูลจำกัดความต้องการแก่คอมไพเลอร์ว่าเราประสงค์ให้ประเภทข้อมูลเจเนอริกมีพฤติกรรมรองรับแบบใด จากนั้นคอมไพเลอร์จะนำข้อมูลเงื่อนไขจำกัดดังกล่าวไปใช้ในการตรวจสอบให้มั่นใจเต็มร้อยว่า ทุกประเภทข้อมูลรูปธรรมที่ถูกนำเข้ามาประมวลผลร่วมกับโค้ดของเรามีพฤติกรรมที่ถูกต้องครบถ้วนจริง ในภาษาโปรแกรมระบบ Dynamic typing เราจะพบเจอข้อผิดพลาดที่รันไทม์ (runtime error) ทันทีหากโปรแกรมไปสั่งเรียกใช้เมธอดที่ประเภทข้อมูลนั้นไม่ได้เขียนเตรียมไว้ แต่ภาษา Rust ได้ดึงเอาข้อผิดพลาดเหล่านี้ขึ้นมาแสดงในขั้นตอนคอมไพล์ เพื่อบังคับให้นักพัฒนาแก้ไขและกำจัดปัญหาเหล่านั้นออกไปให้หมดก่อนที่ตัวโปรแกรมจะเริ่มทำงานเสียด้วยซ้ำ ส่งผลให้เราไม่จำเป็นต้องเขียนโค้ดคอยตรวจสอบเงื่อนไขพฤติกรรมในตอนโปรแกรมรันอีกต่อไป ซึ่งกระบวนการนี้จะช่วยยกระดับความเร็วในการประมวลผลเพิ่มขึ้นโดยไม่ต้องยอมสละความยืดหยุ่นของระบบเจเนอริกไป