ویژگیها (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 این خطاها را به زمان کامپایل منتقل میکند تا ما مجبور شویم مشکلات را قبل از اینکه کد ما اجرا شود برطرف کنیم. علاوه بر این، نیازی به نوشتن کدی نداریم که رفتار را در زمان اجرا بررسی کند زیرا قبلاً آن را در زمان کامپایل بررسی کردهایم. این کار عملکرد را بهبود میبخشد بدون اینکه انعطافپذیری جنریکها را قربانی کند.