นิยามพฤติกรรมร่วมด้วย 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