Advanced Traits
ما ابتدا trait
ها را در بخش «Traits: تعریف رفتار مشترک» در فصل ۱۰ بررسی کردیم، اما وارد جزئیات پیشرفتهتر آن نشدیم. اکنون که با Rust بیشتر آشنا شدهاید، میتوانیم به نکات دقیقتر و تخصصیتر بپردازیم.
Associated Types
نوعهای مرتبط (Associated types) یک نوع جایگزین را با یک trait متصل میکنند، بهگونهای که تعریفهای متد trait میتوانند از این نوعهای جایگزین در امضاهای خود استفاده کنند. پیادهساز یک trait نوع خاصی را برای جایگزینی نوع جایگزین برای پیادهسازی خاص مشخص میکند. به این ترتیب، میتوانیم یک trait تعریف کنیم که از برخی نوعها استفاده میکند بدون اینکه نیاز داشته باشیم دقیقاً بدانیم این نوعها چه هستند تا زمانی که trait پیادهسازی شود.
بیشتر ویژگیهای پیشرفتهای که در این فصل توضیح داده شده است، بهندرت مورد نیاز هستند. نوعهای مرتبط در حد وسط قرار دارند: آنها کمتر از ویژگیهای توضیح دادهشده در بقیه کتاب استفاده میشوند، اما بیشتر از بسیاری از ویژگیهای دیگر مورد بحث در این فصل به کار میروند.
یکی از مثالهای یک trait با یک نوع مرتبط، trait Iterator
است که کتابخانه استاندارد فراهم میکند. نوع مرتبط با نام Item
مشخص شده و بهجای نوع مقادیری که نوع پیادهسازیکننده Iterator
از روی آنها تکرار میکند قرار میگیرد. تعریف trait Iterator
همانطور که در فهرست 20-13 نشان داده شده است:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Iterator
که دارای یک نوع مرتبط به نام Item
استنوع Item
یک جایگزین است و تعریف متد next
نشان میدهد که مقادیری از نوع Option<Self::Item>
را بازمیگرداند. پیادهسازان trait Iterator
نوع خاصی را برای Item
مشخص میکنند و متد next
یک Option
حاوی مقدار از آن نوع خاص بازمیگرداند.
نوعهای مرتبط ممکن است مفهومی مشابه با genericها به نظر برسند، به این معنا که genericها به ما اجازه میدهند یک تابع بدون مشخص کردن نوعهایی که میتواند با آنها کار کند، تعریف کنیم. برای بررسی تفاوت بین این دو مفهوم، به یک پیادهسازی trait Iterator
روی یک نوع به نام Counter
نگاه خواهیم کرد که نوع Item
را بهعنوان u32
مشخص میکند:
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
// --snip--
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
این سینتکس با سینتکس genericها قابل مقایسه به نظر میرسد. پس چرا به جای این کار، trait Iterator
را با استفاده از genericها تعریف نکنیم، همانطور که در فهرست 20-14 نشان داده شده است؟
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
Iterator
با استفاده از genericهاتفاوت این است که هنگام استفاده از genericها، همانطور که در فهرست 20-14 نشان داده شده است، ما باید نوعها را در هر پیادهسازی حاشیهنویسی کنیم. زیرا میتوانیم همچنین Iterator<String> for Counter
یا هر نوع دیگری را پیادهسازی کنیم، بهطوری که بتوانیم پیادهسازیهای متعددی از Iterator
برای Counter
داشته باشیم. به عبارت دیگر، زمانی که یک trait یک پارامتر generic دارد، میتواند برای یک نوع چندین بار پیادهسازی شود و نوعهای خاص پارامترهای generic را هر بار تغییر دهد. زمانی که ما از متد next
بر روی Counter
استفاده میکنیم، باید حاشیهنویسی نوع ارائه دهیم تا مشخص کنیم کدام پیادهسازی Iterator
را میخواهیم استفاده کنیم.
با استفاده از نوعهای وابسته (associated types)، نیازی به مشخصکردن نوعها نداریم، زیرا نمیتوان یک trait
را چند بار برای یک نوع پیادهسازی کرد. در لیستینگ 20-13، با تعریفی که از نوعهای وابسته استفاده میکند، تنها یکبار میتوانیم مشخص کنیم که نوع Item
چه چیزی خواهد بود، چرا که تنها یک impl Iterator for Counter
میتواند وجود داشته باشد. بنابراین، لازم نیست هر بار که روی Counter
تابع next
را صدا میزنیم، مشخص کنیم که میخواهیم یک iterator
از نوع u32
داشته باشیم.
نوعهای وابسته همچنین بخشی از قرارداد trait
محسوب میشوند: پیادهسازان یک trait
باید نوعی را بهجای جاینگهدار (placeholder) نوع وابسته ارائه دهند. معمولاً نام نوعهای وابسته بهگونهای انتخاب میشود که نشان دهد چگونه از آن نوع استفاده خواهد شد، و مستندسازی نوعهای وابسته در مستندات API یک کار بسیار خوب و توصیهشده است.
Default Generic Type Parameters and Operator Overloading
وقتی که از پارامترهای generic type استفاده میکنیم، میتوانیم یک نوع خاص پیشفرض برای پارامتر generic تعیین کنیم. این نیاز به مشخص کردن یک نوع خاص توسط پیادهسازان trait را در صورتی که نوع پیشفرض کار کند، از بین میبرد. شما میتوانید هنگام اعلام یک نوع generic، یک نوع پیشفرض با سینتکس <PlaceholderType=ConcreteType>
مشخص کنید.
یک مثال عالی از وضعیتی که این تکنیک مفید است، بارگذاری مجدد عملگرها است، جایی که شما رفتار یک عملگر (مانند +
) را در شرایط خاص شخصیسازی میکنید.
زبان Rust اجازه نمیدهد که عملگرهای دلخواه خودتان را ایجاد کرده یا هر عملگری را بهدلخواه overload کنید. اما میتوانید عملیات و traitهای متناظر فهرستشده در std::ops
را با پیادهسازی traitهای مربوط به آن عملگر overload کنید. برای مثال، در لیستینگ 20-15 عملگر +
را overload میکنیم تا دو نمونه از Point
را با یکدیگر جمع کنیم. این کار را با پیادهسازی trait Add
برای ساختار Point
انجام میدهیم.
use std::ops::Add; #[derive(Debug, Copy, Clone, PartialEq)] struct Point { x: i32, y: i32, } impl Add for Point { type Output = Point; fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y, } } } fn main() { assert_eq!( Point { x: 1, y: 0 } + Point { x: 2, y: 3 }, Point { x: 3, y: 3 } ); }
Add
برای بارگذاری مجدد عملگر +
برای نمونههای Point
متد add
مقادیر x
دو نمونه Point
و مقادیر y
دو نمونه Point
را اضافه میکند تا یک نمونه جدید از Point
ایجاد کند. trait Add
دارای یک نوع مرتبط با نام Output
است که نوع بازگشتی از متد add
را تعیین میکند.
نوع generic پیشفرض در این کد در داخل trait Add
است. در اینجا تعریف آن آمده است:
#![allow(unused)] fn main() { trait Add<Rhs=Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output; } }
این کد باید بهطور کلی آشنا به نظر برسد: یک trait
با یک متد و یک نوع وابسته. بخش جدید Rhs=Self
است؛ این نحوی به پارامتر نوع پیشفرض (default type parameters) معروف است. پارامتر نوع generic با نام Rhs
(مخفف “right-hand side”) نوع پارامتر rhs
در متد add
را تعریف میکند. اگر هنگام پیادهسازی trait
Add
نوع مشخصی برای Rhs
تعیین نکنیم، مقدار پیشفرض Rhs
برابر با Self
خواهد بود، یعنی همان نوعی که در حال پیادهسازی Add
برای آن هستیم.
هنگامی که Add
را برای Point
پیادهسازی کردیم، از پیشفرض برای Rhs
استفاده کردیم زیرا میخواستیم دو نمونه Point
را به هم اضافه کنیم. حال، بیایید به مثالی از پیادهسازی trait Add
نگاه کنیم که در آن میخواهیم نوع Rhs
را شخصیسازی کنیم و از پیشفرض استفاده نکنیم.
ما دو struct
به نامهای Millimeters
و Meters
داریم که مقادیر را در واحدهای مختلف نگهداری میکنند. این بستهبندی نازک یک نوع موجود درون یک struct
دیگر به الگوی newtype معروف است، که در بخش [«استفاده از الگوی Newtype برای پیادهسازی Traitهای خارجی»][newtype] بهصورت دقیقتر توضیح دادهایم. ما میخواهیم مقادیر millimeters
را با مقادیر meters
جمع کنیم و پیادهسازی Add
تبدیل واحد را بهدرستی انجام دهد. میتوانیم Add
را برای Millimeters
پیادهسازی کنیم بهگونهای که Meters
بهعنوان Rhs
استفاده شود، همانطور که در لیستینگ 20-16 نشان داده شده است.
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
Add
برای Millimeters
جهت افزودن Millimeters
به Meters
برای افزودن Millimeters
و Meters
، ما impl Add<Meters>
را مشخص میکنیم تا مقدار پارامتر نوع Rhs
را به جای استفاده از پیشفرض Self
تنظیم کنیم.
شما از پارامترهای نوع پیشفرض در دو حالت اصلی استفاده خواهید کرد:
۱. برای گسترش یک نوع بدون آنکه کد موجود را دچار شکست کنیم ۲. برای فراهمکردن امکان سفارشیسازی در موارد خاصی که اکثر کاربران به آن نیازی نخواهند داشت
trait Add
در کتابخانه استاندارد یک مثال از هدف دوم است: معمولاً شما دو نوع مشابه را اضافه خواهید کرد، اما trait Add
قابلیت شخصیسازی فراتر از آن را فراهم میکند. استفاده از پارامتر نوع پیشفرض در تعریف trait Add
به این معناست که شما بیشتر اوقات نیازی به مشخص کردن پارامتر اضافی ندارید. به عبارت دیگر، مقدار کمی از کد اضافی حذف میشود و استفاده از trait آسانتر میشود.
هدف اول مشابه هدف دوم است، اما بهصورت معکوس: اگر بخواهید یک پارامتر نوع را به یک trait موجود اضافه کنید، میتوانید برای گسترش قابلیتهای trait بدون شکستن کد پیادهسازی موجود، یک مقدار پیشفرض برای آن تنظیم کنید.
Disambiguating Between Methods with the Same Name
در راست هیچ محدودیتی برای داشتن یک متد با همان نام در یک trait و در نوعی دیگر وجود ندارد و همچنین راست مانع نمیشود که هر دو trait را بر روی یک نوع پیادهسازی کنید. همچنین میتوانید متدی را مستقیماً بر روی نوعی پیادهسازی کنید که همان نام متدهای مربوط به traits را دارد.
هنگام فراخوانی متدهایی با همان نام، باید به راست بگویید که کدام یک را میخواهید استفاده کنید. کد زیر در فهرست 20-17 را در نظر بگیرید که در آن دو trait به نامهای Pilot
و Wizard
تعریف شدهاند که هر دو دارای متدی به نام fly
هستند. سپس هر دو trait بر روی نوع Human
پیادهسازی میشوند که قبلاً متدی به نام fly
نیز بر روی آن پیادهسازی شده است. هر متد fly
کاری متفاوت انجام میدهد.
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() {}
Human
پیادهسازی شدهاند، و یک متد fly
بهطور مستقیم بر روی Human
پیادهسازی شده استوقتی متد fly
را بر روی یک نمونه از Human
فراخوانی میکنیم، کامپایلر به طور پیشفرض متدی را که مستقیماً بر روی نوع پیادهسازی شده است، فراخوانی میکند، همانطور که در فهرست 20-18 نشان داده شده است.
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; person.fly(); }
fly
بر روی یک نمونه از Human
اجرای این کد متن *waving arms furiously*
را چاپ میکند و نشان میدهد که راست متد fly
پیادهسازیشده بر روی Human
را مستقیماً فراخوانی کرده است.
برای فراخوانی متدهای fly
از Pilot
یا Wizard
، باید از سینتکس صریحتری برای مشخص کردن متدی که منظور ماست، استفاده کنیم. فهرست 20-19 این سینتکس را نشان میدهد.
trait Pilot { fn fly(&self); } trait Wizard { fn fly(&self); } struct Human; impl Pilot for Human { fn fly(&self) { println!("This is your captain speaking."); } } impl Wizard for Human { fn fly(&self) { println!("Up!"); } } impl Human { fn fly(&self) { println!("*waving arms furiously*"); } } fn main() { let person = Human; Pilot::fly(&person); Wizard::fly(&person); person.fly(); }
fly
مربوط به کدام trait را میخواهیم فراخوانی کنیممشخص کردن نام trait قبل از نام متد، به راست مشخص میکند که کدام پیادهسازی متد fly
را میخواهیم فراخوانی کنیم. همچنین میتوانیم Human::fly(&person)
بنویسیم که معادل با person.fly()
است که در فهرست 20-19 استفاده کردهایم، اما اگر نیازی به رفع ابهام نباشد، این روش کمی طولانیتر است.
اجرای این کد خروجی زیر را چاپ میکند:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.46s
Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*
از آنجا که متد fly
یک پارامتر self
میگیرد، اگر دو نوع داشته باشیم که یک trait را پیادهسازی کنند، راست میتواند بر اساس نوع self
مشخص کند که کدام پیادهسازی trait را باید استفاده کند.
با این حال، توابع وابستهای (associated functions) که متد نیستند، پارامتر self
ندارند. زمانی که چندین نوع یا trait
توابع غیرمتدی با نام یکسان تعریف میکنند، Rust همیشه نمیتواند تشخیص دهد که منظور شما کدام نوع است، مگر آنکه از نحوی بهنام fully qualified syntax استفاده کنید. برای مثال، در لیستینگ 20-20 یک trait
برای یک پناهگاه حیوانات ایجاد میکنیم که میخواهد نام تمام تولهسگها را Spot بگذارد. یک trait
بهنام Animal
تعریف میکنیم که شامل یک تابع وابسته غیرمتدی baby_name
است. این trait
برای struct
ی بهنام Dog
پیادهسازی میشود، و بر روی خود Dog
نیز مستقیماً یک تابع وابسته غیرمتدی بهنام baby_name
ارائه میدهیم.
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", Dog::baby_name()); }
ما کدی برای نامگذاری تمام سگهای کوچک به نام Spot در تابع مرتبط baby_name
که بر روی Dog
تعریف شده است، پیادهسازی میکنیم. نوع Dog
همچنین trait Animal
را پیادهسازی میکند، که ویژگیهایی که تمام حیوانات دارند را توصیف میکند. سگهای کوچک به نام puppy شناخته میشوند و این در پیادهسازی trait Animal
بر روی Dog
در تابع baby_name
مرتبط با trait Animal
بیان شده است.
در تابع main
، ما تابع Dog::baby_name
را فراخوانی میکنیم، که تابع مرتبط تعریف شده بر روی Dog
را مستقیماً فراخوانی میکند. این کد خروجی زیر را چاپ میکند:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.54s
Running `target/debug/traits-example`
A baby dog is called a Spot
این خروجی آن چیزی نیست که ما میخواستیم. ما میخواهیم تابع baby_name
که بخشی از trait Animal
است و بر روی Dog
پیادهسازی شده است را فراخوانی کنیم تا کد A baby dog is called a puppy
را چاپ کند. تکنیکی که در فهرست 20-19 برای مشخص کردن نام trait استفاده کردیم، اینجا کمکی نمیکند. اگر main
را به کد موجود در فهرست 20-21 تغییر دهیم، خطای کامپایل دریافت خواهیم کرد.
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
baby_name
از trait Animal
، اما راست نمیداند که از کدام پیادهسازی استفاده کنداز آنجا که Animal::baby_name
پارامتر self
ندارد، و ممکن است انواع دیگری وجود داشته باشند که trait Animal
را پیادهسازی کرده باشند، راست نمیتواند تشخیص دهد که کدام پیادهسازی از Animal::baby_name
مورد نظر ما است. در نتیجه این خطای کامپایلر را دریافت خواهیم کرد:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
--> src/main.rs:20:43
|
2 | fn baby_name() -> String;
| ------------------------- `Animal::baby_name` defined here
...
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^^^ cannot call associated function of trait
|
help: use the fully-qualified path to the only available implementation
|
20 | println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
| +++++++ +
For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` (bin "traits-example") due to 1 previous error
برای رفع ابهام و مشخص کردن اینکه ما میخواهیم از پیادهسازی trait Animal
برای Dog
استفاده کنیم، به جای پیادهسازی trait Animal
برای نوع دیگری، باید از fully qualified syntax استفاده کنیم. فهرست 20-22 نشان میدهد چگونه از fully qualified syntax استفاده کنیم.
trait Animal { fn baby_name() -> String; } struct Dog; impl Dog { fn baby_name() -> String { String::from("Spot") } } impl Animal for Dog { fn baby_name() -> String { String::from("puppy") } } fn main() { println!("A baby dog is called a {}", <Dog as Animal>::baby_name()); }
baby_name
از trait Animal
که بر روی Dog
پیادهسازی شده است، فراخوانی کنیمما با استفاده از یک اعلان نوع در داخل angle brackets به راست میگوییم که میخواهیم متد baby_name
از trait Animal
که بر روی Dog
پیادهسازی شده است، فراخوانی شود، با این کار مشخص میکنیم که میخواهیم نوع Dog
را برای این فراخوانی تابع بهعنوان یک Animal
در نظر بگیریم. این کد اکنون خروجی مورد نظر ما را چاپ میکند:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/traits-example`
A baby dog is called a puppy
به طور کلی، fully qualified syntax به صورت زیر تعریف میشود:
<Type as Trait>::function(receiver_if_method, next_arg, ...);
برای توابع مرتبطی که متد نیستند، receiver
وجود نخواهد داشت: فقط لیستی از آرگومانهای دیگر خواهد بود. شما میتوانید fully qualified syntax را در هر جایی که توابع یا متدها را فراخوانی میکنید، استفاده کنید. با این حال، مجاز هستید هر بخشی از این سینتکس را که راست میتواند از اطلاعات دیگر برنامه تشخیص دهد، حذف کنید. شما فقط در مواردی که چندین پیادهسازی با نام یکسان وجود دارد و راست به کمک نیاز دارد تا مشخص کند کدام پیادهسازی را میخواهید فراخوانی کنید، نیاز به استفاده از این سینتکس دقیقتر دارید.
استفاده از Supertraitها
گاهی ممکن است بخواهید یک تعریف trait
بنویسید که به trait
دیگری وابسته باشد: برای آنکه یک نوع بتواند trait
اول را پیادهسازی کند، لازم است آن نوع همچنین trait
دوم را نیز پیادهسازی کرده باشد. این کار را برای آن انجام میدهید که تعریف trait
شما بتواند از اعضای وابستهی (associated items) trait
دوم استفاده کند. trait
ای که تعریف شما به آن وابسته است، supertrait نامیده میشود.
برای مثال، فرض کنید میخواهیم یک trait
بهنام OutlinePrint
ایجاد کنیم با یک متد outline_print
که مقدار دادهشده را بهصورتی فرمتشده چاپ میکند که درون قاب ستارهای قرار گیرد. یعنی، اگر یک struct
بهنام Point
داشته باشیم که trait
استاندارد Display
را پیادهسازی کرده باشد و خروجی آن (x, y)
باشد، وقتی outline_print
را روی یک نمونه از Point
با x
برابر با 1
و y
برابر با 3
فراخوانی کنیم، باید چیزی مشابه زیر چاپ شود:
**********
* *
* (1, 3) *
* *
**********
در پیادهسازی متد outline_print
، میخواهیم از قابلیتهای trait Display
استفاده کنیم. بنابراین، نیاز داریم مشخص کنیم که trait OutlinePrint
فقط برای انواعی کار خواهد کرد که همچنین trait Display
را پیادهسازی کرده باشند و قابلیتهای مورد نیاز OutlinePrint
را ارائه دهند. میتوانیم این کار را در تعریف trait با مشخص کردن OutlinePrint: Display
انجام دهیم. این تکنیک شبیه به اضافه کردن یک محدودیت trait به trait است. فهرست 20-23 یک پیادهسازی از trait OutlinePrint
را نشان میدهد.
use std::fmt; trait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {output} *"); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } fn main() {}
OutlinePrint
که نیاز به قابلیتهای Display
دارداز آنجایی که مشخص کردهایم که OutlinePrint
به trait Display
نیاز دارد، میتوانیم از تابع to_string
استفاده کنیم که به طور خودکار برای هر نوعی که Display
را پیادهسازی کرده باشد، پیادهسازی شده است. اگر سعی کنیم to_string
را بدون اضافه کردن دو نقطه و مشخص کردن trait Display
بعد از نام trait استفاده کنیم، خطایی دریافت خواهیم کرد که میگوید هیچ متدی به نام to_string
برای نوع &Self
در محدوده فعلی یافت نشد.
حالا ببینیم چه اتفاقی میافتد اگر بخواهیم OutlinePrint
را برای یک نوعی که Display
را پیادهسازی نکرده است، مانند ساختار Point
، پیادهسازی کنیم:
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {output} *");
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
fn main() {
let p = Point { x: 1, y: 3 };
p.outline_print();
}
خطایی دریافت میکنیم که میگوید Display
مورد نیاز است ولی پیادهسازی نشده است:
$ cargo run
Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:23
|
20 | impl OutlinePrint for Point {}
| ^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:24:7
|
24 | p.outline_print();
| ^^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
note: required by a bound in `OutlinePrint::outline_print`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint::outline_print`
4 | fn outline_print(&self) {
| ------------- required by a bound in this associated function
For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` (bin "traits-example") due to 2 previous errors
برای رفع این مشکل، باید Display
را برای Point
پیادهسازی کنیم و محدودیت مورد نیاز OutlinePrint
را برآورده کنیم، به این صورت:
trait OutlinePrint: fmt::Display { fn outline_print(&self) { let output = self.to_string(); let len = output.len(); println!("{}", "*".repeat(len + 4)); println!("*{}*", " ".repeat(len + 2)); println!("* {output} *"); println!("*{}*", " ".repeat(len + 2)); println!("{}", "*".repeat(len + 4)); } } struct Point { x: i32, y: i32, } impl OutlinePrint for Point {} use std::fmt; impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "({}, {})", self.x, self.y) } } fn main() { let p = Point { x: 1, y: 3 }; p.outline_print(); }
سپس، پیادهسازی trait
بهنام OutlinePrint
برای Point
با موفقیت کامپایل خواهد شد، و میتوانیم متد outline_print
را روی یک نمونه از Point
فراخوانی کنیم تا آن را درون یک قاب ستارهای نمایش دهد.
استفاده از الگوی Newtype برای پیادهسازی Traitهای خارجی
در بخش «پیادهسازی یک Trait برای یک نوع» در فصل ۱۰، به قانونی به نام قانون یتیم (orphan rule) اشاره کردیم که میگوید تنها در صورتی اجازه داریم یک trait
را برای یک نوع پیادهسازی کنیم که یا خود trait
، یا آن نوع، یا هر دو، محلی (local) به crate
ما باشند. میتوان با استفاده از الگوی newtype این محدودیت را دور زد. این الگو شامل ایجاد یک نوع جدید در قالب یک tuple struct
است. (در فصل ۵ در بخش «استفاده از Tuple Structها بدون فیلدهای نامگذاریشده برای ایجاد انواع مختلف» به این موضوع پرداختیم.)
این tuple struct
فقط یک فیلد خواهد داشت و در واقع یک بستهبندی نازک روی نوعی است که میخواهیم trait
را برای آن پیادهسازی کنیم. از آنجایی که نوع بستهبندیشده محلی به crate
ما خواهد بود، میتوانیم trait
مورد نظر را روی آن پیادهسازی کنیم. واژهی newtype از زبان برنامهنویسی Haskell گرفته شده است. استفاده از این الگو هیچگونه هزینهای در زمان اجرا ندارد، زیرا نوع بستهبندیشده در زمان کامپایل حذف میشود.
بهعنوان مثال، فرض کنید میخواهیم Display
را روی Vec<T>
پیادهسازی کنیم، که قانون orphan مانع انجام این کار بهصورت مستقیم میشود زیرا trait Display
و نوع Vec<T>
خارج از crate ما تعریف شدهاند. میتوانیم یک ساختار Wrapper
بسازیم که شامل یک نمونه از Vec<T>
باشد؛ سپس میتوانیم Display
را روی Wrapper
پیادهسازی کنیم و از مقدار Vec<T>
استفاده کنیم، همانطور که در فهرست 20-24 نشان داده شده است.
use std::fmt; struct Wrapper(Vec<String>); impl fmt::Display for Wrapper { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) } } fn main() { let w = Wrapper(vec![String::from("hello"), String::from("world")]); println!("w = {w}"); }
Wrapper
دور Vec<String>
برای پیادهسازی Display
پیادهسازی Display
از self.0
برای دسترسی به Vec<T>
داخلی استفاده میکند، زیرا Wrapper
یک tuple struct
است و Vec<T>
در موقعیت اندیس ۰ این tuple
قرار دارد. سپس میتوانیم از قابلیتهای trait
Display
روی Wrapper
استفاده کنیم.
نکتهی منفی در استفاده از این تکنیک این است که Wrapper
یک نوع جدید است، بنابراین متدهای نوعی که درون خود نگه میدارد را ندارد. باید تمام متدهای Vec<T>
را مستقیماً روی Wrapper
پیادهسازی کنیم بهگونهای که این متدها به self.0
ارجاع دهند؛ این کار به ما اجازه میدهد که با Wrapper
مانند یک Vec<T>
رفتار کنیم. اگر بخواهیم نوع جدید همهی متدهای نوع درونی را داشته باشد، پیادهسازی trait
Deref
برای Wrapper
که نوع درونی را بازمیگرداند، یک راهحل خواهد بود (در فصل ۱۵ در بخش «رفتار دادن به Smart Pointerها مانند رفرنسهای معمولی با Deref
» دربارهی پیادهسازی Deref
صحبت کردیم). اما اگر نخواهیم نوع Wrapper
همهی متدهای نوع درونی را داشته باشد—برای مثال، برای محدود کردن رفتار نوع Wrapper
—باید فقط متدهایی را که نیاز داریم، بهصورت دستی پیادهسازی کنیم.
این الگوی newtype حتی زمانی که traits درگیر نیستند نیز مفید است. حالا بیایید تمرکز خود را تغییر دهیم و به برخی از روشهای پیشرفته برای تعامل با سیستم نوع Rust بپردازیم.