ویژگیها (Traits): تعریف رفتار مشترک
یک ویژگی (trait) عملکردی را که یک نوع خاص دارد تعریف میکند و میتواند با انواع دیگر به اشتراک بگذارد. ما میتوانیم از traitها برای تعریف رفتار مشترک به صورت انتزاعی استفاده کنیم. همچنین میتوانیم از محدودیتهای ویژگی (trait bounds) برای مشخص کردن اینکه یک نوع جنریک میتواند هر نوعی باشد که رفتار خاصی دارد، استفاده کنیم.
توجه: ویژگیها شبیه به مفهومی هستند که اغلب در زبانهای دیگر به نام interfaces شناخته میشود، البته با برخی تفاوتها.
تعریف یک trait
رفتار یک نوع شامل متدهایی است که میتوانیم روی آن نوع فراخوانی کنیم. انواع مختلف یک رفتار مشترک دارند اگر بتوانیم همان متدها را روی تمام آن انواع فراخوانی کنیم. تعریف ویژگیها راهی برای گروهبندی امضاهای متدها با هم است تا مجموعهای از رفتارها را که برای دستیابی به یک هدف خاص ضروری است، تعریف کنیم.
برای مثال، فرض کنید چندین struct
داریم که انواع مختلفی از متن با اندازههای متفاوت را نگهداری میکنند:
یک ساختار NewsArticle
که یک خبر را در مکان خاصی نگهداری میکند،
و یک SocialPost
که حداکثر میتواند ۲۸۰ کاراکتر داشته باشد
بههمراه متادادهای که مشخص میکند آیا پست جدید، بازنشر (repost)، یا پاسخ به پست دیگری بوده است.
ما میخواهیم یک crate کتابخانهای برای جمعآوری رسانهها به نام aggregator
بسازیم
که بتواند خلاصههایی از دادههایی که ممکن است در نمونههایی از NewsArticle
یا SocialPost
ذخیره شده باشند را نمایش دهد.
برای انجام این کار، به یک خلاصه از هر نوع نیاز داریم،
و این خلاصه را با فراخوانی متد summarize
روی یک نمونه درخواست خواهیم کرد.
لیستینگ 10-12 تعریف یک trait عمومی به نام Summary
را نشان میدهد که این رفتار را بیان میکند.
pub trait Summary {
fn summarize(&self) -> String;
}
Summary
که شامل رفتار ارائهشده توسط یک متد summarize
استدر اینجا، یک ویژگی با استفاده از کلیدواژه trait
و سپس نام ویژگی، که در اینجا Summary
است، اعلام میکنیم. همچنین ویژگی را به عنوان pub
اعلام میکنیم تا کرایتهایی که به این کرایت وابسته هستند نیز بتوانند از این ویژگی استفاده کنند، همانطور که در چند مثال خواهیم دید. در داخل آکولادها، امضاهای متدی را اعلام میکنیم که رفتارهای نوعهایی که این ویژگی را پیادهسازی میکنند توصیف میکنند، که در این مورد fn summarize(&self) -> String
است.
بعد از امضای متد، به جای ارائه یک پیادهسازی در داخل آکولادها، از یک نقطهویرگول استفاده میکنیم. هر نوعی که این ویژگی را پیادهسازی میکند باید رفتار سفارشی خود را برای بدنه متد ارائه دهد. کامپایلر اطمینان خواهد داد که هر نوعی که ویژگی Summary
را دارد، متد summarize
را دقیقاً با این امضا تعریف خواهد کرد.
یک ویژگی میتواند چندین متد در بدنه خود داشته باشد: امضاهای متدها به صورت یک خط در هر خط فهرست میشوند و هر خط با یک نقطهویرگول پایان مییابد.
پیادهسازی یک ویژگی (trait) روی یک نوع
حالا که امضاهای مورد نظر برای متدهای trait به نام Summary
را تعریف کردهایم،
میتوانیم آن را روی نوعهای موجود در گردآورندهی رسانهایمان پیادهسازی کنیم.
لیستینگ 10-13 پیادهسازی trait Summary
را روی struct
ای به نام NewsArticle
نشان میدهد،
که از عنوان (headline)، نویسنده (author)، و مکان (location) برای ایجاد مقدار بازگشتی متد summarize
استفاده میکند.
برای ساختار SocialPost
، متد summarize
را بهگونهای تعریف میکنیم که ابتدا نام کاربری بیاید
و سپس تمام متن پست نمایش داده شود، با این فرض که محتوای پست از پیش به ۲۸۰ کاراکتر محدود شده است.
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
روی نوعهای NewsArticle
و SocialPost
پیادهسازی یک ویژگی روی یک نوع مشابه پیادهسازی متدهای معمولی است. تفاوت این است که بعد از impl
، نام ویژگیای که میخواهیم پیادهسازی کنیم را قرار میدهیم، سپس از کلمه کلیدی for
استفاده میکنیم و سپس نام نوعی که میخواهیم ویژگی را برای آن پیادهسازی کنیم مشخص میکنیم. درون بلوک impl
، امضاهای متدی که تعریف ویژگی مشخص کردهاند را قرار میدهیم. به جای اضافه کردن یک نقطهویرگول بعد از هر امضا، از آکولادها استفاده میکنیم و بدنه متد را با رفتار خاصی که میخواهیم متدهای ویژگی برای نوع خاص داشته باشند پر میکنیم.
حالا که کتابخانه ویژگی Summary
را روی NewsArticle
و Tweet
پیادهسازی کرده است، کاربران این کرایت میتوانند متدهای ویژگی را روی نمونههای NewsArticle
و Tweet
فراخوانی کنند، به همان روشی که متدهای معمولی را فراخوانی میکنیم. تنها تفاوت این است که کاربر باید ویژگی را به همراه نوعها به محدوده وارد کند. در اینجا مثالی از اینکه چگونه یک کرایت باینری میتواند از کرایت کتابخانه 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
را وارد حوزه کنند
و آن را روی نوعهای خودشان پیادهسازی نمایند.
یک محدودیت مهم این است که تنها زمانی میتوانیم یک trait را روی یک نوع پیادهسازی کنیم
که یا trait یا نوع، یا هر دو، در crate ما محلی (local) باشند.
برای مثال، میتوانیم traitهای کتابخانهی استاندارد مانند Display
را روی یک نوع سفارشی مانند SocialPost
پیادهسازی کنیم
زیرا نوع SocialPost
در crate aggregator
محلی است.
همچنین میتوانیم trait Summary
را روی Vec<T>
در crate aggregator
پیادهسازی کنیم
چون trait Summary
در crate ما محلی است.
اما نمیتوانیم ویژگیهای خارجی را روی نوعهای خارجی پیادهسازی کنیم. برای مثال، نمیتوانیم ویژگی Display
را روی Vec<T>
در کرایت aggregator
پیادهسازی کنیم زیرا Display
و Vec<T>
هر دو در کتابخانه استاندارد تعریف شدهاند و به کرایت aggregator
محلی نیستند. این محدودیت بخشی از خاصیتی به نام انسجام (coherence) و به طور خاصتر قانون یتیم (orphan rule) است، که به این دلیل نامگذاری شده است که نوع والد وجود ندارد. این قانون اطمینان میدهد که کد دیگران نمیتواند کد شما را خراب کند و برعکس. بدون این قانون، دو کرایت میتوانستند همان ویژگی را برای همان نوع پیادهسازی کنند و Rust نمیدانست کدام پیادهسازی را استفاده کند.
پیادهسازیهای پیشفرض
گاهی اوقات مفید است که رفتار پیشفرضی برای برخی یا همه متدهای یک ویژگی داشته باشید به جای اینکه پیادهسازیها برای تمام متدها در هر نوع اجباری باشند. سپس، وقتی ویژگی را روی یک نوع خاص پیادهسازی میکنیم، میتوانیم رفتار پیشفرض هر متد را نگه داریم یا جایگزین کنیم.
در لیست ۱۰-۱۴، یک رشته پیشفرض برای متد summarize
ویژگی Summary
مشخص میکنیم به جای اینکه فقط امضای متد را تعریف کنیم، همانطور که در لیست ۱۰-۱۲ انجام دادیم.
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
با یک پیادهسازی پیشفرض برای متد 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...)
را چاپ میکند.
ایجاد یک پیادهسازی پیشفرض (default) نیازی به تغییر در پیادهسازی trait Summary
برای SocialPost
در لیستینگ 10-13 ندارد.
دلیل آن این است که سینتکس بازنویسی (override) یک پیادهسازی پیشفرض،
دقیقاً همان سینتکسی است که برای پیادهسازی یک متد از trait که پیادهسازی پیشفرض ندارد استفاده میشود.
پیادهسازیهای پیشفرض میتوانند متدهای دیگر را در همان ویژگی فراخوانی کنند، حتی اگر آن متدهای دیگر پیادهسازی پیشفرض نداشته باشند. به این روش، یک ویژگی میتواند مقدار زیادی عملکرد مفید ارائه دهد و فقط از پیادهسازان بخواهد که بخشی از آن را مشخص کنند. برای مثال، میتوانیم ویژگی 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
را پیادهسازی کردهایم، 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...)
توجه داشته باشید که امکان فراخوانی پیادهسازی پیشفرض از یک پیادهسازی بازنویسی شده از همان متد وجود ندارد.
ویژگیها (traits) به عنوان پارامترها
حالا که میدانید چگونه یک trait را تعریف و پیادهسازی کنید،
میتوانیم بررسی کنیم که چگونه از traitها برای تعریف توابعی استفاده کنیم
که انواع مختلفی را بهعنوان پارامتر بپذیرند.
ما از trait Summary
که روی نوعهای NewsArticle
و SocialPost
در لیستینگ 10-13 پیادهسازی کردیم،
استفاده خواهیم کرد تا تابعی به نام notify
تعریف کنیم
که متد summarize
را روی پارامتر item
خود فراخوانی میکند—
پارامتری که از نوعی است که trait 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
و نام trait استفاده میکنیم.
این پارامتر هر نوعی را میپذیرد که trait مشخصشده را پیادهسازی کرده باشد.
در بدنهی تابع notify
، میتوانیم هر متدی از trait Summary
را روی item
فراخوانی کنیم،
مانند متد summarize
.
میتوانیم notify
را فراخوانی کرده و هر نمونهای از NewsArticle
یا SocialPost
را به آن پاس دهیم.
کدی که تابع را با نوعی دیگر، مانند String
یا i32
، فراخوانی کند کامپایل نخواهد شد،
زیرا این نوعها trait 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 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
مقداری را بازمیگرداند که trait Summary
را پیادهسازی میکند، بدون اینکه نوع مشخص آن را نام ببریم.
در این حالت، returns_summarizable
یک SocialPost
را بازمیگرداند،
اما کدی که این تابع را فراخوانی میکند نیازی به دانستن این موضوع ندارد.
توانایی مشخص کردن یک نوع بازگشتی تنها بر اساس ویژگیای که پیادهسازی میکند، به ویژه در زمینه closures و iterators مفید است، که در فصل ۱۳ به آنها میپردازیم. closures و iterators نوعهایی ایجاد میکنند که تنها کامپایلر آنها را میشناسد یا نوعهایی که بسیار طولانی هستند تا مشخص شوند. نحو 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
در کامپایلر وجود دارد.
نحوه نوشتن تابعی با چنین رفتاری را در بخش «استفاده از trait objectهایی که امکان داشتن مقادیر با نوعهای مختلف را میدهند» از فصل ۱۸ بررسی خواهیم کرد.
استفاده از محدودیتهای ویژگی برای پیادهسازی شرطی متدها
با استفاده از یک محدودیت ویژگی در یک بلوک impl
که از پارامترهای نوع جنریک استفاده میکند، میتوانیم متدها را به طور شرطی برای نوعهایی که ویژگیهای مشخصشده را پیادهسازی میکنند پیادهسازی کنیم. برای مثال، نوع Pair<T>
در لیست ۱۰-۱۵ همیشه تابع new
را پیادهسازی میکند تا یک نمونه جدید از Pair<T>
بازگرداند (به یاد داشته باشید از بخش “تعریف متدها” در فصل ۵ که Self
یک نام مستعار برای نوع بلوک 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--
}
از آنجا که کتابخانه استاندارد این پیادهسازی کلی را دارد، میتوانیم متد to_string
تعریفشده توسط ویژگی ToString
را روی هر نوعی که ویژگی Display
را پیادهسازی میکند، فراخوانی کنیم. برای مثال، میتوانیم اعداد صحیح را به مقادیر String
متناظرشان تبدیل کنیم مانند این:
#![allow(unused)] fn main() { let s = 3.to_string(); }
پیادهسازیهای کلی در مستندات ویژگی در بخش “Implementors” ظاهر میشوند.
ویژگیها و محدودیتهای ویژگی به ما امکان میدهند که کدی بنویسیم که از پارامترهای نوع جنریک برای کاهش تکرار استفاده کند اما همچنین به کامپایلر مشخص کند که میخواهیم نوع جنریک رفتار خاصی داشته باشد. سپس کامپایلر میتواند از اطلاعات محدودیت ویژگی استفاده کند تا بررسی کند که تمام نوعهای مشخص استفادهشده با کد ما رفتار صحیح را ارائه میدهند. در زبانهای تایپگذاری پویا، ما هنگام اجرا خطا دریافت میکنیم اگر یک متد روی یک نوع که آن متد را تعریف نکرده فراخوانی کنیم. اما Rust این خطاها را به زمان کامپایل منتقل میکند تا ما مجبور شویم مشکلات را قبل از اینکه کد ما اجرا شود برطرف کنیم. علاوه بر این، نیازی به نوشتن کدی نداریم که رفتار را در زمان اجرا بررسی کند زیرا قبلاً آن را در زمان کامپایل بررسی کردهایم. این کار عملکرد را بهبود میبخشد بدون اینکه انعطافپذیری جنریکها را قربانی کند.