مثدمه

یک Trait (به معنی خصیصه یا ویژگی)، روش Rust برای تعریف یک رفتار یا قابلیت مشترک بین نوع‌های داده مختلف است. Traitها به کامپایلر می‌گویند که یک نوع چه متدهایی را باید ارائه دهد. این مفهوم بسیار شبیه به «اینترفیس» (interface) در زبان‌های دیگر است. با استفاده از Traitها، می‌توانیم کدهای جنریک بنویسیم که روی نوع‌های مختلفی که یک رفتار مشترک را به اشتراک می‌گذارند، کار کنند.

تعریف و پیاده‌سازی یک Trait

برای تعریف یک Trait، از کلمه کلیدی trait و سپس نام آن استفاده می‌کنیم. در داخل بدنه Trait، امضای متدهایی که می‌خواهیم بخشی از این رفتار باشند را تعریف می‌کنیم.

بیایید یک Trait به نام Summary تعریف کنیم که رفتار خلاصه‌سازی را مدل می‌کند. هر نوعی که این Trait را پیاده‌سازی کند، باید قابلیت خلاصه‌شدن را داشته باشد.

Copy Icon 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، پیاده‌سازی می‌کنیم.

Copy Icon src/lib.rs
pub struct NewsArticle { /* fields */ }
impl Summary for NewsArticle {
    fn summarize_author(&self) -> String {
        format!("@{}", self.author)
    }
}

pub struct Tweet { /* fields */ }
impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
    // This impl uses the default summarize() method
}

برای پیاده‌سازی یک Trait روی یک نوع، از سینتکس impl TraitName for TypeName استفاده می‌کنیم و متدهای مورد نیاز را در بدنه آن تعریف می‌کنیم.

استفاده از Trait به عنوان پارامتر تابع

قدرت اصلی Traitها زمانی مشخص می‌شود که از آنها به عنوان «محدودیت» (bound) روی پارامترهای جنریک استفاده کنیم. این کار به ما اجازه می‌دهد تا توابعی بنویسیم که هر نوعی را که یک رفتار خاص را پیاده‌سازی کرده باشد، بپذیرند.

سینتکس impl Trait

ساده‌ترین راه برای این کار، استفاده از سینتکس impl Trait در جایگاه نوع پارامتر است.

Copy Icon src/lib.rs
// This function accepts any type that implements the Summary trait.
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

این تابع notify می‌تواند هم یک رفرنس به NewsArticle و هم یک رفرنس به Tweet را بپذیرد، زیرا هر دو Summary را پیاده‌سازی کرده‌اند.

سینتکس Trait Bound (کلاسیک)

سینتکس impl Trait در واقع یک میانبر برای سینتکس کامل‌تر Trait Bound است. در این روش، ما به صورت صریح پارامتر نوع جنریک و محدودیت‌های آن را اعلام می‌کنیم.

Copy Icon 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 را پیاده‌سازی کرده باشد.

Copy Icon src/main.rs
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest { // This now works!
            largest = item;
        }
    }

    largest
}

با افزودن محدودیت T: PartialOrd، ما به کامپایلر تضمین می‌دهیم که هر نوعی که به این تابع پاس داده شود، حتماً قابل مقایسه خواهد بود. اکنون این تابع برای اسلایسی از i32، f64، char و هر نوع دیگری که PartialOrd را پیاده‌سازی کرده باشد، به درستی کار خواهد کرد.

در این درس با قدرت Traitها برای تعریف و اجرای رفتارهای مشترک بر روی نوع‌های مختلف آشنا شدیم. دیدیم که این مفهوم، کلید استفاده مؤثر از نوع‌های جنریک است و به ما اجازه می‌دهد کدهای انتزاعی، ایمن و بسیار انعطاف‌پذیری بنویسیم. اما هنوز یک بخش از امضای تابع largest برای ما مبهم است: کاراکتر & که نشان‌دهنده رفرنس است. این رفرنس‌ها چگونه از نظر ایمنی توسط کامپایلر بررسی می‌شوند؟ در درس بعدی، به سراغ آخرین مفهوم از این سه‌گانه، یعنی «مفهوم Lifetime»، خواهیم رفت.