مثدمه
یک Trait (به معنی خصیصه یا ویژگی)، روش Rust برای تعریف یک رفتار یا قابلیت مشترک بین نوعهای
داده مختلف است. Traitها به کامپایلر میگویند که یک نوع چه متدهایی را باید ارائه دهد. این مفهوم
بسیار شبیه به «اینترفیس» (interface) در زبانهای دیگر است. با استفاده از Traitها، میتوانیم
کدهای جنریک بنویسیم که روی نوعهای مختلفی که یک رفتار مشترک را به اشتراک میگذارند، کار کنند.
تعریف و پیادهسازی یک Trait
برای تعریف یک Trait، از کلمه کلیدی trait و سپس نام آن استفاده میکنیم. در داخل بدنه
Trait، امضای متدهایی که میخواهیم بخشی از این رفتار باشند را تعریف میکنیم.
بیایید یک Trait به نام Summary تعریف کنیم که رفتار خلاصهسازی را مدل میکند. هر نوعی که این
Trait را پیادهسازی کند، باید قابلیت خلاصهشدن را داشته باشد.
src/lib.rs
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Read more from {}...)", self.summarize_author())
}
}
در اینجا، Trait ما دو متد دارد: summarize_author که فقط امضای آن تعریف شده، و summarize که
یک «پیادهسازی پیشفرض» (default implementation) دارد. هر نوعی که Summary را پیادهسازی
میکند، باید حتماً summarize_author را خودش تعریف کند، اما میتواند از پیادهسازی پیشفرض
summarize استفاده کرده یا آن را بازنویسی (override) کند.
پیادهسازی Trait روی یک نوع
حالا این Trait را برای دو struct مختلف، یعنی NewsArticle و Tweet، پیادهسازی میکنیم.
src/lib.rs
pub struct NewsArticle { }
impl Summary for NewsArticle {
fn summarize_author(&self) -> String {
format!("@{}", self.author)
}
}
pub struct Tweet { }
impl Summary for Tweet {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
برای پیادهسازی یک Trait روی یک نوع، از سینتکس impl TraitName for TypeName استفاده میکنیم و
متدهای مورد نیاز را در بدنه آن تعریف میکنیم.
استفاده از Trait به عنوان پارامتر تابع
قدرت اصلی Traitها زمانی مشخص میشود که از آنها به عنوان «محدودیت» (bound) روی پارامترهای جنریک
استفاده کنیم. این کار به ما اجازه میدهد تا توابعی بنویسیم که هر نوعی را که یک رفتار خاص را
پیادهسازی کرده باشد، بپذیرند.
سینتکس impl Trait
سادهترین راه برای این کار، استفاده از سینتکس impl Trait در جایگاه نوع پارامتر است.
src/lib.rs
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
این تابع notify میتواند هم یک رفرنس به NewsArticle و هم یک رفرنس به Tweet را بپذیرد، زیرا
هر دو Summary را پیادهسازی کردهاند.
سینتکس Trait Bound (کلاسیک)
سینتکس impl Trait در واقع یک میانبر برای سینتکس کاملتر Trait Bound است. در این روش، ما به
صورت صریح پارامتر نوع جنریک و محدودیتهای آن را اعلام میکنیم.
src/lib.rs
pub fn notify_generic<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
این دو سینتکس در این مثال ساده معادل هستند، اما روش Trait Bound زمانی که چندین پارامتر جنریک
داریم یا محدودیتها پیچیدهتر میشوند، انعطافپذیری بیشتری دارد.
حل مشکل تابع largest با Trait Bounds
حالا میتوانیم به مشکلی که در درس قبل با تابع largest داشتیم برگردیم. خطای کامپایلر این بود که
نمیدانست چگونه مقادیر از نوع T را با هم مقایسه کند. برای اینکه بتوانیم از عملگر > استفاده
کنیم، نوع T باید Trait مربوط به مقایسه، یعنی std::cmp::PartialOrd را پیادهسازی کرده
باشد.
src/main.rs
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
با افزودن محدودیت T: PartialOrd، ما به کامپایلر تضمین میدهیم که هر نوعی که به این تابع پاس
داده شود، حتماً قابل مقایسه خواهد بود. اکنون این تابع برای اسلایسی از i32، f64، char و هر
نوع دیگری که PartialOrd را پیادهسازی کرده باشد، به درستی کار خواهد کرد.
در این درس با قدرت Traitها برای تعریف و اجرای رفتارهای مشترک بر روی نوعهای مختلف آشنا
شدیم. دیدیم که این مفهوم، کلید استفاده مؤثر از نوعهای جنریک است و به ما اجازه میدهد کدهای
انتزاعی، ایمن و بسیار انعطافپذیری بنویسیم. اما هنوز یک بخش از امضای تابع largest برای ما
مبهم است: کاراکتر & که نشاندهنده رفرنس است. این رفرنسها چگونه از نظر ایمنی توسط کامپایلر
بررسی میشوند؟ در درس بعدی، به سراغ آخرین مفهوم از این سهگانه، یعنی «مفهوم Lifetime»، خواهیم
رفت.