ویژگی‌ها (Traits): تعریف رفتار مشترک

یک ویژگی (trait) عملکردی را که یک نوع خاص دارد تعریف می‌کند و می‌تواند با انواع دیگر به اشتراک بگذارد. ما می‌توانیم از traitها برای تعریف رفتار مشترک به صورت انتزاعی استفاده کنیم. همچنین می‌توانیم از محدودیت‌های ویژگی (trait bounds) برای مشخص کردن اینکه یک نوع جنریک می‌تواند هر نوعی باشد که رفتار خاصی دارد، استفاده کنیم.

توجه: ویژگی‌ها شبیه به مفهومی هستند که اغلب در زبان‌های دیگر به نام interfaces شناخته می‌شود، البته با برخی تفاوت‌ها.

تعریف یک trait

رفتار یک نوع شامل متدهایی است که می‌توانیم روی آن نوع فراخوانی کنیم. انواع مختلف یک رفتار مشترک دارند اگر بتوانیم همان متدها را روی تمام آن انواع فراخوانی کنیم. تعریف ویژگی‌ها راهی برای گروه‌بندی امضاهای متدها با هم است تا مجموعه‌ای از رفتارها را که برای دستیابی به یک هدف خاص ضروری است، تعریف کنیم.

برای مثال، فرض کنید چندین ساختار داده داریم که انواع و مقادیر مختلفی از متن را نگه می‌دارند: یک ساختار NewsArticle که یک خبر ذخیره شده در یک مکان خاص را نگه می‌دارد و یک ساختار Tweet که می‌تواند حداکثر ۲۸۰ کاراکتر به همراه متادیتایی که نشان می‌دهد آیا این یک توییت جدید، بازتوییت، یا پاسخ به توییت دیگری بوده است را نگه دارد.

ما می‌خواهیم یک کتابخانه گردآورنده رسانه به نام aggregator ایجاد کنیم که بتواند خلاصه‌هایی از داده‌هایی که ممکن است در یک نمونه از NewsArticle یا Tweet ذخیره شده باشند، نمایش دهد. برای این کار، نیاز به خلاصه‌ای از هر نوع داریم و این خلاصه را با فراخوانی متد summarize روی یک نمونه درخواست خواهیم کرد. لیست ۱۰-۱۲ تعریف یک ویژگی عمومی Summary را نشان می‌دهد که این رفتار را بیان می‌کند.

Filename: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String;
}
Listing 10-12: ویژگی Summary که شامل رفتار ارائه‌شده توسط یک متد summarize است

در اینجا، یک ویژگی با استفاده از کلیدواژه trait و سپس نام ویژگی، که در اینجا Summary است، اعلام می‌کنیم. همچنین ویژگی را به عنوان pub اعلام می‌کنیم تا کرایت‌هایی که به این کرایت وابسته هستند نیز بتوانند از این ویژگی استفاده کنند، همانطور که در چند مثال خواهیم دید. در داخل آکولادها، امضاهای متدی را اعلام می‌کنیم که رفتارهای نوع‌هایی که این ویژگی را پیاده‌سازی می‌کنند توصیف می‌کنند، که در این مورد fn summarize(&self) -> String است.

بعد از امضای متد، به جای ارائه یک پیاده‌سازی در داخل آکولادها، از یک نقطه‌ویرگول استفاده می‌کنیم. هر نوعی که این ویژگی را پیاده‌سازی می‌کند باید رفتار سفارشی خود را برای بدنه متد ارائه دهد. کامپایلر اطمینان خواهد داد که هر نوعی که ویژگی Summary را دارد، متد summarize را دقیقاً با این امضا تعریف خواهد کرد.

یک ویژگی می‌تواند چندین متد در بدنه خود داشته باشد: امضاهای متدها به صورت یک خط در هر خط فهرست می‌شوند و هر خط با یک نقطه‌ویرگول پایان می‌یابد.

پیاده‌سازی یک ویژگی (trait) روی یک نوع

اکنون که امضاهای مورد نظر متدهای ویژگی Summary را تعریف کرده‌ایم، می‌توانیم آن را روی نوع‌های موجود در گردآورنده رسانه خود پیاده‌سازی کنیم. لیست ۱۰-۱۳ یک پیاده‌سازی از ویژگی Summary روی ساختار NewsArticle را نشان می‌دهد که از تیتر، نویسنده، و مکان برای ایجاد مقدار بازگشتی summarize استفاده می‌کند. برای ساختار Tweet، متد summarize را به صورت نام کاربری به همراه تمام متن توییت تعریف می‌کنیم، با فرض اینکه محتوای توییت قبلاً به ۲۸۰ کاراکتر محدود شده است.

Filename: src/lib.rs
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 Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Listing 10-13: پیاده‌سازی ویژگی Summary روی نوع‌های NewsArticle و Tweet

پیاده‌سازی یک ویژگی روی یک نوع مشابه پیاده‌سازی متدهای معمولی است. تفاوت این است که بعد از impl، نام ویژگی‌ای که می‌خواهیم پیاده‌سازی کنیم را قرار می‌دهیم، سپس از کلمه کلیدی for استفاده می‌کنیم و سپس نام نوعی که می‌خواهیم ویژگی را برای آن پیاده‌سازی کنیم مشخص می‌کنیم. درون بلوک impl، امضاهای متدی که تعریف ویژگی مشخص کرده‌اند را قرار می‌دهیم. به جای اضافه کردن یک نقطه‌ویرگول بعد از هر امضا، از آکولادها استفاده می‌کنیم و بدنه متد را با رفتار خاصی که می‌خواهیم متدهای ویژگی برای نوع خاص داشته باشند پر می‌کنیم.

حالا که کتابخانه ویژگی Summary را روی NewsArticle و Tweet پیاده‌سازی کرده است، کاربران این کرایت می‌توانند متدهای ویژگی را روی نمونه‌های NewsArticle و Tweet فراخوانی کنند، به همان روشی که متدهای معمولی را فراخوانی می‌کنیم. تنها تفاوت این است که کاربر باید ویژگی را به همراه نوع‌ها به محدوده وارد کند. در اینجا مثالی از اینکه چگونه یک کرایت باینری می‌تواند از کرایت کتابخانه aggregator ما استفاده کند آورده شده است:

use aggregator::{Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

این کد 1 new tweet: horse_ebooks: of course, as you probably already know, people را چاپ می‌کند.

کرایت‌های دیگری که به کرایت aggregator وابسته هستند نیز می‌توانند ویژگی Summary را به محدوده وارد کنند تا Summary را روی نوع‌های خودشان پیاده‌سازی کنند. یکی از محدودیت‌هایی که باید به آن توجه داشت این است که ما فقط می‌توانیم یک ویژگی را روی یک نوع پیاده‌سازی کنیم اگر یا ویژگی یا نوع، یا هر دو، به کرایت ما محلی باشند. برای مثال، ما می‌توانیم ویژگی‌هایی از کتابخانه استاندارد مانند Display را روی یک نوع سفارشی مانند Tweet به عنوان بخشی از عملکرد کرایت aggregator پیاده‌سازی کنیم زیرا نوع Tweet به کرایت aggregator محلی است. همچنین می‌توانیم Summary را روی Vec<T> در کرایت aggregator پیاده‌سازی کنیم زیرا ویژگی Summary به کرایت aggregator محلی است.

اما نمی‌توانیم ویژگی‌های خارجی را روی نوع‌های خارجی پیاده‌سازی کنیم. برای مثال، نمی‌توانیم ویژگی Display را روی Vec<T> در کرایت aggregator پیاده‌سازی کنیم زیرا Display و Vec<T> هر دو در کتابخانه استاندارد تعریف شده‌اند و به کرایت aggregator محلی نیستند. این محدودیت بخشی از خاصیتی به نام انسجام (coherence) و به طور خاص‌تر قانون یتیم (orphan rule) است، که به این دلیل نامگذاری شده است که نوع والد وجود ندارد. این قانون اطمینان می‌دهد که کد دیگران نمی‌تواند کد شما را خراب کند و برعکس. بدون این قانون، دو کرایت می‌توانستند همان ویژگی را برای همان نوع پیاده‌سازی کنند و Rust نمی‌دانست کدام پیاده‌سازی را استفاده کند.

پیاده‌سازی‌های پیش‌فرض

گاهی اوقات مفید است که رفتار پیش‌فرضی برای برخی یا همه متدهای یک ویژگی داشته باشید به جای اینکه پیاده‌سازی‌ها برای تمام متدها در هر نوع اجباری باشند. سپس، وقتی ویژگی را روی یک نوع خاص پیاده‌سازی می‌کنیم، می‌توانیم رفتار پیش‌فرض هر متد را نگه داریم یا جایگزین کنیم.

در لیست ۱۰-۱۴، یک رشته پیش‌فرض برای متد summarize ویژگی Summary مشخص می‌کنیم به جای اینکه فقط امضای متد را تعریف کنیم، همانطور که در لیست ۱۰-۱۲ انجام دادیم.

Filename: src/lib.rs
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 Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Listing 10-14: تعریف ویژگی Summary با یک پیاده‌سازی پیش‌فرض برای متد summarize

برای استفاده از یک پیاده‌سازی پیش‌فرض برای خلاصه کردن نمونه‌های 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 روی Tweet در لیست ۱۰-۱۳ ندارد. دلیل آن این است که نحو برای بازنویسی یک پیاده‌سازی پیش‌فرض همانند نحو برای پیاده‌سازی یک متد ویژگی است که پیاده‌سازی پیش‌فرض ندارد.

پیاده‌سازی‌های پیش‌فرض می‌توانند متدهای دیگر را در همان ویژگی فراخوانی کنند، حتی اگر آن متدهای دیگر پیاده‌سازی پیش‌فرض نداشته باشند. به این روش، یک ویژگی می‌تواند مقدار زیادی عملکرد مفید ارائه دهد و فقط از پیاده‌سازان بخواهد که بخشی از آن را مشخص کنند. برای مثال، می‌توانیم ویژگی 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 Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    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 Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

بعد از اینکه summarize_author را تعریف کردیم، می‌توانیم متد summarize را روی نمونه‌های ساختار Tweet فراخوانی کنیم، و پیاده‌سازی پیش‌فرض summarize، تعریف متد summarize_author که ارائه داده‌ایم را فراخوانی خواهد کرد. از آنجا که ما summarize_author را پیاده‌سازی کرده‌ایم، ویژگی Summary رفتار متد summarize را بدون نیاز به نوشتن کد اضافی به ما داده است. به این شکل عمل می‌کند:

use aggregator::{self, Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

این کد 1 new tweet: (Read more from @horse_ebooks...) را چاپ می‌کند.

توجه داشته باشید که امکان فراخوانی پیاده‌سازی پیش‌فرض از یک پیاده‌سازی بازنویسی شده از همان متد وجود ندارد.

ویژگی‌ها (traits) به عنوان پارامترها

اکنون که می‌دانید چگونه ویژگی‌ها را تعریف و پیاده‌سازی کنید، می‌توانیم بررسی کنیم که چگونه از ویژگی‌ها برای تعریف توابعی که انواع مختلفی را می‌پذیرند استفاده کنیم. ما از ویژگی Summary که روی نوع‌های NewsArticle و Tweet در لیست ۱۰-۱۳ پیاده‌سازی کردیم استفاده خواهیم کرد تا تابعی به نام 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 Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    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 را فراخوانی کنیم. می‌توانیم notify را فراخوانی کرده و هر نمونه‌ای از NewsArticle یا Tweet را به آن پاس دهیم. کدی که تابع را با هر نوع دیگری، مانند یک String یا یک i32 فراخوانی کند، کامپایل نمی‌شود زیرا آن نوع‌ها ویژگی Summary را پیاده‌سازی نمی‌کنند.

نحو محدودیت ویژگی (Trait Bound Syntax)

نحو impl Trait برای موارد ساده مناسب است اما در واقع یک شکل کوتاه‌شده از یک فرم طولانی‌تر به نام محدودیت ویژگی (trait bound) است؛ به این صورت:

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

این فرم طولانی معادل مثال بخش قبلی است اما مفصل‌تر است. ما محدودیت‌های ویژگی را با اعلام پارامتر نوع جنریک بعد از یک دو‌نقطه و داخل پرانتزهای زاویه‌ای قرار می‌دهیم.

نحو impl Trait در موارد ساده مناسب است و کد را مختصرتر می‌کند، در حالی که نحو کامل‌تر محدودیت ویژگی می‌تواند پیچیدگی بیشتری را در موارد دیگر بیان کند. برای مثال، می‌توانیم دو پارامتر داشته باشیم که ویژگی Summary را پیاده‌سازی می‌کنند. انجام این کار با نحو impl Trait به این صورت است:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

استفاده از impl Trait مناسب است اگر بخواهیم این تابع اجازه دهد item1 و item2 انواع مختلفی داشته باشند (به شرطی که هر دو نوع ویژگی Summary را پیاده‌سازی کنند). اما اگر بخواهیم هر دو پارامتر یک نوع یکسان داشته باشند، باید از محدودیت ویژگی استفاده کنیم، مانند این:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

نوع جنریک T که به عنوان نوع پارامترهای item1 و item2 مشخص شده است، تابع را محدود می‌کند به این صورت که نوع مشخص مقدار پاس‌داده‌شده به عنوان آرگومان برای item1 و item2 باید یکسان باشد.

مشخص کردن محدودیت‌های ویژگی چندگانه با نحو +

ما همچنین می‌توانیم بیش از یک محدودیت ویژگی مشخص کنیم. فرض کنید می‌خواهیم notify از فرمت‌بندی نمایش (display formatting) و همچنین 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

استفاده از تعداد زیادی محدودیت ویژگی معایب خود را دارد. هر جنریک محدودیت‌های ویژگی مخصوص به خود را دارد، بنابراین توابعی با چندین پارامتر نوع جنریک می‌توانند شامل اطلاعات زیادی درباره محدودیت‌های ویژگی بین نام تابع و لیست پارامترهای آن باشند، که باعث سخت شدن خواندن امضای تابع می‌شود. به همین دلیل، 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!()
}

امضای این تابع کمتر شلوغ است: نام تابع، لیست پارامترها، و نوع بازگشتی به هم نزدیک‌تر هستند، مشابه یک تابع بدون محدودیت‌های ویژگی زیاد.

بازگرداندن نوع‌هایی که ویژگی‌ها را پیاده‌سازی می‌کنند

ما همچنین می‌توانیم از نحو 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 Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

با استفاده از impl Summary برای نوع بازگشتی، مشخص می‌کنیم که تابع returns_summarizable مقداری از نوعی که ویژگی Summary را پیاده‌سازی می‌کند بازمی‌گرداند، بدون نیاز به نام بردن از نوع مشخص. در این مورد، returns_summarizable یک Tweet بازمی‌گرداند، اما کدی که این تابع را فراخوانی می‌کند نیازی به دانستن این موضوع ندارد.

توانایی مشخص کردن یک نوع بازگشتی تنها بر اساس ویژگی‌ای که پیاده‌سازی می‌کند، به ویژه در زمینه closures و iterators مفید است، که در فصل ۱۳ به آن‌ها می‌پردازیم. closures و iterators نوع‌هایی ایجاد می‌کنند که تنها کامپایلر آن‌ها را می‌شناسد یا نوع‌هایی که بسیار طولانی هستند تا مشخص شوند. نحو impl Trait به شما اجازه می‌دهد که به طور مختصر مشخص کنید یک تابع نوعی که ویژگی Iterator را پیاده‌سازی می‌کند بازمی‌گرداند، بدون نیاز به نوشتن یک نوع بسیار طولانی.

با این حال، فقط زمانی می‌توانید از impl Trait استفاده کنید که یک نوع بازگردانده شود. برای مثال، این کد که یا یک NewsArticle یا یک Tweet بازمی‌گرداند و نوع بازگشتی به عنوان 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 Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    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 {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            retweet: false,
        }
    }
}

بازگرداندن یا یک NewsArticle یا یک Tweet مجاز نیست به دلیل محدودیت‌هایی در نحوه پیاده‌سازی نحو impl Trait در کامپایلر. ما نحوه نوشتن یک تابع با این رفتار را در بخش “استفاده از اشیاء ویژگی که مقادیر از نوع‌های مختلف را مجاز می‌سازد” در فصل ۱۸ بررسی خواهیم کرد.

استفاده از محدودیت‌های ویژگی برای پیاده‌سازی شرطی متدها

با استفاده از یک محدودیت ویژگی در یک بلوک impl که از پارامترهای نوع جنریک استفاده می‌کند، می‌توانیم متدها را به طور شرطی برای نوع‌هایی که ویژگی‌های مشخص‌شده را پیاده‌سازی می‌کنند پیاده‌سازی کنیم. برای مثال، نوع Pair<T> در لیست ۱۰-۱۵ همیشه تابع new را پیاده‌سازی می‌کند تا یک نمونه جدید از Pair<T> بازگرداند (به یاد داشته باشید از بخش “تعریف متدها” در فصل ۵ که Self یک نام مستعار برای نوع بلوک impl است که در اینجا Pair<T> است). اما در بلوک impl بعدی، Pair<T> فقط متد cmp_display را پیاده‌سازی می‌کند اگر نوع داخلی T ویژگی PartialOrd که مقایسه را ممکن می‌کند و ویژگی Display که چاپ را ممکن می‌کند، پیاده‌سازی کند.

Filename: src/lib.rs
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);
        }
    }
}
Listing 10-15: پیاده‌سازی شرطی متدها روی یک نوع جنریک بر اساس محدودیت‌های ویژگی

ما همچنین می‌توانیم یک ویژگی را به طور شرطی برای هر نوعی که ویژگی دیگری را پیاده‌سازی می‌کند، پیاده‌سازی کنیم. پیاده‌سازی‌های یک ویژگی روی هر نوعی که محدودیت‌های ویژگی را برآورده می‌کند پیاده‌سازی‌های کلی (blanket implementations) نامیده می‌شوند و به طور گسترده در کتابخانه استاندارد Rust استفاده می‌شوند. برای مثال، کتابخانه استاندارد ویژگی ToString را روی هر نوعی که ویژگی Display را پیاده‌سازی می‌کند، پیاده‌سازی می‌کند. بلوک impl در کتابخانه استاندارد شبیه به این کد است:

impl<T: Display> ToString for T {
    // --snip--
}

از آنجا که کتابخانه استاندارد این پیاده‌سازی کلی را دارد، می‌توانیم متد to_string تعریف‌شده توسط ویژگی ToString را روی هر نوعی که ویژگی Display را پیاده‌سازی می‌کند، فراخوانی کنیم. برای مثال، می‌توانیم اعداد صحیح را به مقادیر String متناظرشان تبدیل کنیم مانند این:

#![allow(unused)]
fn main() {
let s = 3.to_string();
}

پیاده‌سازی‌های کلی در مستندات ویژگی در بخش “Implementors” ظاهر می‌شوند.

ویژگی‌ها و محدودیت‌های ویژگی به ما امکان می‌دهند که کدی بنویسیم که از پارامترهای نوع جنریک برای کاهش تکرار استفاده کند اما همچنین به کامپایلر مشخص کند که می‌خواهیم نوع جنریک رفتار خاصی داشته باشد. سپس کامپایلر می‌تواند از اطلاعات محدودیت ویژگی استفاده کند تا بررسی کند که تمام نوع‌های مشخص استفاده‌شده با کد ما رفتار صحیح را ارائه می‌دهند. در زبان‌های تایپ‌گذاری پویا، ما هنگام اجرا خطا دریافت می‌کنیم اگر یک متد روی یک نوع که آن متد را تعریف نکرده فراخوانی کنیم. اما Rust این خطاها را به زمان کامپایل منتقل می‌کند تا ما مجبور شویم مشکلات را قبل از اینکه کد ما اجرا شود برطرف کنیم. علاوه بر این، نیازی به نوشتن کدی نداریم که رفتار را در زمان اجرا بررسی کند زیرا قبلاً آن را در زمان کامپایل بررسی کرده‌ایم. این کار عملکرد را بهبود می‌بخشد بدون اینکه انعطاف‌پذیری جنریک‌ها را قربانی کند.